summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing')
-rw-r--r--chromium/third_party/catapult/tracing/.allow-devtools-save0
-rw-r--r--chromium/third_party/catapult/tracing/.bowerrc3
-rw-r--r--chromium/third_party/catapult/tracing/.npmignore17
-rw-r--r--chromium/third_party/catapult/tracing/LICENSE27
-rw-r--r--chromium/third_party/catapult/tracing/OWNERS21
-rw-r--r--chromium/third_party/catapult/tracing/PRESUBMIT.py81
-rw-r--r--chromium/third_party/catapult/tracing/README.md52
-rw-r--r--chromium/third_party/catapult/tracing/app.yaml58
-rw-r--r--chromium/third_party/catapult/tracing/bin/PRESUBMIT.py19
-rw-r--r--chromium/third_party/catapult/tracing/bin/README.md42
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/add_reserved_diagnostics94
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/chartjson2histograms.py38
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/compare_samples51
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/generate_about_tracing_contents14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/histograms2csv40
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/histograms2html56
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/html2trace53
-rw-r--r--chromium/third_party/catapult/tracing/bin/index.html115
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/label_histograms56
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/map_traces14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/memory_infra_remote_dump131
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/merge_histograms32
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/merge_traces14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/results2json36
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_dev_server_tests14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_metric76
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_node_tests23
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_py_tests46
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_symbolizer_tests39
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_tests25
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/run_vinn_tests14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/slim_trace16
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/strip_memory_infra_trace13
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/symbolize_trace17
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/trace2html14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/update_gni14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/validate_all_diagnostics112
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/validate_all_metrics43
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/vulcanize_trace_viewer14
-rwxr-xr-xchromium/third_party/catapult/tracing/bin/why_imported48
-rw-r--r--chromium/third_party/catapult/tracing/bower.json22
-rw-r--r--chromium/third_party/catapult/tracing/docs/coordinate-systems.md46
-rw-r--r--chromium/third_party/catapult/tracing/docs/embedding-trace-viewer.md54
-rw-r--r--chromium/third_party/catapult/tracing/docs/extending-and-customizing-trace-viewer.md26
-rw-r--r--chromium/third_party/catapult/tracing/docs/getting-started.md21
-rw-r--r--chromium/third_party/catapult/tracing/docs/trace-viewer-internals.md129
-rw-r--r--chromium/third_party/catapult/tracing/images/third-trace-viewer-circle-blue.pngbin0 -> 3111 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-circle-blue.pngbin0 -> 9703 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-circle-green.pngbin0 -> 9711 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-circle-red.pngbin0 -> 9528 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-circle-yellow.pngbin0 -> 9633 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-square-blue.pngbin0 -> 3199 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-square-green.pngbin0 -> 3170 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-square-red.pngbin0 -> 3122 bytes
-rw-r--r--chromium/third_party/catapult/tracing/images/trace-viewer-square-yellow.pngbin0 -> 3519 bytes
-rw-r--r--chromium/third_party/catapult/tracing/package.json22
-rw-r--r--chromium/third_party/catapult/tracing/skp_data/google_homepage.skpbin0 -> 181402 bytes
-rw-r--r--chromium/third_party/catapult/tracing/skp_data/lthi_cats.skpbin0 -> 616665 bytes
-rw-r--r--chromium/third_party/catapult/tracing/third_party/chai/LICENSE7
-rw-r--r--chromium/third_party/catapult/tracing/third_party/chai/README.chromium15
-rw-r--r--chromium/third_party/catapult/tracing/third_party/chai/chai.js2
-rw-r--r--chromium/third_party/catapult/tracing/third_party/d3/LICENSE26
-rw-r--r--chromium/third_party/catapult/tracing/third_party/d3/README.chromium15
-rw-r--r--chromium/third_party/catapult/tracing/third_party/d3/d3.min.js5
-rw-r--r--chromium/third_party/catapult/tracing/third_party/devscripts/COPYING340
-rw-r--r--chromium/third_party/catapult/tracing/third_party/devscripts/README.chromium12
-rwxr-xr-xchromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl671
-rw-r--r--chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl.vanilla577
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/BUILDING.md7
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/LICENSE.md19
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/README.chromium15
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/README.md22
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/TESTING.md12
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/VERSION1
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/bower.json27
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix-min.js29
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix.js5020
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allclasses.tmpl14
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allfiles.tmpl65
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/class.tmpl340
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/index.tmpl52
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/publish.js201
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/default.css428
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/header.html2
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/index.html19
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/symbol.tmpl35
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/package.json30
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/common-spec.js14
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2-spec.js210
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2d-spec.js194
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat3-spec.js347
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat4-spec.js637
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/quat-spec.js559
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec2-spec.js549
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec3-spec.js661
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec4-spec.js492
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/worker-spec.js44
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/helpers/spec-helper.js32
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/jasmine.yml74
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix.js37
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/common.js52
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2.js302
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2d.js317
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat3.js565
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat4.js1283
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/quat.js553
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec2.js523
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec3.js709
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec4.js537
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build.rake2
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/compile.rake5
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/minify.rake5
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/default.rake1
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/release.rake21
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix.rb84
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/release_helper.rb104
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/version.rb28
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.js47
-rw-r--r--chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.min.js28
-rw-r--r--chromium/third_party/catapult/tracing/third_party/jszip/LICENSE.markdown651
-rw-r--r--chromium/third_party/catapult/tracing/third_party/jszip/README.chromium16
-rw-r--r--chromium/third_party/catapult/tracing/third_party/jszip/README.markdown32
-rw-r--r--chromium/third_party/catapult/tracing/third_party/jszip/jszip.min.js14
-rw-r--r--chromium/third_party/catapult/tracing/third_party/mannwhitneyu/README.chromium15
-rw-r--r--chromium/third_party/catapult/tracing/third_party/mannwhitneyu/mannwhitneyu.js197
-rw-r--r--chromium/third_party/catapult/tracing/third_party/mocha/LICENSE22
-rw-r--r--chromium/third_party/catapult/tracing/third_party/mocha/README.chromium9
-rw-r--r--chromium/third_party/catapult/tracing/third_party/mocha/mocha.css270
-rwxr-xr-xchromium/third_party/catapult/tracing/third_party/mocha/mocha.js6557
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/.npmignore12
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/.travis.yml3
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/CONTRIBUTING.md71
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/Gruntfile.js398
-rwxr-xr-xchromium/third_party/catapult/tracing/third_party/oboe/LICENCE26
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/README.chromium19
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/README.md24
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/.gitIgnore0
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkClient.js127
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkServer.js94
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/build/README.md1
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/component.json18
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.js2707
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.min.js1
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-node.js2587
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/index.js3
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/jasmine.json8
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/logo.pngbin0 -> 14032 bytes
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/package.json86
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/LICENCE.js30
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/ascent.js15
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/ascentManager.js62
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/defaults.js41
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/detectCrossOrigin.browser.js65
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/events.js45
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/functional.js250
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/incrementalContentBuilder.js150
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/instanceApi.js254
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPath.js364
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPathSyntax.js115
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/libs/clarinet.js501
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/lists.js192
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/parseResponseHeaders.browser.js24
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/patternAdapter.js112
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/pubSub.js64
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/publicApi.js56
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/singleEventPubSub.js93
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.browser.js149
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.node.js135
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/util.js44
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/wire.js34
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.browser.js24
-rw-r--r--chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.node.js9
-rw-r--r--chromium/third_party/catapult/tracing/third_party/pako/LICENSE21
-rw-r--r--chromium/third_party/catapult/tracing/third_party/pako/README.chromium15
-rw-r--r--chromium/third_party/catapult/tracing/third_party/pako/pako.min.js1
-rw-r--r--chromium/third_party/catapult/tracing/third_party/symbols/README.chromium21
-rw-r--r--chromium/third_party/catapult/tracing/third_party/symbols/symbols/PRESUBMIT.py21
-rw-r--r--chromium/third_party/catapult/tracing/third_party/symbols/symbols/__init__.py0
-rw-r--r--chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer.py470
-rwxr-xr-xchromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer_unittest.py174
-rw-r--r--chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/__init__.py0
-rwxr-xr-xchromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/mock_addr2line83
-rw-r--r--chromium/third_party/catapult/tracing/tracing/__init__.py6
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/assert_utils.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/base.html190
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/base64.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/base64_test.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/category_util.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/color.html260
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/color_scheme.html237
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/color_scheme_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/color_test.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/event.html104
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/event_target.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/event_target_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/extension_registry.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/extension_registry_base.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/extension_registry_basic.html127
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/extension_registry_test.html132
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/extension_registry_type_based.html162
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/guid.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/headless_tests.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream_test.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/interval_tree.html350
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/interval_tree_test.html235
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/bbox2.html156
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/bbox2_test.html36
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/math.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/math_test.html158
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function.html142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/quad.html233
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/quad_test.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/range.html308
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/range_test.html591
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/range_utils.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/range_utils_test.html133
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/rect.html163
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/rect_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/running_statistics.html207
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/running_statistics_test.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/statistics.html857
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/math/statistics_test.html579
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view.html1217
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view_test.html13382
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/raf.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/raf_test.html245
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/scalar.html50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/scalar_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/serializable.html104
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/serializable_test.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/settings.html214
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/settings_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator.html93
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator_test.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/task.html163
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/task_test.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/time_display_modes.html52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/timing.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/timing_test.html23
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/trace_stream.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unit.html531
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unit_scale.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unit_scale_test.html170
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unit_test.html444
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/constants.html28
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/html_test_results.html540
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/interactive_test_runner.html735
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/suite_loader.html253
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/test_case.html146
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/test_case_test.html18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/test_runner.html285
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/test_suite.html142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest/text_test_results.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/unittest_test.html50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/url_json.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/url_json_test.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/utils.html652
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/utils_test.html433
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/view_state.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/view_state_test.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/base/xhr.html156
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/auditor.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/filter.html130
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/filter_test.html107
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/scripting_controller.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/scripting_controller_test.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/scripting_object.html26
-rw-r--r--chromium/third_party/catapult/tracing/tracing/core/test_utils.html498
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/__init__.py3
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor.html767
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor_test.html424
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context.html75
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_blame_context_test.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_tree_node.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/render_frame.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/top_level.html59
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice_test.html54
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/cc.html12
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/constants.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/debug_colors.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list.html54
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice.html645
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice_test.html702
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_impl.html228
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl.html198
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js345
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_impl.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_as_image_data.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_test.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/raster_task.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/region.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/render_pass.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_coverage_rect.html28
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util_test.html114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor_test.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_processes.html69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_test_utils.html161
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver.html261
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver_test.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time.html164
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_multidimensional_view.md79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test.html1503
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test_utils.html126
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency.html367
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency_test.html384
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils.html234
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils_test.html256
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice_test.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state.html53
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test_data.js22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object.html207
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object_test.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_tree.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer.html38
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer_test.html28
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive.html397
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive_test.html329
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/chrome_config.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/cpu/cpu_usage_auditor.html117
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/full_config.html12
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer.html216
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer_test.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/android/event_log_importer.html324
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer_test.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer.html186
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer_test.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer.html223
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer_test.html188
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer.html483
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer_test.html293
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/parser.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser.html190
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser_test.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser.html256
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser_test.html176
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer.html234
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer_test.html183
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer_test.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer.html252
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer_test.html108
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/heap_dump_trace_event_importer.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/jszip.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/legacy_heap_dump_trace_event_importer.html214
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser.html239
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser_test.html233
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser.html732
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser_test.html273
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser_test.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser_test.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser.html114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser_test.html173
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser.html302
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser.html69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser.html125
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser_test.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser.html163
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser_test.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer.html941
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer_test.html686
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser.html143
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser_test.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser_test.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser.html363
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser_test.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser.html109
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser.html271
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser_test.html60
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser.html659
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser_test.html559
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser_test.html170
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/parser.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser.html179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser_test.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser_test.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser_test.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser.html161
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser_test.html262
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser_test.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser_test.html38
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/oboe.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/pako.html9
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader.html315
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader_test.html275
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer_test.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry.html128
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry_test.html123
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_map.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer.html3394
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_perf_test.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_test.html6593
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/v8/codemap.html383
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/v8/log_reader.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/v8/splaytree.html295
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer.html393
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer_test.html290
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/importer/zip_importer.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/lean_config.html14
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/measure/measure.html8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice_test.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/memory/lowmemory_auditor.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/net/net.html8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice_test.html195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/OWNERS2
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/__init__.py3
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/addr2line-pdb.exe.sha11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/linux_trace_v2_presymbolization.json.gz.sha11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/mac_trace_v2_presymbolization.json.gz.sha11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v1_presymbolization.json.gz.sha11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v2_presymbolization.json.gz.sha11
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace.py1707
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex.py36
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex_unittest.py49
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_end_to_end_test_slow.py195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader.py26
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader_unittest.py26
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_unittest.py69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/system_stats/system_stats_snapshot.html54
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/systrace_config.html19
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/context.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_all_of.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_any_of.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_ancestor.html53
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_duration.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_title.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_is_top_level.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_not.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery.html188
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery_test.html247
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry.html209
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry_test.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry_test.html322
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/v8_cpu_profile_node.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/v8_gc_stats_thread_slice.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/v8_ic_stats_thread_slice.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8/v8_thread_slice.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/v8_config.html11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor.html143
-rw-r--r--chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor_test.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html125
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/context_processor.html206
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html297
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html1409
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html325
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html88
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/import.html339
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/import_test.html228
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/importer.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html202
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html93
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html277
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html1649
-rw-r--r--chromium/third_party/catapult/tracing/tracing/index.js24
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/__init__.py15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html83
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html74
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html10
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html153
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html191
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html224
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html146
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html265
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html347
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt187
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py54
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html225
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py336
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/discover.py34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py20
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html395
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html423
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html216
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html214
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html93
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py75
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html280
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html272
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html301
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html138
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html258
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html366
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html88
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html54
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html248
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html258
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html234
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html275
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html337
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html54
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html65
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html163
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html182
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html462
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html409
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html623
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html1142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html1332
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html4249
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html186
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html348
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html742
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html162
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html152
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html230
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html168
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html228
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html297
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html210
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html374
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html718
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html490
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html189
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html261
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html333
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html117
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html364
-rw-r--r--chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html456
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/activity.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/alert.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/annotation.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/annotation_test.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/async_slice.html169
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html211
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html171
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html467
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html471
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html60
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/constants.html29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/counter.html190
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/counter_sample.html95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/counter_series.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/counter_test.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/cpu.html282
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/cpu_test.html206
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/device.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/device_test.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_container.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_container_test.html95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_info.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_registry.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_set.html302
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_set_test.html360
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/event_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/flow_event.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/frame.html96
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html849
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html3954
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/heap_dump.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html344
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html267
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html74
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html168
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html238
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html120
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html16
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html128
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/instant_event.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/kernel.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/kernel_test.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/location.html158
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html220
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model.html670
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_indices.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_settings.html148
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_stats.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/model_test.html343
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/object_collection.html229
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html230
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/object_instance.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/power_sample.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/power_series.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/power_series_test.html151
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/process.html175
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/process_base.html244
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html247
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html561
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/process_test.html127
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/profile_node.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/profile_tree.html88
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html65
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html74
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html152
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/sample.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/sample_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/scoped_id.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/selectable_item.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/selection_state.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/slice.html302
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/slice_group.html720
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html935
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/slice_test.html239
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html60
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/stack_frame.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/thread.html358
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/thread_slice.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/thread_test.html209
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html154
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html164
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/timed_event.html69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html93
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/vm_region.html444
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html1216
-rw-r--r--chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/__init__.py3
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/cloud_storage.py71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/corpus_driver.py9
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/corpus_driver_cmdline.py23
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/failure.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/failure.py53
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/failure_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/failure_unittest.py56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/file_handle.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/file_handle.py100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/function_handle.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/function_handle.py139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/function_handle_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/function_handle_unittest.py86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/gtest_progress_reporter.py91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/job.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/job.py39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/job_unittest.py15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/json_output_formatter.py20
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/local_directory_corpus_driver.py69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_runner.py105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.py196
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_cmdline.html93
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_unittest.py235
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_traces.py67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/map_traces_handler.py14
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/mre_result.html65
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/mre_result.py48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/mre_result_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/mre_result_unittest.py46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/output_formatter.py19
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/progress_reporter.py27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results.html28
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results_cmdline.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/run_and_convert_errors_to_failures.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/test_trace.json6
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue.py124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue_unittest.py33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/tests.html53
-rw-r--r--chromium/third_party/catapult/tracing/tracing/trace2html.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/trace_data/__init__.py4
-rw-r--r--chromium/third_party/catapult/tracing/tracing/trace_data/trace_data.py346
-rw-r--r--chromium/third_party/catapult/tracing/tracing/trace_data/trace_data_unittest.py98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html147
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html266
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html207
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html351
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html267
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html347
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html893
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html1261
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html354
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html4045
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html774
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html840
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html593
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html915
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html1241
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html382
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html496
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html211
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html207
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html358
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html234
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html104
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html354
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html75
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html356
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html277
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html53
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html186
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html38
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html205
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/animation.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html144
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html166
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html253
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/base.html18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/camera.html350
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html59
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html453
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html571
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html109
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html427
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html276
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/constants.html20
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/d3.html9
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html390
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html169
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html128
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html415
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html147
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/file.html36
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html229
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html258
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/heading.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html310
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html180
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html183
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html577
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html117
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html127
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html351
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html118
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html13
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html16
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html688
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html150
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html273
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html160
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/table.html1808
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html2115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html327
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html78
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui.html172
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html246
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/utils.html83
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html280
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html317
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html26
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css25
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html216
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html396
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html88
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html372
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html187
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html689
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html426
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html14
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html60
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.pngbin0 -> 3344 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html336
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html1200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html165
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html455
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html458
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html505
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html261
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html304
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html10
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.pngbin0 -> 245 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html229
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html123
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html65
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html463
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html19
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html21
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html172
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html347
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html165
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html334
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html148
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html12
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css28
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html728
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html198
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html197
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html17
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_control.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_controller.html154
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html366
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.pngbin0 -> 14088 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.pngbin0 -> 382 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.pngbin0 -> 3264 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.pngbin0 -> 4097 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html132
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html196
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html36
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html284
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html117
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html175
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html85
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html249
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html1179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html641
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html245
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html17
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html217
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html442
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html328
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html566
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html331
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html313
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html281
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html454
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html106
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html138
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html205
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html215
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html145
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html107
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html253
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html270
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html534
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html240
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html294
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html130
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html313
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html249
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html412
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html299
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css7
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html131
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css10
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html309
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html133
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/__init__.py0
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html154
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html401
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py24
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/csv_builder.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html75
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py3
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py180
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py237
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html16
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html133
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html130
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html145
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py201
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram.html1478
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram.py1111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py161
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html163
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py127
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html131
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set.html374
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set.py155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html157
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_test.html809
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py674
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html23
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html271
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html350
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html125
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html557
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html300
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html290
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html459
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html396
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html361
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html299
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html1679
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html210
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html144
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html599
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html300
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html353
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html274
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html312
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html23
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html626
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html1027
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/timings.md78
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html410
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/__init__.py7
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/check_common.py90
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/check_common_unittest.py29
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/check_gni.py29
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents.py66
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents_unittest.py24
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/histograms_viewer.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/html2trace.py106
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/merge_traces.py687
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/merge_traces_unittest.py48
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer.py81
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer_unittest.py95
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/run_profile.py67
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/run_vinn_tests.py47
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/slim_trace.py112
-rwxr-xr-xchromium/third_party/catapult/tracing/tracing_build/strip_memory_infra_trace.py102
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/trace2html.py128
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/trace2html_unittest.py84
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/tracing_dev_server_config.py57
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/update_gni.py123
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/update_gni_unittest.py37
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/vulcanize_histograms_viewer.py27
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer.py114
-rw-r--r--chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer_unittest.py30
-rw-r--r--chromium/third_party/catapult/tracing/tracing_examples/chrome_inspect_test_shell.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing_examples/deep_reports.html128
-rw-r--r--chromium/third_party/catapult/tracing/tracing_examples/metrics_debugger.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing_examples/skia_debugger.html174
-rw-r--r--chromium/third_party/catapult/tracing/tracing_examples/string_convert.js171
-rw-r--r--chromium/third_party/catapult/tracing/tracing_examples/trace_viewer.html184
-rw-r--r--chromium/third_party/catapult/tracing/tracing_project.py209
1335 files changed, 266996 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/.allow-devtools-save b/chromium/third_party/catapult/tracing/.allow-devtools-save
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/.allow-devtools-save
diff --git a/chromium/third_party/catapult/tracing/.bowerrc b/chromium/third_party/catapult/tracing/.bowerrc
new file mode 100644
index 00000000000..d981a1dd485
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "third_party/components"
+}
diff --git a/chromium/third_party/catapult/tracing/.npmignore b/chromium/third_party/catapult/tracing/.npmignore
new file mode 100644
index 00000000000..70d51d239f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/.npmignore
@@ -0,0 +1,17 @@
+test_data
+skp_data
+third_party
+
+images
+tracing_build
+build
+
+app.yaml
+*_test.html
+
+*.gyp
+*.gypi
+*.gn
+
+*.py
+*.pyc
diff --git a/chromium/third_party/catapult/tracing/LICENSE b/chromium/third_party/catapult/tracing/LICENSE
new file mode 100644
index 00000000000..e6c0d72a512
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/LICENSE
@@ -0,0 +1,27 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+//
+// 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.
diff --git a/chromium/third_party/catapult/tracing/OWNERS b/chromium/third_party/catapult/tracing/OWNERS
new file mode 100644
index 00000000000..65df8c2ebb3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/OWNERS
@@ -0,0 +1,21 @@
+# Owners: These folks have a very good understanding of the tracing
+# system and how the UI bits fit together. Good for reviews of large
+# architectual changes, or areas of the code where you may need more
+# information.
+fmeawad@chromium.org
+benjhayden@chromium.org
+lpy@chromium.org
+
+# For changes related to system_health/loading_metric.html and its tests.
+kouhei@chromium.org
+
+# TEAM: tracing@chromium.org
+# COMPONENT: Speed>Tracing
+
+# These folks have made huge contributions to the project, but are no longer
+# actively reviewing patches.
+nduca@chromium.org
+# petrcermak@chromium.org
+# dsinclair@chromium.org
+# charliea@chromium.org
+# eakuefner@chromium.org
diff --git a/chromium/third_party/catapult/tracing/PRESUBMIT.py b/chromium/third_party/catapult/tracing/PRESUBMIT.py
new file mode 100644
index 00000000000..9a1ba361921
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/PRESUBMIT.py
@@ -0,0 +1,81 @@
+# 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.
+
+import sys
+
+def _RunArgs(args, input_api):
+ p = input_api.subprocess.Popen(args, stdout=input_api.subprocess.PIPE,
+ stderr=input_api.subprocess.STDOUT)
+ out, _ = p.communicate()
+ return (out, p.returncode)
+
+
+def _CheckRegisteredMetrics(input_api, output_api):
+ """ Check that all tracing metrics are imported in all_metrics.html """
+ results = []
+ tracing_dir = input_api.PresubmitLocalPath()
+ out, return_code = _RunArgs(
+ [input_api.python_executable,
+ input_api.os_path.join(tracing_dir, 'bin', 'validate_all_metrics')],
+ input_api)
+ if return_code:
+ results.append(output_api.PresubmitError(
+ 'Failed validate_all_metrics: ', long_text=out))
+ return results
+
+
+def _CheckRegisteredDiagnostics(input_api, output_api):
+ """Check that all Diagnostic subclasses are registered."""
+ results = []
+ tracing_dir = input_api.PresubmitLocalPath()
+ out, return_code = _RunArgs(
+ [input_api.python_executable,
+ input_api.os_path.join(tracing_dir, 'bin', 'validate_all_diagnostics')],
+ input_api)
+ if return_code:
+ results.append(output_api.PresubmitError(
+ 'Failed validate_all_diagnostics: ', long_text=out))
+ return results
+
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return _CheckChange(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return _CheckChange(input_api, output_api)
+
+
+def _CheckChange(input_api, output_api):
+ results = []
+
+ original_sys_path = sys.path
+ try:
+ sys.path += [input_api.PresubmitLocalPath()]
+ from tracing_build import check_gni
+ error = check_gni.GniCheck()
+ if error:
+ results.append(output_api.PresubmitError(error))
+ finally:
+ sys.path = original_sys_path
+
+ results += input_api.RunTests(input_api.canned_checks.GetPylint(
+ input_api, output_api, extra_paths_list=_GetPathsToPrepend(input_api),
+ pylintrc='../pylintrc'))
+
+ results += _CheckRegisteredMetrics(input_api, output_api)
+ results += _CheckRegisteredDiagnostics(input_api, output_api)
+
+ return results
+
+
+def _GetPathsToPrepend(input_api):
+ import tracing_project
+ project_dir = input_api.PresubmitLocalPath()
+ catapult_dir = input_api.os_path.join(project_dir, '..')
+ return [
+ project_dir,
+ input_api.os_path.join(catapult_dir, 'third_party', 'mock'),
+ ] + tracing_project.GetDependencyPaths()
diff --git a/chromium/third_party/catapult/tracing/README.md b/chromium/third_party/catapult/tracing/README.md
new file mode 100644
index 00000000000..e4188a170ef
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/README.md
@@ -0,0 +1,52 @@
+
+<!-- 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.
+-->
+![Trace Viewer Logo](https://raw.githubusercontent.com/catapult-project/catapult/master/tracing/images/trace-viewer-circle-blue.png)
+
+Trace-Viewer is the javascript frontend for Chrome [about:tracing](http://dev.chromium.org/developers/how-tos/trace-event-profiling-tool) and [Android
+systrace](http://developer.android.com/tools/help/systrace.html).
+
+It provides rich analysis and visualization capabilities for many types of trace
+files. Its particularly good at viewing linux kernel traces (aka [ftrace](https://www.kernel.org/doc/Documentation/trace/ftrace.txt)) and Chrome's
+[trace_event format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview). Trace viewer can be [embedded](https://github.com/catapult-project/catapult/blob/master/tracing/docs/embedding-trace-viewer.md) as a component in your own code, or used from a plain checkout to turn trace files into standalone, emailable HTML files from the commandline:
+
+```
+$CATAPULT/tracing/bin/trace2html my_trace.json --output=my_trace.html && open my_trace.html
+```
+
+Its easy to [extend trace viewer](https://github.com/catapult-project/catapult/blob/master/tracing/docs/extending-and-customizing-trace-viewer.md) to support your favorite trace format, or add domain specific visualizations to the UI to simplify drilling down into complex data.
+
+Contributing, quick version
+===
+We welcome contributions! To hack on this code.
+
+There are two type of tests.
+
+### In the browser
+
+Run http server `$CATAPULT/bin/run_dev_server`. In any browser, navigate to `http://localhost:8003/`
+
+**Unit tests**| **Descripton**
+--- | ---
+All tests | http://localhost:8003/tests.html
+All tests with short format | http://localhost:8003/tracing/tests.html?shortFormat
+An individual test suite(such as ui/foo_test.js) | http://localhost:8003/tests.html?testSuiteName=ui.foo
+Tests named foo| http://localhost:8003/tests.html?testFilterString=foo
+
+### On command
+
+**Unit tests**| **Description**
+--- | ---
+All python tests | `$CATAPULT/tracing/bin/run_py_tests`
+All tracing tests in d8 environment | `$CATAPULT/tracing/bin/run_vinn_tests`
+All tracing tests in devserver environment | `$CATAPULT/tracing/bin/run_devserver_tests`
+All tests | `$CATAPULT/tracing/bin/run_tests`
+
+Make sure tests pass before sending us changelist. **We use Gerrit for codereview**. For more details, esp on Gerrit, [read our contributing guide](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md) or check out the [Getting Started guide](https://github.com/catapult-project/catapult/blob/master/tracing/docs/getting-started.md).
+
+Contact Us
+===
+Join our Google Group:
+* [tracing@chromium.org](https://groups.google.com/a/chromium.org/forum/#!forum/tracing)
diff --git a/chromium/third_party/catapult/tracing/app.yaml b/chromium/third_party/catapult/tracing/app.yaml
new file mode 100644
index 00000000000..a39a87aedce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/app.yaml
@@ -0,0 +1,58 @@
+# 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.
+
+runtime: python27
+threadsafe: true
+api_version: 1
+
+handlers:
+
+- url: /base
+ static_dir: tracing/tracing/base
+ secure: always
+
+- url: /core
+ static_dir: tracing/tracing/core
+ secure: always
+
+- url: /extras
+ static_dir: tracing/tracing/extras
+ secure: always
+
+- url: /ui
+ static_dir: tracing/tracing/ui
+ secure: always
+
+- url: /components
+ static_dir: tracing/third_party/components
+ secure: always
+
+- url: /trace_viewer
+ static_dir: tracing/tracing
+ secure: always
+
+- url: /gl-matrix-min.js
+ static_files: tracing/third_party/gl-matrix/dist/gl-matrix-min.js
+ upload: tracing/third_party/gl-matrix/dist/gl-matrix-min.js
+ secure: always
+
+- url: /mannwhitneyu.js
+ static_files: tracing/third_party/mannwhitneyu/mannwhitneyu.js
+ upload: tracing/third_party/mannwhitneyu/mannwhitneyu.js
+ secure: always
+
+- url: /pako.min.js
+ static_files: tracing/third_party/pako/pako.min.js
+ upload: tracing/third_party/pako/pako.min.js
+ secure: always
+
+- url: /d3.min.js
+ static_files: tracing/third_party/d3/d3.min.js
+ upload: tracing/third_party/d3/d3.min.js
+ secure: always
+
+- url: /.*
+ static_files: tracing/tracing/ui/extras/drive/index.html
+ upload: tracing/tracing/ui/extras/drive/index.html
+ secure: always
diff --git a/chromium/third_party/catapult/tracing/bin/PRESUBMIT.py b/chromium/third_party/catapult/tracing/bin/PRESUBMIT.py
new file mode 100644
index 00000000000..799215d729d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/PRESUBMIT.py
@@ -0,0 +1,19 @@
+# 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.
+import os
+
+def CheckChange(input_api, output_api):
+ init_py_path = os.path.join(input_api.PresubmitLocalPath(), '__init__.py')
+ res = []
+ if os.path.exists(init_py_path):
+ res += [output_api.PresubmitError(
+ '__init__.py is not allowed to exist in bin/')]
+ return res
+
+def CheckChangeOnUpload(input_api, output_api):
+ return CheckChange(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return CheckChange(input_api, output_api)
diff --git a/chromium/third_party/catapult/tracing/bin/README.md b/chromium/third_party/catapult/tracing/bin/README.md
new file mode 100644
index 00000000000..fae0051fb37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/README.md
@@ -0,0 +1,42 @@
+Quick descriptions of the scripts in tracing/bin/:
+
+ * `chartjson2histograms`: Converts a chartjson file to HistogramSet JSON.
+ * `compare_samples`: Compares metric results between two runs. Supports
+ chart-json, HistogramSet JSON, and buildbot formats.
+ * `generate_about_tracing_contents`: Vulcanizes trace viewer.
+ * `histograms2csv`: Converts HistogramSet JSON to CSV.
+ * `histograms2html`: Vulcanizes results.html. Optionally copies HistogramSet
+ JSON from existing results.html and/or histograms.json file.
+ * `html2trace`: Extracts trace JSON from a vulcanized trace HTML file.
+ * `label_histograms`: Add a label to Histograms in an HTML or JSON file.
+ * `map_traces`: Runs a trace map function over multiple traces. See also
+ `run_metric`.
+ * `memory_infra_remote_dump`: Extracts before/after memory dumps from a
+ devtools remote protocol port.
+ * `merge_histograms`: Merges Histograms from HistogramSet JSON according to a
+ sequence of grouping keys, produces a new HistogramSet JSON.
+ * `merge_traces`: Merge traces from either vulcanized HTML or JSON files to
+ either a vulcanized HTML or a JSON file.
+ * `results2json`: Extracts HistogramSet JSON from a results.html file.
+ * `run_dev_server_tests`: Automatically run tracing dev server tests.
+ * `run_metric`: Run a metric over one or more traces, produce vulcanized
+ results.html.
+ * `run_node_tests`: Automatically run headless tracing tests in node.
+ * `run_py_tests`: Automatically run tracing python tests.
+ * `run_tests`: Automatically run all tracing tests.
+ * `run_vinn_tests`: Automatically run headless tracing tests in vinn.
+ * `slim_trace`: Reads trace data from either HTML or JSON, removes some data,
+ writes a new `slimmed_$filename` file.
+ * `strip_memory_infra_trace`: Reads memory trace JSON, removes some data,
+ writes a new `$filename-filtered.json` file.
+ * `symbolize_trace`: Modifies trace JSON to symbolize symbols using a Chromium
+ Debug build output directory.
+ * `trace2html`: Vulcanizes trace data from a JSON file to an HTML file.
+ * `update_gni`: Updates `trace_viewer.gni`.
+ * `validate_all_diagnostics`: Checks that all Diagnostic classes in
+ `tracing/tracing/value/diagnostics/` are registered correctly.
+ * `validate_all_metrics`: Checks that all metric functions in
+ `tracing/tracing/metrics/` are registered correctly.
+ * `vulcanize_trace_viewer`: Vulcanizes trace viewer. (TODO(benjhayden): What is
+ the difference between this and `generate_about_tracing_contents`?)
+ * `why_imported`: Explain why given modules are imported in trace viewer.
diff --git a/chromium/third_party/catapult/tracing/bin/add_reserved_diagnostics b/chromium/third_party/catapult/tracing/bin/add_reserved_diagnostics
new file mode 100755
index 00000000000..57b835b5285
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/add_reserved_diagnostics
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# Copyright 2017 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 argparse
+import json
+import sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+import tracing_project
+tracing_project.UpdateSysPathIfNeeded()
+from py_utils import camel_case
+from tracing.value.diagnostics import add_reserved_diagnostics
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+
+
+LOG_URLS_CAMELCASE = camel_case.ToUnderscore(reserved_infos.LOG_URLS.name)
+LOG_URLS_K = LOG_URLS_CAMELCASE + '_k'
+LOG_URLS_V = LOG_URLS_CAMELCASE + '_v'
+
+BUILD_URLS_CAMELCASE = camel_case.ToUnderscore(reserved_infos.BUILD_URLS.name)
+BUILD_URLS_K = BUILD_URLS_CAMELCASE + '_k'
+BUILD_URLS_V = BUILD_URLS_CAMELCASE + '_v'
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Adds reserved diagnostics to a HistogramSet.',
+ add_help=False)
+ parser.add_argument('input_path',
+ help='HistogramSet JSON file path (input).')
+ parser.add_argument(
+ '--stdout',
+ action='store_true',
+ help='If present, will print the new HistogramSet instead of '
+ 'clobbering the file referenced by input_path.')
+ parser.add_argument(
+ '--output_path',
+ help='If present, will write new HistogramSet to this file instead of '
+ 'clobbering the file referenced by input_path.')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ arg_names_to_infos = {}
+ for info in reserved_infos.AllInfos():
+ if info.type == 'GenericSet':
+ name = camel_case.ToUnderscore(info.name)
+ arg_names_to_infos[name] = info
+ parser.add_argument('--%s' % name)
+
+ # TODO(#3770): Clean this up.
+ parser.add_argument('--%s' % LOG_URLS_K)
+ parser.add_argument('--%s' % LOG_URLS_V)
+
+ parser.add_argument('--%s' % BUILD_URLS_K)
+ parser.add_argument('--%s' % BUILD_URLS_V)
+
+ args = parser.parse_args()
+
+ names_to_values = {}
+ for name, value in vars(args).iteritems():
+ if name == LOG_URLS_K and value is not None:
+ v_value = vars(args)[LOG_URLS_V]
+ names_to_values[reserved_infos.LOG_URLS.name] = [value, v_value]
+ continue
+ if name == BUILD_URLS_K and value is not None:
+ v_value = vars(args)[BUILD_URLS_V]
+ names_to_values[reserved_infos.BUILD_URLS.name] = [value, v_value]
+ continue
+ if name in arg_names_to_infos and value is not None:
+ diagnostic_name = arg_names_to_infos[name].name
+ ctor = arg_names_to_infos[name].entry_type
+ names_to_values[diagnostic_name] = ctor(value)
+
+ with open(args.input_path, 'r') as f:
+ dicts = json.loads(f.read())
+
+ results_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ dicts, names_to_values)
+
+ if args.stdout:
+ print results_json
+ else:
+ path = args.output_path or args.input_path
+ with open(path, 'w') as f:
+ f.write(results_json)
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/chartjson2histograms.py b/chromium/third_party/catapult/tracing/bin/chartjson2histograms.py
new file mode 100755
index 00000000000..1d8f36a641f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/chartjson2histograms.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# Copyright 2017 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 argparse
+import sys
+import os
+
+TRACING_PATH = os.path.abspath(
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(TRACING_PATH)
+import tracing_project # pylint: disable=wrong-import-position
+tracing_project.UpdateSysPathIfNeeded()
+
+from tracing.value import convert_chart_json # pylint: disable=wrong-import-position
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Converts a chartjson file to HistogramSet JSON.',
+ add_help=False)
+ parser.add_argument('chartjson_path',
+ help='chartjson file path (input).')
+ parser.add_argument('histograms_path',
+ help='HistogramSet JSON file path (output).')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ args = parser.parse_args()
+ result = convert_chart_json.ConvertChartJson(args.chartjson_path)
+ if result.returncode != 0:
+ sys.stderr.write(result.stdout)
+ else:
+ file(args.histograms_path, 'w').write(result.stdout)
+ return result.returncode
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/compare_samples b/chromium/third_party/catapult/tracing/bin/compare_samples
new file mode 100755
index 00000000000..0d67a997837
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/compare_samples
@@ -0,0 +1,51 @@
+#!/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 argparse
+import os
+import sys
+
+tracing_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..'))
+sys.path.append(tracing_path)
+from tracing.metrics import compare_samples
+
+def Main(argv):
+ parser = argparse.ArgumentParser(
+ description='Compare samples.')
+ parser.add_argument('sample_a', type=str,
+ help='comma-separated list of paths to valuesets from '
+ 'sample a')
+ parser.add_argument('sample_b', type=str,
+ help='comma-separated list of paths to valuesets from '
+ 'sample b')
+ parser.add_argument('metric', type=str,
+ help='name of the metric to compare')
+ parser.add_argument('--chartjson', dest='format', action='store_const',
+ const='chartjson',
+ help='assume chartjson format for the input data')
+ parser.add_argument('--buildbot', dest='format', action='store_const',
+ const='buildbot',
+ help='assume buildbot result line format for the data')
+ args = parser.parse_args(argv[1:])
+
+ if not args.format:
+ filename = os.path.basename(sample_a.split(',')[0])
+ args.format = 'chartjson'
+
+ vinn_result = compare_samples.CompareSamples(
+ args.sample_a,
+ args.sample_b,
+ args.metric,
+ args.format
+ )
+ stdout = vinn_result.stdout
+ if not isinstance(stdout, str):
+ stdout = stdout.decode('utf-8')
+ print(stdout)
+ return vinn_result.returncode
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/generate_about_tracing_contents b/chromium/third_party/catapult/tracing/bin/generate_about_tracing_contents
new file mode 100755
index 00000000000..feee8828976
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/generate_about_tracing_contents
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# Copyright (c) 2014 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
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import generate_about_tracing_contents
+ sys.exit(generate_about_tracing_contents.Main(sys.argv[1:]))
diff --git a/chromium/third_party/catapult/tracing/bin/histograms2csv b/chromium/third_party/catapult/tracing/bin/histograms2csv
new file mode 100755
index 00000000000..c36246ec3d2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/histograms2csv
@@ -0,0 +1,40 @@
+#!/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 argparse
+import codecs
+import json
+import sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+import tracing_project
+tracing_project.UpdateSysPathIfNeeded()
+
+from tracing.value import histograms_to_csv
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Convert HistogramSet JSON to CSV.',
+ add_help=False)
+ parser.add_argument('json_path',
+ help='HistogramSet JSON file path (input).')
+ parser.add_argument('csv_path',
+ help='CSV file path (output).')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ args = parser.parse_args()
+ result = histograms_to_csv.HistogramsToCsv(args.json_path)
+ if result.returncode != 0:
+ sys.stderr.write(result.stdout)
+ else:
+ file(args.csv_path, 'w').write(result.stdout)
+ return result.returncode
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/histograms2html b/chromium/third_party/catapult/tracing/bin/histograms2html
new file mode 100755
index 00000000000..454c98d154f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/histograms2html
@@ -0,0 +1,56 @@
+#!/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 argparse
+import codecs
+import json
+import sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+from tracing_build import render_histograms_viewer
+from tracing_build import vulcanize_histograms_viewer
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Upgrade a results.html or add a new HistogramSet.',
+ add_help=False)
+ parser.add_argument('html_path', metavar='HTML_PATH',
+ help='HTML file path (output).')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ parser.add_argument('--html', nargs='+', default=[],
+ help='Zero or more HTML file paths (input).')
+ parser.add_argument('--json', nargs='+', default=[],
+ help='Zero or more HistogramSet JSON file paths (input).')
+ parser.add_argument('--mapresults', nargs='+', default=[],
+ help='Zero or more map results JSON file paths (input).')
+ args = parser.parse_args()
+
+ histograms = []
+
+ for html_path in args.html:
+ histograms.extend(render_histograms_viewer.ReadExistingResults(
+ open(html_path, 'r').read()))
+
+ for json_path in args.json:
+ histograms.extend(json.load(open(json_path, 'r')))
+
+ for json_path in args.mapresults:
+ for filename, results in json.load(open(json_path, 'r')).iteritems():
+ for histogram in results['pairs']['histograms']:
+ histograms.append(histogram)
+
+ open(args.html_path, 'a').close() # Create file if it doesn't exist.
+ with codecs.open(args.html_path,
+ mode='r+', encoding='utf-8') as output_stream:
+ vulcanize_histograms_viewer.VulcanizeAndRenderHistogramsViewer(
+ histograms, output_stream)
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/html2trace b/chromium/third_party/catapult/tracing/bin/html2trace
new file mode 100755
index 00000000000..82161fb1cfd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/html2trace
@@ -0,0 +1,53 @@
+#!/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 argparse
+import codecs
+import sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+from tracing_build import html2trace
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Extract trace data from an '
+ 'HTML trace.', add_help=False)
+ parser.add_argument('html_path', metavar='HTML_PATH',
+ help='HTML file path (input).')
+ parser.add_argument('trace_path', metavar='TRACE_PATH',
+ help='Trace file path (output). If the HTML file '
+ 'contains more than one trace data block, the first '
+ 'block will be extracted into %(metavar)s and the rest '
+ 'will be extracted into separate files %(metavar)s.1, '
+ '%(metavar)s.2, etc.')
+ parser.add_argument('--gzipped_output', choices=['true', 'false', 'auto'],
+ default='auto', help='Flag whether the output trace '
+ 'file should be gzipped.')
+ parser.add_argument('-q', '--quiet', action='store_true',
+ help='Don\'t print the saved file name(s).')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ args = parser.parse_args()
+
+ if args.gzipped_output == 'true':
+ gzipped_output = True
+ elif args.gzipped_output == 'false':
+ gzipped_output = False
+ else:
+ gzipped_output = args.trace_path.endswith('.gz')
+
+ with codecs.open(args.html_path, mode='r', encoding='utf-8') as html_file:
+ saved_paths = html2trace.CopyTraceDataFromHTMLFilePath(
+ html_file, args.trace_path, gzipped_output)
+
+ if not args.quiet:
+ print '\n'.join(saved_paths)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/index.html b/chromium/third_party/catapult/tracing/bin/index.html
new file mode 100644
index 00000000000..a4f68f0635e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/index.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<head>
+<script>
+'use strict';
+
+function onTraceViewerImportFail() {
+ document.addEventListener('DOMContentLoaded', function() {
+ document.body.textContent =
+ 'tracing/bin/trace_viewer_full.html is missing. ' +
+ 'Run vulcanize_trace_viewer from $TRACE_VIEWER and reload.';
+ });
+}
+</script>
+<link rel="import" href="trace_viewer_full.html"
+ onerror="onTraceViewerImportFail(event)">
+
+<style>
+ html, body {
+ box-sizing: border-box;
+ overflow: hidden;
+ margin: 0px;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ #trace-viewer {
+ width: 100%;
+ height: 100%;
+ }
+ #trace-viewer:focus {
+ outline: none;
+ }
+</style>
+<script>
+'use strict';
+
+(function() {
+ var viewer;
+ var url;
+ var model;
+
+ function load() {
+ var req = new XMLHttpRequest();
+ var isBinary = /[.]gz$/.test(url) || /[.]zip$/.test(url);
+ req.overrideMimeType('text/plain; charset=x-user-defined');
+ req.open('GET', url, true);
+ if (isBinary)
+ req.responseType = 'arraybuffer';
+
+ req.onreadystatechange = function(event) {
+ if (req.readyState !== 4)
+ return;
+
+ window.setTimeout(function() {
+ if (req.status === 200)
+ onResult(isBinary ? req.response : req.responseText);
+ else
+ onResultFail(req.status);
+ }, 0);
+ };
+ req.send(null);
+ }
+
+ function onResultFail(err) {
+ var overlay = new tr.ui.b.Overlay();
+ overlay.textContent = err + ': ' + url + ' could not be loaded';
+ overlay.title = 'Failed to fetch data';
+ overlay.visible = true;
+ }
+
+ function onResult(result) {
+ model = new tr.Model();
+ var i = new tr.importer.Import(model);
+ var p = i.importTracesWithProgressDialog([result]);
+ p.then(onModelLoaded, onImportFail);
+ }
+
+ function onModelLoaded() {
+ viewer.model = model;
+ viewer.viewTitle = url;
+ }
+
+ function onImportFail() {
+ var overlay = new tr.ui.b.Overlay();
+ overlay.textContent = tr.b.normalizeException(err).message;
+ overlay.title = 'Import error';
+ overlay.visible = true;
+ }
+
+ document.addEventListener('DOMContentLoaded', function() {
+ var container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ viewer = document.createElement('tr-ui-timeline-view');
+ viewer.track_view_container = container;
+ Polymer.dom(viewer).appendChild(container);
+
+ viewer.id = 'trace-viewer';
+ viewer.globalMode = true;
+ Polymer.dom(document.body).appendChild(viewer);
+
+ url = '../test_data/big_trace.json';
+ load();
+ });
+}());
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/bin/label_histograms b/chromium/third_party/catapult/tracing/bin/label_histograms
new file mode 100755
index 00000000000..07488964b99
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/label_histograms
@@ -0,0 +1,56 @@
+#!/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 argparse
+import codecs
+import json
+import sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+from tracing.value import histogram_set
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+from tracing_build import render_histograms_viewer
+from tracing_build import vulcanize_histograms_viewer
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Add a label to Histograms in an HTML or JSON file.',
+ add_help=False)
+ parser.add_argument('path', metavar='PATH',
+ help='HTML file path (output).')
+ parser.add_argument('label', metavar='LABEL',
+ help='The label to add to Histograms.')
+ args = parser.parse_args()
+
+ histograms = []
+
+ if args.path.endswith('.html'):
+ histograms.extend(render_histograms_viewer.ReadExistingResults(
+ open(args.path, 'r').read()))
+ elif args.path.endswith('.json'):
+ histograms.extend(json.load(open(json_path, 'r')))
+ else:
+ raise Error('Use either .html or .json extension.')
+ histograms = histogram_set.HistogramSet(histograms)
+ histograms.AddSharedDiagnosticToAllHistograms(
+ reserved_infos.LABELS.name,
+ generic_set.GenericSet([args.label]))
+
+ with codecs.open(args.path,
+ mode='r+', encoding='utf-8') as output_stream:
+ if args.path.endswith('.html'):
+ vulcanize_histograms_viewer.VulcanizeAndRenderHistogramsViewer(
+ histograms, output_stream)
+ else:
+ json.dump(histograms.AsDicts(), output_stream)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/map_traces b/chromium/third_party/catapult/tracing/bin/map_traces
new file mode 100755
index 00000000000..af58491504d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/map_traces
@@ -0,0 +1,14 @@
+#!/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
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..'))
+ sys.path.append(tracing_path)
+ from tracing.mre import map_traces
+ sys.exit(map_traces.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/memory_infra_remote_dump b/chromium/third_party/catapult/tracing/bin/memory_infra_remote_dump
new file mode 100755
index 00000000000..3125e9fe10d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/memory_infra_remote_dump
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+"""Grabs before/after memory dumps using the devtools remote protocol.
+
+To use it you first start Chrome with remote debugging enabled then run the
+script which will take a memory dump, wait for you to press enter, take
+another memory dump and finally save a trace file. For example:
+
+On OSX:
+$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
+ --remote-debugging-port=9222 \\
+ --memlog=all --memlog-sampling --memlog-stack-mode=pseudo \\
+$ ./tracing/bin/memory_infra_remote_dump --port=9222
+...
+[Press enter to stop tracing]
+...
+/var/folders/18/gl6q632j20nc_tw5g9l03dhc007g45/T/trace_20s191.json: 835 KB
+
+On Android:
+$ ./build/android/adb_chrome_public_command_line \\
+ --memlog=all --memlog-sampling --memlog-stack-mode=pseudo \\
+ --enable-remote-debugging
+$ ./build/android/adb_run_chrome_public
+$ adb forward tcp:1234 localabstract:chrome_devtools_remote
+$ ./third_party/catapult/tracing/bin/memory_infra_remote_dump --port=1234
+...
+[Press enter to stop tracing]
+...
+/var/folders/18/gl6q632j20nc_tw5g9l03dhc007g45/T/trace_20s191.json: 835 KB
+"""
+
+import argparse
+import json
+import os
+import requests
+import sys
+import tempfile
+import time
+
+try:
+ import websocket
+except ImportError:
+ print 'Please run: pip install --user websocket-client'
+ sys.exit(1)
+
+
+class TracingDevtoolsClient(object):
+ def __init__(self, host, port):
+ r = requests.get('http://%s:%s/json/version' % (host, port))
+ url = r.json()['webSocketDebuggerUrl']
+ print 'Connecting to ' + url
+ self.ws = websocket.create_connection(url)
+ self.cmd = 0
+
+ def send(self, method, params={}):
+ self.cmd += 1
+ self.ws.send(
+ json.dumps({'id': self.cmd, 'method': method, 'params': params}))
+ resp = self.recv()
+ assert resp['id'] == self.cmd
+ return resp.get('result', {})
+
+ def recv(self):
+ return json.loads(self.ws.recv())
+
+ def req_memory_dump(self):
+ print 'Requesting memory dump...',
+ resp = self.send('Tracing.requestMemoryDump')
+ assert resp['success'] == True
+ print ' ...done'
+
+ def dump(self, trace_fd):
+ trace_config = {
+ 'excludedCategories': ['*'],
+ 'includedCategories': ['disabled-by-default-memory-infra'],
+ 'memoryDumpConfig': {'triggers': []}
+ }
+ print 'Starting trace with trace_config', trace_config
+ params = {'traceConfig': trace_config, 'transferMode': 'ReturnAsStream'}
+ self.send('Tracing.start', params)
+ self.req_memory_dump()
+
+ if sys.stdin.isatty():
+ while True:
+ try:
+ print '[Press enter to trigger a new dump, q to finish the trace]'
+ cmd = raw_input()
+ except KeyboardInterrupt:
+ break
+ if cmd == 'q':
+ break
+ self.req_memory_dump()
+
+ self.send('Tracing.end')
+
+ # Wait for trace completion
+ print 'Flushing trace'
+ resp = self.recv()
+ assert resp['method'] == 'Tracing.tracingComplete'
+ stream_handle = resp['params']['stream']
+
+ # Read back the trace stream
+ resp = {'eof': False}
+ while not resp['eof']:
+ resp = self.send('IO.read', {'handle': stream_handle})
+ trace_fd.write(resp['data'].encode('utf-8'))
+
+ self.send('IO.close', {'handle': stream_handle})
+ trace_fd.close()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawTextHelpFormatter)
+ parser.add_argument('--host', default='localhost')
+ parser.add_argument('--port', '-p', default=9222)
+ parser.add_argument('--output-trace', '-o', default=None)
+ args = parser.parse_args()
+
+ if args.output_trace is None:
+ trace_fd = tempfile.NamedTemporaryFile(prefix='trace_', suffix='.json',
+ delete=False)
+ else:
+ trace_fd = open(args.output_trace, 'wb')
+
+ cli = TracingDevtoolsClient(args.host, args.port)
+ cli.dump(trace_fd)
+ print '\n%s: %d KB' % (trace_fd.name, os.stat(trace_fd.name).st_size / 1000)
diff --git a/chromium/third_party/catapult/tracing/bin/merge_histograms b/chromium/third_party/catapult/tracing/bin/merge_histograms
new file mode 100755
index 00000000000..1d3f1ccca12
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/merge_histograms
@@ -0,0 +1,32 @@
+#!/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 argparse
+import json
+import os
+import sys
+
+tracing_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+sys.path.append(tracing_path)
+from tracing.value import merge_histograms
+
+def Main(argv):
+ parser = argparse.ArgumentParser(
+ description='Merge Histograms.')
+ parser.add_argument('input', type=str,
+ help='Path to a HistogramSet JSON file. (input)')
+ parser.add_argument('output', type=str,
+ help='Path to a HistogramSet JSON file. (output)')
+ parser.add_argument('groupby', nargs='+',
+ help='One or more grouping keys (name, benchmark, ' +
+ 'time, storyset_repeat, story_repeat, story, tir, label)')
+ args = parser.parse_args(argv[1:])
+
+ merged = merge_histograms.MergeHistograms(args.input, args.groupby)
+ json.dump(merged, file(args.output, 'w'))
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/merge_traces b/chromium/third_party/catapult/tracing/bin/merge_traces
new file mode 100755
index 00000000000..4b610dd0699
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/merge_traces
@@ -0,0 +1,14 @@
+#!/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 os
+import sys
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import merge_traces
+ sys.exit(merge_traces.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/results2json b/chromium/third_party/catapult/tracing/bin/results2json
new file mode 100755
index 00000000000..fe90f196614
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/results2json
@@ -0,0 +1,36 @@
+#!/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 argparse
+import json
+import sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+from tracing_build import render_histograms_viewer
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Extract HistogramSet JSON from results.html.',
+ add_help=False)
+ parser.add_argument('html_path', metavar='HTML_PATH',
+ help='HTML file path (input).')
+ parser.add_argument('json_path', metavar='JSON_PATH',
+ help='JSON file path (input/output).')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ args = parser.parse_args()
+
+ histograms = render_histograms_viewer.ReadExistingResults(
+ open(args.html_path, 'r').read())
+ if os.path.exists(args.json_path):
+ histograms.extend(json.load(open(args.json_path, 'r')))
+ json.dump(histograms, open(args.json_path, 'w'))
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/chromium/third_party/catapult/tracing/bin/run_dev_server_tests b/chromium/third_party/catapult/tracing/bin/run_dev_server_tests
new file mode 100755
index 00000000000..bd09476721e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_dev_server_tests
@@ -0,0 +1,14 @@
+#!/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
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..', '..'))
+ sys.path.append(tracing_path)
+ from catapult_build import run_dev_server_tests
+ sys.exit(run_dev_server_tests.Main(sys.argv + ['--tests=tracing']))
diff --git a/chromium/third_party/catapult/tracing/bin/run_metric b/chromium/third_party/catapult/tracing/bin/run_metric
new file mode 100755
index 00000000000..d10719141c0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_metric
@@ -0,0 +1,76 @@
+#!/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 argparse
+import codecs
+import json
+import os
+import sys
+
+sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..'))
+from tracing_build import vulcanize_histograms_viewer
+from tracing.metrics import metric_runner
+from tracing.metrics import discover
+
+def Main(argv):
+ all_metrics = discover.DiscoverMetrics(
+ ['/tracing/metrics/all_metrics.html'])
+
+ parser = argparse.ArgumentParser(
+ description='Runs metrics on local traces')
+ parser.add_argument('trace_file_or_dir',
+ help='A trace file, or a dir containing trace files')
+ parser.add_argument('metrics', nargs='+',
+ help=('Function names of registered metrics '
+ '(not filenames.) '
+ 'Available metrics are: %s' %
+ ', '.join(all_metrics)),
+ choices=all_metrics, metavar='metricName')
+ parser.add_argument('--filename', default='results', type=str,
+ help='Output file name (no extension)')
+ parser.add_argument('--reset', action='store_true',
+ help=('Whether to ignore existing results in HTML file '
+ '(if it exists'))
+ parser.add_argument('--also-output-json', action='store_true',
+ help=('Also output json file containing values. Note that'
+ 'this only contains the results of current run'))
+
+ args = parser.parse_args(argv[1:])
+ trace_file_or_dir = os.path.abspath(args.trace_file_or_dir)
+
+ if os.path.isdir(trace_file_or_dir):
+ trace_dir = trace_file_or_dir
+ traces = [os.path.join(trace_dir, trace) for trace in os.listdir(trace_dir)]
+ else:
+ traces = [trace_file_or_dir]
+
+ failures = []
+ histograms = []
+ for trace_url, mre_result in metric_runner.RunMetricOnTraces(
+ traces, args.metrics).iteritems():
+ failures.extend(mre_result.failures)
+ histograms.extend(mre_result.pairs.get('histograms', []))
+
+ if failures:
+ print 'Running metric failed:'
+ for failure in failures:
+ print failure.stack
+
+ output_file = args.filename + '.html'
+ open(output_file, 'a').close() # Create file if it doesn't exist.
+ with codecs.open(output_file, mode='r+', encoding='utf-8') as output_stream:
+ vulcanize_histograms_viewer.VulcanizeAndRenderHistogramsViewer(
+ histograms, output_stream, args.reset)
+ print 'HTML result created in file://' + os.path.abspath(output_file)
+
+ if args.also_output_json:
+ output_file = args.filename + '.json'
+ with open(output_file, 'w') as f:
+ json.dump(histograms, f, indent=2, sort_keys=True, separators=(',', ': '))
+ print 'JSON result created in file://' + os.path.abspath(output_file)
+
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/run_node_tests b/chromium/third_party/catapult/tracing/bin/run_node_tests
new file mode 100755
index 00000000000..d044e0f1c22
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_node_tests
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+// Copyright (c) 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.
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+
+var catapultPath = fs.realpathSync(path.join(__dirname, '..', '..'));
+var catapultBuildPath = path.join(catapultPath, 'catapult_build');
+
+var node_bootstrap = require(path.join(catapultBuildPath, 'node_bootstrap.js'));
+
+HTMLImportsLoader.addArrayToSourcePath(
+ node_bootstrap.getSourcePathsForProject('tracing'));
+
+// Go!
+var headless_test_module_filenames =
+ node_bootstrap.getHeadlessTestModuleFilenamesForProject('tracing');
+
+HTMLImportsLoader.loadHTML('/tracing/base/headless_tests.html');
+tr.b.unittest.loadAndRunTests(headless_test_module_filenames); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/bin/run_py_tests b/chromium/third_party/catapult/tracing/bin/run_py_tests
new file mode 100755
index 00000000000..e81cb180b79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_py_tests
@@ -0,0 +1,46 @@
+#!/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 platform
+import sys
+
+_CATAPULT_PATH = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ os.path.pardir,
+ os.path.pardir))
+_TRACING_PATH = os.path.join(_CATAPULT_PATH, 'tracing')
+
+sys.path.insert(0, _TRACING_PATH)
+from tracing import tracing_project
+
+
+def _RunTestsOrDie(top_level_dir):
+ path = [_TRACING_PATH]
+ path.extend(tracing_project.GetDependencyPaths())
+ exit_code = run_with_typ.Run(top_level_dir, path=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(os.path.join(_TRACING_PATH, 'tracing'))
+ _RunTestsOrDie(os.path.join(_TRACING_PATH, 'tracing_build'))
+ sys.exit(0)
diff --git a/chromium/third_party/catapult/tracing/bin/run_symbolizer_tests b/chromium/third_party/catapult/tracing/bin/run_symbolizer_tests
new file mode 100755
index 00000000000..3f311753676
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_symbolizer_tests
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+
+"""
+The symbolizer tests take a long time to run [potentially 5+ minutes each], so
+they are invoked directly from separately from run_py_tests.
+"""
+
+import os
+import sys
+
+_CATAPULT_PATH = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ os.path.pardir,
+ os.path.pardir))
+_TRACING_PATH = os.path.join(_CATAPULT_PATH, 'tracing')
+
+
+def _RunTestsOrDie(top_level_dir):
+ exit_code = run_with_typ.Run(top_level_dir, path=[_TRACING_PATH],
+ suffixes=['*_test_slow.py'])
+ 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(os.path.join(_TRACING_PATH, 'tracing', 'extras', 'symbolizer'))
+ sys.exit(0)
diff --git a/chromium/third_party/catapult/tracing/bin/run_tests b/chromium/third_party/catapult/tracing/bin/run_tests
new file mode 100755
index 00000000000..3f94c67af0b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_tests
@@ -0,0 +1,25 @@
+#!/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
+
+
+_THIS_PATH = os.path.dirname(os.path.realpath(__file__))
+_TESTS = [
+ {'path': os.path.join(_THIS_PATH, 'run_py_tests')},
+ {'path': os.path.join(_THIS_PATH, 'run_vinn_tests'),
+ 'disabled': {'win32'}},
+ {'path': os.path.join(_THIS_PATH, 'run_dev_server_tests'),
+ 'chrome_path_arg': True}
+]
+
+
+if __name__ == '__main__':
+ catapult_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..', '..'))
+ sys.path.append(catapult_path)
+ from catapult_build import test_runner
+ sys.exit(test_runner.Main('tracing', _TESTS, sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/run_vinn_tests b/chromium/third_party/catapult/tracing/bin/run_vinn_tests
new file mode 100755
index 00000000000..42ec180f119
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/run_vinn_tests
@@ -0,0 +1,14 @@
+#!/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
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import run_vinn_tests
+ sys.exit(run_vinn_tests.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/slim_trace b/chromium/third_party/catapult/tracing/bin/slim_trace
new file mode 100755
index 00000000000..2e170777840
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/slim_trace
@@ -0,0 +1,16 @@
+#!/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 sys
+import os
+
+tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+sys.path.append(tracing_path)
+from tracing_build import slim_trace
+
+
+if __name__ == '__main__':
+ sys.exit(slim_trace.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/strip_memory_infra_trace b/chromium/third_party/catapult/tracing/bin/strip_memory_infra_trace
new file mode 100755
index 00000000000..8bdc8375d75
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/strip_memory_infra_trace
@@ -0,0 +1,13 @@
+#!/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 os
+import sys
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import strip_memory_infra_trace
+ sys.exit(strip_memory_infra_trace.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/symbolize_trace b/chromium/third_party/catapult/tracing/bin/symbolize_trace
new file mode 100755
index 00000000000..c2bee858e43
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/symbolize_trace
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# Copyright 2017 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
+
+tracing_path = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..'))
+sys.path.append(tracing_path)
+import tracing_project
+tracing_project.UpdateSysPathIfNeeded()
+
+if __name__ == '__main__':
+ from tracing.extras.symbolizer import symbolize_trace
+ sys.exit(symbolize_trace.main(sys.argv[1:]))
diff --git a/chromium/third_party/catapult/tracing/bin/trace2html b/chromium/third_party/catapult/tracing/bin/trace2html
new file mode 100755
index 00000000000..0457d39a66b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/trace2html
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# Copyright (c) 2014 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 sys
+import os
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import trace2html
+ sys.exit(trace2html.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/update_gni b/chromium/third_party/catapult/tracing/bin/update_gni
new file mode 100755
index 00000000000..b2bc2291846
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/update_gni
@@ -0,0 +1,14 @@
+#!/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
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import update_gni
+ sys.exit(update_gni.Update())
diff --git a/chromium/third_party/catapult/tracing/bin/validate_all_diagnostics b/chromium/third_party/catapult/tracing/bin/validate_all_diagnostics
new file mode 100755
index 00000000000..f8bd5badaf0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/validate_all_diagnostics
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# Copyright 2017 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 argparse
+import json
+import os
+import string
+import sys
+
+sys.path.insert(
+ 1,
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
+import tracing_project
+tracing_project.UpdateSysPathIfNeeded()
+
+from py_utils import camel_case
+from py_utils import discover
+
+from tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import all_diagnostics
+
+import vinn
+
+
+_DISCOVER_CMDLINE = os.path.join(
+ os.path.dirname(__file__), '..', 'tracing', 'value', 'diagnostics',
+ 'discover_cmdline.html')
+
+
+def DiscoverJSDiagnostics(project, js_args):
+ res = vinn.RunFile(_DISCOVER_CMDLINE, source_paths=list(project.source_paths),
+ js_args=js_args)
+ if res.returncode != 0:
+ raise RuntimeError('Error running diagnostics/discover_cmdline: ' + res.stdout)
+ else:
+ return set([str(m) for m in json.loads(res.stdout)])
+
+
+def DiscoverPythonDiagnostics():
+ return discover.DiscoverClasses(
+ os.path.join(tracing_project.TracingProject.tracing_src_path,
+ 'value'),
+ tracing_project.TracingProject.tracing_root_path,
+ diagnostic.Diagnostic, index_by_class_name=True)
+
+
+def CheckPythonDiagnostics():
+ discovered_diagnostics = DiscoverPythonDiagnostics()
+
+ registered_diagnostic_names = [
+ camel_case.ToUnderscore(name)
+ for name in all_diagnostics.GetDiagnosticTypenames()]
+
+ unregistered_diagnostics = (set(discovered_diagnostics.keys()) -
+ set(registered_diagnostic_names))
+
+ return_code = 0
+ if unregistered_diagnostics:
+ print ('These diagnostics are unregistered: %s. Please add them to '
+ 'tracing/tracing/value/diagnostics/all_diagnostics.py.' %
+ ', '.join(unregistered_diagnostics))
+ return_code = 1
+
+ for name in all_diagnostics.GetDiagnosticTypenames():
+ diagnostic = all_diagnostics.GetDiagnosticClassForName(name)
+ if name != diagnostic.__name__:
+ print 'This diagnostic refers to the wrong class: %s: %s' % (
+ name, diagnostic.__name__)
+ return_code = 1
+
+ return return_code
+
+
+def CheckJSDiagnostics():
+ project = tracing_project.TracingProject()
+ all_registered_diagnostics = DiscoverJSDiagnostics(
+ project, ['registry', '/tracing/value/diagnostics/all_diagnostics.html'])
+ all_modules = list(
+ '/' + rel_path for rel_path in
+ tracing_project.TracingProject().FindAllDiagnosticsModuleRelPaths())
+ all_possible_diagnostics = DiscoverJSDiagnostics(
+ project, ['namespace'] + all_modules)
+
+ unregistered_diagnostics = (all_possible_diagnostics -
+ all_registered_diagnostics)
+ if unregistered_diagnostics:
+ print ('These diagnostics are unregistered: %s. Please import their modules in '
+ 'tracing/tracing/value/diagnostics/all_diagnostics.html and '
+ 'ensure that they call Diagnostic.register().' %
+ ', '.join(unregistered_diagnostics))
+ return 1
+
+ lowercased_diagnostics = []
+ for m in all_possible_diagnostics:
+ if str.islower(m[0]):
+ lowercased_diagnostics.append(m)
+ if lowercased_diagnostics:
+ print ('These diagnostics must be renamed to start with a upper-case: %s' %
+ lowercased_diagnostics)
+ return 1
+
+ return 0
+
+
+def Main():
+ return (CheckJSDiagnostics() or CheckPythonDiagnostics())
+
+
+if __name__ == '__main__':
+ sys.exit(Main())
diff --git a/chromium/third_party/catapult/tracing/bin/validate_all_metrics b/chromium/third_party/catapult/tracing/bin/validate_all_metrics
new file mode 100755
index 00000000000..b4b26307bc1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/validate_all_metrics
@@ -0,0 +1,43 @@
+#!/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 argparse
+import json
+import os
+import string
+import sys
+
+sys.path.insert(
+ 1,
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
+from tracing.metrics import discover
+import tracing_project
+
+
+def Main():
+ all_registered_metrics = set(discover.DiscoverMetrics(
+ ['/tracing/metrics/all_metrics.html']))
+ all_modules = list(
+ '/' + rel_path for rel_path in
+ tracing_project.TracingProject().FindAllMetricsModuleRelPaths())
+ all_possible_metrics = set(discover.DiscoverMetrics(all_modules))
+ unregistered_metrics = all_possible_metrics - all_registered_metrics
+ if unregistered_metrics:
+ print ('These metrics are unregistered: %s. Please import their modules in '
+ 'tracing/tracing/metrics/all_metrics.html' %
+ ', '.join(unregistered_metrics))
+ return 1
+ uppercased_metrics = []
+ for m in all_possible_metrics:
+ if str.isupper(m[0]):
+ uppercased_metrics.append(m)
+ if uppercased_metrics:
+ print ('These metrics must be renamed to start with a lower-case: %s' %
+ uppercased_metrics)
+ return 1
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(Main())
diff --git a/chromium/third_party/catapult/tracing/bin/vulcanize_trace_viewer b/chromium/third_party/catapult/tracing/bin/vulcanize_trace_viewer
new file mode 100755
index 00000000000..c3602d40447
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/vulcanize_trace_viewer
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# Copyright (c) 2014 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
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ from tracing_build import vulcanize_trace_viewer
+ sys.exit(vulcanize_trace_viewer.Main(sys.argv))
diff --git a/chromium/third_party/catapult/tracing/bin/why_imported b/chromium/third_party/catapult/tracing/bin/why_imported
new file mode 100755
index 00000000000..9b5a59a6f49
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bin/why_imported
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# 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.
+
+"""Produces a dot file showing dependency relationships between modules.
+
+The dot file contains a text-based representation of a directed graph that
+explains why given module names were included in a trace_viewer config.
+
+Example usage:
+$ ./why_imported tracing.ui.analysis.analysis_view > ~/analysis_view.dot
+
+This can then be converted to a graphical representation with the dot tool:
+$ dot -Grankdir=LR -Tpng ~/analysis_view.dot -o ~/analysis_view.png
+"""
+
+import os
+import sys
+import argparse
+
+
+def Main():
+ project = tracing_project.TracingProject()
+
+ parser = argparse.ArgumentParser(
+ usage='%(prog)s <options> moduleNames', epilog=__doc__)
+ parser.add_argument('--config', choices=project.GetConfigNames())
+ parser.add_argument('module_names', nargs='+')
+ args = parser.parse_args()
+
+ if args.config:
+ names = [project.GetModuleNameForConfigName(options.config)]
+ vulcanizer = project.CreateVulcanizer()
+ load_sequence = vulcanizer.CalcLoadSequenceForModuleNames(names)
+ else:
+ parser.error('No config specified.')
+ print vulcanizer.GetDominatorGraphForModulesNamed(
+ args.module_names, load_sequence)
+
+
+if __name__ == '__main__':
+ tracing_path = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..'))
+ sys.path.append(tracing_path)
+ import tracing_project
+ tracing_project.UpdateSysPathIfNeeded()
+ sys.exit(Main())
diff --git a/chromium/third_party/catapult/tracing/bower.json b/chromium/third_party/catapult/tracing/bower.json
new file mode 100644
index 00000000000..429f8995649
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/bower.json
@@ -0,0 +1,22 @@
+{
+ "name": "trace-viewer",
+ "version": "0.0.0",
+ "homepage": "http://google.github.io/trace-viewer",
+ "authors": [
+ "Nat Duca <nduca@chromium.org>",
+ "dan sinclair <dsinclair@chromium.org>"
+ ],
+ "description": "TraceViewer for chrome://tracing and Android Systrace",
+ "license": "BSD",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "trace-viewer/components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "polymer": "Polymer/polymer#~0.5.5"
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/docs/coordinate-systems.md b/chromium/third_party/catapult/tracing/docs/coordinate-systems.md
new file mode 100644
index 00000000000..011fed4835e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/docs/coordinate-systems.md
@@ -0,0 +1,46 @@
+# Trace-Viewer Coordinate Systems.
+
+## Coordinate Systems
+
+To represent browser content in trace-viewer we need to draw boxes and
+pictures created in one browser in the DOM of another browser window.
+How does a pixel in the output relate to a pixel in the original browser view?
+
+### Scaling
+
+The snapshot view lives in a quad-stack-viewer DOM element. This is area of
+pixels in trace-viewer, for example 685x342 px.
+
+The quad-stack-viewer contains a view-container with a CSS transform. The
+transform will zoom (CSS scale), pan (CSS translateX, translateY),
+orient (CSS rotateX, rotateY) its contents, a canvas. Common scale factors
+will be 0.1 - 2.0. The transformation is controlled by user inputs.
+
+Internally the canvas has the _world_ coordinates.
+
+The _world_ coordinates completely enclose the boxes we may draw, plus some
+padding so the edges of boxes do not sit against the edge of the world. For
+example, padding space of .75 times the minimum of width and height may be
+added. Since the original browser has a few thousand pixels, the padded world
+may be 5-6000 pixels on a side.
+
+The _world_ coordinates are scaled by several factors:
+ * _quad_stack_scale_ adjusts the size of the canvas (eg 0.5).
+ * _devicePixelRatio_ adjusts for high-res devices (eg 1 or 2),
+ * _ui.RASTER_SCALE_, adjusts the size of the canvas. (eg 0.75)
+
+*Do we still need RASTER_SCALE?*
+
+### Translation (origins)
+
+The quad-stack-viewer DOM element is positioned by CSS at some offset in the
+document. All of our origins are relative to the top left corner of the
+quad-stack-viewer.
+
+The CSS transforms move us from the DOM coordinate system to the world system.
+*What is the origin of the canvas in the DOM coordinate system
+when the final size of the canvas is less than the element?*
+
+The _deviceViewportRect_ is the visible browser window in _world_ coordinates.
+Typically it will be at X,Y = 0,0. Thus the _world_ origin will be eg
+-0.75\*3000px , -0.75\*2500px, due to the world padding.
diff --git a/chromium/third_party/catapult/tracing/docs/embedding-trace-viewer.md b/chromium/third_party/catapult/tracing/docs/embedding-trace-viewer.md
new file mode 100644
index 00000000000..2f3778c0d8e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/docs/embedding-trace-viewer.md
@@ -0,0 +1,54 @@
+# Making standalone HTML files
+
+If you have a trace file that you want to turn into a html file with a viewer then:
+
+```
+sys.path.append(os.path.join(path_to_catapult, 'tracing'))
+from trace_viewer_build import trace2html
+with open('my_trace.html', 'w') as new_file:
+ trace2html.WriteHTMLForTracesToFile(['my_trace.json'], new_file)
+```
+
+This will produce a standalone trace viewer with my_trace packed inside.
+
+# Embedding the Easy Way
+Running `$CATAPULT/tracing/bin/vulcanize_trace_viewer` will create `$CATAPULT/tracing/bin/trace_viewer_full.html`. That file has all the js, css and html-templates that you need for a standalone trace viewer instance.
+
+In your code, `<link rel="import" href="trace_viewer_full.html">`. Then, to get a trace viewer up, you need to do two things: make the timeline viewer, and make a model and give it to the viewer:
+```
+ var container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ viewer = document.createElement('tr-ui-timeline-view');
+ viewer.track_view_container = container;
+ Polymer.dom(viewer).appendChild(container);
+
+ viewer.id = 'trace-viewer';
+ viewer.globalMode = true;
+ Polymer.dom(document.body).appendChild(viewer);
+```
+
+With the viewer created, you need to then make a TraceModel:
+```
+ var model = new tr.Model();
+ var i = new tr.importer.Import(m);
+ var p = i.importTracesWithProgressDialog([result]);
+ p.then(function() {
+ viewer.model = model;
+ }, onImportFail);
+
+```
+
+Model has a variety of import options, from synchronous import to importWithProgressDialog. And, it
+lets you customize the types of postprocessing to be done on the model before it is displayed by the view.
+
+# Configs
+Trace viewer has a lot of extra pieces, for domain-specific use cases. By default, trace2html and vulcanize take everything and combine them together. We call this the "full" config. Passing --help to
+vulcanize or trace2html will show the current set of configs we support, which maps to
+`trace_viewer/extras/*_config.html`. Some of the other configs are smaller, leading to a more compact redistributable.
+
+# Customizing
+For more information on how to customize and extend trace viewer, see [Extending-and-Customizing-Trace-Viewer](Extending-and-Customizing-Trace-Viewer)
+
+# Example
+See bin/index.html for an example of using the embedding system.
diff --git a/chromium/third_party/catapult/tracing/docs/extending-and-customizing-trace-viewer.md b/chromium/third_party/catapult/tracing/docs/extending-and-customizing-trace-viewer.md
new file mode 100644
index 00000000000..aa493ce17e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/docs/extending-and-customizing-trace-viewer.md
@@ -0,0 +1,26 @@
+Though there are some concepts hold the same across all trace formats we've encountered, there are an always plenty of domain-specific details to a given expertise area that defy standard treatment.
+
+In trace-viewer, we distinguish between "core" pieces, which are domain-neutral and belong in `trace_viewer/core` and domain-specific pieces, which we are in `trace_viewer/extras`. As such, core/ has a variety of extension points that then extras/ pulls in.
+
+# Importers
+TraceViewer is not tied to one specific trace file format: everyone has their own ideal way for getting performance data, storing it, and eventually getting it into the HTML file for viewing. And, since trace-viewer tries to be able to view traces from multiple systems all together, it may not even be possible to get traces into a single file format. Thats fine, as we see it.
+
+The main unit of extension here is the Importer object, `core/importer/importer.html`. To teach trace viewer about a new file format, subclass that importer, then hook it up to `default_importers.html`. Voila, you have the beginnings
+
+When you call TraceModel.import, you pass array of objects. We then run over this array one at a time, then walk through the registered importers looking for one that `.canHandle` that trace. Once it is found, we assign that trace to the importer.
+
+Because some trace formats are container formats, we support sub-traces, where an importer does a bit of processing, then yields another trace that needs more importing. This is, for instance, how we import gzip files.
+
+# Slice Views
+The display and storage of slices can be overridden based on their model-level name and category. This allows domain specific customization of that particular type of data. Some keywords to search for are SliceView.register and AsyncSlice.register.
+
+One way this is used is to customize the display title of a slice. In the trace files and the model, slices with the "net" category are traced with titles that correspond to their probe point. And, the URL of a request is just one of many events in the trace that is discovered quite late in the overall sequence of events. But, when viewing a network trace, the most interesting thing to see is the URL for which a traces corresponds. This transformation is accomplished by registering a custom net async slice, which overrides the `displayTitle` property: this leaves the model in-tact [e.g. exactly as it was traced] but improves on the display.
+
+# Object Views and Types
+In Chrome, some of our traces have a complex and massive JSON dump from our graphics subsystem that, when
+interpreted exactly the right way, let us reconstruct a view of the page just from the trace.
+
+There are two extension points that make this possible:
+- We allow subtypes to be registered for ObjectSnapshots and ObjectInstances. This way you can build up a domain-specific model of the trace instead of having to parse the trace yourself after the fact. See `extras/cc/layer_tree_host_impl.html` for an example.
+
+- We allow custom viewer objects to be registered for Snapshots and Instances. When a user clicks on one, we look for a viewer and use that object instead. See `extras/cc/layer_tree_host_impl_view.html` as an example.
diff --git a/chromium/third_party/catapult/tracing/docs/getting-started.md b/chromium/third_party/catapult/tracing/docs/getting-started.md
new file mode 100644
index 00000000000..472f2f54df6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/docs/getting-started.md
@@ -0,0 +1,21 @@
+Using Trace Viewer Casually
+==================================
+ * [Embedding-Trace-Viewer](https://github.com/catapult-project/catapult/blob/master/tracing/docs/embedding-trace-viewer.md) the trace-viewer in your own app.
+ * How to [extend and customize](https://github.com/catapult-project/catapult/blob/master/tracing/docs/extending-and-customizing-trace-viewer.md) the trace-viewer to suit your domain
+
+Making Traces
+=============
+ * [Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit?usp=sharing) if you want to generate traces yourself
+ * [py-trace-event](https://github.com/natduca/py_trace_event) for generating traces from python
+ * [Chrome's trace_event.h](http://src.chromium.org/chrome/trunk/src/base/debug/trace_event.h) if you're in Chrome's ecosystem
+ * [ftrace](https://www.kernel.org/doc/Documentation/trace/ftrace.txt) for generating traces on Linux
+
+Note: trace-viewer supports custom trace file formats. Just [add an importer](https://github.com/catapult-project/catapult/blob/master/tracing/docs/extending-and-customizing-trace-viewer.md) to trace viewer for your favorite file format.
+
+Contributing New Stuff
+======================
+ * Join our Google Groups: [trace-viewer](https://groups.google.com/forum/#!forum/trace-viewer), [trace-viewer-bugs](https://groups.google.com/forum/#!forum/trace-viewer-bugs)
+ * Learn how to start: [Contributing](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md)
+ * Read the [Trace Viewer style guide](https://docs.google.com/document/d/1MMOfywou2Oaho4jOttUk-ZSJcHVd5G5BTsD48rPrBtQ/edit)
+ * Pick a feature from the [tracing wish list](https://docs.google.com/a/chromium.org/document/d/1T1UJHIgImSEPSugCt2TFrkNsraBFITPHpYFGDJStePc/preview).
+ * Familiarize yourself with the [Trace-Viewer's-Internals](https://github.com/catapult-project/catapult/blob/master/tracing/docs/trace-viewer-internals.md).
diff --git a/chromium/third_party/catapult/tracing/docs/trace-viewer-internals.md b/chromium/third_party/catapult/tracing/docs/trace-viewer-internals.md
new file mode 100644
index 00000000000..fc85929cf79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/docs/trace-viewer-internals.md
@@ -0,0 +1,129 @@
+# TraceViewer’s Internals
+
+## Module system
+
+ * Tracing currently uses html imports for modules.
+ * We aspire to one-class-per file. Feel free to break up big files as you
+encounter them, they exist purely for legacy reasons.
+
+## Tests
+
+ * See unittest.html -- mostly compatible with closure tests
+ * See [[/docs/dev-server-tests.md]] for more information
+
+## Components
+
+ * New UI elements should be Polymer components.
+ * You will see some old references to tvcm.ui.define('x'). This is our old
+approach for building components. Its like polymer, in that you can subclass
+the element, but it doesn't use shadow dom or have any templating or data
+binding.
+
+## Rough module breakdown
+
+ * *Importers:* load files, produce a model
+ * *Model:* stateless, just the data that came from different trace formats
+ * *TimelineTrackView:* shows the data in gantt-chart form
+ * *Tracks:* visualize a particular part of the model
+ * *Selection:* a vector of things in the tracks that the user has selected (counter samples, slices)
+ * *Analysis:* provides summary of selection
+ * *TimelineView:* glues everything together
+ * *ProfilingView:* chrome-specific UI and glue
+
+## Importer notes
+
+ * The importer to model abstraction is meant to allow us to support multiple trace formats
+
+## Model notes
+
+ * The most important concept in the model is a slice. A slice is a range of time, and some metadata about that range, e.g. title, arguments, etc.
+ * Model has
+ * Processes
+ * Counters
+ * Counter samples (at ts=0.2s, we had 10mb allocated and 3mb free)
+ * Threads
+ * Slices (the FFT::compute function ran from 0.7s to 0.9s)
+ * AsyncSlices (at 0.2s we started a file read in the background and it finished at 0.5s)
+ * CpuSlices (at ts=0.2s we were running on cpu2)
+ * CPUs
+ * Slices (at ts=0.2 to 0.4 we were running "top")
+ * Counters (the clock frequency was 1.2ghz at ts=0.1s)
+
+## Slice
+A slice is something which consumes time synchronously on a CPU or a thread. The
+canonical example of this would be a B/E event pair. An async operation is also
+considered a slice. Things get a bit more murky when looking at instant events.
+A thread scoped instant event is a duration 0 slice. Other instant events,
+process or global scoped, don't correlate to a thread or CPU and aren't
+considered slices.
+
+A flow event, on the other hand, is not a slice. It doesn't consume time and is,
+conceptually, a graph of data flow in the system.
+
+
+## Slice groups
+
+ * When you see the tracing UI, you see lots of things like this:
+
+```
+Thread 7:     [  a     ]   [    c   ]
+                        [ b ]
+```
+
+ * This of visualization starts as a *flat* array of slices:
+
+```
+ [{title: “a”, start: 0, end: 1), {title: “c”, start: 1.5, end: 3.5}, {title: “b”, start: 0.5, end: 0.8}]
+```
+
+ * We call this a slice group. A slice group can be composed into subRows -- a subRow is an array of slices that are all non-overlapping. E.g. in the thread7 example above, there are two subrows:
+
+```
+subrow 1:     [  a     ]   [    c   ]
+subrow 2:     [ b ]
+```
+
+ * The SliceTrack is built around the idea of visualizing a single subrow. So when you see a thread like thread 7, you’re really looking at 2 !SliceTracks, each of which has its own subrow.
+
+ * We have two slice group types:
+ * SliceGroup, for nested data. Used for threads.
+ * e.g.  like ( (0,2), (0.1,0.3) )
+ * We convert the slices into subrows based on containment.
+ * b is considered contained by a if b.start >= a.start && b.end <= a.end
+ * AsyncSliceGroup, for overlapping data. Used for async operations.
+ * e.g. ( (0, 2), (1, 3) )
+ * We convert the slices into subrows by greedily packing them into rows, adding rows as needed when there’s no room on an existing subrow
+
+## Timeline notes
+
+ * Timeline is just an array of tracks. A track is one of the rows in the UI. A single thread of data may turn into 5+ tracks, one track for each row of squares.
+ * The core of the Timeline is Slice
+ * Panning/zooming state is on the TimelineViewport, as is the grid and user defined markers
+
+## Tracks
+### there are three broad types of tracks
+
+ * Building blocks
+ * *Container track*
+ * A track that is itself made up of more tracks. Just a div plus logic to delegate overall track interface calls down to its children.
+ * *CanvasBasedTrack*
+ * A track that renders its content using HTML5 canvas
+ * Visualizations
+ * *SliceTrack:* visualizes an array of non-overlapping monotonically-increasing slices. Has some simple but critical logic to efficiently render even with thousands (or more) slices by merging small slices together when they really close together.
+ * *CounterTrack:* visualizes an array of samples values over time. Has support for stacked area charts. Tries to merge samples together when they are not perceptually significant to reduce canvas drawing overhead.
+ * *Model tracks:* e.g. ThreadTrack
+ * Derives from a container track, takes a timeline model object, e.g. a thread, and creates the appropriate child tracks that visualize that thread
+
+## Selection notes
+
+ * When you drag to select, that creates a selection object by asking every track to append things that intersect the dragged-box to the selection
+ * A selection object is an array of hits.
+ * A hit is the pairing of the track-level entity that was selected with the model-level entity that that visual thing represents.
+ * Eg a thread has a bunch of slices in it. That gets turned into a bunch of subrows that we then turn into !SliceTracks. When you click on a slice in a thread in the UI, the hit is {slice: <the slice you clicked>, thread: <the thread it came from>}.
+ * The hit concept exists partly because slices can’t know their parent (see model section for why). Yet, analysis code want to know parentage in order to do things like group-by-thread.
+
+## Analysis code
+
+ * Takes as input a selection
+ * Does the numeric analysis and dumps the numeric results to a builder
+ * The builder is responsible for creating HTML (or any other textual representation of the results)
diff --git a/chromium/third_party/catapult/tracing/images/third-trace-viewer-circle-blue.png b/chromium/third_party/catapult/tracing/images/third-trace-viewer-circle-blue.png
new file mode 100644
index 00000000000..46ceef1f8f6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/third-trace-viewer-circle-blue.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-circle-blue.png b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-blue.png
new file mode 100644
index 00000000000..0eb12eca4e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-blue.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-circle-green.png b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-green.png
new file mode 100644
index 00000000000..e6ba7610903
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-green.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-circle-red.png b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-red.png
new file mode 100644
index 00000000000..915d70649a1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-red.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-circle-yellow.png b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-yellow.png
new file mode 100644
index 00000000000..a805cc1b4d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-circle-yellow.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-square-blue.png b/chromium/third_party/catapult/tracing/images/trace-viewer-square-blue.png
new file mode 100644
index 00000000000..441f684bb04
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-square-blue.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-square-green.png b/chromium/third_party/catapult/tracing/images/trace-viewer-square-green.png
new file mode 100644
index 00000000000..a4da2ef051f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-square-green.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-square-red.png b/chromium/third_party/catapult/tracing/images/trace-viewer-square-red.png
new file mode 100644
index 00000000000..fc8e7fe71f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-square-red.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/images/trace-viewer-square-yellow.png b/chromium/third_party/catapult/tracing/images/trace-viewer-square-yellow.png
new file mode 100644
index 00000000000..7216975bef6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/images/trace-viewer-square-yellow.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/package.json b/chromium/third_party/catapult/tracing/package.json
new file mode 100644
index 00000000000..7153679fd2e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "traceviewer",
+ "version": "1.0.5",
+ "description": "Trace-Viewer is the javascript frontend for Chrome about:tracing and Android systrace.",
+ "main": "tracing/index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/catapult-project/catapult/tree/master/tracing"
+ },
+ "keywords": [
+ "tracing",
+ "traceviewer",
+ "trace",
+ "events"
+ ],
+ "author": "The Chromium Authors",
+ "license": "BSD-2-Clause",
+ "gypfile": false
+}
diff --git a/chromium/third_party/catapult/tracing/skp_data/google_homepage.skp b/chromium/third_party/catapult/tracing/skp_data/google_homepage.skp
new file mode 100644
index 00000000000..52d34bf5d63
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/skp_data/google_homepage.skp
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/skp_data/lthi_cats.skp b/chromium/third_party/catapult/tracing/skp_data/lthi_cats.skp
new file mode 100644
index 00000000000..02a2ea50203
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/skp_data/lthi_cats.skp
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/third_party/chai/LICENSE b/chromium/third_party/catapult/tracing/third_party/chai/LICENSE
new file mode 100644
index 00000000000..227b598fc61
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/chai/LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) 2011-2015 Jake Luer jake@alogicalparadox.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/chromium/third_party/catapult/tracing/third_party/chai/README.chromium b/chromium/third_party/catapult/tracing/third_party/chai/README.chromium
new file mode 100644
index 00000000000..4a1fb8e791a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/chai/README.chromium
@@ -0,0 +1,15 @@
+Name: chai
+Short Name: chai
+URL: https://github.com/chaijs/chai
+Version: 2.1.2
+Revision: 1abc3845e3ef96afa003d62478692a1cb9a435b0
+Date: Fri Mar 20 09:46:02 2015 +0000
+License: MIT
+License File: NOT_SHIPPED
+Security Critical: no
+
+Description:
+BDD / TDD assertion framework for node.js and the browser that can be paired with any testing framework.
+
+Local Modifications:
+Copied license text out of README.md. Unminified chai.js and ripped out parts pertaining to Buffer because we're not using node and don't want could-not-require exceptions to be thrown. Reminified to prevent jslint from complaining.
diff --git a/chromium/third_party/catapult/tracing/third_party/chai/chai.js b/chromium/third_party/catapult/tracing/third_party/chai/chai.js
new file mode 100644
index 00000000000..5956853750b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/chai/chai.js
@@ -0,0 +1,2 @@
+!function(){function require(e){var t=require.modules[e];if(t)return"exports"in t||"function"!=typeof t.definition||(t.client=t.component=!0,t.definition.call(this,t.exports={},t),delete t.definition),t.exports}require.loader="component",require.helper={},require.helper.semVerSort=function(e,t){for(var r=e.version.split("."),i=t.version.split("."),n=0;n<r.length;++n){var o=parseInt(r[n],10),s=parseInt(i[n],10);if(o!==s)return o>s?1:-1;var a=r[n].substr((""+o).length),c=i[n].substr((""+s).length);if(""===a&&""!==c)return 1;if(""!==a&&""===c)return-1;if(""!==a&&""!==c)return a>c?1:-1}return 0},require.latest=function(e,t){function r(e){throw new Error('failed to find latest module of "'+e+'"')}var i=/(.*)~(.*)@v?(\d+\.\d+\.\d+[^\/]*)$/,n=/(.*)~(.*)/;n.test(e)||r(e);for(var o=Object.keys(require.modules),s=[],a=[],c=0;c<o.length;c++){var u=o[c];if(new RegExp(e+"@").test(u)){var h=u.substr(e.length+1),l=i.exec(u);null!=l?s.push({version:h,name:u}):a.push({version:h,name:u})}}if(0===s.concat(a).length&&r(e),s.length>0){var f=s.sort(require.helper.semVerSort).pop().name;return t===!0?f:require(f)}var f=a.sort(function(e,t){return e.name>t.name})[0].name;return t===!0?f:require(f)},require.modules={},require.register=function(e,t){require.modules[e]={definition:t}},require.define=function(e,t){require.modules[e]={exports:t}},require.register("chaijs~assertion-error@1.0.0",function(e,t){function r(){function e(e,r){Object.keys(r).forEach(function(i){~t.indexOf(i)||(e[i]=r[i])})}var t=[].slice.call(arguments);return function(){for(var t=[].slice.call(arguments),r=0,i={};r<t.length;r++)e(i,t[r]);return i}}function i(e,t,i){var n=r("name","message","stack","constructor","toJSON"),o=n(t||{});this.message=e||"Unspecified AssertionError",this.showDiff=!1;for(var s in o)this[s]=o[s];i=i||arguments.callee,i&&Error.captureStackTrace&&Error.captureStackTrace(this,i)}t.exports=i,i.prototype=Object.create(Error.prototype),i.prototype.name="AssertionError",i.prototype.constructor=i,i.prototype.toJSON=function(e){var t=r("constructor","toJSON","stack"),i=t({name:this.name},this);return!1!==e&&this.stack&&(i.stack=this.stack),i}}),require.register("chaijs~type-detect@0.1.1",function(e,t){function r(e){var t=Object.prototype.toString.call(e);return n[t]?n[t]:null===e?"null":void 0===e?"undefined":e===Object(e)?"object":typeof e}function i(){this.tests={}}var e=t.exports=r,n={"[object Array]":"array","[object RegExp]":"regexp","[object Function]":"function","[object Arguments]":"arguments","[object Date]":"date"};e.Library=i,i.prototype.of=r,i.prototype.define=function(e,t){return 1===arguments.length?this.tests[e]:(this.tests[e]=t,this)},i.prototype.test=function(e,t){if(t===r(e))return!0;var i=this.tests[t];if(i&&"regexp"===r(i))return i.test(e);if(i&&"function"===r(i))return i(e);throw new ReferenceError('Type test "'+t+'" not defined or invalid.')}}),require.register("chaijs~deep-eql@0.1.3",function(e,t){function r(e,t,r){return i(e,t)?!0:"date"===f(e)?o(e,t):"regexp"===f(e)?s(e,t):"arguments"===f(e)?a(e,t,r):n(e,t)?"object"!==f(e)&&"object"!==f(t)&&"array"!==f(e)&&"array"!==f(t)?i(e,t):l(e,t,r):!1}function i(e,t){return e===t?0!==e||1/e===1/t:e!==e&&t!==t}function n(e,t){return f(e)===f(t)}function o(e,t){return"date"!==f(t)?!1:i(e.getTime(),t.getTime())}function s(e,t){return"regexp"!==f(t)?!1:i(e.toString(),t.toString())}function a(e,t,i){return"arguments"!==f(t)?!1:(e=[].slice.call(e),t=[].slice.call(t),r(e,t,i))}function c(e){var t=[];for(var r in e)t.push(r);return t}function u(e,t){if(e.length!==t.length)return!1;for(var r=0,i=!0;r<e.length;r++)if(e[r]!==t[r]){i=!1;break}return i}function h(e){return null!==e&&void 0!==e}function l(e,t,i){if(!h(e)||!h(t))return!1;if(e.prototype!==t.prototype)return!1;var n;if(i){for(n=0;n<i.length;n++)if(i[n][0]===e&&i[n][1]===t||i[n][0]===t&&i[n][1]===e)return!0}else i=[];try{var o=c(e),s=c(t)}catch(a){return!1}if(o.sort(),s.sort(),!u(o,s))return!1;i.push([e,t]);var l;for(n=o.length-1;n>=0;n--)if(l=o[n],!r(e[l],t[l],i))return!1;return!0}var f=require("chaijs~type-detect@0.1.1");t.exports=r}),require.register("chai",function(e,t){t.exports=require("chai/lib/chai.js")}),require.register("chai/lib/chai.js",function(e,t){var r=[],e=t.exports={};e.version="2.1.0",e.AssertionError=require("chaijs~assertion-error@1.0.0");var i=require("chai/lib/chai/utils/index.js");e.use=function(e){return~r.indexOf(e)||(e(this,i),r.push(e)),this},e.util=i;var n=require("chai/lib/chai/config.js");e.config=n;var o=require("chai/lib/chai/assertion.js");e.use(o);var s=require("chai/lib/chai/core/assertions.js");e.use(s);var a=require("chai/lib/chai/interface/expect.js");e.use(a);var c=require("chai/lib/chai/interface/should.js");e.use(c);var u=require("chai/lib/chai/interface/assert.js");e.use(u)}),require.register("chai/lib/chai/assertion.js",function(e,t){var r=require("chai/lib/chai/config.js");t.exports=function(e,t){function i(e,t,r){o(this,"ssfi",r||arguments.callee),o(this,"object",e),o(this,"message",t)}var n=e.AssertionError,o=t.flag;e.Assertion=i,Object.defineProperty(i,"includeStack",{get:function(){return console.warn("Assertion.includeStack is deprecated, use chai.config.includeStack instead."),r.includeStack},set:function(e){console.warn("Assertion.includeStack is deprecated, use chai.config.includeStack instead."),r.includeStack=e}}),Object.defineProperty(i,"showDiff",{get:function(){return console.warn("Assertion.showDiff is deprecated, use chai.config.showDiff instead."),r.showDiff},set:function(e){console.warn("Assertion.showDiff is deprecated, use chai.config.showDiff instead."),r.showDiff=e}}),i.addProperty=function(e,r){t.addProperty(this.prototype,e,r)},i.addMethod=function(e,r){t.addMethod(this.prototype,e,r)},i.addChainableMethod=function(e,r,i){t.addChainableMethod(this.prototype,e,r,i)},i.overwriteProperty=function(e,r){t.overwriteProperty(this.prototype,e,r)},i.overwriteMethod=function(e,r){t.overwriteMethod(this.prototype,e,r)},i.overwriteChainableMethod=function(e,r,i){t.overwriteChainableMethod(this.prototype,e,r,i)},i.prototype.assert=function(e,i,s,a,c,u){var h=t.test(this,arguments);if(!0!==u&&(u=!1),!0!==r.showDiff&&(u=!1),!h){var i=t.getMessage(this,arguments),l=t.getActual(this,arguments);throw new n(i,{actual:l,expected:a,showDiff:u},r.includeStack?this.assert:o(this,"ssfi"))}},Object.defineProperty(i.prototype,"_obj",{get:function(){return o(this,"object")},set:function(e){o(this,"object",e)}})}}),require.register("chai/lib/chai/config.js",function(e,t){t.exports={includeStack:!1,showDiff:!0,truncateThreshold:40}}),require.register("chai/lib/chai/core/assertions.js",function(e,t){t.exports=function(e,t){function r(e,r){r&&q(this,"message",r),e=e.toLowerCase();var i=q(this,"object"),n=~["a","e","i","o","u"].indexOf(e.charAt(0))?"an ":"a ";this.assert(e===t.type(i),"expected #{this} to be "+n+e,"expected #{this} not to be "+n+e)}function i(){q(this,"contains",!0)}function n(e,r){r&&q(this,"message",r);var i=q(this,"object"),n=!1;if("array"===t.type(i)&&"object"===t.type(e)){for(var o in i)if(t.eql(i[o],e)){n=!0;break}}else if("object"===t.type(e)){if(!q(this,"negate")){for(var s in e)new m(i).property(s,e[s]);return}var a={};for(var s in e)a[s]=i[s];n=t.eql(a,e)}else n=i&&~i.indexOf(e);this.assert(n,"expected #{this} to include "+t.inspect(e),"expected #{this} to not include "+t.inspect(e))}function o(){var e=q(this,"object"),t=Object.prototype.toString.call(e);this.assert("[object Arguments]"===t,"expected #{this} to be arguments but got "+t,"expected #{this} to not be arguments")}function s(e,t){t&&q(this,"message",t);var r=q(this,"object");return q(this,"deep")?this.eql(e):void this.assert(e===r,"expected #{this} to equal #{exp}","expected #{this} to not equal #{exp}",e,this._obj,!0)}function a(e,r){r&&q(this,"message",r),this.assert(t.eql(e,q(this,"object")),"expected #{this} to deeply equal #{exp}","expected #{this} to not deeply equal #{exp}",e,this._obj,!0)}function c(e,t){t&&q(this,"message",t);var r=q(this,"object");if(q(this,"doLength")){new m(r,t).to.have.property("length");var i=r.length;this.assert(i>e,"expected #{this} to have a length above #{exp} but got #{act}","expected #{this} to not have a length above #{exp}",e,i)}else this.assert(r>e,"expected #{this} to be above "+e,"expected #{this} to be at most "+e)}function u(e,t){t&&q(this,"message",t);var r=q(this,"object");if(q(this,"doLength")){new m(r,t).to.have.property("length");var i=r.length;this.assert(i>=e,"expected #{this} to have a length at least #{exp} but got #{act}","expected #{this} to have a length below #{exp}",e,i)}else this.assert(r>=e,"expected #{this} to be at least "+e,"expected #{this} to be below "+e)}function h(e,t){t&&q(this,"message",t);var r=q(this,"object");if(q(this,"doLength")){new m(r,t).to.have.property("length");var i=r.length;this.assert(e>i,"expected #{this} to have a length below #{exp} but got #{act}","expected #{this} to not have a length below #{exp}",e,i)}else this.assert(e>r,"expected #{this} to be below "+e,"expected #{this} to be at least "+e)}function l(e,t){t&&q(this,"message",t);var r=q(this,"object");if(q(this,"doLength")){new m(r,t).to.have.property("length");var i=r.length;this.assert(e>=i,"expected #{this} to have a length at most #{exp} but got #{act}","expected #{this} to have a length above #{exp}",e,i)}else this.assert(e>=r,"expected #{this} to be at most "+e,"expected #{this} to be above "+e)}function f(e,r){r&&q(this,"message",r);var i=t.getName(e);this.assert(q(this,"object")instanceof e,"expected #{this} to be an instance of "+i,"expected #{this} to not be an instance of "+i)}function p(e,r){r&&q(this,"message",r);var i=q(this,"object");this.assert(i.hasOwnProperty(e),"expected #{this} to have own property "+t.inspect(e),"expected #{this} to not have own property "+t.inspect(e))}function d(){q(this,"doLength",!0)}function g(e,t){t&&q(this,"message",t);var r=q(this,"object");new m(r,t).to.have.property("length");var i=r.length;this.assert(i==e,"expected #{this} to have a length of #{exp} but got #{act}","expected #{this} to not have a length of #{act}",e,i)}function b(e){var r,i=q(this,"object"),n=!0,o="keys must be given single argument of Array|Object|String, or multiple String arguments";switch(t.type(e)){case"array":if(arguments.length>1)throw new Error(o);break;case"object":if(arguments.length>1)throw new Error(o);e=Object.keys(e);break;default:e=Array.prototype.slice.call(arguments)}if(!e.length)throw new Error("keys required");var s=Object.keys(i),a=e,c=e.length,u=q(this,"any"),h=q(this,"all");if(u||h||(h=!0),u){var l=a.filter(function(e){return~s.indexOf(e)});n=l.length>0}if(h&&(n=e.every(function(e){return~s.indexOf(e)}),q(this,"negate")||q(this,"contains")||(n=n&&e.length==s.length)),c>1){e=e.map(function(e){return t.inspect(e)});var f=e.pop();h&&(r=e.join(", ")+", and "+f),u&&(r=e.join(", ")+", or "+f)}else r=t.inspect(e[0]);r=(c>1?"keys ":"key ")+r,r=(q(this,"contains")?"contain ":"have ")+r,this.assert(n,"expected #{this} to "+r,"expected #{this} to not "+r,a.slice(0).sort(),s.sort(),!0)}function v(e,r,i){i&&q(this,"message",i);var n=q(this,"object");new m(n,i).is.a("function");var o=!1,s=null,a=null,c=null;0===arguments.length?(r=null,e=null):e&&(e instanceof RegExp||"string"==typeof e)?(r=e,e=null):e&&e instanceof Error?(s=e,e=null,r=null):"function"==typeof e?(a=e.prototype.name||e.name,"Error"===a&&e!==Error&&(a=(new e).name)):e=null;try{n()}catch(u){if(s)return this.assert(u===s,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp}",s instanceof Error?s.toString():s,u instanceof Error?u.toString():u),q(this,"object",u),this;if(e&&(this.assert(u instanceof e,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp} but #{act} was thrown",a,u instanceof Error?u.toString():u),!r))return q(this,"object",u),this;var h="object"===t.type(u)&&"message"in u?u.message:""+u;if(null!=h&&r&&r instanceof RegExp)return this.assert(r.exec(h),"expected #{this} to throw error matching #{exp} but got #{act}","expected #{this} to throw error not matching #{exp}",r,h),q(this,"object",u),this;if(null!=h&&r&&"string"==typeof r)return this.assert(~h.indexOf(r),"expected #{this} to throw error including #{exp} but got #{act}","expected #{this} to throw error not including #{act}",r,h),q(this,"object",u),this;o=!0,c=u}var l="",f=null!==a?a:s?"#{exp}":"an error";o&&(l=" but #{act} was thrown"),this.assert(o===!0,"expected #{this} to throw "+f+l,"expected #{this} to not throw "+f+l,s instanceof Error?s.toString():s,c instanceof Error?c.toString():c),q(this,"object",c)}function y(e,t,r){return e.every(function(e){return r?t.some(function(t){return r(e,t)}):-1!==t.indexOf(e)})}function j(e,t,r){r&&q(this,"message",r);var i=q(this,"object");new m(e,r).to.have.property(t),new m(i).is.a("function");var n=e[t];i(),this.assert(n!==e[t],"expected ."+t+" to change","expected ."+t+" to not change")}function w(e,t,r){r&&q(this,"message",r);var i=q(this,"object");new m(e,r).to.have.property(t),new m(i).is.a("function");var n=e[t];i(),this.assert(e[t]-n>0,"expected ."+t+" to increase","expected ."+t+" to not increase")}function x(e,t,r){r&&q(this,"message",r);var i=q(this,"object");new m(e,r).to.have.property(t),new m(i).is.a("function");var n=e[t];i(),this.assert(e[t]-n<0,"expected ."+t+" to decrease","expected ."+t+" to not decrease")}var m=e.Assertion,q=(Object.prototype.toString,t.flag);["to","be","been","is","and","has","have","with","that","which","at","of","same"].forEach(function(e){m.addProperty(e,function(){return this})}),m.addProperty("not",function(){q(this,"negate",!0)}),m.addProperty("deep",function(){q(this,"deep",!0)}),m.addProperty("any",function(){q(this,"any",!0),q(this,"all",!1)}),m.addProperty("all",function(){q(this,"all",!0),q(this,"any",!1)}),m.addChainableMethod("an",r),m.addChainableMethod("a",r),m.addChainableMethod("include",n,i),m.addChainableMethod("contain",n,i),m.addChainableMethod("contains",n,i),m.addChainableMethod("includes",n,i),m.addProperty("ok",function(){this.assert(q(this,"object"),"expected #{this} to be truthy","expected #{this} to be falsy")}),m.addProperty("true",function(){this.assert(!0===q(this,"object"),"expected #{this} to be true","expected #{this} to be false",this.negate?!1:!0)}),m.addProperty("false",function(){this.assert(!1===q(this,"object"),"expected #{this} to be false","expected #{this} to be true",this.negate?!0:!1)}),m.addProperty("null",function(){this.assert(null===q(this,"object"),"expected #{this} to be null","expected #{this} not to be null")}),m.addProperty("undefined",function(){this.assert(void 0===q(this,"object"),"expected #{this} to be undefined","expected #{this} not to be undefined")}),m.addProperty("exist",function(){this.assert(null!=q(this,"object"),"expected #{this} to exist","expected #{this} to not exist")}),m.addProperty("empty",function(){var e=q(this,"object"),t=e;Array.isArray(e)||"string"==typeof object?t=e.length:"object"==typeof e&&(t=Object.keys(e).length),this.assert(!t,"expected #{this} to be empty","expected #{this} not to be empty")}),m.addProperty("arguments",o),m.addProperty("Arguments",o),m.addMethod("equal",s),m.addMethod("equals",s),m.addMethod("eq",s),m.addMethod("eql",a),m.addMethod("eqls",a),m.addMethod("above",c),m.addMethod("gt",c),m.addMethod("greaterThan",c),m.addMethod("least",u),m.addMethod("gte",u),m.addMethod("below",h),m.addMethod("lt",h),m.addMethod("lessThan",h),m.addMethod("most",l),m.addMethod("lte",l),m.addMethod("within",function(e,t,r){r&&q(this,"message",r);var i=q(this,"object"),n=e+".."+t;if(q(this,"doLength")){new m(i,r).to.have.property("length");var o=i.length;this.assert(o>=e&&t>=o,"expected #{this} to have a length within "+n,"expected #{this} to not have a length within "+n)}else this.assert(i>=e&&t>=i,"expected #{this} to be within "+n,"expected #{this} to not be within "+n)}),m.addMethod("instanceof",f),m.addMethod("instanceOf",f),m.addMethod("property",function(e,r,i){i&&q(this,"message",i);var n=!!q(this,"deep"),o=n?"deep property ":"property ",s=q(this,"negate"),a=q(this,"object"),c=n?t.getPathInfo(e,a):null,u=n?c.exists:t.hasProperty(e,a),h=n?c.value:a[e];if(s&&void 0!==r){if(void 0===h)throw i=null!=i?i+": ":"",new Error(i+t.inspect(a)+" has no "+o+t.inspect(e))}else this.assert(u,"expected #{this} to have a "+o+t.inspect(e),"expected #{this} to not have "+o+t.inspect(e));void 0!==r&&this.assert(r===h,"expected #{this} to have a "+o+t.inspect(e)+" of #{exp}, but got #{act}","expected #{this} to not have a "+o+t.inspect(e)+" of #{act}",r,h),q(this,"object",h)}),m.addMethod("ownProperty",p),m.addMethod("haveOwnProperty",p),m.addChainableMethod("length",g,d),m.addMethod("lengthOf",g),m.addMethod("match",function(e,t){t&&q(this,"message",t);var r=q(this,"object");this.assert(e.exec(r),"expected #{this} to match "+e,"expected #{this} not to match "+e)}),m.addMethod("string",function(e,r){r&&q(this,"message",r);var i=q(this,"object");new m(i,r).is.a("string"),this.assert(~i.indexOf(e),"expected #{this} to contain "+t.inspect(e),"expected #{this} to not contain "+t.inspect(e))}),m.addMethod("keys",b),m.addMethod("key",b),m.addMethod("throw",v),m.addMethod("throws",v),m.addMethod("Throw",v),m.addMethod("respondTo",function(e,r){r&&q(this,"message",r);var i=q(this,"object"),n=q(this,"itself"),o="function"!==t.type(i)||n?i[e]:i.prototype[e];this.assert("function"==typeof o,"expected #{this} to respond to "+t.inspect(e),"expected #{this} to not respond to "+t.inspect(e))}),m.addProperty("itself",function(){q(this,"itself",!0)}),m.addMethod("satisfy",function(e,r){r&&q(this,"message",r);var i=q(this,"object"),n=e(i);this.assert(n,"expected #{this} to satisfy "+t.objDisplay(e),"expected #{this} to not satisfy"+t.objDisplay(e),this.negate?!1:!0,n)}),m.addMethod("closeTo",function(e,r,i){i&&q(this,"message",i);var n=q(this,"object");if(new m(n,i).is.a("number"),"number"!==t.type(e)||"number"!==t.type(r))throw new Error("the arguments to closeTo must be numbers");this.assert(Math.abs(n-e)<=r,"expected #{this} to be close to "+e+" +/- "+r,"expected #{this} not to be close to "+e+" +/- "+r)}),m.addMethod("members",function(e,r){r&&q(this,"message",r);var i=q(this,"object");new m(i).to.be.an("array"),new m(e).to.be.an("array");var n=q(this,"deep")?t.eql:void 0;return q(this,"contains")?this.assert(y(e,i,n),"expected #{this} to be a superset of #{act}","expected #{this} to not be a superset of #{act}",i,e):void this.assert(y(i,e,n)&&y(e,i,n),"expected #{this} to have the same members as #{act}","expected #{this} to not have the same members as #{act}",i,e)}),m.addChainableMethod("change",j),m.addChainableMethod("changes",j),m.addChainableMethod("increase",w),m.addChainableMethod("increases",w),m.addChainableMethod("decrease",x),m.addChainableMethod("decreases",x)}}),require.register("chai/lib/chai/interface/assert.js",function(exports,module){module.exports=function(chai,util){var Assertion=chai.Assertion,flag=util.flag,assert=chai.assert=function(e,t){var r=new Assertion(null,null,chai.assert);r.assert(e,t,"[ negation message unavailable ]")};assert.fail=function(e,t,r,i){throw r=r||"assert.fail()",new chai.AssertionError(r,{actual:e,expected:t,operator:i},assert.fail)},assert.ok=function(e,t){new Assertion(e,t).is.ok},assert.notOk=function(e,t){new Assertion(e,t).is.not.ok},assert.equal=function(e,t,r){var i=new Assertion(e,r,assert.equal);i.assert(t==flag(i,"object"),"expected #{this} to equal #{exp}","expected #{this} to not equal #{act}",t,e)},assert.notEqual=function(e,t,r){var i=new Assertion(e,r,assert.notEqual);i.assert(t!=flag(i,"object"),"expected #{this} to not equal #{exp}","expected #{this} to equal #{act}",t,e)},assert.strictEqual=function(e,t,r){new Assertion(e,r).to.equal(t)},assert.notStrictEqual=function(e,t,r){new Assertion(e,r).to.not.equal(t)},assert.deepEqual=function(e,t,r){new Assertion(e,r).to.eql(t)},assert.notDeepEqual=function(e,t,r){new Assertion(e,r).to.not.eql(t)},assert.isAbove=function(e,t,r){new Assertion(e,r).to.be.above(t)},assert.isBelow=function(e,t,r){new Assertion(e,r).to.be.below(t)},assert.isTrue=function(e,t){new Assertion(e,t).is["true"]},assert.isFalse=function(e,t){new Assertion(e,t).is["false"]},assert.isNull=function(e,t){new Assertion(e,t).to.equal(null)},assert.isNotNull=function(e,t){new Assertion(e,t).to.not.equal(null)},assert.isUndefined=function(e,t){new Assertion(e,t).to.equal(void 0)},assert.isDefined=function(e,t){new Assertion(e,t).to.not.equal(void 0)},assert.isFunction=function(e,t){new Assertion(e,t).to.be.a("function")},assert.isNotFunction=function(e,t){new Assertion(e,t).to.not.be.a("function")},assert.isObject=function(e,t){new Assertion(e,t).to.be.a("object")},assert.isNotObject=function(e,t){new Assertion(e,t).to.not.be.a("object")},assert.isArray=function(e,t){new Assertion(e,t).to.be.an("array")},assert.isNotArray=function(e,t){new Assertion(e,t).to.not.be.an("array")},assert.isString=function(e,t){new Assertion(e,t).to.be.a("string")},assert.isNotString=function(e,t){new Assertion(e,t).to.not.be.a("string")},assert.isNumber=function(e,t){new Assertion(e,t).to.be.a("number")},assert.isNotNumber=function(e,t){new Assertion(e,t).to.not.be.a("number")},assert.isBoolean=function(e,t){new Assertion(e,t).to.be.a("boolean")},assert.isNotBoolean=function(e,t){new Assertion(e,t).to.not.be.a("boolean")},assert.typeOf=function(e,t,r){new Assertion(e,r).to.be.a(t)},assert.notTypeOf=function(e,t,r){new Assertion(e,r).to.not.be.a(t)},assert.instanceOf=function(e,t,r){new Assertion(e,r).to.be.instanceOf(t)},assert.notInstanceOf=function(e,t,r){new Assertion(e,r).to.not.be.instanceOf(t)},assert.include=function(e,t,r){new Assertion(e,r,assert.include).include(t)},assert.notInclude=function(e,t,r){new Assertion(e,r,assert.notInclude).not.include(t)},assert.match=function(e,t,r){new Assertion(e,r).to.match(t)},assert.notMatch=function(e,t,r){new Assertion(e,r).to.not.match(t)},assert.property=function(e,t,r){new Assertion(e,r).to.have.property(t)},assert.notProperty=function(e,t,r){new Assertion(e,r).to.not.have.property(t)},assert.deepProperty=function(e,t,r){new Assertion(e,r).to.have.deep.property(t)},assert.notDeepProperty=function(e,t,r){new Assertion(e,r).to.not.have.deep.property(t)},assert.propertyVal=function(e,t,r,i){new Assertion(e,i).to.have.property(t,r)},assert.propertyNotVal=function(e,t,r,i){new Assertion(e,i).to.not.have.property(t,r)},assert.deepPropertyVal=function(e,t,r,i){new Assertion(e,i).to.have.deep.property(t,r)},assert.deepPropertyNotVal=function(e,t,r,i){new Assertion(e,i).to.not.have.deep.property(t,r)},assert.lengthOf=function(e,t,r){new Assertion(e,r).to.have.length(t)},assert.Throw=function(e,t,r,i){("string"==typeof t||t instanceof RegExp)&&(r=t,t=null);var n=new Assertion(e,i).to.Throw(t,r);return flag(n,"object")},assert.doesNotThrow=function(e,t,r){"string"==typeof t&&(r=t,t=null),new Assertion(e,r).to.not.Throw(t)},assert.operator=function(val,operator,val2,msg){if(!~["==","===",">",">=","<","<=","!=","!=="].indexOf(operator))throw new Error('Invalid operator "'+operator+'"');var test=new Assertion(eval(val+operator+val2),msg);test.assert(!0===flag(test,"object"),"expected "+util.inspect(val)+" to be "+operator+" "+util.inspect(val2),"expected "+util.inspect(val)+" to not be "+operator+" "+util.inspect(val2))},assert.closeTo=function(e,t,r,i){new Assertion(e,i).to.be.closeTo(t,r)},assert.sameMembers=function(e,t,r){new Assertion(e,r).to.have.same.members(t)},assert.sameDeepMembers=function(e,t,r){new Assertion(e,r).to.have.same.deep.members(t)},assert.includeMembers=function(e,t,r){new Assertion(e,r).to.include.members(t)},assert.changes=function(e,t,r){new Assertion(e).to.change(t,r)},assert.doesNotChange=function(e,t,r){new Assertion(e).to.not.change(t,r)},assert.increases=function(e,t,r){new Assertion(e).to.increase(t,r)},assert.doesNotIncrease=function(e,t,r){new Assertion(e).to.not.increase(t,r)},assert.decreases=function(e,t,r){new Assertion(e).to.decrease(t,r)},assert.doesNotDecrease=function(e,t,r){new Assertion(e).to.not.decrease(t,r)},assert.ifError=function(e,t){new Assertion(e,t).to.not.be.ok},function e(t,r){return assert[r]=assert[t],e}("Throw","throw")("Throw","throws")}}),require.register("chai/lib/chai/interface/expect.js",function(e,t){t.exports=function(e){e.expect=function(t,r){return new e.Assertion(t,r)},e.expect.fail=function(t,r,i,n){throw i=i||"expect.fail()",new e.AssertionError(i,{actual:t,expected:r,operator:n},e.expect.fail)}}}),require.register("chai/lib/chai/interface/should.js",function(e,t){t.exports=function(e){function t(){function t(){return this instanceof String||this instanceof Number?new r(this.constructor(this),null,t):this instanceof Boolean?new r(1==this,null,t):new r(this,null,t)}function i(e){Object.defineProperty(this,"should",{value:e,enumerable:!0,configurable:!0,writable:!0})}Object.defineProperty(Object.prototype,"should",{set:i,get:t,configurable:!0});var n={};return n.fail=function(t,r,i,o){throw i=i||"should.fail()",new e.AssertionError(i,{actual:t,expected:r,operator:o},n.fail)},n.equal=function(e,t,i){new r(e,i).to.equal(t)},n.Throw=function(e,t,i,n){new r(e,n).to.Throw(t,i)},n.exist=function(e,t){new r(e,t).to.exist},n.not={},n.not.equal=function(e,t,i){new r(e,i).to.not.equal(t)},n.not.Throw=function(e,t,i,n){new r(e,n).to.not.Throw(t,i)},n.not.exist=function(e,t){new r(e,t).to.not.exist},n["throw"]=n.Throw,n.not["throw"]=n.not.Throw,n}var r=e.Assertion;e.should=t,e.Should=t}}),require.register("chai/lib/chai/utils/addChainableMethod.js",function(e,t){var r=require("chai/lib/chai/utils/transferFlags.js"),i=require("chai/lib/chai/utils/flag.js"),n=require("chai/lib/chai/config.js"),o="__proto__"in Object,s=/^(?:length|name|arguments|caller)$/,a=Function.prototype.call,c=Function.prototype.apply;t.exports=function(e,t,u,h){"function"!=typeof h&&(h=function(){});var l={method:u,chainingBehavior:h};e.__methods||(e.__methods={}),e.__methods[t]=l,Object.defineProperty(e,t,{get:function(){l.chainingBehavior.call(this);var t=function f(){var e=i(this,"ssfi");e&&n.includeStack===!1&&i(this,"ssfi",f);var t=l.method.apply(this,arguments);return void 0===t?this:t};if(o){var u=t.__proto__=Object.create(this);u.call=a,u.apply=c}else{var h=Object.getOwnPropertyNames(e);h.forEach(function(r){if(!s.test(r)){var i=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,i)}})}return r(this,t),t},configurable:!0})}}),require.register("chai/lib/chai/utils/addMethod.js",function(e,t){var r=require("chai/lib/chai/config.js"),i=require("chai/lib/chai/utils/flag.js");t.exports=function(e,t,n){e[t]=function(){var o=i(this,"ssfi");o&&r.includeStack===!1&&i(this,"ssfi",e[t]);var s=n.apply(this,arguments);return void 0===s?this:s}}}),require.register("chai/lib/chai/utils/addProperty.js",function(e,t){t.exports=function(e,t,r){Object.defineProperty(e,t,{get:function(){var e=r.call(this);return void 0===e?this:e},configurable:!0})}}),require.register("chai/lib/chai/utils/flag.js",function(e,t){t.exports=function(e,t,r){var i=e.__flags||(e.__flags=Object.create(null));return 3!==arguments.length?i[t]:void(i[t]=r)}}),require.register("chai/lib/chai/utils/getActual.js",function(e,t){t.exports=function(e,t){return t.length>4?t[4]:e._obj}}),require.register("chai/lib/chai/utils/getEnumerableProperties.js",function(e,t){t.exports=function(e){var t=[];for(var r in e)t.push(r);return t}}),require.register("chai/lib/chai/utils/getMessage.js",function(e,t){var r=require("chai/lib/chai/utils/flag.js"),i=require("chai/lib/chai/utils/getActual.js"),n=(require("chai/lib/chai/utils/inspect.js"),require("chai/lib/chai/utils/objDisplay.js"));t.exports=function(e,t){var o=r(e,"negate"),s=r(e,"object"),a=t[3],c=i(e,t),u=o?t[2]:t[1],h=r(e,"message");return"function"==typeof u&&(u=u()),u=u||"",u=u.replace(/#{this}/g,n(s)).replace(/#{act}/g,n(c)).replace(/#{exp}/g,n(a)),h?h+": "+u:u}}),require.register("chai/lib/chai/utils/getName.js",function(e,t){t.exports=function(e){if(e.name)return e.name;var t=/^\s?function ([^(]*)\(/.exec(e);return t&&t[1]?t[1]:""}}),require.register("chai/lib/chai/utils/getPathValue.js",function(e,t){var r=require("chai/lib/chai/utils/getPathInfo.js");t.exports=function(e,t){var i=r(e,t);return i.value}}),require.register("chai/lib/chai/utils/getPathInfo.js",function(e,t){function r(e){var t=e.replace(/\[/g,".["),r=t.match(/(\\\.|[^.]+?)+/g);return r.map(function(e){var t=/\[(\d+)\]$/,r=t.exec(e);return r?{i:parseFloat(r[1])}:{p:e}})}function i(e,t,r){var i,n=t;r=void 0===r?e.length:r;for(var o=0,s=r;s>o;o++){var a=e[o];n?("undefined"!=typeof a.p?n=n[a.p]:"undefined"!=typeof a.i&&(n=n[a.i]),o==s-1&&(i=n)):i=void 0}return i}var n=require("chai/lib/chai/utils/hasProperty.js");t.exports=function(e,t){var o=r(e),s=o[o.length-1],a={parent:i(o,t,o.length-1),name:s.p||s.i,value:i(o,t)};return a.exists=n(a.name,a.parent),a}}),require.register("chai/lib/chai/utils/hasProperty.js",function(e,t){var r=require("chai/lib/chai/utils/type.js"),i={number:Number,string:String};t.exports=function(e,t){var n=r(t);return"null"===n||"undefined"===n?!1:(i[n]&&"object"!=typeof t&&(t=new i[n](t)),e in t)}}),require.register("chai/lib/chai/utils/getProperties.js",function(e,t){t.exports=function(){function e(e){-1===t.indexOf(e)&&t.push(e)}for(var t=Object.getOwnPropertyNames(subject),r=Object.getPrototypeOf(subject);null!==r;)Object.getOwnPropertyNames(r).forEach(e),r=Object.getPrototypeOf(r);return t}}),require.register("chai/lib/chai/utils/index.js",function(e,t){var e=t.exports={};e.test=require("chai/lib/chai/utils/test.js"),e.type=require("chai/lib/chai/utils/type.js"),e.getMessage=require("chai/lib/chai/utils/getMessage.js"),e.getActual=require("chai/lib/chai/utils/getActual.js"),e.inspect=require("chai/lib/chai/utils/inspect.js"),e.objDisplay=require("chai/lib/chai/utils/objDisplay.js"),e.flag=require("chai/lib/chai/utils/flag.js"),e.transferFlags=require("chai/lib/chai/utils/transferFlags.js"),e.eql=require("chaijs~deep-eql@0.1.3"),e.getPathValue=require("chai/lib/chai/utils/getPathValue.js"),e.getPathInfo=require("chai/lib/chai/utils/getPathInfo.js"),e.hasProperty=require("chai/lib/chai/utils/hasProperty.js"),e.getName=require("chai/lib/chai/utils/getName.js"),e.addProperty=require("chai/lib/chai/utils/addProperty.js"),e.addMethod=require("chai/lib/chai/utils/addMethod.js"),e.overwriteProperty=require("chai/lib/chai/utils/overwriteProperty.js"),e.overwriteMethod=require("chai/lib/chai/utils/overwriteMethod.js"),e.addChainableMethod=require("chai/lib/chai/utils/addChainableMethod.js"),e.overwriteChainableMethod=require("chai/lib/chai/utils/overwriteChainableMethod.js")}),require.register("chai/lib/chai/utils/inspect.js",function(e,t){function r(e,t,r){var n={showHidden:t,seen:[],stylize:function(e){return e}};return i(n,e,"undefined"==typeof r?2:r)}function i(t,r,p){if(r&&"function"==typeof r.inspect&&r.inspect!==e.inspect&&(!r.constructor||r.constructor.prototype!==r)){var y=r.inspect(p);return"string"!=typeof y&&(y=i(t,y,p)),y}var j=n(t,r);if(j)return j;if(v(r)){if("outerHTML"in r)return r.outerHTML;try{if(document.xmlVersion){var w=new XMLSerializer;return w.serializeToString(r)}var x="http://www.w3.org/1999/xhtml",m=document.createElementNS(x,"_");return m.appendChild(r.cloneNode(!1)),html=m.innerHTML.replace("><",">"+r.innerHTML+"<"),m.innerHTML="",html}catch(q){}}var A=b(r),O=t.showHidden?g(r):A;if(0===O.length||f(r)&&(1===O.length&&"stack"===O[0]||2===O.length&&"description"===O[0]&&"stack"===O[1])){if("function"==typeof r){var M=d(r),P=M?": "+M:"";return t.stylize("[Function"+P+"]","special")}if(h(r))return t.stylize(RegExp.prototype.toString.call(r),"regexp");if(l(r))return t.stylize(Date.prototype.toUTCString.call(r),"date");if(f(r))return o(r)}var S="",E=!1,_=["{","}"];if(u(r)&&(E=!0,_=["[","]"]),"function"==typeof r){var M=d(r),P=M?": "+M:"";S=" [Function"+P+"]"}if(h(r)&&(S=" "+RegExp.prototype.toString.call(r)),l(r)&&(S=" "+Date.prototype.toUTCString.call(r)),
+f(r))return o(r);if(0===O.length&&(!E||0==r.length))return _[0]+S+_[1];if(0>p)return h(r)?t.stylize(RegExp.prototype.toString.call(r),"regexp"):t.stylize("[Object]","special");t.seen.push(r);var k;return k=E?s(t,r,p,A,O):O.map(function(e){return a(t,r,p,A,e,E)}),t.seen.pop(),c(k,S,_)}function n(e,t){switch(typeof t){case"undefined":return e.stylize("undefined","undefined");case"string":var r="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(r,"string");case"number":return 0===t&&1/t===-(1/0)?e.stylize("-0","number"):e.stylize(""+t,"number");case"boolean":return e.stylize(""+t,"boolean")}return null===t?e.stylize("null","null"):void 0}function o(e){return"["+Error.prototype.toString.call(e)+"]"}function s(e,t,r,i,n){for(var o=[],s=0,c=t.length;c>s;++s)o.push(Object.prototype.hasOwnProperty.call(t,String(s))?a(e,t,r,i,String(s),!0):"");return n.forEach(function(n){n.match(/^\d+$/)||o.push(a(e,t,r,i,n,!0))}),o}function a(e,t,r,n,o,s){var a,c;if(t.__lookupGetter__&&(t.__lookupGetter__(o)?c=t.__lookupSetter__(o)?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):t.__lookupSetter__(o)&&(c=e.stylize("[Setter]","special"))),n.indexOf(o)<0&&(a="["+o+"]"),c||(e.seen.indexOf(t[o])<0?(c=null===r?i(e,t[o],null):i(e,t[o],r-1),c.indexOf("\n")>-1&&(c=s?c.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+c.split("\n").map(function(e){return" "+e}).join("\n"))):c=e.stylize("[Circular]","special")),"undefined"==typeof a){if(s&&o.match(/^\d+$/))return c;a=JSON.stringify(""+o),a.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+c}function c(e,t,r){var i=0,n=e.reduce(function(e,t){return i++,t.indexOf("\n")>=0&&i++,e+t.length+1},0);return n>60?r[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+r[1]:r[0]+t+" "+e.join(", ")+" "+r[1]}function u(e){return Array.isArray(e)||"object"==typeof e&&"[object Array]"===p(e)}function h(e){return"object"==typeof e&&"[object RegExp]"===p(e)}function l(e){return"object"==typeof e&&"[object Date]"===p(e)}function f(e){return"object"==typeof e&&"[object Error]"===p(e)}function p(e){return Object.prototype.toString.call(e)}var d=require("chai/lib/chai/utils/getName.js"),g=require("chai/lib/chai/utils/getProperties.js"),b=require("chai/lib/chai/utils/getEnumerableProperties.js");t.exports=r;var v=function(e){return"object"==typeof HTMLElement?e instanceof HTMLElement:e&&"object"==typeof e&&1===e.nodeType&&"string"==typeof e.nodeName}}),require.register("chai/lib/chai/utils/objDisplay.js",function(e,t){var r=require("chai/lib/chai/utils/inspect.js"),i=require("chai/lib/chai/config.js");t.exports=function(e){var t=r(e),n=Object.prototype.toString.call(e);if(i.truncateThreshold&&t.length>=i.truncateThreshold){if("[object Function]"===n)return e.name&&""!==e.name?"[Function: "+e.name+"]":"[Function]";if("[object Array]"===n)return"[ Array("+e.length+") ]";if("[object Object]"===n){var o=Object.keys(e),s=o.length>2?o.splice(0,2).join(", ")+", ...":o.join(", ");return"{ Object ("+s+") }"}return t}return t}}),require.register("chai/lib/chai/utils/overwriteMethod.js",function(e,t){t.exports=function(e,t,r){var i=e[t],n=function(){return this};i&&"function"==typeof i&&(n=i),e[t]=function(){var e=r(n).apply(this,arguments);return void 0===e?this:e}}}),require.register("chai/lib/chai/utils/overwriteProperty.js",function(e,t){t.exports=function(e,t,r){var i=Object.getOwnPropertyDescriptor(e,t),n=function(){};i&&"function"==typeof i.get&&(n=i.get),Object.defineProperty(e,t,{get:function(){var e=r(n).call(this);return void 0===e?this:e},configurable:!0})}}),require.register("chai/lib/chai/utils/overwriteChainableMethod.js",function(e,t){t.exports=function(e,t,r,i){var n=e.__methods[t],o=n.chainingBehavior;n.chainingBehavior=function(){var e=i(o).call(this);return void 0===e?this:e};var s=n.method;n.method=function(){var e=r(s).apply(this,arguments);return void 0===e?this:e}}}),require.register("chai/lib/chai/utils/test.js",function(e,t){var r=require("chai/lib/chai/utils/flag.js");t.exports=function(e,t){var i=r(e,"negate"),n=t[0];return i?!n:n}}),require.register("chai/lib/chai/utils/transferFlags.js",function(e,t){t.exports=function(e,t,r){var i=e.__flags||(e.__flags=Object.create(null));t.__flags||(t.__flags=Object.create(null)),r=3===arguments.length?r:!0;for(var n in i)(r||"object"!==n&&"ssfi"!==n&&"message"!=n)&&(t.__flags[n]=i[n])}}),require.register("chai/lib/chai/utils/type.js",function(e,t){var r={"[object Arguments]":"arguments","[object Array]":"array","[object Date]":"date","[object Function]":"function","[object Number]":"number","[object RegExp]":"regexp","[object String]":"string"};t.exports=function(e){var t=Object.prototype.toString.call(e);return r[t]?r[t]:null===e?"null":void 0===e?"undefined":e===Object(e)?"object":typeof e}}),"object"==typeof exports?module.exports=require("chai"):"function"==typeof define&&define.amd?define("chai",[],function(){return require("chai")}):(this||window).chai=require("chai")}();
diff --git a/chromium/third_party/catapult/tracing/third_party/d3/LICENSE b/chromium/third_party/catapult/tracing/third_party/d3/LICENSE
new file mode 100644
index 00000000000..fb7d95d70ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/d3/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2014, Michael Bostock
+All rights reserved.
+
+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.
+
+* The name Michael Bostock may not 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 MICHAEL BOSTOCK 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.
diff --git a/chromium/third_party/catapult/tracing/third_party/d3/README.chromium b/chromium/third_party/catapult/tracing/third_party/d3/README.chromium
new file mode 100644
index 00000000000..35362f83284
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/d3/README.chromium
@@ -0,0 +1,15 @@
+Name: d3
+Short Name: d3
+URL: https://github.com/mbostock/d3
+Version: 0
+Revision: 0b2fe8fe9eaa529f70f6ab93bc8580b2a5328f43
+Date: Sat Mar 1 11:31:09 2014 -0800
+License: BSD
+License File: NOT_SHIPPED
+Security Critical: no
+
+Description:
+A JavaScript visualization library for HTML and SVG.
+
+Local Modifications:
+Took only the minified JS file and associated license.
diff --git a/chromium/third_party/catapult/tracing/third_party/d3/d3.min.js b/chromium/third_party/catapult/tracing/third_party/d3/d3.min.js
new file mode 100644
index 00000000000..b0178b69e86
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/d3/d3.min.js
@@ -0,0 +1,5 @@
+!function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(n){return aa+n in this}function o(n){return n=aa+n,n in this&&delete this[n]}function a(){var n=[];return this.forEach(function(t){n.push(t)}),n}function c(){var n=0;for(var t in this)t.charCodeAt(0)===ca&&++n;return n}function s(){for(var n in this)if(n.charCodeAt(0)===ca)return!1;return!0}function l(){}function f(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function h(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=sa.length;r>e;++e){var u=sa[e]+t;if(u in n)return u}}function g(){}function p(){}function v(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new u;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function d(){Xo.event.preventDefault()}function m(){for(var n,t=Xo.event;n=t.sourceEvent;)t=n;return t}function y(n){for(var t=new p,e=0,r=arguments.length;++e<r;)t[arguments[e]]=v(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Xo.event;u.target=n,Xo.event=u,t[u.type].apply(e,r)}finally{Xo.event=i}}},t}function x(n){return fa(n,da),n}function M(n){return"function"==typeof n?n:function(){return ha(n,this)}}function _(n){return"function"==typeof n?n:function(){return ga(n,this)}}function b(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Xo.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function w(n){return n.trim().replace(/\s+/g," ")}function S(n){return new RegExp("(?:^|\\s+)"+Xo.requote(n)+"(?:\\s+|$)","g")}function k(n){return n.trim().split(/^|\s+/)}function E(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=k(n).map(A);var u=n.length;return"function"==typeof t?r:e}function A(n){var t=S(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",w(u+" "+n))):e.setAttribute("class",w(u.replace(t," ")))}}function C(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?i:u}function N(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function L(n){return"function"==typeof n?n:(n=Xo.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function T(n){return{__data__:n}}function q(n){return function(){return va(this,n)}}function z(n){return arguments.length||(n=Xo.ascending),function(t,e){return t&&e?n(t.__data__,e.__data__):!t-!e}}function R(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function D(n){return fa(n,ya),n}function P(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function U(){var n=this.__transition__;n&&++n.active}function j(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Bo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+Xo.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=H;a>0&&(n=n.substring(0,a));var s=Ma.get(n);return s&&(n=s,c=F),a?t?u:r:t?g:i}function H(n,t){return function(e){var r=Xo.event;Xo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Xo.event=r}}}function F(n,t){var e=H(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function O(){var n=".dragsuppress-"+ ++ba,t="click"+n,e=Xo.select(Go).on("touchmove"+n,d).on("dragstart"+n,d).on("selectstart"+n,d);if(_a){var r=Jo.style,u=r[_a];r[_a]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),_a&&(r[_a]=u),i&&(e.on(t,function(){d(),o()},!0),setTimeout(o,0))}}function Y(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>wa&&(Go.scrollX||Go.scrollY)){e=Xo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();wa=!(u.f||u.e),e.remove()}return wa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function I(n){return n>0?1:0>n?-1:0}function Z(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function V(n){return n>1?0:-1>n?Sa:Math.acos(n)}function X(n){return n>1?Ea:-1>n?-Ea:Math.asin(n)}function $(n){return((n=Math.exp(n))-1/n)/2}function B(n){return((n=Math.exp(n))+1/n)/2}function W(n){return((n=Math.exp(2*n))-1)/(n+1)}function J(n){return(n=Math.sin(n/2))*n}function G(){}function K(n,t,e){return new Q(n,t,e)}function Q(n,t,e){this.h=n,this.s=t,this.l=e}function nt(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,gt(u(n+120),u(n),u(n-120))}function tt(n,t,e){return new et(n,t,e)}function et(n,t,e){this.h=n,this.c=t,this.l=e}function rt(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),ut(e,Math.cos(n*=Na)*t,Math.sin(n)*t)}function ut(n,t,e){return new it(n,t,e)}function it(n,t,e){this.l=n,this.a=t,this.b=e}function ot(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=ct(u)*Fa,r=ct(r)*Oa,i=ct(i)*Ya,gt(lt(3.2404542*u-1.5371385*r-.4985314*i),lt(-.969266*u+1.8760108*r+.041556*i),lt(.0556434*u-.2040259*r+1.0572252*i))}function at(n,t,e){return n>0?tt(Math.atan2(e,t)*La,Math.sqrt(t*t+e*e),n):tt(0/0,0/0,n)}function ct(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function st(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function lt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function ft(n){return gt(n>>16,255&n>>8,255&n)}function ht(n){return ft(n)+""}function gt(n,t,e){return new pt(n,t,e)}function pt(n,t,e){this.r=n,this.g=t,this.b=e}function vt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function dt(n,t,e){var r,u,i,o,a=0,c=0,s=0;if(u=/([a-z]+)\((.*)\)/i.exec(n))switch(i=u[2].split(","),u[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Mt(i[0]),Mt(i[1]),Mt(i[2]))}return(o=Va.get(n))?t(o.r,o.g,o.b):(null!=n&&"#"===n.charAt(0)&&(r=parseInt(n.substring(1),16),isNaN(r)||(4===n.length?(a=(3840&r)>>4,a=a>>4|a,c=240&r,c=c>>4|c,s=15&r,s=s<<4|s):7===n.length&&(a=(16711680&r)>>16,c=(65280&r)>>8,s=255&r))),t(a,c,s))}function mt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),K(r,u,c)}function yt(n,t,e){n=xt(n),t=xt(t),e=xt(e);var r=st((.4124564*n+.3575761*t+.1804375*e)/Fa),u=st((.2126729*n+.7151522*t+.072175*e)/Oa),i=st((.0193339*n+.119192*t+.9503041*e)/Ya);return ut(116*u-16,500*(r-u),200*(u-i))}function xt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Mt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function _t(n){return"function"==typeof n?n:function(){return n}}function bt(n){return n}function wt(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),St(t,e,n,r)}}function St(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Xo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Go.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Xo.event;Xo.event=n;try{o.progress.call(i,c)}finally{Xo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Bo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Xo.rebind(i,o,"on"),null==r?i:i.get(kt(r))}function kt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Et(){var n=At(),t=Ct()-n;t>24?(isFinite(t)&&(clearTimeout(Wa),Wa=setTimeout(Et,t)),Ba=0):(Ba=1,Ga(Et))}function At(){var n=Date.now();for(Ja=Xa;Ja;)n>=Ja.t&&(Ja.f=Ja.c(n-Ja.t)),Ja=Ja.n;return n}function Ct(){for(var n,t=Xa,e=1/0;t;)t.f?t=n?n.n=t.n:Xa=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return $a=n,e}function Nt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Lt(n,t){var e=Math.pow(10,3*oa(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Tt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:bt;return function(n){var e=Qa.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=nc.get(g)||qt;var y=s&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var c=Xo.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf("."),M=0>x?n:n.substring(0,x),_=0>x?"":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):"";return y&&(M=i(w+M)),u+=v,n=M+_,("<"===o?u+n+w:">"===o?w+u+n:"^"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function qt(n){return n+""}function zt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Rt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new ec(e-1)),1),e}function i(n,e){return t(n=new ec(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{ec=zt;var r=new zt;return r._=n,o(r,t,e)}finally{ec=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Dt(n);return c.floor=c,c.round=Dt(r),c.ceil=Dt(u),c.offset=Dt(i),c.range=a,n}function Dt(n){return function(t,e){try{ec=zt;var r=new zt;return r._=t,n(r,e)._}finally{ec=Date}}}function Pt(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=uc[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&ec!==zt,o=new(i?zt:ec);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in uc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{ec=zt;var t=new ec;return t._=n,r(t)}finally{ec=Date}}var r=t(n);return e.parse=function(n){try{ec=zt;var t=r.parse(n);return t&&t._}finally{ec=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ee;var x=Xo.map(),M=jt(v),_=Ht(v),b=jt(d),w=Ht(d),S=jt(m),k=Ht(m),E=jt(y),A=Ht(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Ut(n.getDate(),t,2)},e:function(n,t){return Ut(n.getDate(),t,2)},H:function(n,t){return Ut(n.getHours(),t,2)},I:function(n,t){return Ut(n.getHours()%12||12,t,2)},j:function(n,t){return Ut(1+tc.dayOfYear(n),t,3)},L:function(n,t){return Ut(n.getMilliseconds(),t,3)},m:function(n,t){return Ut(n.getMonth()+1,t,2)},M:function(n,t){return Ut(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Ut(n.getSeconds(),t,2)},U:function(n,t){return Ut(tc.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ut(tc.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Ut(n.getFullYear()%100,t,2)},Y:function(n,t){return Ut(n.getFullYear()%1e4,t,4)},Z:ne,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Bt,e:Bt,H:Jt,I:Jt,j:Wt,L:Qt,m:$t,M:Gt,p:l,S:Kt,U:Ot,w:Ft,W:Yt,x:c,X:s,y:Zt,Y:It,Z:Vt,"%":te};return t}function Ut(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function jt(n){return new RegExp("^(?:"+n.map(Xo.requote).join("|")+")","i")}function Ht(n){for(var t=new u,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Ft(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Ot(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Yt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function It(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Zt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.y=Xt(+r[0]),e+r[0].length):-1}function Vt(n,t,e){return/^[+-]\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=+t,e+5):-1}function Xt(n){return n+(n>68?1900:2e3)}function $t(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Bt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Wt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Jt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Gt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Kt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function Qt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ne(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(oa(t)/60),u=oa(t)%60;return e+Ut(r,"0",2)+Ut(u,"0",2)}function te(n,t,e){oc.lastIndex=0;var r=oc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ee(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function re(){}function ue(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function ie(n,t){n&&lc.hasOwnProperty(n.type)&&lc[n.type](n,t)}function oe(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function ae(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)oe(n[e],t,1);t.polygonEnd()}function ce(){function n(n,t){n*=Na,t=t*Na/2+Sa/4;var e=n-r,o=e>=0?1:-1,a=o*e,c=Math.cos(t),s=Math.sin(t),l=i*s,f=u*c+l*Math.cos(a),h=l*o*Math.sin(a);hc.add(Math.atan2(h,f)),r=n,u=c,i=s}var t,e,r,u,i;gc.point=function(o,a){gc.point=n,r=(t=o)*Na,u=Math.cos(a=(e=a)*Na/2+Sa/4),i=Math.sin(a)},gc.lineEnd=function(){n(t,e)}}function se(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function le(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function fe(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function he(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ge(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function pe(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function ve(n){return[Math.atan2(n[1],n[0]),X(n[2])]}function de(n,t){return oa(n[0]-t[0])<Aa&&oa(n[1]-t[1])<Aa}function me(n,t){n*=Na;var e=Math.cos(t*=Na);ye(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function ye(n,t,e){++pc,dc+=(n-dc)/pc,mc+=(t-mc)/pc,yc+=(e-yc)/pc}function xe(){function n(n,u){n*=Na;var i=Math.cos(u*=Na),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);vc+=s,xc+=s*(t+(t=o)),Mc+=s*(e+(e=a)),_c+=s*(r+(r=c)),ye(t,e,r)}var t,e,r;kc.point=function(u,i){u*=Na;var o=Math.cos(i*=Na);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),kc.point=n,ye(t,e,r)}}function Me(){kc.point=me}function _e(){function n(n,t){n*=Na;var e=Math.cos(t*=Na),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-V(g)/h,v=Math.atan2(h,g);bc+=p*s,wc+=p*l,Sc+=p*f,vc+=v,xc+=v*(r+(r=o)),Mc+=v*(u+(u=a)),_c+=v*(i+(i=c)),ye(r,u,i)}var t,e,r,u,i;kc.point=function(o,a){t=o,e=a,kc.point=n,o*=Na;var c=Math.cos(a*=Na);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),ye(r,u,i)},kc.lineEnd=function(){n(t,e),kc.lineEnd=Me,kc.point=me}}function be(){return!0}function we(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(de(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new ke(e,n,null,!0),s=new ke(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new ke(r,n,null,!1),s=new ke(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Se(i),Se(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Se(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function ke(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Ee(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r){if(1&t){n=e[0];var u,r=n.length-1,o=-1;for(i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);return i.lineEnd(),void 0}r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ae))}}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[],i.polygonStart()},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Xo.merge(g);var n=Le(m,p);g.length?we(g,Ne,n,e,i):n&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ce(),M=t(x);return y}}function Ae(n){return n.length>1}function Ce(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:g,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ne(n,t){return((n=n.x)[0]<0?n[1]-Ea-Aa:Ea-n[1])-((t=t.x)[0]<0?t[1]-Ea-Aa:Ea-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;hc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Sa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Sa/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=_>=0?1:-1,w=b*_,S=w>Sa,k=p*x;if(hc.add(Math.atan2(k*b*Math.sin(w),v*M+k*Math.cos(w))),i+=S?_+b*ka:_,S^h>=e^m>=e){var E=fe(se(f),se(n));pe(E);var A=fe(u,E);pe(A);var C=(S^_>=0?-1:1)*X(A[2]);(r>C||r===C&&(E[0]||E[1]))&&(o+=S^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Aa>i||Aa>i&&0>hc)^1&o}function Te(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Sa:-Sa,c=oa(i-e);oa(c-Sa)<Aa?(n.point(e,r=(r+o)/2>0?Ea:-Ea),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Sa&&(oa(e-u)<Aa&&(e-=u*Aa),oa(i-a)<Aa&&(i-=a*Aa),r=qe(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function qe(n,t,e,r){var u,i,o=Math.sin(n-e);return oa(o)>Aa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function ze(n,t,e,r){var u;if(null==n)u=e*Ea,r.point(-Sa,u),r.point(0,u),r.point(Sa,u),r.point(Sa,0),r.point(Sa,-u),r.point(0,-u),r.point(-Sa,-u),r.point(-Sa,0),r.point(-Sa,u);else if(oa(n[0]-t[0])>Aa){var i=n[0]<t[0]?Sa:-Sa;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function Re(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Sa:-Sa),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(de(e,g)||de(p,g))&&(p[0]+=Aa,p[1]+=Aa,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&de(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=se(n),u=se(t),o=[1,0,0],a=fe(r,u),c=le(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=fe(o,a),p=ge(o,f),v=ge(a,h);he(p,v);var d=g,m=le(p,d),y=le(d,d),x=m*m-y*(le(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ge(d,(-m-M)/y);if(he(_,p),_=ve(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=oa(A-Sa)<Aa,N=C||Aa>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(oa(_[0]-w)<Aa?k:E):k<=_[1]&&_[1]<=E:A>Sa^(w<=_[0]&&_[0]<=S)){var L=ge(d,(-m+M)/y);return he(L,p),[_,ve(L)]}}}function u(t,e){var r=o?n:Sa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=oa(i)>Aa,c=cr(n,6*Na);return Ee(t,e,c,o?[0,-n]:[-Sa,n-Sa])}function De(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Pe(n,t,e,r){function u(r,u){return oa(r[0]-n)<Aa?u>0?0:3:oa(r[0]-e)<Aa?u>0?2:1:oa(r[1]-t)<Aa?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&Z(s,i,n)>0&&++t:i[1]<=r&&Z(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Ac,Math.min(Ac,n)),t=Math.max(-Ac,Math.min(Ac,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ce(),C=De(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Xo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&we(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ue(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function je(n){var t=0,e=Sa/3,r=nr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Sa/180,e=n[1]*Sa/180):[180*(t/Sa),180*(e/Sa)]},u}function He(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,X((i-(n*n+e*e)*u*u)/(2*u))]},e}function Fe(){function n(n,t){Nc+=u*n-r*t,r=n,u=t}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,t=r=i,e=u=o},Rc.lineEnd=function(){n(t,e)}}function Oe(n,t){Lc>n&&(Lc=n),n>qc&&(qc=n),Tc>t&&(Tc=t),t>zc&&(zc=t)}function Ye(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ie(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ie(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ie(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ze(n,t){dc+=n,mc+=t,++yc}function Ve(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);xc+=o*(t+n)/2,Mc+=o*(e+r)/2,_c+=o,Ze(t=n,e=r)}var t,e;Pc.point=function(r,u){Pc.point=n,Ze(t=r,e=u)}}function Xe(){Pc.point=Ze}function $e(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);xc+=o*(r+n)/2,Mc+=o*(u+t)/2,_c+=o,o=u*n-r*t,bc+=o*(r+n),wc+=o*(u+t),Sc+=3*o,Ze(r=n,u=t)}var t,e,r,u;Pc.point=function(i,o){Pc.point=n,Ze(t=r=i,e=u=o)},Pc.lineEnd=function(){n(t,e)}}function Be(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,ka)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:g};return a}function We(n){function t(n){return(a?r:e)(n)}function e(t){return Ke(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=se([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=oa(oa(w)-1)<Aa||oa(r-h)<Aa?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],L=C-t,T=N-e,q=x*L-y*T;(q*q/M>i||oa((y*L+x*T)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Na),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Je(n){var t=We(function(t,e){return n([t*La,e*La])});return function(n){return tr(t(n))}}function Ge(n){this.stream=n}function Ke(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function Qe(n){return nr(function(){return n})()}function nr(n){function t(n){return n=a(n[0]*Na,n[1]*Na),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*La,n[1]*La]}function r(){a=Ue(o=ur(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=We(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Ec,_=bt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=tr(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Ec):Re((b=+n)*Na),u()):b
+},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Pe(n[0][0],n[0][1],n[1][0],n[1][1]):bt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Na,d=n[1]%360*Na,r()):[v*La,d*La]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Na,y=n[1]%360*Na,x=n.length>2?n[2]%360*Na:0,r()):[m*La,y*La,x*La]},Xo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function tr(n){return Ke(n,function(t,e){n.point(t*Na,e*Na)})}function er(n,t){return[n,t]}function rr(n,t){return[n>Sa?n-ka:-Sa>n?n+ka:n,t]}function ur(n,t,e){return n?t||e?Ue(or(n),ar(t,e)):or(n):t||e?ar(t,e):rr}function ir(n){return function(t,e){return t+=n,[t>Sa?t-ka:-Sa>t?t+ka:t,e]}}function or(n){var t=ir(n);return t.invert=ir(-n),t}function ar(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),X(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),X(l*r-a*u)]},e}function cr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=sr(e,u),i=sr(e,i),(o>0?i>u:u>i)&&(u+=o*ka)):(u=n+o*ka,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=ve([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function sr(n,t){var e=se(t);e[0]-=n,pe(e);var r=V(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Aa)%(2*Math.PI)}function lr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function fr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function hr(n){return n.source}function gr(n){return n.target}function pr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(J(r-t)+u*o*J(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*La,Math.atan2(o,Math.sqrt(r*r+u*u))*La]}:function(){return[n*La,t*La]};return p.distance=h,p}function vr(){function n(n,u){var i=Math.sin(u*=Na),o=Math.cos(u),a=oa((n*=Na)-t),c=Math.cos(a);Uc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;jc.point=function(u,i){t=u*Na,e=Math.sin(i*=Na),r=Math.cos(i),jc.point=n},jc.lineEnd=function(){jc.point=jc.lineEnd=g}}function dr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function mr(n,t){function e(n,t){var e=oa(oa(t)-Ea)<Aa?0:o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Sa/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=I(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Ea]},e):xr}function yr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return oa(u)<Aa?er:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-I(u)*Math.sqrt(n*n+e*e)]},e)}function xr(n,t){return[n,Math.log(Math.tan(Sa/4+t/2))]}function Mr(n){var t,e=Qe(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=Sa*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function _r(n,t){return[Math.log(Math.tan(Sa/4+t/2)),-n]}function br(n){return n[0]}function wr(n){return n[1]}function Sr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&Z(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function kr(n,t){return n[0]-t[0]||n[1]-t[1]}function Er(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Ar(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Cr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Nr(){Jr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Jc.pop()||new Nr;return t.site=n,t}function Tr(n){Or(n),$c.remove(n),Jc.push(n),Jr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];Tr(n);for(var c=i;c.circle&&oa(e-c.circle.x)<Aa&&oa(r-c.circle.cy)<Aa;)i=c.P,a.unshift(c),Tr(c),c=i;a.unshift(c),Or(c);for(var s=o;s.circle&&oa(e-s.circle.x)<Aa&&oa(r-s.circle.cy)<Aa;)o=s.N,a.push(s),Tr(s),s=o;a.push(s),Or(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],$r(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Vr(c.site,s.site,null,u),Fr(c),Fr(s)}function zr(n){for(var t,e,r,u,i=n.x,o=n.y,a=$c._;a;)if(r=Rr(a,o)-i,r>Aa)a=a.L;else{if(u=i-Dr(a,o),!(u>Aa)){r>-Aa?(t=a.P,e=a):u>-Aa?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if($c.insert(t,c),t||e){if(t===e)return Or(t),e=Lr(t.site),$c.insert(c,e),c.edge=e.edge=Vr(t.site,c.site),Fr(t),Fr(e),void 0;if(!e)return c.edge=Vr(t.site,c.site),void 0;Or(t),Or(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};$r(e.edge,s,p,M),c.edge=Vr(s,n,null,M),e.edge=Vr(n,p,null,M),Fr(t),Fr(e)}}function Rr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Dr(n,t){var e=n.N;if(e)return Rr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Pr(n){this.site=n,this.edges=[]}function Ur(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Xc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(oa(r-t)>Aa||oa(u-e)>Aa)&&(a.splice(o,0,new Br(Xr(i.site,l,oa(r-f)<Aa&&p-u>Aa?{x:f,y:oa(t-f)<Aa?e:p}:oa(u-p)<Aa&&h-r>Aa?{x:oa(e-p)<Aa?t:h,y:p}:oa(r-h)<Aa&&u-g>Aa?{x:h,y:oa(t-h)<Aa?e:g}:oa(u-g)<Aa&&r-f>Aa?{x:oa(e-g)<Aa?t:f,y:g}:null),i.site,null)),++c)}function jr(n,t){return t.angle-n.angle}function Hr(){Jr(this),this.x=this.y=this.arc=this.site=this.cy=null}function Fr(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-Ca)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Gc.pop()||new Hr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Wc._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}Wc.insert(y,m),y||(Bc=m)}}}}function Or(n){var t=n.circle;t&&(t.P||(Bc=t.N),Wc.remove(t),Gc.push(t),Jr(t),n.circle=null)}function Yr(n){for(var t,e=Vc,r=De(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Ir(t,n)||!r(t)||oa(t.a.x-t.b.x)<Aa&&oa(t.a.y-t.b.y)<Aa)&&(t.a=t.b=null,e.splice(u,1))}function Ir(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Zr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Vr(n,t,e,r){var u=new Zr(n,t);return Vc.push(u),e&&$r(u,n,t,e),r&&$r(u,t,n,r),Xc[n.i].edges.push(new Br(u,n,t)),Xc[t.i].edges.push(new Br(u,t,n)),u}function Xr(n,t,e){var r=new Zr(n,null);return r.a=t,r.b=e,Vc.push(r),r}function $r(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Br(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function Wr(){this._=null}function Jr(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function Gr(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function Kr(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function Qr(n){for(;n.L;)n=n.L;return n}function nu(n,t){var e,r,u,i=n.sort(tu).pop();for(Vc=[],Xc=new Array(n.length),$c=new Wr,Wc=new Wr;;)if(u=Bc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Xc[i.i]=new Pr(i),zr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;qr(u.arc)}t&&(Yr(t),Ur(t));var o={cells:Xc,edges:Vc};return $c=Wc=Vc=Xc=null,o}function tu(n,t){return t.y-n.y||t.x-n.x}function eu(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function ru(n){return n.x}function uu(n){return n.y}function iu(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function ou(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&ou(n,c[0],e,r,o,a),c[1]&&ou(n,c[1],o,r,u,a),c[2]&&ou(n,c[2],e,a,o,i),c[3]&&ou(n,c[3],o,a,u,i)}}function au(n,t){n=Xo.rgb(n),t=Xo.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+vt(Math.round(e+i*n))+vt(Math.round(r+o*n))+vt(Math.round(u+a*n))}}function cu(n,t){var e,r={},u={};for(e in n)e in t?r[e]=fu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function su(n,t){return t-=n=+n,function(e){return n+t*e}}function lu(n,t){var e,r,u,i,o,a=0,c=0,s=[],l=[];for(n+="",t+="",Qc.lastIndex=0,r=0;e=Qc.exec(t);++r)e.index&&s.push(t.substring(a,c=e.index)),l.push({i:s.length,x:e[0]}),s.push(null),a=Qc.lastIndex;for(a<t.length&&s.push(t.substring(a)),r=0,i=l.length;(e=Qc.exec(n))&&i>r;++r)if(o=l[r],o.x==e[0]){if(o.i)if(null==s[o.i+1])for(s[o.i-1]+=o.x,s.splice(o.i,1),u=r+1;i>u;++u)l[u].i--;else for(s[o.i-1]+=o.x+s[o.i+1],s.splice(o.i,2),u=r+1;i>u;++u)l[u].i-=2;else if(null==s[o.i+1])s[o.i]=o.x;else for(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1),u=r+1;i>u;++u)l[u].i--;l.splice(r,1),i--,r--}else o.x=su(parseFloat(e[0]),parseFloat(o.x));for(;i>r;)o=l.pop(),null==s[o.i+1]?s[o.i]=o.x:(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1)),i--;return 1===s.length?null==s[0]?(o=l[0].x,function(n){return o(n)+""}):function(){return t}:function(n){for(r=0;i>r;++r)s[(o=l[r]).i]=o.x(n);return s.join("")}}function fu(n,t){for(var e,r=Xo.interpolators.length;--r>=0&&!(e=Xo.interpolators[r](n,t)););return e}function hu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(fu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function gu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function pu(n){return function(t){return 1-n(1-t)}}function vu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function du(n){return n*n}function mu(n){return n*n*n}function yu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function xu(n){return function(t){return Math.pow(t,n)}}function Mu(n){return 1-Math.cos(n*Ea)}function _u(n){return Math.pow(2,10*(n-1))}function bu(n){return 1-Math.sqrt(1-n*n)}function wu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ka*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ka/t)}}function Su(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function ku(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Eu(n,t){n=Xo.hcl(n),t=Xo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return rt(e+i*n,r+o*n,u+a*n)+""}}function Au(n,t){n=Xo.hsl(n),t=Xo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return nt(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Xo.lab(n),t=Xo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=Tu(t,e),i=qu(zu(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*La,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*La:0}function Tu(n,t){return n[0]*t[0]+n[1]*t[1]}function qu(n){var t=Math.sqrt(Tu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function zu(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Ru(n,t){var e,r=[],u=[],i=Xo.transform(n),o=Xo.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:su(a[0],c[0])},{i:3,x:su(a[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:su(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:su(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:su(g[0],p[0])},{i:e-2,x:su(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Du(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Pu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Uu(n){for(var t=n.source,e=n.target,r=Hu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function ju(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Hu(n,t){if(n===t)return n;for(var e=ju(n),r=ju(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Fu(n){n.fixed|=2}function Ou(n){n.fixed&=-7}function Yu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Iu(n){n.fixed&=-5}function Zu(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Zu(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Vu(n,t){return Xo.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=Wu,n}function Xu(n){return n.children}function $u(n){return n.value}function Bu(n,t){return t.value-n.value}function Wu(n){return Xo.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function Ju(n){return n.x}function Gu(n){return n.y}function Ku(n,t,e){n.y0=t,n.y=e}function Qu(n){return Xo.range(n.length)}function ni(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function ti(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ei(n){return n.reduce(ri,0)}function ri(n,t){return n+t[1]}function ui(n,t){return ii(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ii(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function oi(n){return[Xo.min(n),Xo.max(n)]}function ai(n,t){return n.parent==t.parent?1:2}function ci(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function si(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function li(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i<u;)t(r=li(e[i],t),n)>0&&(n=r);return n}function fi(n,t){return n.x-t.x}function hi(n,t){return t.x-n.x}function gi(n,t){return n.depth-t.depth}function pi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c<o;)i=u[c],e(i,a),a=i;t(n,r)}e(n,null)}function vi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function di(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function mi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function yi(n,t){return n.value-t.value}function xi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Mi(n,t){n._pack_next=t,t._pack_prev=n}function _i(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function bi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(wi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ei(r,u,i),t(i),xi(r,i),r._pack_prev=i,xi(i,u),u=r._pack_next,o=3;s>o;o++){Ei(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(_i(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!_i(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?Mi(r,u=a):Mi(r=c,u),o--):(xi(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Si)}}function wi(n){n._pack_next=n._pack_prev=n}function Si(n){delete n._pack_next,delete n._pack_prev}function ki(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)ki(u[i],t,e,r)}function Ei(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function Ai(n){return 1+Xo.max(n,function(n){return n.y})}function Ci(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ni(n){var t=n.children;return t&&t.length?Ni(t[0]):n}function Li(n){var t,e=n.children;return e&&(t=e.length)?Li(e[t-1]):n}function Ti(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function qi(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function zi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ri(n){return n.rangeExtent?n.rangeExtent():zi(n.range())}function Di(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ui(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ls}function ji(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Xo.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Hi(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?ji:Di,c=r?Pu:Du;return o=u(n,t,c,e),a=u(t,n,c,fu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Nu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Ii(n,t)},i.tickFormat=function(t,e){return Zi(n,t,e)},i.nice=function(t){return Oi(n,t),u()},i.copy=function(){return Hi(n,t,e,r)},u()}function Fi(n,t){return Xo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Oi(n,t){return Pi(n,Ui(Yi(n,t)[2]))}function Yi(n,t){null==t&&(t=10);var e=zi(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Ii(n,t){return Xo.range.apply(Xo,Yi(n,t))}function Zi(n,t,e){var r=Yi(n,t);return Xo.format(e?e.replace(Qa,function(n,t,e,u,i,o,a,c,s,l){return[t,e,u,i,o,a,c,s||"."+Xi(l,r),l].join("")}):",."+Vi(r[2])+"f")}function Vi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Xi(n,t){var e=Vi(t[2]);return n in fs?Math.abs(e-Vi(Math.max(Math.abs(t[0]),Math.abs(t[1]))))+ +("e"!==n):e-2*("%"===n)}function $i(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Pi(r.map(u),e?Math:gs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=zi(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return hs;arguments.length<2?t=hs:"function"!=typeof t&&(t=Xo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return $i(n.copy(),t,e,r)},Fi(o,n)}function Bi(n,t,e){function r(t){return n(u(t))}var u=Wi(t),i=Wi(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Ii(e,n)},r.tickFormat=function(n,t){return Zi(e,n,t)},r.nice=function(n){return r.domain(Oi(e,n))},r.exponent=function(o){return arguments.length?(u=Wi(t=o),i=Wi(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Bi(n.copy(),t,e)},Fi(r,n)}function Wi(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Ji(n,t){function e(e){return o[((i.get(e)||"range"===t.t&&i.set(e,n.push(e)))-1)%o.length]}function r(t,e){return Xo.range(n.length).map(function(n){return t+e*n})}var i,o,a;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var o,a=-1,c=r.length;++a<c;)i.has(o=r[a])||i.set(o,n.push(o));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(o=n,a=0,t={t:"range",a:arguments},e):o},e.rangePoints=function(u,i){arguments.length<2&&(i=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+i);return o=r(n.length<2?(c+s)/2:c+l*i/2,l),a=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-i+2*c);return o=r(l+h*c,h),s&&o.reverse(),a=h*(1-i),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-i+2*c)),g=f-l-(n.length-i)*h;return o=r(l+Math.round(g/2),h),s&&o.reverse(),a=Math.round(h*(1-i)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return zi(t.a[0])},e.copy=function(){return Ji(n,t)},e.domain(n)}function Gi(n,t){function e(){var e=0,i=t.length;for(u=[];++e<i;)u[e-1]=Xo.quantile(n,e/i);return r}function r(n){return isNaN(n=+n)?void 0:t[Xo.bisect(u,n)]}var u;return r.domain=function(t){return arguments.length?(n=t.filter(function(n){return!isNaN(n)}).sort(Xo.ascending),e()):n},r.range=function(n){return arguments.length?(t=n,e()):t},r.quantiles=function(){return u},r.invertExtent=function(e){return e=t.indexOf(e),0>e?[0/0,0/0]:[e>0?u[e-1]:n[0],e<u.length?u[e]:n[n.length-1]]},r.copy=function(){return Gi(n,t)},e()}function Ki(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ki(n,t,e)},u()}function Qi(n,t){function e(e){return e>=e?t[Xo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Qi(n,t)},e}function no(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ii(n,t)},t.tickFormat=function(t,e){return Zi(n,t,e)},t.copy=function(){return no(n)},t}function to(n){return n.innerRadius}function eo(n){return n.outerRadius}function ro(n){return n.startAngle}function uo(n){return n.endAngle}function io(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=_t(e),p=_t(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(""):null}var e=br,r=wr,u=be,i=oo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=Ms.get(n)||oo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function oo(n){return n.join("L")}function ao(n){return oo(n)+"Z"}function co(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function so(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function lo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function fo(n,t){return n.length<4?oo(n):n[1]+po(n.slice(1,n.length-1),vo(n,t))}function ho(n,t){return n.length<3?oo(n):n[0]+po((n.push(n[0]),n),vo([n[n.length-2]].concat(n,[n[1]]),t))}function go(n,t){return n.length<3?oo(n):n[0]+po(n,vo(n,t))}function po(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return oo(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var l=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+l[0]+","+l[1]}return r}function vo(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function mo(n){if(n.length<3)return oo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",_o(ws,o),",",_o(ws,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),bo(c,o,a);return n.pop(),c.push("L",r),c.join("")}function yo(n){if(n.length<4)return oo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(_o(ws,i)+","+_o(ws,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),bo(e,i,o);return e.join("")}function xo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[_o(ws,o),",",_o(ws,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),bo(t,o,a);return t.join("")}function Mo(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return mo(n)}function _o(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function bo(n,t,e){n.push("C",_o(_s,t),",",_o(_s,e),",",_o(bs,t),",",_o(bs,e),",",_o(ws,t),",",_o(ws,e))}function wo(n,t){return(t[1]-n[1])/(t[0]-n[0])}function So(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=wo(u,i);++t<e;)r[t]=(o+(o=wo(u=i,i=n[t+1])))/2;return r[t]=o,r}function ko(n){for(var t,e,r,u,i=[],o=So(n),a=-1,c=n.length-1;++a<c;)t=wo(n[a],n[a+1]),oa(t)<Aa?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function Eo(n){return n.length<3?oo(n):n[0]+po(n,ko(n))}function Ao(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ys,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Co(n){function t(t){function c(){v.push("M",a(n(m),f),l,s(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=_t(e),_=_t(u),b=e===r?function(){return g}:_t(r),w=u===i?function(){return p}:_t(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=br,r=br,u=0,i=wr,o=be,a=oo,c=a.key,s=a,l="L",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=Ms.get(n)||oo).key,s=a.reverse||a,l=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function No(n){return n.radius}function Lo(n){return[n.x,n.y]}function To(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ys;return[e*Math.cos(r),e*Math.sin(r)]}}function qo(){return 64}function zo(){return"circle"}function Ro(n){var t=Math.sqrt(n/Sa);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Do(n,t){return fa(n,Ns),n.id=t,n}function Po(n,t,e,r){var u=n.id;return R(n,"function"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Uo(n){return null==n&&(n=""),function(){this.textContent=n}}function jo(n,t,e,r){var i=n.__transition__||(n.__transition__={active:0,count:0}),o=i[e];if(!o){var a=r.time;o=i[e]={tween:new u,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++i.count,Xo.timer(function(r){function u(r){return i.active>e?s():(i.active=e,o.event&&o.event.start.call(n,l,t),o.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Xo.timer(function(){return p.c=c(r||1)?be:c,1},0,a),void 0)}function c(r){if(i.active!==e)return s();for(var u=r/g,a=f(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,l,t),s()):void 0}function s(){return--i.count?delete i[e]:delete n.__transition__,1}var l=n.__data__,f=o.ease,h=o.delay,g=o.duration,p=Ja,v=[];return p.t=h+a,r>=h?u(r-h):(p.c=u,void 0)},0,a)}}function Ho(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function Fo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Oo(n){return n.toISOString()}function Yo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Xo.bisect(js,u);return i==js.length?[t.year,Yi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/js[i-1]<js[i]/u?i-1:i]:[Os,Yi(n,e)[2]]
+}return r.invert=function(t){return Io(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Io)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Io(+e+1),t).length}var i=r.domain(),o=zi(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Pi(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=zi(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Yo(n.copy(),t,e)},Fi(r,n)}function Io(n){return new Date(n)}function Zo(n){return JSON.parse(n.responseText)}function Vo(n){var t=Wo.createRange();return t.selectNode(Wo.body),t.createContextualFragment(n.responseText)}var Xo={version:"3.4.3"};Date.now||(Date.now=function(){return+new Date});var $o=[].slice,Bo=function(n){return $o.call(n)},Wo=document,Jo=Wo.documentElement,Go=window;try{Bo(Jo.childNodes)[0].nodeType}catch(Ko){Bo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{Wo.createElement("div").style.setProperty("opacity",0,"")}catch(Qo){var na=Go.Element.prototype,ta=na.setAttribute,ea=na.setAttributeNS,ra=Go.CSSStyleDeclaration.prototype,ua=ra.setProperty;na.setAttribute=function(n,t){ta.call(this,n,t+"")},na.setAttributeNS=function(n,t,e){ea.call(this,n,t,e+"")},ra.setProperty=function(n,t,e){ua.call(this,n,t+"",e)}}Xo.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},Xo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Xo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Xo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Xo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Xo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Xo.mean=function(t,e){var r,u=t.length,i=0,o=-1,a=0;if(1===arguments.length)for(;++o<u;)n(r=t[o])&&(i+=(r-i)/++a);else for(;++o<u;)n(r=e.call(t,t[o],o))&&(i+=(r-i)/++a);return a?i:void 0},Xo.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Xo.median=function(t,e){return arguments.length>1&&(t=t.map(e)),t=t.filter(n),t.length?Xo.quantile(t.sort(Xo.ascending),.5):void 0},Xo.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)<e?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;e<n.call(t,t[i],i)?u=i:r=i+1}return r}}};var ia=Xo.bisector(function(n){return n});Xo.bisectLeft=ia.left,Xo.bisect=Xo.bisectRight=ia.right,Xo.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Xo.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Xo.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Xo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,e=Xo.min(arguments,t),r=new Array(e);++n<e;)for(var u,i=-1,o=r[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return r},Xo.transpose=function(n){return Xo.zip.apply(Xo,n)},Xo.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Xo.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Xo.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Xo.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var oa=Math.abs;Xo.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw new Error("infinite range");var u,i=[],o=e(oa(r)),a=-1;if(n*=o,t*=o,r*=o,0>r)for(;(u=n+r*++a)>t;)i.push(u/o);else for(;(u=n+r*++a)<t;)i.push(u/o);return i},Xo.map=function(n){var t=new u;if(n instanceof u)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},r(u,{has:i,get:function(n){return this[aa+n]},set:function(n,t){return this[aa+n]=t},remove:o,keys:a,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1),this[t])}});var aa="\x00",ca=aa.charCodeAt(0);Xo.nest=function(){function n(t,a,c){if(c>=o.length)return r?r.call(i,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=o[c++],d=new u;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(Xo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},Xo.set=function(n){var t=new l;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(l,{has:i,add:function(n){return this[aa+n]=!0,n},remove:function(n){return n=aa+n,n in this&&delete this[n]},values:a,size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1))}}),Xo.behavior={},Xo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=f(n,t,t[e]);return n};var sa=["webkit","ms","moz","Moz","o","O"];Xo.dispatch=function(){for(var n=new p,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=v(n);return n},p.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Xo.event=null,Xo.requote=function(n){return n.replace(la,"\\$&")};var la=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,fa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ha=function(n,t){return t.querySelector(n)},ga=function(n,t){return t.querySelectorAll(n)},pa=Jo[h(Jo,"matchesSelector")],va=function(n,t){return pa.call(n,t)};"function"==typeof Sizzle&&(ha=function(n,t){return Sizzle(n,t)[0]||null},ga=Sizzle,va=Sizzle.matchesSelector),Xo.selection=function(){return xa};var da=Xo.selection.prototype=[];da.select=function(n){var t,e,r,u,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return x(i)},da.selectAll=function(n){var t,e,r=[];n=_(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Bo(n.call(e,e.__data__,a,u))),t.parentNode=e);return x(r)};var ma={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};Xo.ns={prefix:ma,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),ma.hasOwnProperty(e)?{space:ma[e],local:n}:n}},da.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Xo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(b(t,n[t]));return this}return this.each(b(n,t))},da.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=k(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!S(n[u]).test(t))return!1;return!0}for(t in n)this.each(E(t,n[t]));return this}return this.each(E(n,t))},da.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return Go.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(C(n,t,e))},da.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(N(t,n[t]));return this}return this.each(N(n,t))},da.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},da.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},da.append=function(n){return n=L(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},da.insert=function(n,t){return n=L(n),t=M(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},da.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},da.data=function(n,t){function e(n,e){var r,i,o,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new u,y=new u,x=[];for(r=-1;++r<a;)d=t.call(i=n[r],i.__data__,r),m.has(d)?v[r]=i:m.set(d,i),x.push(d);for(r=-1;++r<f;)d=t.call(e,o=e[r],r),(i=m.get(d))?(g[r]=i,i.__data__=o):y.has(d)||(p[r]=T(o)),y.set(d,o),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)i=n[r],o=e[r],i?(i.__data__=o,g[r]=i):p[r]=T(o);for(;f>r;++r)p[r]=T(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,i,o=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++o<a;)(i=r[o])&&(n[o]=i.__data__);return n}var c=D([]),s=x([]),l=x([]);if("function"==typeof n)for(;++o<a;)e(r=this[o],n.call(r,r.parentNode.__data__,o));else for(;++o<a;)e(r=this[o],n);return s.enter=function(){return c},s.exit=function(){return l},s},da.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},da.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return x(u)},da.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},da.sort=function(n){n=z.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},da.each=function(n){return R(this,function(t,e,r){n.call(t,t.__data__,e,r)})},da.call=function(n){var t=Bo(arguments);return n.apply(t[0]=this,t),this},da.empty=function(){return!this.node()},da.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},da.size=function(){var n=0;return this.each(function(){++n}),n};var ya=[];Xo.selection.enter=D,Xo.selection.enter.prototype=ya,ya.append=da.append,ya.empty=da.empty,ya.node=da.node,ya.call=da.call,ya.size=da.size,ya.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return x(o)},ya.insert=function(n,t){return arguments.length<2&&(t=P(this)),da.insert.call(this,n,t)},da.transition=function(){for(var n,t,e=ks||++Ls,r=[],u=Es||{time:Date.now(),ease:yu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&jo(t,c,e,u),n.push(t)}return Do(r,e)},da.interrupt=function(){return this.each(U)},Xo.select=function(n){var t=["string"==typeof n?ha(n,Wo):n];return t.parentNode=Jo,x([t])},Xo.selectAll=function(n){var t=Bo("string"==typeof n?ga(n,Wo):n);return t.parentNode=Jo,x([t])};var xa=Xo.select(Jo);da.on=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(j(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(j(n,t,e))};var Ma=Xo.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ma.forEach(function(n){"on"+n in Wo&&Ma.remove(n)});var _a="onselectstart"in Wo?null:h(Jo.style,"userSelect"),ba=0;Xo.mouse=function(n){return Y(n,m())};var wa=/WebKit/.test(Go.navigator.userAgent)?-1:0;Xo.touches=function(n,t){return arguments.length<2&&(t=m().touches),t?Bo(t).map(function(t){var e=Y(n,t);return e.identifier=t.identifier,e}):[]},Xo.behavior.drag=function(){function n(){this.on("mousedown.drag",o).on("touchstart.drag",a)}function t(){return Xo.event.changedTouches[0].identifier}function e(n,t){return Xo.touches(n).filter(function(n){return n.identifier===t})[0]}function r(n,t,e,r){return function(){function o(){var n=t(l,g),e=n[0]-v[0],r=n[1]-v[1];d|=e|r,v=n,f({type:"drag",x:n[0]+c[0],y:n[1]+c[1],dx:e,dy:r})}function a(){m.on(e+"."+p,null).on(r+"."+p,null),y(d&&Xo.event.target===h),f({type:"dragend"})}var c,s=this,l=s.parentNode,f=u.of(s,arguments),h=Xo.event.target,g=n(),p=null==g?"drag":"drag-"+g,v=t(l,g),d=0,m=Xo.select(Go).on(e+"."+p,o).on(r+"."+p,a),y=O();i?(c=i.apply(s,arguments),c=[c.x-v[0],c.y-v[1]]):c=[0,0],f({type:"dragstart"})}}var u=y(n,"drag","dragstart","dragend"),i=null,o=r(g,Xo.mouse,"mousemove","mouseup"),a=r(t,e,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},Xo.rebind(n,u,"on")};var Sa=Math.PI,ka=2*Sa,Ea=Sa/2,Aa=1e-6,Ca=Aa*Aa,Na=Sa/180,La=180/Sa,Ta=Math.SQRT2,qa=2,za=4;Xo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=B(v),o=i/(qa*h)*(e*W(Ta*t+v)-$(v));return[r+o*s,u+o*l,i*e/B(Ta*t+v)]}return[r+n*s,u+n*l,i*Math.exp(Ta*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+za*f)/(2*i*qa*h),p=(c*c-i*i-za*f)/(2*c*qa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/Ta;return e.duration=1e3*y,e},Xo.behavior.zoom=function(){function n(n){n.on(A,s).on(Pa+".zoom",f).on(C,h).on("dblclick.zoom",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(M.range().map(function(n){return(n-S.x)/S.k}).map(M.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Xo.mouse(r),g),a(i)}function e(){f.on(C,Go===r?h:null).on(N,null),p(l&&Xo.event.target===s),c(i)}var r=this,i=T.of(r,arguments),s=Xo.event.target,l=0,f=Xo.select(Go).on(C,n).on(N,e),g=t(Xo.mouse(r)),p=O();U.call(r),o(i)}function l(){function n(){var n=Xo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Xo.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-x){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),d(),a(p)}x=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function i(){for(var n,t,e,i,o=Xo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=m&&Math.sqrt(l/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}x=null,u(n,t),a(p)}function f(){if(Xo.event.touches.length){for(var t=Xo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(M,null).on(_,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=T.of(g,arguments),v={},m=0,y=Xo.event.changedTouches[0].identifier,M="touchmove.zoom-"+y,_="touchend.zoom-"+y,b=Xo.select(Go).on(M,i).on(_,f),w=Xo.select(g).on(A,null).on(L,e),k=O();U.call(g),e(),o(p)}function f(){var n=T.of(this,arguments);m?clearTimeout(m):(U.call(this),o(n)),m=setTimeout(function(){m=null,c(n)},50),d();var e=v||Xo.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*Ra())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=T.of(this,arguments),e=Xo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Xo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,m,x,M,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Da,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",L="touchstart.zoom",T=y(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=T.of(this,arguments),t=S;ks?Xo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Xo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,M=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Xo.rebind(n,T,"on")};var Ra,Da=[0,1/0],Pa="onwheel"in Wo?(Ra=function(){return-Xo.event.deltaY*(Xo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in Wo?(Ra=function(){return Xo.event.wheelDelta},"mousewheel"):(Ra=function(){return-Xo.event.detail},"MozMousePixelScroll");G.prototype.toString=function(){return this.rgb()+""},Xo.hsl=function(n,t,e){return 1===arguments.length?n instanceof Q?K(n.h,n.s,n.l):dt(""+n,mt,K):K(+n,+t,+e)};var Ua=Q.prototype=new G;Ua.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,this.l/n)},Ua.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,n*this.l)},Ua.rgb=function(){return nt(this.h,this.s,this.l)},Xo.hcl=function(n,t,e){return 1===arguments.length?n instanceof et?tt(n.h,n.c,n.l):n instanceof it?at(n.l,n.a,n.b):at((n=yt((n=Xo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):tt(+n,+t,+e)};var ja=et.prototype=new G;ja.brighter=function(n){return tt(this.h,this.c,Math.min(100,this.l+Ha*(arguments.length?n:1)))},ja.darker=function(n){return tt(this.h,this.c,Math.max(0,this.l-Ha*(arguments.length?n:1)))},ja.rgb=function(){return rt(this.h,this.c,this.l).rgb()},Xo.lab=function(n,t,e){return 1===arguments.length?n instanceof it?ut(n.l,n.a,n.b):n instanceof et?rt(n.l,n.c,n.h):yt((n=Xo.rgb(n)).r,n.g,n.b):ut(+n,+t,+e)};var Ha=18,Fa=.95047,Oa=1,Ya=1.08883,Ia=it.prototype=new G;Ia.brighter=function(n){return ut(Math.min(100,this.l+Ha*(arguments.length?n:1)),this.a,this.b)},Ia.darker=function(n){return ut(Math.max(0,this.l-Ha*(arguments.length?n:1)),this.a,this.b)},Ia.rgb=function(){return ot(this.l,this.a,this.b)},Xo.rgb=function(n,t,e){return 1===arguments.length?n instanceof pt?gt(n.r,n.g,n.b):dt(""+n,gt,nt):gt(~~n,~~t,~~e)};var Za=pt.prototype=new G;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),gt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):gt(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),gt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Za.hsl=function(){return mt(this.r,this.g,this.b)},Za.toString=function(){return"#"+vt(this.r)+vt(this.g)+vt(this.b)};var Va=Xo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Va.forEach(function(n,t){Va.set(n,ft(t))}),Xo.functor=_t,Xo.xhr=wt(bt),Xo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=St(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new l,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Xo.csv=Xo.dsv(",","text/csv"),Xo.tsv=Xo.dsv(" ","text/tab-separated-values");var Xa,$a,Ba,Wa,Ja,Ga=Go[h(Go,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Xo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};$a?$a.n=i:Xa=i,$a=i,Ba||(Wa=clearTimeout(Wa),Ba=1,Ga(Et))},Xo.timer.flush=function(){At(),Ct()},Xo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ka=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Xo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Xo.round(n,Nt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((0>=e?e+1:e-1)/3)))),Ka[8+e/3]};var Qa=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,nc=Xo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Xo.round(n,Nt(n,t))).toFixed(Math.max(0,Math.min(20,Nt(n*(1+1e-15),t))))}}),tc=Xo.time={},ec=Date;zt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){rc.setUTCDate.apply(this._,arguments)},setDay:function(){rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){rc.setTime.apply(this._,arguments)}};var rc=Date.prototype;tc.year=Rt(function(n){return n=tc.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),tc.years=tc.year.range,tc.years.utc=tc.year.utc.range,tc.day=Rt(function(n){var t=new ec(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),tc.days=tc.day.range,tc.days.utc=tc.day.utc.range,tc.dayOfYear=function(n){var t=tc.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=tc[n]=Rt(function(n){return(n=tc.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});tc[n+"s"]=e.range,tc[n+"s"].utc=e.utc.range,tc[n+"OfYear"]=function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)}}),tc.week=tc.sunday,tc.weeks=tc.sunday.range,tc.weeks.utc=tc.sunday.utc.range,tc.weekOfYear=tc.sundayOfYear;var uc={"-":"",_:" ",0:"0"},ic=/^\s*\d+/,oc=/^%/;Xo.locale=function(n){return{numberFormat:Tt(n),timeFormat:Pt(n)}};var ac=Xo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Xo.format=ac.numberFormat,Xo.geo={},re.prototype={s:0,t:0,add:function(n){ue(n,this.t,cc),ue(cc.s,this.s,this),this.s?this.t+=cc.t:this.s=cc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var cc=new re;Xo.geo.stream=function(n,t){n&&sc.hasOwnProperty(n.type)?sc[n.type](n,t):ie(n,t)};var sc={Feature:function(n,t){ie(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)ie(e[r].geometry,t)}},lc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){oe(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)oe(e[r],t,0)},Polygon:function(n,t){ae(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ae(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)ie(e[r],t)}};Xo.geo.area=function(n){return fc=0,Xo.geo.stream(n,gc),fc};var fc,hc=new re,gc={sphere:function(){fc+=4*Sa},point:g,lineStart:g,lineEnd:g,polygonStart:function(){hc.reset(),gc.lineStart=ce},polygonEnd:function(){var n=2*hc;fc+=0>n?4*Sa+n:n,gc.lineStart=gc.lineEnd=gc.point=g}};Xo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=se([t*Na,e*Na]);if(m){var u=fe(m,r),i=[u[1],-u[0],0],o=fe(i,u);pe(o),o=ve(o);var c=t-p,s=c>0?1:-1,v=o[0]*La*s,d=oa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*La;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*La;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=oa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;gc.point(n,e),t(n,e)}function i(){gc.lineStart()}function o(){u(v,d),gc.lineEnd(),oa(y)>Aa&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,gc.polygonStart()},polygonEnd:function(){gc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>hc?(l=-(h=180),f=-(g=90)):y>Aa?g=90:-Aa>y&&(f=-90),M[0]=l,M[1]=h}};return function(n){g=h=-(l=f=1/0),x=[],Xo.geo.stream(n,_);
+var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Xo.geo.centroid=function(n){pc=vc=dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,kc);var t=bc,e=wc,r=Sc,u=t*t+e*e+r*r;return Ca>u&&(t=xc,e=Mc,r=_c,Aa>vc&&(t=dc,e=mc,r=yc),u=t*t+e*e+r*r,Ca>u)?[0/0,0/0]:[Math.atan2(e,t)*La,X(r/Math.sqrt(u))*La]};var pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc,Sc,kc={sphere:g,point:me,lineStart:xe,lineEnd:Me,polygonStart:function(){kc.lineStart=_e},polygonEnd:function(){kc.lineStart=xe}},Ec=Ee(be,Te,ze,[-Sa,-Sa/2]),Ac=1e9;Xo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Pe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Xo.geo.conicEqualArea=function(){return je(He)}).raw=He,Xo.geo.albers=function(){return Xo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Xo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Xo.geo.albers(),o=Xo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Xo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Aa,f+.12*s+Aa],[l-.214*s-Aa,f+.234*s-Aa]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Aa,f+.166*s+Aa],[l-.115*s-Aa,f+.234*s-Aa]]).stream(c).point,n},n.scale(1070)};var Cc,Nc,Lc,Tc,qc,zc,Rc={point:g,lineStart:g,lineEnd:g,polygonStart:function(){Nc=0,Rc.lineStart=Fe},polygonEnd:function(){Rc.lineStart=Rc.lineEnd=Rc.point=g,Cc+=oa(Nc/2)}},Dc={point:Oe,lineStart:g,lineEnd:g,polygonStart:g,polygonEnd:g},Pc={point:Ze,lineStart:Ve,lineEnd:Xe,polygonStart:function(){Pc.lineStart=$e},polygonEnd:function(){Pc.point=Ze,Pc.lineStart=Ve,Pc.lineEnd=Xe}};Xo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Xo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Cc=0,Xo.geo.stream(n,u(Rc)),Cc},n.centroid=function(n){return dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,u(Pc)),Sc?[bc/Sc,wc/Sc]:_c?[xc/_c,Mc/_c]:yc?[dc/yc,mc/yc]:[0/0,0/0]},n.bounds=function(n){return qc=zc=-(Lc=Tc=1/0),Xo.geo.stream(n,u(Dc)),[[Lc,Tc],[qc,zc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Je(n):bt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ye:new Be(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Xo.geo.albersUsa()).context(null)},Xo.geo.transform=function(n){return{stream:function(t){var e=new Ge(t);for(var r in n)e[r]=n[r];return e}}},Ge.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Xo.geo.projection=Qe,Xo.geo.projectionMutator=nr,(Xo.geo.equirectangular=function(){return Qe(er)}).raw=er.invert=er,Xo.geo.rotation=function(n){function t(t){return t=n(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t}return n=ur(n[0]%360*Na,n[1]*Na,n.length>2?n[2]*Na:0),t.invert=function(t){return t=n.invert(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t},t},rr.invert=er,Xo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ur(-n[0]*Na,-n[1]*Na,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=La,n[1]*=La}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=cr((t=+r)*Na,u*Na),n):t},n.precision=function(r){return arguments.length?(e=cr(t*Na,(u=+r)*Na),n):u},n.angle(90)},Xo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Na,u=n[1]*Na,i=t[1]*Na,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Xo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Xo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Xo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Xo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return oa(n%d)>Aa}).map(l)).concat(Xo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return oa(n%m)>Aa}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=lr(a,o,90),f=fr(r,e,y),h=lr(s,c,90),g=fr(i,u,y),n):y},n.majorExtent([[-180,-90+Aa],[180,90-Aa]]).minorExtent([[-180,-80-Aa],[180,80+Aa]])},Xo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=hr,u=gr;return n.distance=function(){return Xo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Xo.geo.interpolate=function(n,t){return pr(n[0]*Na,n[1]*Na,t[0]*Na,t[1]*Na)},Xo.geo.length=function(n){return Uc=0,Xo.geo.stream(n,jc),Uc};var Uc,jc={sphere:g,point:g,lineStart:vr,lineEnd:g,polygonStart:g,polygonEnd:g},Hc=dr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Xo.geo.azimuthalEqualArea=function(){return Qe(Hc)}).raw=Hc;var Fc=dr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},bt);(Xo.geo.azimuthalEquidistant=function(){return Qe(Fc)}).raw=Fc,(Xo.geo.conicConformal=function(){return je(mr)}).raw=mr,(Xo.geo.conicEquidistant=function(){return je(yr)}).raw=yr;var Oc=dr(function(n){return 1/n},Math.atan);(Xo.geo.gnomonic=function(){return Qe(Oc)}).raw=Oc,xr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ea]},(Xo.geo.mercator=function(){return Mr(xr)}).raw=xr;var Yc=dr(function(){return 1},Math.asin);(Xo.geo.orthographic=function(){return Qe(Yc)}).raw=Yc;var Ic=dr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Xo.geo.stereographic=function(){return Qe(Ic)}).raw=Ic,_r.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ea]},(Xo.geo.transverseMercator=function(){var n=Mr(_r),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=_r,Xo.geom={},Xo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=_t(e),i=_t(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(kr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Sr(a),l=Sr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=br,r=wr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Xo.geom.polygon=function(n){return fa(n,Zc),n};var Zc=Xo.geom.polygon.prototype=[];Zc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Zc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Zc.clip=function(n){for(var t,e,r,u,i,o,a=Cr(n),c=-1,s=this.length-Cr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Er(o,l,u)?(Er(i,l,u)||n.push(Ar(i,o,l,u)),n.push(o)):Er(i,l,u)&&n.push(Ar(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Vc,Xc,$c,Bc,Wc,Jc=[],Gc=[];Pr.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(jr),t.length},Br.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},Wr.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=Qr(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(Gr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Kr(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(Kr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Gr(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?Qr(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,Gr(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,Kr(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,Gr(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,Kr(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,Gr(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,Kr(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Xo.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return nu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Aa)*Aa,y:Math.round(o(n,t)/Aa)*Aa,i:t}})}var r=br,u=wr,i=r,o=u,a=Kc;return n?t(n):(t.links=function(n){return nu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return nu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(jr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&eu(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=_t(r=n),t):r},t.y=function(n){return arguments.length?(o=_t(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?Kc:n,t):a===Kc?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===Kc?null:a&&a[1]},t)};var Kc=[[-1e6,-1e6],[1e6,1e6]];Xo.geom.delaunay=function(n){return Xo.geom.voronoi().triangles(n)},Xo.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(oa(c-e)+oa(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=iu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=_t(a),M=_t(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=iu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){ou(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=br,c=wr;return(o=arguments.length)?(a=ru,c=uu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Xo.interpolateRgb=au,Xo.interpolateObject=cu,Xo.interpolateNumber=su,Xo.interpolateString=lu;var Qc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;Xo.interpolate=fu,Xo.interpolators=[function(n,t){var e=typeof t;return("string"===e?Va.has(t)||/^(#|rgb\(|hsl\()/.test(t)?au:lu:t instanceof G?au:"object"===e?Array.isArray(t)?hu:cu:su)(n,t)}],Xo.interpolateArray=hu;var ns=function(){return bt},ts=Xo.map({linear:ns,poly:xu,quad:function(){return du},cubic:function(){return mu},sin:function(){return Mu},exp:function(){return _u},circle:function(){return bu},elastic:wu,back:Su,bounce:function(){return ku}}),es=Xo.map({"in":bt,out:pu,"in-out":vu,"out-in":function(n){return vu(pu(n))}});Xo.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ts.get(e)||ns,r=es.get(r)||bt,gu(r(e.apply(null,$o.call(arguments,1))))},Xo.interpolateHcl=Eu,Xo.interpolateHsl=Au,Xo.interpolateLab=Cu,Xo.interpolateRound=Nu,Xo.transform=function(n){var t=Wo.createElementNS(Xo.ns.prefix.svg,"g");return(Xo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:rs)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var rs={a:1,b:0,c:0,d:1,e:0,f:0};Xo.interpolateTransform=Ru,Xo.layout={},Xo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Uu(n[e]));return t}},Xo.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Xo.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Xo.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(ka-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+"-"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Xo.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Xo.event.x,n.py=Xo.event.y,a.resume()}var e,r,u,i,o,a={},c=Xo.dispatch("start","tick","end"),s=[1,1],l=.9,f=us,h=is,g=-30,p=os,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Zu(t=Xo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Xo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Xo.behavior.drag().origin(bt).on("dragstart.force",Fu).on("drag.force",t).on("dragend.force",Ou)),arguments.length?(this.on("mouseover.force",Yu).on("mouseout.force",Iu).call(e),void 0):e},Xo.rebind(a,c,"on")};var us=20,is=1,os=1/0;Xo.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++f<s;)l=h[f]=n(c[f],p,a),l.parent=t,g+=l.value;r&&h.sort(r),i&&(t.value=g)}else delete t.children,i&&(t.value=+i.call(e,t,o)||0);return t}function t(n,r){var u=n.children,o=0;if(u&&(a=u.length))for(var a,c=-1,s=r+1;++c<a;)o+=t(u[c],s);else i&&(o=+i.call(e,n,r)||0);return i&&(n.value=o),o}function e(t){var e=[];return n(t,0,e),e}var r=Bu,u=Xu,i=$u;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(u=n,e):u},e.value=function(n){return arguments.length?(i=n,e):i},e.revalue=function(n){return t(n,0),n},e},Xo.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Xo.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Vu(e,r)},Xo.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof u?u.apply(this,arguments):u)-a)/Xo.sum(o),s=Xo.range(i.length);null!=e&&s.sort(e===as?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=as,r=0,u=ka;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var as={};Xo.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Xo.permute(s,f),l=Xo.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=bt,e=Qu,r=ni,u=Ku,i=Ju,o=Gu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:cs.get(t)||Qu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:ss.get(t)||ni,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var cs=Xo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ti),i=n.map(ei),o=Xo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Xo.range(n.length).reverse()},"default":Qu}),ss=Xo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ni});Xo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Xo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=oi,u=ui;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=_t(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ii(n,t)}:_t(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Xo.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h<i;)s=r[h],o(s,a),f=c(s,a,f),a=s;vi(n);var g=.5*(l._tree.prelim+s._tree.prelim);t?(u.prelim=t._tree.prelim+e(n,t),u.mod=u.prelim-g):u.prelim=g}else t&&(u.prelim=t._tree.prelim+e(n,t))}function a(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,u=-1;for(t+=n._tree.mod;++u<r;)a(e[u],t)}}function c(n,t,r){if(t){for(var u,i=n,o=n,a=t,c=n.parent.children[0],s=i._tree.mod,l=o._tree.mod,f=a._tree.mod,h=c._tree.mod;a=si(a),i=ci(i),a&&i;)c=ci(c),o=si(o),o._tree.ancestor=n,u=a._tree.prelim+f-i._tree.prelim-s+e(a,i),u>0&&(di(mi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!si(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!ci(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];pi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=li(l,hi),h=li(l,fi),g=li(l,gi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return pi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,pi(a,function(n){n.r=+l(n.value)}),pi(a,bi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;pi(a,function(n){n.r+=f}),pi(a,bi),pi(a,function(n){n.r-=f})}return ki(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Xo.layout.hierarchy().sort(yi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Vu(n,e)},Xo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;pi(c,function(n){var t=n.children;t&&t.length?(n.x=Ci(t),n.y=Ai(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ni(c),f=Li(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return pi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Xo.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=Ti,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?Ti(t):qi(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return qi(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?Ti:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Vu(i,a)},Xo.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Xo.random.normal.apply(Xo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Xo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Xo.scale={};var ls={floor:bt,ceil:bt};Xo.scale.linear=function(){return Hi([0,1],[0,1],fu,!1)};var fs={s:1,g:1,p:1,r:1,e:1};Xo.scale.log=function(){return $i(Xo.scale.linear().domain([0,1]),10,!0,[1,10])};var hs=Xo.format(".0e"),gs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Xo.scale.pow=function(){return Bi(Xo.scale.linear(),1,[0,1])},Xo.scale.sqrt=function(){return Xo.scale.pow().exponent(.5)},Xo.scale.ordinal=function(){return Ji([],{t:"range",a:[[]]})},Xo.scale.category10=function(){return Xo.scale.ordinal().range(ps)},Xo.scale.category20=function(){return Xo.scale.ordinal().range(vs)},Xo.scale.category20b=function(){return Xo.scale.ordinal().range(ds)},Xo.scale.category20c=function(){return Xo.scale.ordinal().range(ms)};var ps=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(ht),vs=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(ht),ds=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(ht),ms=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(ht);Xo.scale.quantile=function(){return Gi([],[])},Xo.scale.quantize=function(){return Ki(0,1,[0,1])},Xo.scale.threshold=function(){return Qi([.5],[0,1])
+},Xo.scale.identity=function(){return no([0,1])},Xo.svg={},Xo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ys,a=u.apply(this,arguments)+ys,c=(o>a&&(c=o,o=a,a=c),a-o),s=Sa>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);return c>=xs?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=to,e=eo,r=ro,u=uo;return n.innerRadius=function(e){return arguments.length?(t=_t(e),n):t},n.outerRadius=function(t){return arguments.length?(e=_t(t),n):e},n.startAngle=function(t){return arguments.length?(r=_t(t),n):r},n.endAngle=function(t){return arguments.length?(u=_t(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ys;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ys=-Ea,xs=ka-Aa;Xo.svg.line=function(){return io(bt)};var Ms=Xo.map({linear:oo,"linear-closed":ao,step:co,"step-before":so,"step-after":lo,basis:mo,"basis-open":yo,"basis-closed":xo,bundle:Mo,cardinal:go,"cardinal-open":fo,"cardinal-closed":ho,monotone:Eo});Ms.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var _s=[0,2/3,1/3,0],bs=[0,1/3,2/3,0],ws=[0,1/6,2/3,1/6];Xo.svg.line.radial=function(){var n=io(Ao);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},so.reverse=lo,lo.reverse=so,Xo.svg.area=function(){return Co(bt)},Xo.svg.area.radial=function(){var n=Co(Ao);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Xo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ys,l=s.call(n,u,r)+ys;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Sa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=hr,o=gr,a=No,c=ro,s=uo;return n.radius=function(t){return arguments.length?(a=_t(t),n):a},n.source=function(t){return arguments.length?(i=_t(t),n):i},n.target=function(t){return arguments.length?(o=_t(t),n):o},n.startAngle=function(t){return arguments.length?(c=_t(t),n):c},n.endAngle=function(t){return arguments.length?(s=_t(t),n):s},n},Xo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=hr,e=gr,r=Lo;return n.source=function(e){return arguments.length?(t=_t(e),n):t},n.target=function(t){return arguments.length?(e=_t(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Xo.svg.diagonal.radial=function(){var n=Xo.svg.diagonal(),t=Lo,e=n.projection;return n.projection=function(n){return arguments.length?e(To(t=n)):t},n},Xo.svg.symbol=function(){function n(n,r){return(Ss.get(t.call(this,n,r))||Ro)(e.call(this,n,r))}var t=zo,e=qo;return n.type=function(e){return arguments.length?(t=_t(e),n):t},n.size=function(t){return arguments.length?(e=_t(t),n):e},n};var Ss=Xo.map({circle:Ro,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Cs)),e=t*Cs;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Xo.svg.symbolTypes=Ss.keys();var ks,Es,As=Math.sqrt(3),Cs=Math.tan(30*Na),Ns=[],Ls=0;Ns.call=da.call,Ns.empty=da.empty,Ns.node=da.node,Ns.size=da.size,Xo.transition=function(n){return arguments.length?ks?n.transition():n:xa.transition()},Xo.transition.prototype=Ns,Ns.select=function(n){var t,e,r,u=this.id,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?("__data__"in r&&(e.__data__=r.__data__),jo(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return Do(i,u)},Ns.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=_(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&jo(u,g,o,i),t.push(u)}return Do(a,o)},Ns.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Do(u,this.id)},Ns.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):R(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Ns.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Ru:fu,a=Xo.ns.qualify(n);return Po(this,"attr."+n,t,a.local?i:u)},Ns.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Xo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Ns.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Go.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=fu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Po(this,"style."+n,t,u)},Ns.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Go.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Ns.text=function(n){return Po(this,"text",n,Uo)},Ns.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Ns.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Xo.ease.apply(Xo,arguments)),R(this,function(e){e.__transition__[t].ease=n}))},Ns.delay=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Ns.duration=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Ns.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Es,u=ks;ks=e,R(this,function(t,r,u){Es=t.__transition__[e],n.call(t,t.__data__,r,u)}),Es=r,ks=u}else R(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Xo.dispatch("start","end"))).on(n,t)});return this},Ns.transition=function(){for(var n,t,e,r,u=this.id,i=++Ls,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,jo(e,s,i,r)),n.push(e)}return Do(o,i)},Xo.svg.axis=function(){function n(n){n.each(function(){var n,s=Xo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):bt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Aa),d=Xo.transition(p.exit()).style("opacity",Aa).remove(),m=Xo.transition(p).style("opacity",1),y=Ri(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Xo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Ho,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Ho,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=Fo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=Fo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Xo.scale.linear(),r=Ts,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in qs?t+"":Ts,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Ts="bottom",qs={top:1,right:1,bottom:1,left:1};Xo.svg.brush=function(){function n(i){i.each(function(){var i=Xo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,bt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return zs[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Xo.transition(i),h=Xo.transition(o);c&&(l=Ri(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ri(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Xo.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),d())}function p(){32==Xo.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,d())}function v(){var n=Xo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Xo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]<x[0])],L[1]=f[+(n[1]<x[1])]):x=null),E&&m(n,c,0)&&(e(S),u=!0),A&&m(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:"brush",mode:C?"move":"resize"}))}function m(n,t,e){var r,u,a=Ri(t),c=a[0],s=a[1],p=L[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function y(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Xo.select("body").style("cursor",null),T.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Xo.select(Xo.event.target),w=a.of(_,arguments),S=Xo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=O(),L=Xo.mouse(_),T=Xo.select(Go).on("keydown.brush",u).on("keyup.brush",p);if(Xo.event.changedTouches?T.on("touchmove.brush",v).on("touchend.brush",y):T.on("mousemove.brush",v).on("mouseup.brush",y),S.interrupt().selectAll("*").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),z=+/^n/.test(k);M=[l[1-q]-L[0],f[1-z]-L[1]],L[0]=l[q],L[1]=f[z]}else Xo.event.altKey&&(x=L.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Xo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=y(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Rs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,ks?Xo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=hu(l,t.x),r=hu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=Rs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Rs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Xo.rebind(n,a,"on")};var zs={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Rs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Ds=tc.format=ac.timeFormat,Ps=Ds.utc,Us=Ps("%Y-%m-%dT%H:%M:%S.%LZ");Ds.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Oo:Us,Oo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Oo.toString=Us.toString,tc.second=Rt(function(n){return new ec(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),tc.seconds=tc.second.range,tc.seconds.utc=tc.second.utc.range,tc.minute=Rt(function(n){return new ec(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),tc.minutes=tc.minute.range,tc.minutes.utc=tc.minute.utc.range,tc.hour=Rt(function(n){var t=n.getTimezoneOffset()/60;return new ec(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),tc.hours=tc.hour.range,tc.hours.utc=tc.hour.utc.range,tc.month=Rt(function(n){return n=tc.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),tc.months=tc.month.range,tc.months.utc=tc.month.utc.range;var js=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Hs=[[tc.second,1],[tc.second,5],[tc.second,15],[tc.second,30],[tc.minute,1],[tc.minute,5],[tc.minute,15],[tc.minute,30],[tc.hour,1],[tc.hour,3],[tc.hour,6],[tc.hour,12],[tc.day,1],[tc.day,2],[tc.week,1],[tc.month,1],[tc.month,3],[tc.year,1]],Fs=Ds.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",be]]),Os={range:function(n,t,e){return Xo.range(Math.ceil(n/e)*e,+t,e).map(Io)},floor:bt,ceil:bt};Hs.year=tc.year,tc.scale=function(){return Yo(Xo.scale.linear(),Hs,Fs)};var Ys=Hs.map(function(n){return[n[0].utc,n[1]]}),Is=Ps.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",be]]);Ys.year=tc.year.utc,tc.scale.utc=function(){return Yo(Xo.scale.linear(),Ys,Is)},Xo.text=wt(function(n){return n.responseText}),Xo.json=function(n,t){return St(n,"application/json",Zo,t)},Xo.html=function(n,t){return St(n,"text/html",Vo,t)},Xo.xml=wt(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Xo):"object"==typeof module&&module.exports?module.exports=Xo:this.d3=Xo}(); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/devscripts/COPYING b/chromium/third_party/catapult/tracing/third_party/devscripts/COPYING
new file mode 100644
index 00000000000..c74d291485b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/devscripts/COPYING
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) 19yy <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) 19yy name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/chromium/third_party/catapult/tracing/third_party/devscripts/README.chromium b/chromium/third_party/catapult/tracing/third_party/devscripts/README.chromium
new file mode 100644
index 00000000000..1016e4cbebb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/devscripts/README.chromium
@@ -0,0 +1,12 @@
+Name: devscripts
+URL: http://anonscm.debian.org/gitweb/?p=devscripts/devscripts.git
+Version: 2.12.4
+Security Critical: no
+License: GPL 2.0
+
+Description:
+This directory contains selected tools from the Debian's devscripts collection.
+
+A .vanilla file is checked in so that our patched version can be easily
+compared with the unpatched script (e.g. when sending the changes upstream).
+Having a .patch file checked in was too inconvenient to keep up to date.
diff --git a/chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl b/chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl
new file mode 100755
index 00000000000..a59bbf96320
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl
@@ -0,0 +1,671 @@
+#!/usr/bin/perl -w
+# This script was originally based on the script of the same name from
+# the KDE SDK (by dfaure@kde.org)
+#
+# This version is
+# Copyright (C) 2007, 2008 Adam D. Barratt
+# Copyright (C) 2012 Francesco Poli
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+=head1 NAME
+
+licensecheck - simple license checker for source files
+
+=head1 SYNOPSIS
+
+B<licensecheck> B<--help>|B<--version>
+
+B<licensecheck> [B<--no-conf>] [B<--verbose>] [B<--copyright>]
+[B<-l>|B<--lines=>I<N>] [B<-i>|B<--ignore=>I<regex>] [B<-c>|B<--check=>I<regex>]
+[B<-m>|B<--machine>] [B<-r>|B<--recursive>]
+I<list of files and directories to check>
+
+=head1 DESCRIPTION
+
+B<licensecheck> attempts to determine the license that applies to each file
+passed to it, by searching the start of the file for text belonging to
+various licenses.
+
+If any of the arguments passed are directories, B<licensecheck> will add
+the files contained within to the list of files to process.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--verbose>, B<--no-verbose>
+
+Specify whether to output the text being processed from each file before
+the corresponding license information.
+
+Default is to be quiet.
+
+=item B<-l=>I<N>, B<--lines=>I<N>
+
+Specify the number of lines of each file's header which should be parsed
+for license information. (Default is 60).
+
+=item B<-i=>I<regex>, B<--ignore=>I<regex>
+
+When processing the list of files and directories, the regular
+expression specified by this option will be used to indicate those which
+should not be considered (e.g. backup files, VCS metadata).
+
+=item B<-r>, B<--recursive>
+
+Specify that the contents of directories should be added
+recursively.
+
+=item B<-c=>I<regex>, B<--check=>I<regex>
+
+Specify a pattern against which filenames will be matched in order to
+decide which files to check the license of.
+
+The default includes common source files.
+
+=item B<--copyright>
+
+Also display copyright text found within the file
+
+=item B<-m>, B<--machine>
+
+Display the information in a machine readable way, i.e. in the form
+<file><tab><license>[<tab><copyright>] so that it can be easily sorted
+and/or filtered, e.g. with the B<awk> and B<sort> commands.
+Note that using the B<--verbose> option will kill the readability.
+
+=item B<--no-conf>, B<--noconf>
+
+Do not read any configuration files. This can only be used as the first
+option given on the command-line.
+
+=back
+
+=head1 CONFIGURATION VARIABLES
+
+The two configuration files F</etc/devscripts.conf> and
+F<~/.devscripts> are sourced by a shell in that order to set
+configuration variables. Command line options can be used to override
+configuration file settings. Environment variable settings are
+ignored for this purpose. The currently recognised variables are:
+
+=over 4
+
+=item B<LICENSECHECK_VERBOSE>
+
+If this is set to I<yes>, then it is the same as the B<--verbose> command
+line parameter being used. The default is I<no>.
+
+=item B<LICENSECHECK_PARSELINES>
+
+If this is set to a positive number then the specified number of lines
+at the start of each file will be read whilst attempting to determine
+the license(s) in use. This is equivalent to the B<--lines> command line
+option.
+
+=back
+
+=head1 LICENSE
+
+This code is copyright by Adam D. Barratt <I<adam@adam-barratt.org.uk>>,
+all rights reserved; based on a script of the same name from the KDE
+SDK, which is copyright by <I<dfaure@kde.org>>.
+This program comes with ABSOLUTELY NO WARRANTY.
+You are free to redistribute this code under the terms of the GNU
+General Public License, version 2 or later.
+
+=head1 AUTHOR
+
+Adam D. Barratt <adam@adam-barratt.org.uk>
+
+=cut
+
+use strict;
+use warnings;
+use Getopt::Long qw(:config gnu_getopt);
+use File::Basename;
+use Tie::File;
+use Fcntl 'O_RDONLY';
+
+sub fatal($);
+sub parse_copyright($);
+sub parselicense($);
+sub remove_comments($);
+
+my $progname = basename($0);
+
+# From dpkg-source
+my $default_ignore_regex = '
+# Ignore general backup files
+(?:^|/).*~$|
+# Ignore emacs recovery files
+(?:^|/)\.#.*$|
+# Ignore vi swap files
+(?:^|/)\..*\.swp$|
+# Ignore baz-style junk files or directories
+(?:^|/),,.*(?:$|/.*$)|
+# File-names that should be ignored (never directories)
+(?:^|/)(?:DEADJOE|\.cvsignore|\.arch-inventory|\.bzrignore|\.gitignore)$|
+# File or directory names that should be ignored
+(?:^|/)(?:CVS|RCS|\.deps|\{arch\}|\.arch-ids|\.svn|\.hg|_darcs|\.git|
+\.shelf|_MTN|\.bzr(?:\.backup|tags)?)(?:$|/.*$)
+';
+
+# Take out comments and newlines
+$default_ignore_regex =~ s/^#.*$//mg;
+$default_ignore_regex =~ s/\n//sg;
+
+my $default_check_regex = '\.(c(c|pp|xx)?|h(h|pp|xx)?|f(77|90)?|p(l|m)|xs|sh|php|py(|x)|rb|java|vala|el|sc(i|e)|cs|pas|inc|dtd|xsl|mod|m|tex|mli?)$';
+
+my $modified_conf_msg;
+
+my ($opt_verbose, $opt_lines, $opt_noconf) = ('', '', '');
+my $opt_ignore_regex = $default_ignore_regex;
+my $opt_check_regex = $default_check_regex;
+my $opt_recursive = 0;
+my $opt_copyright = 0;
+my $opt_machine = 0;
+my ($opt_help, $opt_version);
+my $def_lines = 60;
+
+# Read configuration files and then command line
+# This is boilerplate
+
+if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
+ $modified_conf_msg = " (no configuration files read)";
+ shift;
+} else {
+ my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
+ my %config_vars = (
+ 'LICENSECHECK_VERBOSE' => 'no',
+ 'LICENSECHECK_PARSELINES' => $def_lines,
+ );
+ my %config_default = %config_vars;
+
+ my $shell_cmd;
+ # Set defaults
+ foreach my $var (keys %config_vars) {
+ $shell_cmd .= qq[$var="$config_vars{$var}";\n];
+ }
+ $shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
+ $shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
+ # Read back values
+ foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
+ my $shell_out = `/bin/bash -c '$shell_cmd'`;
+ @config_vars{keys %config_vars} = split /\n/, $shell_out, -1;
+
+ # Check validity
+ $config_vars{'LICENSECHECK_VERBOSE'} =~ /^(yes|no)$/
+ or $config_vars{'LICENSECHECK_VERBOSE'} = 'no';
+ $config_vars{'LICENSECHECK_PARSELINES'} =~ /^[1-9][0-9]*$/
+ or $config_vars{'LICENSECHECK_PARSELINES'} = $def_lines;
+
+ foreach my $var (sort keys %config_vars) {
+ if ($config_vars{$var} ne $config_default{$var}) {
+ $modified_conf_msg .= " $var=$config_vars{$var}\n";
+ }
+ }
+ $modified_conf_msg ||= " (none)\n";
+ chomp $modified_conf_msg;
+
+ $opt_verbose = $config_vars{'LICENSECHECK_VERBOSE'} eq 'yes' ? 1 : 0;
+ $opt_lines = $config_vars{'LICENSECHECK_PARSELINES'};
+}
+
+GetOptions("help|h" => \$opt_help,
+ "version|v" => \$opt_version,
+ "verbose!" => \$opt_verbose,
+ "lines|l=i" => \$opt_lines,
+ "ignore|i=s" => \$opt_ignore_regex,
+ "recursive|r" => \$opt_recursive,
+ "check|c=s" => \$opt_check_regex,
+ "copyright" => \$opt_copyright,
+ "machine|m" => \$opt_machine,
+ "noconf" => \$opt_noconf,
+ "no-conf" => \$opt_noconf,
+ )
+ or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
+
+$opt_lines = $def_lines if $opt_lines !~ /^[1-9][0-9]*$/;
+
+if ($opt_noconf) {
+ fatal "--no-conf is only acceptable as the first command-line option!";
+}
+if ($opt_help) { help(); exit 0; }
+if ($opt_version) { version(); exit 0; }
+
+die "Usage: $progname [options] filelist\nRun $progname --help for more details\n" unless @ARGV;
+
+$opt_lines = $def_lines if not defined $opt_lines;
+
+my @files = ();
+my @find_args = ();
+my $files_count = @ARGV;
+
+push @find_args, qw(-not ( -path */LayoutTests/* -prune ) );
+push @find_args, qw(-not ( -path */out/Debug/* -prune ) );
+push @find_args, qw(-not ( -path */out/Release/* -prune ) );
+push @find_args, qw(-not ( -path .git* -prune ) );
+push @find_args, qw(-not ( -path .svn* -prune ) );
+
+push @find_args, qw(-maxdepth 1) unless $opt_recursive;
+push @find_args, qw(-follow -type f -print);
+
+while (@ARGV) {
+ my $file = shift @ARGV;
+
+ if (-d $file) {
+ open FIND, '-|', 'find', $file, @find_args
+ or die "$progname: couldn't exec find: $!\n";
+
+ while (<FIND>) {
+ chomp;
+ next unless m%$opt_check_regex%;
+ # Skip empty files
+ next if (-z $_);
+ push @files, $_ unless m%$opt_ignore_regex%;
+ }
+ close FIND;
+ } else {
+ next unless ($files_count == 1) or $file =~ m%$opt_check_regex%;
+ push @files, $file unless $file =~ m%$opt_ignore_regex%;
+ }
+}
+
+while (@files) {
+ my $file = shift @files;
+ my $header = '';
+ my $copyright_match;
+ my $copyright = '';
+ my $license = '';
+ my %copyrights;
+
+ open (F, "<$file") or die "Unable to access $file\n";
+ while (<F>) {
+ last if ($. > $opt_lines);
+ $header .= $_;
+ }
+ close(F);
+
+ $copyright = join(" / ", values %copyrights);
+
+ print qq(----- $file header -----\n$header----- end header -----\n\n)
+ if $opt_verbose;
+
+ remove_comments($header);
+ $license = parselicense($header);
+
+ # If no license in header, check footer (slow, because read file backwards)
+ # Need for instance for Perl files, which often use the footer
+ if ($license eq "UNKNOWN") {
+ my $footer = '';
+ tie(my @file_lines, "Tie::File", $file, autochomp => 0, mode => O_RDONLY) or die("Unable to access $file\n");
+ # Avoid indexing error if header is entire file
+ if ($#file_lines >= $opt_lines) {
+ foreach (@file_lines[-$opt_lines .. -1]) {
+ $footer .= $_;
+ }
+ }
+ print qq(----- $file footer -----\n$header----- end footer -----\n\n)
+ if $opt_verbose;
+ remove_comments($footer);
+ $license = parselicense($footer);
+ }
+
+ if ($opt_machine) {
+ print "$file\t$license";
+ print "\t" . ($copyright or "*No copyright*") if $opt_copyright;
+ print "\n";
+ } else {
+ print "$file: ";
+ print "*No copyright* " unless $copyright;
+ print $license . "\n";
+ print " [Copyright: " . $copyright . "]\n"
+ if $copyright and $opt_copyright;
+ print "\n" if $opt_copyright;
+ }
+}
+
+sub remove_comments($) {
+ $_ = $_[0];
+ # Remove Fortran comments
+ s/^[cC] //gm;
+ # Remove .ASM comments
+ s#^;\*?##gm;
+ # Remove .S comments
+ s#^@ ##gm;
+ # Remove new lines
+ tr/\t\r\n/ /;
+ # Remove C / C++ comments
+ s#(\*/|/[/*])##g;
+ # Remove all characters not matching search
+ tr% A-Za-z.,@;0-9\(\)/-%%cd;
+ # Collapse multiple spaces into single space
+ tr/ //s;
+ $_[0] = $_;
+}
+
+sub parse_copyright($) {
+ my $copyright = '';
+ my $match;
+
+ my $copyright_indicator_regex = '
+ (?:copyright # The full word
+ |copr\. # Legally-valid abbreviation
+ |\x{00a9} # Unicode character COPYRIGHT SIGN
+ |\xc2\xa9 # Unicode copyright sign encoded in iso8859
+ |\(c\) # Legally-null representation of sign
+ )';
+ my $copyright_disindicator_regex = '
+ \b(?:info(?:rmation)? # Discussing copyright information
+ |notice # Discussing the notice
+ |and|or # Part of a sentence
+ )\b';
+
+ if (m%$copyright_indicator_regex(?::\s*|\s+)(\S.*)$%ix) {
+ $match = $1;
+
+ # Ignore lines matching "see foo for copyright information" etc.
+ if ($match !~ m%^\s*$copyright_disindicator_regex%ix) {
+ # De-cruft
+ $match =~ s/([,.])?\s*$//;
+ $match =~ s/$copyright_indicator_regex//igx;
+ $match =~ s/^\s+//;
+ $match =~ s/\s{2,}/ /g;
+ $match =~ s/\\@/@/g;
+ $copyright = $match;
+ }
+ }
+
+ return $copyright;
+}
+
+sub help {
+ print <<"EOF";
+Usage: $progname [options] filename [filename ...]
+Valid options are:
+ --help, -h Display this message
+ --version, -v Display version and copyright info
+ --no-conf, --noconf Don't read devscripts config files; must be
+ the first option given
+ --verbose Display the header of each file before its
+ license information
+ --lines, -l Specify how many lines of the file header
+ should be parsed for license information
+ (Default: $def_lines)
+ --check, -c Specify a pattern indicating which files should
+ be checked
+ (Default: '$default_check_regex')
+ --machine, -m Display in a machine readable way (good for awk)
+ --recursive, -r Add the contents of directories recursively
+ --copyright Also display the file's copyright
+ --ignore, -i Specify that files / directories matching the
+ regular expression should be ignored when
+ checking files
+ (Default: '$default_ignore_regex')
+
+Default settings modified by devscripts configuration files:
+$modified_conf_msg
+EOF
+}
+
+sub version {
+ print <<"EOF";
+This is $progname, from the Debian devscripts package, version ###VERSION###
+Copyright (C) 2007, 2008 by Adam D. Barratt <adam\@adam-barratt.org.uk>; based
+on a script of the same name from the KDE SDK by <dfaure\@kde.org>.
+
+This program comes with ABSOLUTELY NO WARRANTY.
+You are free to redistribute this code under the terms of the
+GNU General Public License, version 2, or (at your option) any
+later version.
+EOF
+}
+
+sub parselicense($) {
+ my ($licensetext) = @_;
+
+ my $gplver = "";
+ my $lgplver = "";
+ my $extrainfo = "";
+ my $license = "";
+
+ if ($licensetext =~ /version ([^, ]+?)[.,]? (?:\(?only\)?.? )?(?:of the GNU (Affero )?General Public License )?(as )?published by the Free Software Foundation/i or
+ $licensetext =~ /GNU (?:Affero )?General Public License (?:as )?published by the Free Software Foundation; version ([^, ]+?)[.,]? /i or
+ $licensetext =~ /GNU (?:Affero )?General Public License,? [Vv]ersion (\d+(?:\.\d+)?)[ \.]/) {
+ $gplver = " (v$1)";
+ } elsif ($licensetext =~ /either version ([^ ]+)(?: of the License)?, or \(at your option\) any later version/) {
+ $gplver = " (v$1 or later)";
+ }
+
+ if ($licensetext =~ /version ([^, ]+?)[.,]? (?:or later|or any later version) (?:of the GNU (?:Lesser |Library )General Public License )(as )?published by the Free Software Foundation/i or
+ $licensetext =~ /(?:GNU (?:Lesser |Library )|(?:Lesser|Library) GNU )General Public License (?:(?:as )?published by the Free Software Foundation;)?,? (?:either )?[Vv]ersion ([^, ]+?)(?: of the license)?[.,]? (?:or later|or (?:\(at your option\) )?any later version)/i or
+ $licensetext =~ /GNU (?:Lesser |Library )General Public License(?: \(LGPL\))?,? [Vv]ersion (\d+(?:\.\d+)?)[ \.]/) {
+ $lgplver = " (v$1 or later)";
+ }
+
+ if ($licensetext =~ /permission (?:is (also granted|given))? to link (the code of )?this program with (any edition of )?(Qt|the Qt library)/i) {
+ $extrainfo = " (with Qt exception)$extrainfo"
+ }
+
+ if ($licensetext =~ /(All changes made in this file will be lost|DO NOT (EDIT|delete this file)|Generated (automatically|by|from)|generated.*file)/i) {
+ $license = "GENERATED FILE";
+ }
+
+ if ($licensetext =~ /is (free software.? you can redistribute it and\/or modify it|licensed) under the terms of (version [^ ]+ of )?the (GNU (Library |Lesser )General Public License|LGPL)/i or
+ $licensetext =~ /(is distributed|may be used|can redistribute).*terms.*(LGPL|(Lesser|Library) GNU General Public License)/) {
+ if ($lgplver) {
+ $license = "LGPL$lgplver$extrainfo $license";
+ } else {
+ $license = "LGPL (unversioned/unknown version) $license";
+ }
+ }
+
+ if ($licensetext =~ /is free software.? you (can|may) redistribute it and\/or modify it under the terms of (?:version [^ ]+ (?:\(?only\)? )?of )?the GNU General Public License/i) {
+ $license = "GPL$gplver$extrainfo $license";
+ } elsif ($licensetext =~ /is distributed under the terms of the GNU General Public License,/
+ and $gplver) {
+ $license = "GPL$gplver$extrainfo $license";
+ } elsif ($licensetext =~ /is distributed.*terms.*[^L]GPL/) {
+ if ($gplver) {
+ $license = "GPL$gplver$extrainfo $license";
+ } else {
+ $license = "GPL (unversioned/unknown version) $license";
+ }
+ }
+
+ if ($licensetext =~ /This file is part of the .*Qt GUI Toolkit. This file may be distributed under the terms of the Q Public License as defined/) {
+ $license = "QPL (part of Qt) $license";
+ } elsif ($licensetext =~ /may be distributed under the terms of the Q Public License as defined/) {
+ $license = "QPL $license";
+ }
+
+ if ($licensetext =~ /opensource\.org\/licenses\/mit/) {
+ $license = "MIT/X11 (BSD like) $license";
+ } elsif ($licensetext =~ /Permission is hereby granted, free of charge, to any person obtaining a copy of this software and(\/or)? associated documentation files \(the (Software|Materials)\), to deal in the (Software|Materials)/) {
+ $license = "MIT/X11 (BSD like) $license";
+ } elsif ($licensetext =~ /Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose/) {
+ $license = "MIT/X11 (BSD like) $license";
+ } elsif ($licensetext =~ /Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee/) {
+ $license = "MIT/X11 (BSD like) $license";
+ } elsif ($licensetext =~ /MIT .* License/) {
+ $license = "MIT/X11 (BSD like) $license";
+ }
+
+ if ($licensetext =~ /This file is part of the Independent JPEG Group(')?s software.*For conditions of distribution and use, see the accompanying README file/i) {
+ $license = "Independent JPEG Group License $license";
+ }
+
+ if ($licensetext =~ /the University of Illinois Open Source License/){
+ $license = "University of Illinois/NCSA Open Source License (BSD like) $license";
+ }
+
+ if ($licensetext =~ /Permission to use, copy, modify, and(\/or)? distribute this software (and its documentation )?for any purpose (with or )?without fee is hereby granted, provided.*(copyright|entire) notice.*all copies/i) {
+ $license = "ISC $license";
+ }
+
+ if ($licensetext =~ /THIS SOFTWARE IS PROVIDED .*AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY/ ||
+ $licensetext =~ /THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- ITY/) {
+ if ($licensetext =~ /All advertising materials mentioning features or use of this software must display the following/) {
+ $license = "BSD (4 clause) $license";
+ } elsif ($licensetext =~ /be used to endorse or promote products derived from this software/) {
+ $license = "BSD (3 clause) $license";
+ } elsif ($licensetext =~ /Redistributions of source code must retain the above copyright notice/) {
+ $license = "BSD (2 clause) $license";
+ } else {
+ $license = "BSD $license";
+ }
+ } elsif ($licensetext =~ /Use of this source code is governed by a BSD-style license/) {
+ $license = "BSD-like $license";
+ } elsif ($licensetext =~ /BSD terms apply/) {
+ $license = "BSD-like $license";
+ } elsif ($licensetext =~ /subject to the BSD License/) {
+ # TODO(sbc): remove this case once we fix: http://crbug.com/177268
+ $license = "BSD-like $license";
+ } elsif ($licensetext =~ /license BSD/) {
+ $license = "BSD-like $license";
+ } elsif ($licensetext =~ /GOVERNED BY A BSD-STYLE SOURCE LICENSE/) {
+ $license = "BSD-like $license";
+ } elsif ($licensetext =~ /BSD 3-Clause license/) {
+ $license = "BSD (3 clause) $license";
+ }
+
+ if ($licensetext =~ /Mozilla Public License( Version|, v.) ([^ ]+[^., ]),?/) {
+ $license = "MPL (v$2) $license";
+ }
+
+ if ($licensetext =~ /Released under the terms of the Artistic License ([^ ]+)/) {
+ $license = "Artistic (v$1) $license";
+ }
+
+ if ($licensetext =~ /is free software under the Artistic [Ll]icense/) {
+ $license = "Artistic $license";
+ }
+
+ if ($licensetext =~ /This (program|library) is free software; you can redistribute it and\/or modify it under the same terms as Perl itself/) {
+ $license = "Perl $license";
+ }
+
+ if ($licensetext =~ /under the terms of the Apache ([^ ]+) License OR version 2 of the GNU/) {
+ $license = "Apache (v$1) GPL (v2) $license";
+ } elsif ($licensetext =~ /under the Apache License, Version ([^ ]+)/) {
+ $license = "Apache (v$1) $license";
+ }
+
+ if ($licensetext =~ /(THE BEER-WARE LICENSE)/i) {
+ $license = "Beerware $license";
+ }
+
+ if ($licensetext =~ /This source file is subject to version ([^ ]+) of the PHP license/) {
+ $license = "PHP (v$1) $license";
+ }
+
+ if ($licensetext =~ /under the terms of the CeCILL /) {
+ $license = "CeCILL $license";
+ }
+
+ if ($licensetext =~ /under the terms of the CeCILL-([^ ]+) /) {
+ $license = "CeCILL-$1 $license";
+ }
+
+ if ($licensetext =~ /under the SGI Free Software (B License|License B)/) {
+ $license = "SGI Free Software License B $license";
+ }
+
+ if ($licensetext =~ /(in|into) the public domain/i) {
+ $license = "Public domain $license";
+ }
+
+ if ($licensetext =~ /terms of the Common Development and Distribution License(, Version ([^(]+))? \(the License\)/) {
+ $license = "CDDL " . ($1 ? "(v$2) " : '') . $license;
+ }
+
+ if ($licensetext =~ /Microsoft Permissive License \(Ms-PL\)/) {
+ $license = "Ms-PL $license";
+ }
+
+ if ($licensetext =~ /as defined in and that are subject to the Apple Public Source License([ ,-]+Version ([^ ]+)?(\.))/) {
+ $license = "APSL " . ($1 ? "(v$2) " : '') . $license;
+ } elsif ($licensetext =~ /provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software/) {
+ # https://fedoraproject.org/wiki/Licensing/Apple_MIT_License
+ $license = "Apple MIT $license";
+ }
+
+ if ($licensetext =~ /Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license \([\"]?the Software[\"]?\)/ or
+ $licensetext =~ /Boost Software License([ ,-]+Version ([^ ]+)?(\.))/i) {
+ $license = "BSL " . ($1 ? "(v$2) " : '') . $license;
+ }
+
+ if ($licensetext =~ /PYTHON SOFTWARE FOUNDATION LICENSE (VERSION ([^ ]+))/i) {
+ $license = "PSF " . ($1 ? "(v$2) " : '') . $license;
+ }
+
+ if ($licensetext =~ /The origin of this software must not be misrepresented.*Altered source versions must be plainly marked as such.*This notice may not be removed or altered from any source distribution/ or
+ $licensetext =~ /see copyright notice in zlib\.h/) {
+ $license = "zlib/libpng $license";
+ } elsif ($licensetext =~ /This code is released under the libpng license/) {
+ $license = "libpng $license";
+ }
+
+ if ($licensetext =~ /under MIT license/) {
+ $license = "MIT/X11 (BSD like) $license";
+ }
+
+ if ($licensetext =~ /License MIT(-| )License/) {
+ $license = "MIT/X11 (BSD like) $license";
+ }
+
+ if ($licensetext =~ /As a special exception, you may create a larger work that contains part or all of the Bison parser skeleton and distribute that work under terms of your choice/) {
+ $license = $license . "with Bison parser exception";
+ }
+
+ if ($licensetext =~ /As a special exception to the GNU General Public License, if you distribute this file as part of a program or library that is built using GNU Libtool, you may include this file under the same distribution terms that you use for the rest of that program/) {
+ $license = $license . "with libtool exception";
+ }
+
+ if ($licensetext =~ /These materials are protected by copyright laws and contain material proprietary to the Khronos Group, Inc\. You may use these materials for implementing Khronos specifications, without altering or removing any trademark, copyright or other notice from the specification/) {
+ $license = $license . "Khronos Group";
+ }
+
+ if ($licensetext =~ /This file is part of the FreeType project, and may only be used(,)? modified(,)? and distributed under the terms of the FreeType project license, LICENSE\.TXT\. By continuing to use, modify, or distribute this file you indicate that you have read the license and understand and accept it fully/) {
+ $license = "FreeType (BSD like) $license";
+ }
+ if ($licensetext =~ /This software, and all works of authorship, whether in source or object code form as indicated by the copyright notice.*is made available, and may only be used, modified, and distributed under the FreeType Project License, LICENSE\.TXT\. Additionally, subject to the terms and conditions of the FreeType Project License, each contributor to the Work hereby grants to any individual or legal entity exercising permissions granted by the FreeType Project License and this section.*a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable.*patent license to make/) {
+ $license = "FreeType (BSD like) with patent clause $license";
+ }
+
+ if ($licensetext =~ /Anti-Grain Geometry.*Permission to copy, use, modify, sell and distribute this software is granted provided this copyright notice appears in all copies. This software is provided as is without express or impl/) {
+ $license = "Anti-Grain Geometry $license";
+ }
+
+ if ($licensetext =~ /Developed at SunSoft, a Sun Microsystems, Inc\. business\. Permission to use, copy, modify, and distribute this software is freely granted, provided that this notice is preserved\./) {
+ $license = "SunSoft (BSD like) $license";
+ }
+
+ $license = "UNKNOWN" unless $license;
+
+ # Remove trailing spaces.
+ $license =~ s/\s+$//;
+
+ return $license;
+}
+
+sub fatal($) {
+ my ($pack,$file,$line);
+ ($pack,$file,$line) = caller();
+ (my $msg = "$progname: fatal error at line $line:\n@_\n") =~ tr/\0//d;
+ $msg =~ s/\n\n$/\n/;
+ die $msg;
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl.vanilla b/chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl.vanilla
new file mode 100644
index 00000000000..d98b974adda
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/devscripts/licensecheck.pl.vanilla
@@ -0,0 +1,577 @@
+#!/usr/bin/perl -w
+# This script was originally based on the script of the same name from
+# the KDE SDK (by dfaure@kde.org)
+#
+# This version is
+# Copyright (C) 2007, 2008 Adam D. Barratt
+# Copyright (C) 2012 Francesco Poli
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+=head1 NAME
+
+licensecheck - simple license checker for source files
+
+=head1 SYNOPSIS
+
+B<licensecheck> B<--help>|B<--version>
+
+B<licensecheck> [B<--no-conf>] [B<--verbose>] [B<--copyright>]
+[B<-l>|B<--lines=>I<N>] [B<-i>|B<--ignore=>I<regex>] [B<-c>|B<--check=>I<regex>]
+[B<-m>|B<--machine>] [B<-r>|B<--recursive>]
+I<list of files and directories to check>
+
+=head1 DESCRIPTION
+
+B<licensecheck> attempts to determine the license that applies to each file
+passed to it, by searching the start of the file for text belonging to
+various licenses.
+
+If any of the arguments passed are directories, B<licensecheck> will add
+the files contained within to the list of files to process.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--verbose>, B<--no-verbose>
+
+Specify whether to output the text being processed from each file before
+the corresponding license information.
+
+Default is to be quiet.
+
+=item B<-l=>I<N>, B<--lines=>I<N>
+
+Specify the number of lines of each file's header which should be parsed
+for license information. (Default is 60).
+
+=item B<-i=>I<regex>, B<--ignore=>I<regex>
+
+When processing the list of files and directories, the regular
+expression specified by this option will be used to indicate those which
+should not be considered (e.g. backup files, VCS metadata).
+
+=item B<-r>, B<--recursive>
+
+Specify that the contents of directories should be added
+recursively.
+
+=item B<-c=>I<regex>, B<--check=>I<regex>
+
+Specify a pattern against which filenames will be matched in order to
+decide which files to check the license of.
+
+The default includes common source files.
+
+=item B<--copyright>
+
+Also display copyright text found within the file
+
+=item B<-m>, B<--machine>
+
+Display the information in a machine readable way, i.e. in the form
+<file><tab><license>[<tab><copyright>] so that it can be easily sorted
+and/or filtered, e.g. with the B<awk> and B<sort> commands.
+Note that using the B<--verbose> option will kill the readability.
+
+=item B<--no-conf>, B<--noconf>
+
+Do not read any configuration files. This can only be used as the first
+option given on the command-line.
+
+=back
+
+=head1 CONFIGURATION VARIABLES
+
+The two configuration files F</etc/devscripts.conf> and
+F<~/.devscripts> are sourced by a shell in that order to set
+configuration variables. Command line options can be used to override
+configuration file settings. Environment variable settings are
+ignored for this purpose. The currently recognised variables are:
+
+=over 4
+
+=item B<LICENSECHECK_VERBOSE>
+
+If this is set to I<yes>, then it is the same as the B<--verbose> command
+line parameter being used. The default is I<no>.
+
+=item B<LICENSECHECK_PARSELINES>
+
+If this is set to a positive number then the specified number of lines
+at the start of each file will be read whilst attempting to determine
+the license(s) in use. This is equivalent to the B<--lines> command line
+option.
+
+=back
+
+=head1 LICENSE
+
+This code is copyright by Adam D. Barratt <I<adam@adam-barratt.org.uk>>,
+all rights reserved; based on a script of the same name from the KDE
+SDK, which is copyright by <I<dfaure@kde.org>>.
+This program comes with ABSOLUTELY NO WARRANTY.
+You are free to redistribute this code under the terms of the GNU
+General Public License, version 2 or later.
+
+=head1 AUTHOR
+
+Adam D. Barratt <adam@adam-barratt.org.uk>
+
+=cut
+
+use strict;
+use warnings;
+use Getopt::Long qw(:config gnu_getopt);
+use File::Basename;
+
+sub fatal($);
+sub parse_copyright($);
+sub parselicense($);
+
+my $progname = basename($0);
+
+# From dpkg-source
+my $default_ignore_regex = '
+# Ignore general backup files
+(?:^|/).*~$|
+# Ignore emacs recovery files
+(?:^|/)\.#.*$|
+# Ignore vi swap files
+(?:^|/)\..*\.swp$|
+# Ignore baz-style junk files or directories
+(?:^|/),,.*(?:$|/.*$)|
+# File-names that should be ignored (never directories)
+(?:^|/)(?:DEADJOE|\.cvsignore|\.arch-inventory|\.bzrignore|\.gitignore)$|
+# File or directory names that should be ignored
+(?:^|/)(?:CVS|RCS|\.deps|\{arch\}|\.arch-ids|\.svn|\.hg|_darcs|\.git|
+\.shelf|_MTN|\.bzr(?:\.backup|tags)?)(?:$|/.*$)
+';
+
+# Take out comments and newlines
+$default_ignore_regex =~ s/^#.*$//mg;
+$default_ignore_regex =~ s/\n//sg;
+
+my $default_check_regex = '\.(c(c|pp|xx)?|h(h|pp|xx)?|f(77|90)?|p(l|m)|xs|sh|php|py(|x)|rb|java|vala|el|sc(i|e)|cs|pas|inc|dtd|xsl|mod|m|tex|mli?)$';
+
+my $modified_conf_msg;
+
+my ($opt_verbose, $opt_lines, $opt_noconf, $opt_ignore_regex, $opt_check_regex)
+ = ('', '', '', '', '');
+my $opt_recursive = 0;
+my $opt_copyright = 0;
+my $opt_machine = 0;
+my ($opt_help, $opt_version);
+my $def_lines = 60;
+
+# Read configuration files and then command line
+# This is boilerplate
+
+if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
+ $modified_conf_msg = " (no configuration files read)";
+ shift;
+} else {
+ my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
+ my %config_vars = (
+ 'LICENSECHECK_VERBOSE' => 'no',
+ 'LICENSECHECK_PARSELINES' => $def_lines,
+ );
+ my %config_default = %config_vars;
+
+ my $shell_cmd;
+ # Set defaults
+ foreach my $var (keys %config_vars) {
+ $shell_cmd .= qq[$var="$config_vars{$var}";\n];
+ }
+ $shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
+ $shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
+ # Read back values
+ foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
+ my $shell_out = `/bin/bash -c '$shell_cmd'`;
+ @config_vars{keys %config_vars} = split /\n/, $shell_out, -1;
+
+ # Check validity
+ $config_vars{'LICENSECHECK_VERBOSE'} =~ /^(yes|no)$/
+ or $config_vars{'LICENSECHECK_VERBOSE'} = 'no';
+ $config_vars{'LICENSECHECK_PARSELINES'} =~ /^[1-9][0-9]*$/
+ or $config_vars{'LICENSECHECK_PARSELINES'} = $def_lines;
+
+ foreach my $var (sort keys %config_vars) {
+ if ($config_vars{$var} ne $config_default{$var}) {
+ $modified_conf_msg .= " $var=$config_vars{$var}\n";
+ }
+ }
+ $modified_conf_msg ||= " (none)\n";
+ chomp $modified_conf_msg;
+
+ $opt_verbose = $config_vars{'LICENSECHECK_VERBOSE'} eq 'yes' ? 1 : 0;
+ $opt_lines = $config_vars{'LICENSECHECK_PARSELINES'};
+}
+
+GetOptions("help|h" => \$opt_help,
+ "version|v" => \$opt_version,
+ "verbose!" => \$opt_verbose,
+ "lines|l=i" => \$opt_lines,
+ "ignore|i=s" => \$opt_ignore_regex,
+ "recursive|r" => \$opt_recursive,
+ "check|c=s" => \$opt_check_regex,
+ "copyright" => \$opt_copyright,
+ "machine|m" => \$opt_machine,
+ "noconf" => \$opt_noconf,
+ "no-conf" => \$opt_noconf,
+ )
+ or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
+
+$opt_lines = $def_lines if $opt_lines !~ /^[1-9][0-9]*$/;
+$opt_ignore_regex = $default_ignore_regex if ! length $opt_ignore_regex;
+$opt_check_regex = $default_check_regex if ! length $opt_check_regex;
+
+if ($opt_noconf) {
+ fatal "--no-conf is only acceptable as the first command-line option!";
+}
+if ($opt_help) { help(); exit 0; }
+if ($opt_version) { version(); exit 0; }
+
+die "Usage: $progname [options] filelist\nRun $progname --help for more details\n" unless @ARGV;
+
+$opt_lines = $def_lines if not defined $opt_lines;
+
+my @files = ();
+my @find_args = ();
+my $files_count = @ARGV;
+
+push @find_args, qw(-maxdepth 1) unless $opt_recursive;
+push @find_args, qw(-follow -type f -print);
+
+while (@ARGV) {
+ my $file = shift @ARGV;
+
+ if (-d $file) {
+ open FIND, '-|', 'find', $file, @find_args
+ or die "$progname: couldn't exec find: $!\n";
+
+ while (<FIND>) {
+ chomp;
+ next unless m%$opt_check_regex%;
+ # Skip empty files
+ next if (-z $_);
+ push @files, $_ unless m%$opt_ignore_regex%;
+ }
+ close FIND;
+ } else {
+ next unless ($files_count == 1) or $file =~ m%$opt_check_regex%;
+ push @files, $file unless $file =~ m%$opt_ignore_regex%;
+ }
+}
+
+while (@files) {
+ my $file = shift @files;
+ my $content = '';
+ my $copyright_match;
+ my $copyright = '';
+ my $license = '';
+ my %copyrights;
+
+ open (F, "<$file") or die "Unable to access $file\n";
+ while (<F>) {
+ last if ($. > $opt_lines);
+ $content .= $_;
+ $copyright_match = parse_copyright($_);
+ if ($copyright_match) {
+ $copyrights{lc("$copyright_match")} = "$copyright_match";
+ }
+ }
+ close(F);
+
+ $copyright = join(" / ", values %copyrights);
+
+ print qq(----- $file header -----\n$content----- end header -----\n\n)
+ if $opt_verbose;
+
+ # Remove Fortran comments
+ $content =~ s/^[cC] //gm;
+ $content =~ tr/\t\r\n/ /;
+ # Remove C / C++ comments
+ $content =~ s#(\*/|/[/*])##g;
+ $content =~ tr% A-Za-z.,@;0-9\(\)/-%%cd;
+ $content =~ tr/ //s;
+
+ $license = parselicense($content);
+ if ($opt_machine) {
+ print "$file\t$license";
+ print "\t" . ($copyright or "*No copyright*") if $opt_copyright;
+ print "\n";
+ } else {
+ print "$file: ";
+ print "*No copyright* " unless $copyright;
+ print $license . "\n";
+ print " [Copyright: " . $copyright . "]\n"
+ if $copyright and $opt_copyright;
+ print "\n" if $opt_copyright;
+ }
+}
+
+sub parse_copyright($) {
+ my $copyright = '';
+ my $match;
+
+ my $copyright_indicator_regex = '
+ (?:copyright # The full word
+ |copr\. # Legally-valid abbreviation
+ |\x{00a9} # Unicode character COPYRIGHT SIGN
+ |\xc2\xa9 # Unicode copyright sign encoded in iso8859
+ |\(c\) # Legally-null representation of sign
+ )';
+ my $copyright_disindicator_regex = '
+ \b(?:info(?:rmation)? # Discussing copyright information
+ |notice # Discussing the notice
+ |and|or # Part of a sentence
+ )\b';
+
+ if (m%$copyright_indicator_regex(?::\s*|\s+)(\S.*)$%ix) {
+ $match = $1;
+
+ # Ignore lines matching "see foo for copyright information" etc.
+ if ($match !~ m%^\s*$copyright_disindicator_regex%ix) {
+ # De-cruft
+ $match =~ s/([,.])?\s*$//;
+ $match =~ s/$copyright_indicator_regex//igx;
+ $match =~ s/^\s+//;
+ $match =~ s/\s{2,}/ /g;
+ $match =~ s/\\@/@/g;
+ $copyright = $match;
+ }
+ }
+
+ return $copyright;
+}
+
+sub help {
+ print <<"EOF";
+Usage: $progname [options] filename [filename ...]
+Valid options are:
+ --help, -h Display this message
+ --version, -v Display version and copyright info
+ --no-conf, --noconf Don't read devscripts config files; must be
+ the first option given
+ --verbose Display the header of each file before its
+ license information
+ --lines, -l Specify how many lines of the file header
+ should be parsed for license information
+ (Default: $def_lines)
+ --check, -c Specify a pattern indicating which files should
+ be checked
+ (Default: '$default_check_regex')
+ --machine, -m Display in a machine readable way (good for awk)
+ --recursive, -r Add the contents of directories recursively
+ --copyright Also display the file's copyright
+ --ignore, -i Specify that files / directories matching the
+ regular expression should be ignored when
+ checking files
+ (Default: '$default_ignore_regex')
+
+Default settings modified by devscripts configuration files:
+$modified_conf_msg
+EOF
+}
+
+sub version {
+ print <<"EOF";
+This is $progname, from the Debian devscripts package, version ###VERSION###
+Copyright (C) 2007, 2008 by Adam D. Barratt <adam\@adam-barratt.org.uk>; based
+on a script of the same name from the KDE SDK by <dfaure\@kde.org>.
+
+This program comes with ABSOLUTELY NO WARRANTY.
+You are free to redistribute this code under the terms of the
+GNU General Public License, version 2, or (at your option) any
+later version.
+EOF
+}
+
+sub parselicense($) {
+ my ($licensetext) = @_;
+
+ my $gplver = "";
+ my $extrainfo = "";
+ my $license = "";
+
+ if ($licensetext =~ /version ([^, ]+?)[.,]? (?:\(?only\)?.? )?(?:of the GNU (Affero )?(Lesser |Library )?General Public License )?(as )?published by the Free Software Foundation/i or
+ $licensetext =~ /GNU (?:Affero )?(?:Lesser |Library )?General Public License (?:as )?published by the Free Software Foundation; version ([^, ]+?)[.,]? /i) {
+
+ $gplver = " (v$1)";
+ } elsif ($licensetext =~ /GNU (?:Affero )?(?:Lesser |Library )?General Public License, version (\d+(?:\.\d+)?)[ \.]/) {
+ $gplver = " (v$1)";
+ } elsif ($licensetext =~ /either version ([^ ]+)(?: of the License)?, or \(at your option\) any later version/) {
+ $gplver = " (v$1 or later)";
+ }
+
+ if ($licensetext =~ /(?:675 Mass Ave|59 Temple Place|51 Franklin Steet|02139|02111-1307)/i) {
+ $extrainfo = " (with incorrect FSF address)$extrainfo";
+ }
+
+ if ($licensetext =~ /permission (?:is (also granted|given))? to link (the code of )?this program with (any edition of )?(Qt|the Qt library)/i) {
+ $extrainfo = " (with Qt exception)$extrainfo"
+ }
+
+ if ($licensetext =~ /(All changes made in this file will be lost|DO NOT (EDIT|delete this file)|Generated (automatically|by|from)|generated.*file)/i) {
+ $license = "GENERATED FILE";
+ }
+
+ if ($licensetext =~ /is (free software.? you can redistribute it and\/or modify it|licensed) under the terms of (version [^ ]+ of )?the (GNU (Library |Lesser )General Public License|LGPL)/i) {
+ $license = "LGPL$gplver$extrainfo $license";
+ }
+
+ if ($licensetext =~ /is free software.? you can redistribute it and\/or modify it under the terms of the (GNU Affero General Public License|AGPL)/i) {
+ $license = "AGPL$gplver$extrainfo $license";
+ }
+
+ if ($licensetext =~ /is free software.? you (can|may) redistribute it and\/or modify it under the terms of (?:version [^ ]+ (?:\(?only\)? )?of )?the GNU General Public License/i) {
+ $license = "GPL$gplver$extrainfo $license";
+ }
+
+ if ($licensetext =~ /is distributed under the terms of the GNU General Public License,/
+ and length $gplver) {
+ $license = "GPL$gplver$extrainfo $license";
+ }
+
+ if ($licensetext =~ /is distributed.*terms.*GPL/) {
+ $license = "GPL (unversioned/unknown version) $license";
+ }
+
+ if ($licensetext =~ /This file is part of the .*Qt GUI Toolkit. This file may be distributed under the terms of the Q Public License as defined/) {
+ $license = "QPL (part of Qt) $license";
+ } elsif ($licensetext =~ /may be distributed under the terms of the Q Public License as defined/) {
+ $license = "QPL $license";
+ }
+
+ if ($licensetext =~ /opensource\.org\/licenses\/mit-license\.php/) {
+ $license = "MIT/X11 (BSD like) $license";
+ } elsif ($licensetext =~ /Permission is hereby granted, free of charge, to any person obtaining a copy of this software and(\/or)? associated documentation files \(the (Software|Materials)\), to deal in the (Software|Materials)/) {
+ $license = "MIT/X11 (BSD like) $license";
+ } elsif ($licensetext =~ /Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose/) {
+ $license = "MIT/X11 (BSD like) $license";
+ }
+
+ if ($licensetext =~ /Permission to use, copy, modify, and(\/or)? distribute this software for any purpose with or without fee is hereby granted, provided.*copyright notice.*permission notice.*all copies/) {
+ $license = "ISC $license";
+ }
+
+ if ($licensetext =~ /THIS SOFTWARE IS PROVIDED .*AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY/) {
+ if ($licensetext =~ /All advertising materials mentioning features or use of this software must display the following acknowledge?ment.*This product includes software developed by/i) {
+ $license = "BSD (4 clause) $license";
+ } elsif ($licensetext =~ /(The name .*? may not|Neither the names? .*? nor the names of (its|their) contributors may) be used to endorse or promote products derived from this software/i) {
+ $license = "BSD (3 clause) $license";
+ } elsif ($licensetext =~ /Redistributions of source code must retain the above copyright notice/i) {
+ $license = "BSD (2 clause) $license";
+ } else {
+ $license = "BSD $license";
+ }
+ }
+
+ if ($licensetext =~ /Mozilla Public License Version ([^ ]+)/) {
+ $license = "MPL (v$1) $license";
+ }
+
+ if ($licensetext =~ /Released under the terms of the Artistic License ([^ ]+)/) {
+ $license = "Artistic (v$1) $license";
+ }
+
+ if ($licensetext =~ /is free software under the Artistic [Ll]icense/) {
+ $license = "Artistic $license";
+ }
+
+ if ($licensetext =~ /This program is free software; you can redistribute it and\/or modify it under the same terms as Perl itself/) {
+ $license = "Perl $license";
+ }
+
+ if ($licensetext =~ /under the Apache License, Version ([^ ]+)/) {
+ $license = "Apache (v$1) $license";
+ }
+
+ if ($licensetext =~ /(THE BEER-WARE LICENSE)/i) {
+ $license = "Beerware $license";
+ }
+
+ if ($licensetext =~ /This source file is subject to version ([^ ]+) of the PHP license/) {
+ $license = "PHP (v$1) $license";
+ }
+
+ if ($licensetext =~ /under the terms of the CeCILL /) {
+ $license = "CeCILL $license";
+ }
+
+ if ($licensetext =~ /under the terms of the CeCILL-([^ ]+) /) {
+ $license = "CeCILL-$1 $license";
+ }
+
+ if ($licensetext =~ /under the SGI Free Software License B/) {
+ $license = "SGI Free Software License B $license";
+ }
+
+ if ($licensetext =~ /is in the public domain/i) {
+ $license = "Public domain $license";
+ }
+
+ if ($licensetext =~ /terms of the Common Development and Distribution License(, Version ([^(]+))? \(the License\)/) {
+ $license = "CDDL " . ($1 ? "(v$2) " : '') . $license;
+ }
+
+ if ($licensetext =~ /Microsoft Permissive License \(Ms-PL\)/) {
+ $license = "Ms-PL $license";
+ }
+
+ if ($licensetext =~ /Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license \(the \"Software\"\)/ or
+ $licensetext =~ /Boost Software License([ ,-]+Version ([^ ]+)?(\.))/i) {
+ $license = "BSL " . ($1 ? "(v$2) " : '') . $license;
+ }
+
+ if ($licensetext =~ /PYTHON SOFTWARE FOUNDATION LICENSE (VERSION ([^ ]+))/i) {
+ $license = "PSF " . ($1 ? "(v$2) " : '') . $license;
+ }
+
+ if ($licensetext =~ /The origin of this software must not be misrepresented.*Altered source versions must be plainly marked as such.*This notice may not be removed or altered from any source distribution/ or
+ $licensetext =~ /see copyright notice in zlib\.h/) {
+ $license = "zlib/libpng $license";
+ } elsif ($licensetext =~ /This code is released under the libpng license/) {
+ $license = "libpng $license";
+ }
+
+ if ($licensetext =~ /Do What The Fuck You Want To Public License, Version ([^, ]+)/i) {
+ $license = "WTFPL (v$1) $license";
+ }
+
+ if ($licensetext =~ /Do what The Fuck You Want To Public License/i) {
+ $license = "WTFPL $license";
+ }
+
+ if ($licensetext =~ /(License WTFPL|Under (the|a) WTFPL)/i) {
+ $license = "WTFPL $license";
+ }
+
+ $license = "UNKNOWN" if (!length($license));
+
+ # Remove trailing spaces.
+ $license =~ s/\s+$//;
+
+ return $license;
+}
+
+sub fatal($) {
+ my ($pack,$file,$line);
+ ($pack,$file,$line) = caller();
+ (my $msg = "$progname: fatal error at line $line:\n@_\n") =~ tr/\0//d;
+ $msg =~ s/\n\n$/\n/;
+ die $msg;
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/BUILDING.md b/chromium/third_party/catapult/tracing/third_party/gl-matrix/BUILDING.md
new file mode 100644
index 00000000000..9170ab3d5b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/BUILDING.md
@@ -0,0 +1,7 @@
+Building for the browser
+========================
+
+
+To build `gl-matrix.js` and `gl-matrix-min.js` for use in the browser run the following command:
+
+ webpack && webpack --config webpack.config.min.js \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/LICENSE.md b/chromium/third_party/catapult/tracing/third_party/gl-matrix/LICENSE.md
new file mode 100644
index 00000000000..79698edab2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/README.chromium b/chromium/third_party/catapult/tracing/third_party/gl-matrix/README.chromium
new file mode 100644
index 00000000000..4e5c3d49ea1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/README.chromium
@@ -0,0 +1,15 @@
+Name: gl-matrix
+Short Name: gl-matrix
+URL: https://github.com/toji/gl-matrix
+Version: 0
+Revision: 8fc4869031e4ab1daf771e5206c2578e044cf495
+Date: Tue Feb 12 14:21:14 2013 -0800
+License: BSD
+License File: NOT_SHIPPED
+Security Critical: no
+
+Description:
+Matrix math in javascript
+
+Local Modifications:
+None.
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/README.md b/chromium/third_party/catapult/tracing/third_party/gl-matrix/README.md
new file mode 100644
index 00000000000..ad66cb07f84
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/README.md
@@ -0,0 +1,22 @@
+glMatrix
+=======================
+
+Javascript has evolved into a language capable of handling realtime 3D graphics,
+via WebGL, and computationally intensive tasks such as physics simulations.
+These types of applications demand high performance vector and matrix math,
+which is something that Javascript doesn't provide by default.
+glMatrix to the rescue!
+
+glMatrix is designed to perform vector and matrix operations stupidly fast! By
+hand-tuning each function for maximum performance and encouraging efficient
+usage patterns through API conventions, glMatrix will help you get the most out
+of your browsers Javascript engine.
+
+Learn More
+----------------------
+For documentation, news, tutorials, and more visit the [glMatrix Homepage](http://glmatrix.net/)
+
+Contributing
+----------------------
+Contributions are welcome! Please make pull requests agains the `dev` branch,
+and please provide unit tests for new functionality. (See TESTING.md for details)
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/TESTING.md b/chromium/third_party/catapult/tracing/third_party/gl-matrix/TESTING.md
new file mode 100644
index 00000000000..affd5b27b41
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/TESTING.md
@@ -0,0 +1,12 @@
+Running the test suite
+=======================
+
+
+The unit tests are built upon the following tools:
+
+* Jasmine -- the underlying test suite which executes the test and reports feedback
+* node.js -- used for testing at the command line, via the `jasmine-node` package
+
+To run the unit tests use `jasmine-node`:
+
+ jasmine-node spec \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/VERSION b/chromium/third_party/catapult/tracing/third_party/gl-matrix/VERSION
new file mode 100644
index 00000000000..cc6612c36e0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/VERSION
@@ -0,0 +1 @@
+2.3.0 \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/bower.json b/chromium/third_party/catapult/tracing/third_party/gl-matrix/bower.json
new file mode 100644
index 00000000000..7e52c486673
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/bower.json
@@ -0,0 +1,27 @@
+{
+ "name": "gl-matrix",
+ "homepage": "http://glmatrix.net",
+ "authors": [
+ "Brandon Jones <tojiro@gmail.com>",
+ "Colin MacKenzie IV <sinisterchipmunk@gmail.com>"
+ ],
+ "description": "Javascript Matrix and Vector library for High Performance WebGL apps",
+ "main": "dist/gl-matrix-min.js",
+ "ignore": [
+ "**/.*",
+ "jsdoc-template",
+ "spec",
+ "src",
+ "tasks",
+ "Gemfile",
+ "Gemfile.lock",
+ "Rakefile",
+ "TESTING.md"
+ ],
+ "keywords": [
+ "webGL",
+ "matrix",
+ "vector"
+ ],
+ "license": "MIT"
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix-min.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix-min.js
new file mode 100644
index 00000000000..62db82edfd9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix-min.js
@@ -0,0 +1,29 @@
+/**
+ * @fileoverview gl-matrix - High performance matrix and vector operations
+ * @author Brandon Jones
+ * @author Colin MacKenzie IV
+ * @version 2.3.1
+ */
+
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+!function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define(n);else{var r=n();for(var a in r)("object"==typeof exports?exports:t)[a]=r[a]}}(this,function(){return function(t){function n(a){if(r[a])return r[a].exports;var e=r[a]={exports:{},id:a,loaded:!1};return t[a].call(e.exports,e,e.exports,n),e.loaded=!0,e.exports}var r={};return n.m=t,n.c=r,n.p="",n(0)}([function(t,n,r){n.glMatrix=r(1),n.mat2=r(2),n.mat2d=r(3),n.mat3=r(4),n.mat4=r(5),n.quat=r(6),n.vec2=r(9),n.vec3=r(7),n.vec4=r(8)},function(t,n,r){var a={};a.EPSILON=1e-6,a.ARRAY_TYPE="undefined"!=typeof Float32Array?Float32Array:Array,a.RANDOM=Math.random,a.setMatrixArrayType=function(t){GLMAT_ARRAY_TYPE=t};var e=Math.PI/180;a.toRadian=function(t){return t*e},t.exports=a},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(4);return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t},e.clone=function(t){var n=new a.ARRAY_TYPE(4);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t},e.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t},e.transpose=function(t,n){if(t===n){var r=n[1];t[1]=n[2],t[2]=r}else t[0]=n[0],t[1]=n[2],t[2]=n[1],t[3]=n[3];return t},e.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*u-e*a;return o?(o=1/o,t[0]=u*o,t[1]=-a*o,t[2]=-e*o,t[3]=r*o,t):null},e.adjoint=function(t,n){var r=n[0];return t[0]=n[3],t[1]=-n[1],t[2]=-n[2],t[3]=r,t},e.determinant=function(t){return t[0]*t[3]-t[2]*t[1]},e.multiply=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],c=r[1],f=r[2],s=r[3];return t[0]=a*i+u*c,t[1]=e*i+o*c,t[2]=a*f+u*s,t[3]=e*f+o*s,t},e.mul=e.multiply,e.rotate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),c=Math.cos(r);return t[0]=a*c+u*i,t[1]=e*c+o*i,t[2]=a*-i+u*c,t[3]=e*-i+o*c,t},e.scale=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],c=r[1];return t[0]=a*i,t[1]=e*i,t[2]=u*c,t[3]=o*c,t},e.fromRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=-r,t[3]=a,t},e.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=n[1],t},e.str=function(t){return"mat2("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"},e.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2))},e.LDU=function(t,n,r,a){return t[2]=a[2]/a[0],r[0]=a[0],r[1]=a[1],r[3]=a[3]-t[2]*r[1],[t,n,r]},t.exports=e},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(6);return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t},e.clone=function(t){var n=new a.ARRAY_TYPE(6);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t},e.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t},e.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],c=r*u-a*e;return c?(c=1/c,t[0]=u*c,t[1]=-a*c,t[2]=-e*c,t[3]=r*c,t[4]=(e*i-u*o)*c,t[5]=(a*o-r*i)*c,t):null},e.determinant=function(t){return t[0]*t[3]-t[1]*t[2]},e.multiply=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=r[0],s=r[1],h=r[2],M=r[3],l=r[4],v=r[5];return t[0]=a*f+u*s,t[1]=e*f+o*s,t[2]=a*h+u*M,t[3]=e*h+o*M,t[4]=a*l+u*v+i,t[5]=e*l+o*v+c,t},e.mul=e.multiply,e.rotate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=Math.sin(r),s=Math.cos(r);return t[0]=a*s+u*f,t[1]=e*s+o*f,t[2]=a*-f+u*s,t[3]=e*-f+o*s,t[4]=i,t[5]=c,t},e.scale=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=r[0],s=r[1];return t[0]=a*f,t[1]=e*f,t[2]=u*s,t[3]=o*s,t[4]=i,t[5]=c,t},e.translate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=r[0],s=r[1];return t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=a*f+u*s+i,t[5]=e*f+o*s+c,t},e.fromRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=-r,t[3]=a,t[4]=0,t[5]=0,t},e.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=n[1],t[4]=0,t[5]=0,t},e.fromTranslation=function(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=n[0],t[5]=n[1],t},e.str=function(t){return"mat2d("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+")"},e.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+1)},t.exports=e},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(9);return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},e.fromMat4=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[4],t[4]=n[5],t[5]=n[6],t[6]=n[8],t[7]=n[9],t[8]=n[10],t},e.clone=function(t){var n=new a.ARRAY_TYPE(9);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n[6]=t[6],n[7]=t[7],n[8]=t[8],n},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t},e.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},e.transpose=function(t,n){if(t===n){var r=n[1],a=n[2],e=n[5];t[1]=n[3],t[2]=n[6],t[3]=r,t[5]=n[7],t[6]=a,t[7]=e}else t[0]=n[0],t[1]=n[3],t[2]=n[6],t[3]=n[1],t[4]=n[4],t[5]=n[7],t[6]=n[2],t[7]=n[5],t[8]=n[8];return t},e.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],c=n[6],f=n[7],s=n[8],h=s*o-i*f,M=-s*u+i*c,l=f*u-o*c,v=r*h+a*M+e*l;return v?(v=1/v,t[0]=h*v,t[1]=(-s*a+e*f)*v,t[2]=(i*a-e*o)*v,t[3]=M*v,t[4]=(s*r-e*c)*v,t[5]=(-i*r+e*u)*v,t[6]=l*v,t[7]=(-f*r+a*c)*v,t[8]=(o*r-a*u)*v,t):null},e.adjoint=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],c=n[6],f=n[7],s=n[8];return t[0]=o*s-i*f,t[1]=e*f-a*s,t[2]=a*i-e*o,t[3]=i*c-u*s,t[4]=r*s-e*c,t[5]=e*u-r*i,t[6]=u*f-o*c,t[7]=a*c-r*f,t[8]=r*o-a*u,t},e.determinant=function(t){var n=t[0],r=t[1],a=t[2],e=t[3],u=t[4],o=t[5],i=t[6],c=t[7],f=t[8];return n*(f*u-o*c)+r*(-f*e+o*i)+a*(c*e-u*i)},e.multiply=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=n[6],s=n[7],h=n[8],M=r[0],l=r[1],v=r[2],m=r[3],p=r[4],d=r[5],A=r[6],R=r[7],w=r[8];return t[0]=M*a+l*o+v*f,t[1]=M*e+l*i+v*s,t[2]=M*u+l*c+v*h,t[3]=m*a+p*o+d*f,t[4]=m*e+p*i+d*s,t[5]=m*u+p*c+d*h,t[6]=A*a+R*o+w*f,t[7]=A*e+R*i+w*s,t[8]=A*u+R*c+w*h,t},e.mul=e.multiply,e.translate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=n[6],s=n[7],h=n[8],M=r[0],l=r[1];return t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=i,t[5]=c,t[6]=M*a+l*o+f,t[7]=M*e+l*i+s,t[8]=M*u+l*c+h,t},e.rotate=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=n[6],s=n[7],h=n[8],M=Math.sin(r),l=Math.cos(r);return t[0]=l*a+M*o,t[1]=l*e+M*i,t[2]=l*u+M*c,t[3]=l*o-M*a,t[4]=l*i-M*e,t[5]=l*c-M*u,t[6]=f,t[7]=s,t[8]=h,t},e.scale=function(t,n,r){var a=r[0],e=r[1];return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=e*n[3],t[4]=e*n[4],t[5]=e*n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t},e.fromTranslation=function(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=1,t[5]=0,t[6]=n[0],t[7]=n[1],t[8]=1,t},e.fromRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=0,t[3]=-r,t[4]=a,t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},e.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=0,t[4]=n[1],t[5]=0,t[6]=0,t[7]=0,t[8]=1,t},e.fromMat2d=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=0,t[3]=n[2],t[4]=n[3],t[5]=0,t[6]=n[4],t[7]=n[5],t[8]=1,t},e.fromQuat=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,c=e+e,f=r*o,s=a*o,h=a*i,M=e*o,l=e*i,v=e*c,m=u*o,p=u*i,d=u*c;return t[0]=1-h-v,t[3]=s-d,t[6]=M+p,t[1]=s+d,t[4]=1-f-v,t[7]=l-m,t[2]=M-p,t[5]=l+m,t[8]=1-f-h,t},e.normalFromMat4=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],c=n[6],f=n[7],s=n[8],h=n[9],M=n[10],l=n[11],v=n[12],m=n[13],p=n[14],d=n[15],A=r*i-a*o,R=r*c-e*o,w=r*f-u*o,q=a*c-e*i,Y=a*f-u*i,g=e*f-u*c,y=s*m-h*v,x=s*p-M*v,P=s*d-l*v,E=h*p-M*m,T=h*d-l*m,b=M*d-l*p,D=A*b-R*T+w*E+q*P-Y*x+g*y;return D?(D=1/D,t[0]=(i*b-c*T+f*E)*D,t[1]=(c*P-o*b-f*x)*D,t[2]=(o*T-i*P+f*y)*D,t[3]=(e*T-a*b-u*E)*D,t[4]=(r*b-e*P+u*x)*D,t[5]=(a*P-r*T-u*y)*D,t[6]=(m*g-p*Y+d*q)*D,t[7]=(p*w-v*g-d*R)*D,t[8]=(v*Y-m*w+d*A)*D,t):null},e.str=function(t){return"mat3("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+")"},e.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2))},t.exports=e},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(16);return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=1,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=1,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.clone=function(t){var n=new a.ARRAY_TYPE(16);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n[4]=t[4],n[5]=t[5],n[6]=t[6],n[7]=t[7],n[8]=t[8],n[9]=t[9],n[10]=t[10],n[11]=t[11],n[12]=t[12],n[13]=t[13],n[14]=t[14],n[15]=t[15],n},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t[9]=n[9],t[10]=n[10],t[11]=n[11],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15],t},e.identity=function(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=1,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=1,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.transpose=function(t,n){if(t===n){var r=n[1],a=n[2],e=n[3],u=n[6],o=n[7],i=n[11];t[1]=n[4],t[2]=n[8],t[3]=n[12],t[4]=r,t[6]=n[9],t[7]=n[13],t[8]=a,t[9]=u,t[11]=n[14],t[12]=e,t[13]=o,t[14]=i}else t[0]=n[0],t[1]=n[4],t[2]=n[8],t[3]=n[12],t[4]=n[1],t[5]=n[5],t[6]=n[9],t[7]=n[13],t[8]=n[2],t[9]=n[6],t[10]=n[10],t[11]=n[14],t[12]=n[3],t[13]=n[7],t[14]=n[11],t[15]=n[15];return t},e.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],c=n[6],f=n[7],s=n[8],h=n[9],M=n[10],l=n[11],v=n[12],m=n[13],p=n[14],d=n[15],A=r*i-a*o,R=r*c-e*o,w=r*f-u*o,q=a*c-e*i,Y=a*f-u*i,g=e*f-u*c,y=s*m-h*v,x=s*p-M*v,P=s*d-l*v,E=h*p-M*m,T=h*d-l*m,b=M*d-l*p,D=A*b-R*T+w*E+q*P-Y*x+g*y;return D?(D=1/D,t[0]=(i*b-c*T+f*E)*D,t[1]=(e*T-a*b-u*E)*D,t[2]=(m*g-p*Y+d*q)*D,t[3]=(M*Y-h*g-l*q)*D,t[4]=(c*P-o*b-f*x)*D,t[5]=(r*b-e*P+u*x)*D,t[6]=(p*w-v*g-d*R)*D,t[7]=(s*g-M*w+l*R)*D,t[8]=(o*T-i*P+f*y)*D,t[9]=(a*P-r*T-u*y)*D,t[10]=(v*Y-m*w+d*A)*D,t[11]=(h*w-s*Y-l*A)*D,t[12]=(i*x-o*E-c*y)*D,t[13]=(r*E-a*x+e*y)*D,t[14]=(m*R-v*q-p*A)*D,t[15]=(s*q-h*R+M*A)*D,t):null},e.adjoint=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=n[4],i=n[5],c=n[6],f=n[7],s=n[8],h=n[9],M=n[10],l=n[11],v=n[12],m=n[13],p=n[14],d=n[15];return t[0]=i*(M*d-l*p)-h*(c*d-f*p)+m*(c*l-f*M),t[1]=-(a*(M*d-l*p)-h*(e*d-u*p)+m*(e*l-u*M)),t[2]=a*(c*d-f*p)-i*(e*d-u*p)+m*(e*f-u*c),t[3]=-(a*(c*l-f*M)-i*(e*l-u*M)+h*(e*f-u*c)),t[4]=-(o*(M*d-l*p)-s*(c*d-f*p)+v*(c*l-f*M)),t[5]=r*(M*d-l*p)-s*(e*d-u*p)+v*(e*l-u*M),t[6]=-(r*(c*d-f*p)-o*(e*d-u*p)+v*(e*f-u*c)),t[7]=r*(c*l-f*M)-o*(e*l-u*M)+s*(e*f-u*c),t[8]=o*(h*d-l*m)-s*(i*d-f*m)+v*(i*l-f*h),t[9]=-(r*(h*d-l*m)-s*(a*d-u*m)+v*(a*l-u*h)),t[10]=r*(i*d-f*m)-o*(a*d-u*m)+v*(a*f-u*i),t[11]=-(r*(i*l-f*h)-o*(a*l-u*h)+s*(a*f-u*i)),t[12]=-(o*(h*p-M*m)-s*(i*p-c*m)+v*(i*M-c*h)),t[13]=r*(h*p-M*m)-s*(a*p-e*m)+v*(a*M-e*h),t[14]=-(r*(i*p-c*m)-o*(a*p-e*m)+v*(a*c-e*i)),t[15]=r*(i*M-c*h)-o*(a*M-e*h)+s*(a*c-e*i),t},e.determinant=function(t){var n=t[0],r=t[1],a=t[2],e=t[3],u=t[4],o=t[5],i=t[6],c=t[7],f=t[8],s=t[9],h=t[10],M=t[11],l=t[12],v=t[13],m=t[14],p=t[15],d=n*o-r*u,A=n*i-a*u,R=n*c-e*u,w=r*i-a*o,q=r*c-e*o,Y=a*c-e*i,g=f*v-s*l,y=f*m-h*l,x=f*p-M*l,P=s*m-h*v,E=s*p-M*v,T=h*p-M*m;return d*T-A*E+R*P+w*x-q*y+Y*g},e.multiply=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=n[6],s=n[7],h=n[8],M=n[9],l=n[10],v=n[11],m=n[12],p=n[13],d=n[14],A=n[15],R=r[0],w=r[1],q=r[2],Y=r[3];return t[0]=R*a+w*i+q*h+Y*m,t[1]=R*e+w*c+q*M+Y*p,t[2]=R*u+w*f+q*l+Y*d,t[3]=R*o+w*s+q*v+Y*A,R=r[4],w=r[5],q=r[6],Y=r[7],t[4]=R*a+w*i+q*h+Y*m,t[5]=R*e+w*c+q*M+Y*p,t[6]=R*u+w*f+q*l+Y*d,t[7]=R*o+w*s+q*v+Y*A,R=r[8],w=r[9],q=r[10],Y=r[11],t[8]=R*a+w*i+q*h+Y*m,t[9]=R*e+w*c+q*M+Y*p,t[10]=R*u+w*f+q*l+Y*d,t[11]=R*o+w*s+q*v+Y*A,R=r[12],w=r[13],q=r[14],Y=r[15],t[12]=R*a+w*i+q*h+Y*m,t[13]=R*e+w*c+q*M+Y*p,t[14]=R*u+w*f+q*l+Y*d,t[15]=R*o+w*s+q*v+Y*A,t},e.mul=e.multiply,e.translate=function(t,n,r){var a,e,u,o,i,c,f,s,h,M,l,v,m=r[0],p=r[1],d=r[2];return n===t?(t[12]=n[0]*m+n[4]*p+n[8]*d+n[12],t[13]=n[1]*m+n[5]*p+n[9]*d+n[13],t[14]=n[2]*m+n[6]*p+n[10]*d+n[14],t[15]=n[3]*m+n[7]*p+n[11]*d+n[15]):(a=n[0],e=n[1],u=n[2],o=n[3],i=n[4],c=n[5],f=n[6],s=n[7],h=n[8],M=n[9],l=n[10],v=n[11],t[0]=a,t[1]=e,t[2]=u,t[3]=o,t[4]=i,t[5]=c,t[6]=f,t[7]=s,t[8]=h,t[9]=M,t[10]=l,t[11]=v,t[12]=a*m+i*p+h*d+n[12],t[13]=e*m+c*p+M*d+n[13],t[14]=u*m+f*p+l*d+n[14],t[15]=o*m+s*p+v*d+n[15]),t},e.scale=function(t,n,r){var a=r[0],e=r[1],u=r[2];return t[0]=n[0]*a,t[1]=n[1]*a,t[2]=n[2]*a,t[3]=n[3]*a,t[4]=n[4]*e,t[5]=n[5]*e,t[6]=n[6]*e,t[7]=n[7]*e,t[8]=n[8]*u,t[9]=n[9]*u,t[10]=n[10]*u,t[11]=n[11]*u,t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15],t},e.rotate=function(t,n,r,e){var u,o,i,c,f,s,h,M,l,v,m,p,d,A,R,w,q,Y,g,y,x,P,E,T,b=e[0],D=e[1],L=e[2],_=Math.sqrt(b*b+D*D+L*L);return Math.abs(_)<a.EPSILON?null:(_=1/_,b*=_,D*=_,L*=_,u=Math.sin(r),o=Math.cos(r),i=1-o,c=n[0],f=n[1],s=n[2],h=n[3],M=n[4],l=n[5],v=n[6],m=n[7],p=n[8],d=n[9],A=n[10],R=n[11],w=b*b*i+o,q=D*b*i+L*u,Y=L*b*i-D*u,g=b*D*i-L*u,y=D*D*i+o,x=L*D*i+b*u,P=b*L*i+D*u,E=D*L*i-b*u,T=L*L*i+o,t[0]=c*w+M*q+p*Y,t[1]=f*w+l*q+d*Y,t[2]=s*w+v*q+A*Y,t[3]=h*w+m*q+R*Y,t[4]=c*g+M*y+p*x,t[5]=f*g+l*y+d*x,t[6]=s*g+v*y+A*x,t[7]=h*g+m*y+R*x,t[8]=c*P+M*E+p*T,t[9]=f*P+l*E+d*T,t[10]=s*P+v*E+A*T,t[11]=h*P+m*E+R*T,n!==t&&(t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15]),t)},e.rotateX=function(t,n,r){var a=Math.sin(r),e=Math.cos(r),u=n[4],o=n[5],i=n[6],c=n[7],f=n[8],s=n[9],h=n[10],M=n[11];return n!==t&&(t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15]),t[4]=u*e+f*a,t[5]=o*e+s*a,t[6]=i*e+h*a,t[7]=c*e+M*a,t[8]=f*e-u*a,t[9]=s*e-o*a,t[10]=h*e-i*a,t[11]=M*e-c*a,t},e.rotateY=function(t,n,r){var a=Math.sin(r),e=Math.cos(r),u=n[0],o=n[1],i=n[2],c=n[3],f=n[8],s=n[9],h=n[10],M=n[11];return n!==t&&(t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15]),t[0]=u*e-f*a,t[1]=o*e-s*a,t[2]=i*e-h*a,t[3]=c*e-M*a,t[8]=u*a+f*e,t[9]=o*a+s*e,t[10]=i*a+h*e,t[11]=c*a+M*e,t},e.rotateZ=function(t,n,r){var a=Math.sin(r),e=Math.cos(r),u=n[0],o=n[1],i=n[2],c=n[3],f=n[4],s=n[5],h=n[6],M=n[7];return n!==t&&(t[8]=n[8],t[9]=n[9],t[10]=n[10],t[11]=n[11],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15]),t[0]=u*e+f*a,t[1]=o*e+s*a,t[2]=i*e+h*a,t[3]=c*e+M*a,t[4]=f*e-u*a,t[5]=s*e-o*a,t[6]=h*e-i*a,t[7]=M*e-c*a,t},e.fromTranslation=function(t,n){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=1,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=1,t[11]=0,t[12]=n[0],t[13]=n[1],t[14]=n[2],t[15]=1,t},e.fromScaling=function(t,n){return t[0]=n[0],t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=n[1],t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=n[2],t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.fromRotation=function(t,n,r){var e,u,o,i=r[0],c=r[1],f=r[2],s=Math.sqrt(i*i+c*c+f*f);return Math.abs(s)<a.EPSILON?null:(s=1/s,i*=s,c*=s,f*=s,e=Math.sin(n),u=Math.cos(n),o=1-u,t[0]=i*i*o+u,t[1]=c*i*o+f*e,t[2]=f*i*o-c*e,t[3]=0,t[4]=i*c*o-f*e,t[5]=c*c*o+u,t[6]=f*c*o+i*e,t[7]=0,t[8]=i*f*o+c*e,t[9]=c*f*o-i*e,t[10]=f*f*o+u,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t)},e.fromXRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=a,t[6]=r,t[7]=0,t[8]=0,t[9]=-r,t[10]=a,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.fromYRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=0,t[2]=-r,t[3]=0,t[4]=0,t[5]=1,t[6]=0,t[7]=0,t[8]=r,t[9]=0,t[10]=a,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.fromZRotation=function(t,n){var r=Math.sin(n),a=Math.cos(n);return t[0]=a,t[1]=r,t[2]=0,t[3]=0,t[4]=-r,t[5]=a,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=1,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.fromRotationTranslation=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=a+a,c=e+e,f=u+u,s=a*i,h=a*c,M=a*f,l=e*c,v=e*f,m=u*f,p=o*i,d=o*c,A=o*f;return t[0]=1-(l+m),t[1]=h+A,t[2]=M-d,t[3]=0,t[4]=h-A,t[5]=1-(s+m),t[6]=v+p,t[7]=0,t[8]=M+d,t[9]=v-p,t[10]=1-(s+l),t[11]=0,t[12]=r[0],t[13]=r[1],t[14]=r[2],t[15]=1,t},e.fromRotationTranslationScale=function(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3],c=e+e,f=u+u,s=o+o,h=e*c,M=e*f,l=e*s,v=u*f,m=u*s,p=o*s,d=i*c,A=i*f,R=i*s,w=a[0],q=a[1],Y=a[2];return t[0]=(1-(v+p))*w,t[1]=(M+R)*w,t[2]=(l-A)*w,t[3]=0,t[4]=(M-R)*q,t[5]=(1-(h+p))*q,t[6]=(m+d)*q,t[7]=0,t[8]=(l+A)*Y,t[9]=(m-d)*Y,t[10]=(1-(h+v))*Y,t[11]=0,t[12]=r[0],t[13]=r[1],t[14]=r[2],t[15]=1,t},e.fromRotationTranslationScaleOrigin=function(t,n,r,a,e){var u=n[0],o=n[1],i=n[2],c=n[3],f=u+u,s=o+o,h=i+i,M=u*f,l=u*s,v=u*h,m=o*s,p=o*h,d=i*h,A=c*f,R=c*s,w=c*h,q=a[0],Y=a[1],g=a[2],y=e[0],x=e[1],P=e[2];return t[0]=(1-(m+d))*q,t[1]=(l+w)*q,t[2]=(v-R)*q,t[3]=0,t[4]=(l-w)*Y,t[5]=(1-(M+d))*Y,t[6]=(p+A)*Y,t[7]=0,t[8]=(v+R)*g,t[9]=(p-A)*g,t[10]=(1-(M+m))*g,t[11]=0,t[12]=r[0]+y-(t[0]*y+t[4]*x+t[8]*P),t[13]=r[1]+x-(t[1]*y+t[5]*x+t[9]*P),t[14]=r[2]+P-(t[2]*y+t[6]*x+t[10]*P),t[15]=1,t},e.fromQuat=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r+r,i=a+a,c=e+e,f=r*o,s=a*o,h=a*i,M=e*o,l=e*i,v=e*c,m=u*o,p=u*i,d=u*c;return t[0]=1-h-v,t[1]=s+d,t[2]=M-p,t[3]=0,t[4]=s-d,t[5]=1-f-v,t[6]=l+m,t[7]=0,t[8]=M+p,t[9]=l-m,t[10]=1-f-h,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},e.frustum=function(t,n,r,a,e,u,o){var i=1/(r-n),c=1/(e-a),f=1/(u-o);return t[0]=2*u*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=2*u*c,t[6]=0,t[7]=0,t[8]=(r+n)*i,t[9]=(e+a)*c,t[10]=(o+u)*f,t[11]=-1,t[12]=0,t[13]=0,t[14]=o*u*2*f,t[15]=0,t},e.perspective=function(t,n,r,a,e){var u=1/Math.tan(n/2),o=1/(a-e);return t[0]=u/r,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=u,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=(e+a)*o,t[11]=-1,t[12]=0,t[13]=0,t[14]=2*e*a*o,t[15]=0,t},e.perspectiveFromFieldOfView=function(t,n,r,a){var e=Math.tan(n.upDegrees*Math.PI/180),u=Math.tan(n.downDegrees*Math.PI/180),o=Math.tan(n.leftDegrees*Math.PI/180),i=Math.tan(n.rightDegrees*Math.PI/180),c=2/(o+i),f=2/(e+u);return t[0]=c,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=f,t[6]=0,t[7]=0,t[8]=-((o-i)*c*.5),t[9]=(e-u)*f*.5,t[10]=a/(r-a),t[11]=-1,t[12]=0,t[13]=0,t[14]=a*r/(r-a),t[15]=0,t},e.ortho=function(t,n,r,a,e,u,o){var i=1/(n-r),c=1/(a-e),f=1/(u-o);return t[0]=-2*i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=-2*c,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=2*f,t[11]=0,t[12]=(n+r)*i,t[13]=(e+a)*c,t[14]=(o+u)*f,t[15]=1,t},e.lookAt=function(t,n,r,u){var o,i,c,f,s,h,M,l,v,m,p=n[0],d=n[1],A=n[2],R=u[0],w=u[1],q=u[2],Y=r[0],g=r[1],y=r[2];return Math.abs(p-Y)<a.EPSILON&&Math.abs(d-g)<a.EPSILON&&Math.abs(A-y)<a.EPSILON?e.identity(t):(M=p-Y,l=d-g,v=A-y,m=1/Math.sqrt(M*M+l*l+v*v),M*=m,l*=m,v*=m,o=w*v-q*l,i=q*M-R*v,c=R*l-w*M,m=Math.sqrt(o*o+i*i+c*c),m?(m=1/m,o*=m,i*=m,c*=m):(o=0,i=0,c=0),f=l*c-v*i,s=v*o-M*c,h=M*i-l*o,m=Math.sqrt(f*f+s*s+h*h),m?(m=1/m,f*=m,s*=m,h*=m):(f=0,s=0,h=0),t[0]=o,t[1]=f,t[2]=M,t[3]=0,t[4]=i,t[5]=s,t[6]=l,t[7]=0,t[8]=c,t[9]=h,t[10]=v,t[11]=0,t[12]=-(o*p+i*d+c*A),t[13]=-(f*p+s*d+h*A),t[14]=-(M*p+l*d+v*A),t[15]=1,t)},e.str=function(t){return"mat4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+", "+t[4]+", "+t[5]+", "+t[6]+", "+t[7]+", "+t[8]+", "+t[9]+", "+t[10]+", "+t[11]+", "+t[12]+", "+t[13]+", "+t[14]+", "+t[15]+")"},e.frob=function(t){return Math.sqrt(Math.pow(t[0],2)+Math.pow(t[1],2)+Math.pow(t[2],2)+Math.pow(t[3],2)+Math.pow(t[4],2)+Math.pow(t[5],2)+Math.pow(t[6],2)+Math.pow(t[7],2)+Math.pow(t[8],2)+Math.pow(t[9],2)+Math.pow(t[10],2)+Math.pow(t[11],2)+Math.pow(t[12],2)+Math.pow(t[13],2)+Math.pow(t[14],2)+Math.pow(t[15],2))},t.exports=e},function(t,n,r){var a=r(1),e=r(4),u=r(7),o=r(8),i={};i.create=function(){var t=new a.ARRAY_TYPE(4);return t[0]=0,t[1]=0,t[2]=0,t[3]=1,t},i.rotationTo=function(){var t=u.create(),n=u.fromValues(1,0,0),r=u.fromValues(0,1,0);return function(a,e,o){var c=u.dot(e,o);return-.999999>c?(u.cross(t,n,e),u.length(t)<1e-6&&u.cross(t,r,e),u.normalize(t,t),i.setAxisAngle(a,t,Math.PI),a):c>.999999?(a[0]=0,a[1]=0,a[2]=0,a[3]=1,a):(u.cross(t,e,o),a[0]=t[0],a[1]=t[1],a[2]=t[2],a[3]=1+c,i.normalize(a,a))}}(),i.setAxes=function(){var t=e.create();return function(n,r,a,e){return t[0]=a[0],t[3]=a[1],t[6]=a[2],t[1]=e[0],t[4]=e[1],t[7]=e[2],t[2]=-r[0],t[5]=-r[1],t[8]=-r[2],i.normalize(n,i.fromMat3(n,t))}}(),i.clone=o.clone,i.fromValues=o.fromValues,i.copy=o.copy,i.set=o.set,i.identity=function(t){return t[0]=0,t[1]=0,t[2]=0,t[3]=1,t},i.setAxisAngle=function(t,n,r){r=.5*r;var a=Math.sin(r);return t[0]=a*n[0],t[1]=a*n[1],t[2]=a*n[2],t[3]=Math.cos(r),t},i.add=o.add,i.multiply=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3],i=r[0],c=r[1],f=r[2],s=r[3];return t[0]=a*s+o*i+e*f-u*c,t[1]=e*s+o*c+u*i-a*f,t[2]=u*s+o*f+a*c-e*i,t[3]=o*s-a*i-e*c-u*f,t},i.mul=i.multiply,i.scale=o.scale,i.rotateX=function(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),c=Math.cos(r);return t[0]=a*c+o*i,t[1]=e*c+u*i,t[2]=u*c-e*i,t[3]=o*c-a*i,t},i.rotateY=function(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),c=Math.cos(r);return t[0]=a*c-u*i,t[1]=e*c+o*i,t[2]=u*c+a*i,t[3]=o*c-e*i,t},i.rotateZ=function(t,n,r){r*=.5;var a=n[0],e=n[1],u=n[2],o=n[3],i=Math.sin(r),c=Math.cos(r);return t[0]=a*c+e*i,t[1]=e*c-a*i,t[2]=u*c+o*i,t[3]=o*c-u*i,t},i.calculateW=function(t,n){var r=n[0],a=n[1],e=n[2];return t[0]=r,t[1]=a,t[2]=e,t[3]=Math.sqrt(Math.abs(1-r*r-a*a-e*e)),t},i.dot=o.dot,i.lerp=o.lerp,i.slerp=function(t,n,r,a){var e,u,o,i,c,f=n[0],s=n[1],h=n[2],M=n[3],l=r[0],v=r[1],m=r[2],p=r[3];return u=f*l+s*v+h*m+M*p,0>u&&(u=-u,l=-l,v=-v,m=-m,p=-p),1-u>1e-6?(e=Math.acos(u),o=Math.sin(e),i=Math.sin((1-a)*e)/o,c=Math.sin(a*e)/o):(i=1-a,c=a),t[0]=i*f+c*l,t[1]=i*s+c*v,t[2]=i*h+c*m,t[3]=i*M+c*p,t},i.sqlerp=function(){var t=i.create(),n=i.create();return function(r,a,e,u,o,c){return i.slerp(t,a,o,c),i.slerp(n,e,u,c),i.slerp(r,t,n,2*c*(1-c)),r}}(),i.invert=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*r+a*a+e*e+u*u,i=o?1/o:0;return t[0]=-r*i,t[1]=-a*i,t[2]=-e*i,t[3]=u*i,t},i.conjugate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t[3]=n[3],t},i.length=o.length,i.len=i.length,i.squaredLength=o.squaredLength,i.sqrLen=i.squaredLength,i.normalize=o.normalize,i.fromMat3=function(t,n){var r,a=n[0]+n[4]+n[8];if(a>0)r=Math.sqrt(a+1),t[3]=.5*r,r=.5/r,t[0]=(n[5]-n[7])*r,t[1]=(n[6]-n[2])*r,t[2]=(n[1]-n[3])*r;else{var e=0;n[4]>n[0]&&(e=1),n[8]>n[3*e+e]&&(e=2);var u=(e+1)%3,o=(e+2)%3;r=Math.sqrt(n[3*e+e]-n[3*u+u]-n[3*o+o]+1),t[e]=.5*r,r=.5/r,t[3]=(n[3*u+o]-n[3*o+u])*r,t[u]=(n[3*u+e]+n[3*e+u])*r,t[o]=(n[3*o+e]+n[3*e+o])*r}return t},i.str=function(t){return"quat("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"},t.exports=i},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(3);return t[0]=0,t[1]=0,t[2]=0,t},e.clone=function(t){var n=new a.ARRAY_TYPE(3);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n},e.fromValues=function(t,n,r){var e=new a.ARRAY_TYPE(3);return e[0]=t,e[1]=n,e[2]=r,e},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t},e.set=function(t,n,r,a){return t[0]=n,t[1]=r,t[2]=a,t},e.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t},e.subtract=function(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t},e.sub=e.subtract,e.multiply=function(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t[2]=n[2]*r[2],t},e.mul=e.multiply,e.divide=function(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t[2]=n[2]/r[2],t},e.div=e.divide,e.min=function(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t[2]=Math.min(n[2],r[2]),t},e.max=function(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t[2]=Math.max(n[2],r[2]),t},e.scale=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t},e.scaleAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t},e.distance=function(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return Math.sqrt(r*r+a*a+e*e)},e.dist=e.distance,e.squaredDistance=function(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2];return r*r+a*a+e*e},e.sqrDist=e.squaredDistance,e.length=function(t){var n=t[0],r=t[1],a=t[2];return Math.sqrt(n*n+r*r+a*a)},e.len=e.length,e.squaredLength=function(t){var n=t[0],r=t[1],a=t[2];return n*n+r*r+a*a},e.sqrLen=e.squaredLength,e.negate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t},e.inverse=function(t,n){return t[0]=1/n[0],t[1]=1/n[1],t[2]=1/n[2],t},e.normalize=function(t,n){var r=n[0],a=n[1],e=n[2],u=r*r+a*a+e*e;return u>0&&(u=1/Math.sqrt(u),t[0]=n[0]*u,t[1]=n[1]*u,t[2]=n[2]*u),t},e.dot=function(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]},e.cross=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],c=r[2];return t[0]=e*c-u*i,t[1]=u*o-a*c,t[2]=a*i-e*o,t},e.lerp=function(t,n,r,a){var e=n[0],u=n[1],o=n[2];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t},e.hermite=function(t,n,r,a,e,u){var o=u*u,i=o*(2*u-3)+1,c=o*(u-2)+u,f=o*(u-1),s=o*(3-2*u);return t[0]=n[0]*i+r[0]*c+a[0]*f+e[0]*s,t[1]=n[1]*i+r[1]*c+a[1]*f+e[1]*s,t[2]=n[2]*i+r[2]*c+a[2]*f+e[2]*s,t},e.bezier=function(t,n,r,a,e,u){var o=1-u,i=o*o,c=u*u,f=i*o,s=3*u*i,h=3*c*o,M=c*u;return t[0]=n[0]*f+r[0]*s+a[0]*h+e[0]*M,t[1]=n[1]*f+r[1]*s+a[1]*h+e[1]*M,t[2]=n[2]*f+r[2]*s+a[2]*h+e[2]*M,t},e.random=function(t,n){n=n||1;var r=2*a.RANDOM()*Math.PI,e=2*a.RANDOM()-1,u=Math.sqrt(1-e*e)*n;return t[0]=Math.cos(r)*u,t[1]=Math.sin(r)*u,t[2]=e*n,t},e.transformMat4=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[3]*a+r[7]*e+r[11]*u+r[15];return o=o||1,t[0]=(r[0]*a+r[4]*e+r[8]*u+r[12])/o,t[1]=(r[1]*a+r[5]*e+r[9]*u+r[13])/o,t[2]=(r[2]*a+r[6]*e+r[10]*u+r[14])/o,t},e.transformMat3=function(t,n,r){var a=n[0],e=n[1],u=n[2];return t[0]=a*r[0]+e*r[3]+u*r[6],t[1]=a*r[1]+e*r[4]+u*r[7],t[2]=a*r[2]+e*r[5]+u*r[8],t},e.transformQuat=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],c=r[2],f=r[3],s=f*a+i*u-c*e,h=f*e+c*a-o*u,M=f*u+o*e-i*a,l=-o*a-i*e-c*u;return t[0]=s*f+l*-o+h*-c-M*-i,t[1]=h*f+l*-i+M*-o-s*-c,t[2]=M*f+l*-c+s*-i-h*-o,t},e.rotateX=function(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[0],u[1]=e[1]*Math.cos(a)-e[2]*Math.sin(a),u[2]=e[1]*Math.sin(a)+e[2]*Math.cos(a),t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t},e.rotateY=function(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[2]*Math.sin(a)+e[0]*Math.cos(a),u[1]=e[1],u[2]=e[2]*Math.cos(a)-e[0]*Math.sin(a),t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t},e.rotateZ=function(t,n,r,a){var e=[],u=[];return e[0]=n[0]-r[0],e[1]=n[1]-r[1],e[2]=n[2]-r[2],u[0]=e[0]*Math.cos(a)-e[1]*Math.sin(a),u[1]=e[0]*Math.sin(a)+e[1]*Math.cos(a),u[2]=e[2],t[0]=u[0]+r[0],t[1]=u[1]+r[1],t[2]=u[2]+r[2],t},e.forEach=function(){var t=e.create();return function(n,r,a,e,u,o){var i,c;for(r||(r=3),a||(a=0),c=e?Math.min(e*r+a,n.length):n.length,i=a;c>i;i+=r)t[0]=n[i],t[1]=n[i+1],t[2]=n[i+2],u(t,t,o),n[i]=t[0],n[i+1]=t[1],n[i+2]=t[2];return n}}(),e.angle=function(t,n){var r=e.fromValues(t[0],t[1],t[2]),a=e.fromValues(n[0],n[1],n[2]);e.normalize(r,r),e.normalize(a,a);var u=e.dot(r,a);return u>1?0:Math.acos(u)},e.str=function(t){return"vec3("+t[0]+", "+t[1]+", "+t[2]+")"},t.exports=e},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(4);return t[0]=0,t[1]=0,t[2]=0,t[3]=0,t},e.clone=function(t){var n=new a.ARRAY_TYPE(4);return n[0]=t[0],n[1]=t[1],n[2]=t[2],n[3]=t[3],n},e.fromValues=function(t,n,r,e){var u=new a.ARRAY_TYPE(4);return u[0]=t,u[1]=n,u[2]=r,u[3]=e,u},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t},e.set=function(t,n,r,a,e){return t[0]=n,t[1]=r,t[2]=a,t[3]=e,t},e.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t[2]=n[2]+r[2],t[3]=n[3]+r[3],t},e.subtract=function(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t[2]=n[2]-r[2],t[3]=n[3]-r[3],t},e.sub=e.subtract,e.multiply=function(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t[2]=n[2]*r[2],t[3]=n[3]*r[3],t},e.mul=e.multiply,e.divide=function(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t[2]=n[2]/r[2],t[3]=n[3]/r[3],t},e.div=e.divide,e.min=function(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t[2]=Math.min(n[2],r[2]),t[3]=Math.min(n[3],r[3]),t},e.max=function(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t[2]=Math.max(n[2],r[2]),t[3]=Math.max(n[3],r[3]),t},e.scale=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t[2]=n[2]*r,t[3]=n[3]*r,t},e.scaleAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t[2]=n[2]+r[2]*a,t[3]=n[3]+r[3]*a,t},e.distance=function(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2],u=n[3]-t[3];return Math.sqrt(r*r+a*a+e*e+u*u)},e.dist=e.distance,e.squaredDistance=function(t,n){var r=n[0]-t[0],a=n[1]-t[1],e=n[2]-t[2],u=n[3]-t[3];return r*r+a*a+e*e+u*u},e.sqrDist=e.squaredDistance,e.length=function(t){var n=t[0],r=t[1],a=t[2],e=t[3];return Math.sqrt(n*n+r*r+a*a+e*e)},e.len=e.length,e.squaredLength=function(t){var n=t[0],r=t[1],a=t[2],e=t[3];return n*n+r*r+a*a+e*e},e.sqrLen=e.squaredLength,e.negate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t[2]=-n[2],t[3]=-n[3],t},e.inverse=function(t,n){return t[0]=1/n[0],t[1]=1/n[1],t[2]=1/n[2],t[3]=1/n[3],t},e.normalize=function(t,n){var r=n[0],a=n[1],e=n[2],u=n[3],o=r*r+a*a+e*e+u*u;return o>0&&(o=1/Math.sqrt(o),t[0]=r*o,t[1]=a*o,t[2]=e*o,t[3]=u*o),t},e.dot=function(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]+t[3]*n[3]},e.lerp=function(t,n,r,a){var e=n[0],u=n[1],o=n[2],i=n[3];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t[2]=o+a*(r[2]-o),t[3]=i+a*(r[3]-i),t},e.random=function(t,n){return n=n||1,t[0]=a.RANDOM(),t[1]=a.RANDOM(),t[2]=a.RANDOM(),t[3]=a.RANDOM(),e.normalize(t,t),e.scale(t,t,n),t},e.transformMat4=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=n[3];return t[0]=r[0]*a+r[4]*e+r[8]*u+r[12]*o,t[1]=r[1]*a+r[5]*e+r[9]*u+r[13]*o,t[2]=r[2]*a+r[6]*e+r[10]*u+r[14]*o,t[3]=r[3]*a+r[7]*e+r[11]*u+r[15]*o,t},e.transformQuat=function(t,n,r){var a=n[0],e=n[1],u=n[2],o=r[0],i=r[1],c=r[2],f=r[3],s=f*a+i*u-c*e,h=f*e+c*a-o*u,M=f*u+o*e-i*a,l=-o*a-i*e-c*u;return t[0]=s*f+l*-o+h*-c-M*-i,t[1]=h*f+l*-i+M*-o-s*-c,t[2]=M*f+l*-c+s*-i-h*-o,t[3]=n[3],t},e.forEach=function(){var t=e.create();return function(n,r,a,e,u,o){var i,c;for(r||(r=4),a||(a=0),c=e?Math.min(e*r+a,n.length):n.length,i=a;c>i;i+=r)t[0]=n[i],t[1]=n[i+1],t[2]=n[i+2],t[3]=n[i+3],u(t,t,o),n[i]=t[0],n[i+1]=t[1],n[i+2]=t[2],n[i+3]=t[3];return n}}(),e.str=function(t){return"vec4("+t[0]+", "+t[1]+", "+t[2]+", "+t[3]+")"},t.exports=e},function(t,n,r){var a=r(1),e={};e.create=function(){var t=new a.ARRAY_TYPE(2);return t[0]=0,t[1]=0,t},e.clone=function(t){var n=new a.ARRAY_TYPE(2);return n[0]=t[0],n[1]=t[1],n},e.fromValues=function(t,n){var r=new a.ARRAY_TYPE(2);return r[0]=t,r[1]=n,r},e.copy=function(t,n){return t[0]=n[0],t[1]=n[1],t},e.set=function(t,n,r){return t[0]=n,t[1]=r,t},e.add=function(t,n,r){return t[0]=n[0]+r[0],t[1]=n[1]+r[1],t},e.subtract=function(t,n,r){return t[0]=n[0]-r[0],t[1]=n[1]-r[1],t},e.sub=e.subtract,e.multiply=function(t,n,r){return t[0]=n[0]*r[0],t[1]=n[1]*r[1],t},e.mul=e.multiply,e.divide=function(t,n,r){return t[0]=n[0]/r[0],t[1]=n[1]/r[1],t},e.div=e.divide,e.min=function(t,n,r){return t[0]=Math.min(n[0],r[0]),t[1]=Math.min(n[1],r[1]),t},e.max=function(t,n,r){return t[0]=Math.max(n[0],r[0]),t[1]=Math.max(n[1],r[1]),t},e.scale=function(t,n,r){return t[0]=n[0]*r,t[1]=n[1]*r,t},e.scaleAndAdd=function(t,n,r,a){return t[0]=n[0]+r[0]*a,t[1]=n[1]+r[1]*a,t},e.distance=function(t,n){var r=n[0]-t[0],a=n[1]-t[1];return Math.sqrt(r*r+a*a)},e.dist=e.distance,e.squaredDistance=function(t,n){var r=n[0]-t[0],a=n[1]-t[1];return r*r+a*a},e.sqrDist=e.squaredDistance,e.length=function(t){var n=t[0],r=t[1];return Math.sqrt(n*n+r*r)},e.len=e.length,e.squaredLength=function(t){var n=t[0],r=t[1];return n*n+r*r},e.sqrLen=e.squaredLength,e.negate=function(t,n){return t[0]=-n[0],t[1]=-n[1],t},e.inverse=function(t,n){return t[0]=1/n[0],t[1]=1/n[1],t},e.normalize=function(t,n){var r=n[0],a=n[1],e=r*r+a*a;return e>0&&(e=1/Math.sqrt(e),t[0]=n[0]*e,t[1]=n[1]*e),t},e.dot=function(t,n){return t[0]*n[0]+t[1]*n[1]},e.cross=function(t,n,r){var a=n[0]*r[1]-n[1]*r[0];return t[0]=t[1]=0,t[2]=a,t},e.lerp=function(t,n,r,a){var e=n[0],u=n[1];return t[0]=e+a*(r[0]-e),t[1]=u+a*(r[1]-u),t},e.random=function(t,n){n=n||1;var r=2*a.RANDOM()*Math.PI;return t[0]=Math.cos(r)*n,t[1]=Math.sin(r)*n,t},e.transformMat2=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e,t[1]=r[1]*a+r[3]*e,t},e.transformMat2d=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[2]*e+r[4],t[1]=r[1]*a+r[3]*e+r[5],t},e.transformMat3=function(t,n,r){
+var a=n[0],e=n[1];return t[0]=r[0]*a+r[3]*e+r[6],t[1]=r[1]*a+r[4]*e+r[7],t},e.transformMat4=function(t,n,r){var a=n[0],e=n[1];return t[0]=r[0]*a+r[4]*e+r[12],t[1]=r[1]*a+r[5]*e+r[13],t},e.forEach=function(){var t=e.create();return function(n,r,a,e,u,o){var i,c;for(r||(r=2),a||(a=0),c=e?Math.min(e*r+a,n.length):n.length,i=a;c>i;i+=r)t[0]=n[i],t[1]=n[i+1],u(t,t,o),n[i]=t[0],n[i+1]=t[1];return n}}(),e.str=function(t){return"vec2("+t[0]+", "+t[1]+")"},t.exports=e}])}); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix.js
new file mode 100644
index 00000000000..ab1e9b0aa58
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/dist/gl-matrix.js
@@ -0,0 +1,5020 @@
+/**
+ * @fileoverview gl-matrix - High performance matrix and vector operations
+ * @author Brandon Jones
+ * @author Colin MacKenzie IV
+ * @version 2.3.1
+ */
+
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define(factory);
+ else {
+ var a = factory();
+ for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
+ }
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * @fileoverview gl-matrix - High performance matrix and vector operations
+ * @author Brandon Jones
+ * @author Colin MacKenzie IV
+ * @version 2.3.1
+ */
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+ // END HEADER
+
+ exports.glMatrix = __webpack_require__(1);
+ exports.mat2 = __webpack_require__(2);
+ exports.mat2d = __webpack_require__(3);
+ exports.mat3 = __webpack_require__(4);
+ exports.mat4 = __webpack_require__(5);
+ exports.quat = __webpack_require__(6);
+ exports.vec2 = __webpack_require__(9);
+ exports.vec3 = __webpack_require__(7);
+ exports.vec4 = __webpack_require__(8);
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ /**
+ * @class Common utilities
+ * @name glMatrix
+ */
+ var glMatrix = {};
+
+ // Constants
+ glMatrix.EPSILON = 0.000001;
+ glMatrix.ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array;
+ glMatrix.RANDOM = Math.random;
+
+ /**
+ * Sets the type of array used when creating new vectors and matrices
+ *
+ * @param {Type} type Array type, such as Float32Array or Array
+ */
+ glMatrix.setMatrixArrayType = function(type) {
+ GLMAT_ARRAY_TYPE = type;
+ }
+
+ var degree = Math.PI / 180;
+
+ /**
+ * Convert Degree To Radian
+ *
+ * @param {Number} Angle in Degrees
+ */
+ glMatrix.toRadian = function(a){
+ return a * degree;
+ }
+
+ module.exports = glMatrix;
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 2x2 Matrix
+ * @name mat2
+ */
+ var mat2 = {};
+
+ /**
+ * Creates a new identity mat2
+ *
+ * @returns {mat2} a new 2x2 matrix
+ */
+ mat2.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+ };
+
+ /**
+ * Creates a new mat2 initialized with values from an existing matrix
+ *
+ * @param {mat2} a matrix to clone
+ * @returns {mat2} a new 2x2 matrix
+ */
+ mat2.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+ };
+
+ /**
+ * Copy the values from one mat2 to another
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+ mat2.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+ };
+
+ /**
+ * Set a mat2 to the identity matrix
+ *
+ * @param {mat2} out the receiving matrix
+ * @returns {mat2} out
+ */
+ mat2.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+ };
+
+ /**
+ * Transpose the values of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+ mat2.transpose = function(out, a) {
+ // If we are transposing ourselves we can skip a few steps but have to cache some values
+ if (out === a) {
+ var a1 = a[1];
+ out[1] = a[2];
+ out[2] = a1;
+ } else {
+ out[0] = a[0];
+ out[1] = a[2];
+ out[2] = a[1];
+ out[3] = a[3];
+ }
+
+ return out;
+ };
+
+ /**
+ * Inverts a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+ mat2.invert = function(out, a) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+
+ // Calculate the determinant
+ det = a0 * a3 - a2 * a1;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = a3 * det;
+ out[1] = -a1 * det;
+ out[2] = -a2 * det;
+ out[3] = a0 * det;
+
+ return out;
+ };
+
+ /**
+ * Calculates the adjugate of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+ mat2.adjoint = function(out, a) {
+ // Caching this value is nessecary if out == a
+ var a0 = a[0];
+ out[0] = a[3];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ out[3] = a0;
+
+ return out;
+ };
+
+ /**
+ * Calculates the determinant of a mat2
+ *
+ * @param {mat2} a the source matrix
+ * @returns {Number} determinant of a
+ */
+ mat2.determinant = function (a) {
+ return a[0] * a[3] - a[2] * a[1];
+ };
+
+ /**
+ * Multiplies two mat2's
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the first operand
+ * @param {mat2} b the second operand
+ * @returns {mat2} out
+ */
+ mat2.multiply = function (out, a, b) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3];
+ var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+ out[0] = a0 * b0 + a2 * b1;
+ out[1] = a1 * b0 + a3 * b1;
+ out[2] = a0 * b2 + a2 * b3;
+ out[3] = a1 * b2 + a3 * b3;
+ return out;
+ };
+
+ /**
+ * Alias for {@link mat2.multiply}
+ * @function
+ */
+ mat2.mul = mat2.multiply;
+
+ /**
+ * Rotates a mat2 by the given angle
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2} out
+ */
+ mat2.rotate = function (out, a, rad) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+ s = Math.sin(rad),
+ c = Math.cos(rad);
+ out[0] = a0 * c + a2 * s;
+ out[1] = a1 * c + a3 * s;
+ out[2] = a0 * -s + a2 * c;
+ out[3] = a1 * -s + a3 * c;
+ return out;
+ };
+
+ /**
+ * Scales the mat2 by the dimensions in the given vec2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2} out
+ **/
+ mat2.scale = function(out, a, v) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+ v0 = v[0], v1 = v[1];
+ out[0] = a0 * v0;
+ out[1] = a1 * v0;
+ out[2] = a2 * v1;
+ out[3] = a3 * v1;
+ return out;
+ };
+
+ /**
+ * Creates a matrix from a given angle
+ * This is equivalent to (but much faster than):
+ *
+ * mat2.identity(dest);
+ * mat2.rotate(dest, dest, rad);
+ *
+ * @param {mat2} out mat2 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2} out
+ */
+ mat2.fromRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+ out[0] = c;
+ out[1] = s;
+ out[2] = -s;
+ out[3] = c;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat2.identity(dest);
+ * mat2.scale(dest, dest, vec);
+ *
+ * @param {mat2} out mat2 receiving operation result
+ * @param {vec2} v Scaling vector
+ * @returns {mat2} out
+ */
+ mat2.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = v[1];
+ return out;
+ }
+
+ /**
+ * Returns a string representation of a mat2
+ *
+ * @param {mat2} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+ mat2.str = function (a) {
+ return 'mat2(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+ };
+
+ /**
+ * Returns Frobenius norm of a mat2
+ *
+ * @param {mat2} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+ mat2.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2)))
+ };
+
+ /**
+ * Returns L, D and U matrices (Lower triangular, Diagonal and Upper triangular) by factorizing the input matrix
+ * @param {mat2} L the lower triangular matrix
+ * @param {mat2} D the diagonal matrix
+ * @param {mat2} U the upper triangular matrix
+ * @param {mat2} a the input matrix to factorize
+ */
+
+ mat2.LDU = function (L, D, U, a) {
+ L[2] = a[2]/a[0];
+ U[0] = a[0];
+ U[1] = a[1];
+ U[3] = a[3] - L[2] * U[1];
+ return [L, D, U];
+ };
+
+
+ module.exports = mat2;
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 2x3 Matrix
+ * @name mat2d
+ *
+ * @description
+ * A mat2d contains six elements defined as:
+ * <pre>
+ * [a, c, tx,
+ * b, d, ty]
+ * </pre>
+ * This is a short form for the 3x3 matrix:
+ * <pre>
+ * [a, c, tx,
+ * b, d, ty,
+ * 0, 0, 1]
+ * </pre>
+ * The last row is ignored so the array is shorter and operations are faster.
+ */
+ var mat2d = {};
+
+ /**
+ * Creates a new identity mat2d
+ *
+ * @returns {mat2d} a new 2x3 matrix
+ */
+ mat2d.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(6);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+ };
+
+ /**
+ * Creates a new mat2d initialized with values from an existing matrix
+ *
+ * @param {mat2d} a matrix to clone
+ * @returns {mat2d} a new 2x3 matrix
+ */
+ mat2d.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(6);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ return out;
+ };
+
+ /**
+ * Copy the values from one mat2d to another
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+ mat2d.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ return out;
+ };
+
+ /**
+ * Set a mat2d to the identity matrix
+ *
+ * @param {mat2d} out the receiving matrix
+ * @returns {mat2d} out
+ */
+ mat2d.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+ };
+
+ /**
+ * Inverts a mat2d
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+ mat2d.invert = function(out, a) {
+ var aa = a[0], ab = a[1], ac = a[2], ad = a[3],
+ atx = a[4], aty = a[5];
+
+ var det = aa * ad - ab * ac;
+ if(!det){
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = ad * det;
+ out[1] = -ab * det;
+ out[2] = -ac * det;
+ out[3] = aa * det;
+ out[4] = (ac * aty - ad * atx) * det;
+ out[5] = (ab * atx - aa * aty) * det;
+ return out;
+ };
+
+ /**
+ * Calculates the determinant of a mat2d
+ *
+ * @param {mat2d} a the source matrix
+ * @returns {Number} determinant of a
+ */
+ mat2d.determinant = function (a) {
+ return a[0] * a[3] - a[1] * a[2];
+ };
+
+ /**
+ * Multiplies two mat2d's
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the first operand
+ * @param {mat2d} b the second operand
+ * @returns {mat2d} out
+ */
+ mat2d.multiply = function (out, a, b) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
+ out[0] = a0 * b0 + a2 * b1;
+ out[1] = a1 * b0 + a3 * b1;
+ out[2] = a0 * b2 + a2 * b3;
+ out[3] = a1 * b2 + a3 * b3;
+ out[4] = a0 * b4 + a2 * b5 + a4;
+ out[5] = a1 * b4 + a3 * b5 + a5;
+ return out;
+ };
+
+ /**
+ * Alias for {@link mat2d.multiply}
+ * @function
+ */
+ mat2d.mul = mat2d.multiply;
+
+ /**
+ * Rotates a mat2d by the given angle
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2d} out
+ */
+ mat2d.rotate = function (out, a, rad) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ s = Math.sin(rad),
+ c = Math.cos(rad);
+ out[0] = a0 * c + a2 * s;
+ out[1] = a1 * c + a3 * s;
+ out[2] = a0 * -s + a2 * c;
+ out[3] = a1 * -s + a3 * c;
+ out[4] = a4;
+ out[5] = a5;
+ return out;
+ };
+
+ /**
+ * Scales the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2d} out
+ **/
+ mat2d.scale = function(out, a, v) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ v0 = v[0], v1 = v[1];
+ out[0] = a0 * v0;
+ out[1] = a1 * v0;
+ out[2] = a2 * v1;
+ out[3] = a3 * v1;
+ out[4] = a4;
+ out[5] = a5;
+ return out;
+ };
+
+ /**
+ * Translates the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to translate the matrix by
+ * @returns {mat2d} out
+ **/
+ mat2d.translate = function(out, a, v) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ v0 = v[0], v1 = v[1];
+ out[0] = a0;
+ out[1] = a1;
+ out[2] = a2;
+ out[3] = a3;
+ out[4] = a0 * v0 + a2 * v1 + a4;
+ out[5] = a1 * v0 + a3 * v1 + a5;
+ return out;
+ };
+
+ /**
+ * Creates a matrix from a given angle
+ * This is equivalent to (but much faster than):
+ *
+ * mat2d.identity(dest);
+ * mat2d.rotate(dest, dest, rad);
+ *
+ * @param {mat2d} out mat2d receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2d} out
+ */
+ mat2d.fromRotation = function(out, rad) {
+ var s = Math.sin(rad), c = Math.cos(rad);
+ out[0] = c;
+ out[1] = s;
+ out[2] = -s;
+ out[3] = c;
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat2d.identity(dest);
+ * mat2d.scale(dest, dest, vec);
+ *
+ * @param {mat2d} out mat2d receiving operation result
+ * @param {vec2} v Scaling vector
+ * @returns {mat2d} out
+ */
+ mat2d.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = v[1];
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat2d.identity(dest);
+ * mat2d.translate(dest, dest, vec);
+ *
+ * @param {mat2d} out mat2d receiving operation result
+ * @param {vec2} v Translation vector
+ * @returns {mat2d} out
+ */
+ mat2d.fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ out[4] = v[0];
+ out[5] = v[1];
+ return out;
+ }
+
+ /**
+ * Returns a string representation of a mat2d
+ *
+ * @param {mat2d} a matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+ mat2d.str = function (a) {
+ return 'mat2d(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' +
+ a[3] + ', ' + a[4] + ', ' + a[5] + ')';
+ };
+
+ /**
+ * Returns Frobenius norm of a mat2d
+ *
+ * @param {mat2d} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+ mat2d.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + 1))
+ };
+
+ module.exports = mat2d;
+
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 3x3 Matrix
+ * @name mat3
+ */
+ var mat3 = {};
+
+ /**
+ * Creates a new identity mat3
+ *
+ * @returns {mat3} a new 3x3 matrix
+ */
+ mat3.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(9);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 1;
+ out[5] = 0;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+ };
+
+ /**
+ * Copies the upper-left 3x3 values into the given mat3.
+ *
+ * @param {mat3} out the receiving 3x3 matrix
+ * @param {mat4} a the source 4x4 matrix
+ * @returns {mat3} out
+ */
+ mat3.fromMat4 = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[4];
+ out[4] = a[5];
+ out[5] = a[6];
+ out[6] = a[8];
+ out[7] = a[9];
+ out[8] = a[10];
+ return out;
+ };
+
+ /**
+ * Creates a new mat3 initialized with values from an existing matrix
+ *
+ * @param {mat3} a matrix to clone
+ * @returns {mat3} a new 3x3 matrix
+ */
+ mat3.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(9);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ return out;
+ };
+
+ /**
+ * Copy the values from one mat3 to another
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+ mat3.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ return out;
+ };
+
+ /**
+ * Set a mat3 to the identity matrix
+ *
+ * @param {mat3} out the receiving matrix
+ * @returns {mat3} out
+ */
+ mat3.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 1;
+ out[5] = 0;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+ };
+
+ /**
+ * Transpose the values of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+ mat3.transpose = function(out, a) {
+ // If we are transposing ourselves we can skip a few steps but have to cache some values
+ if (out === a) {
+ var a01 = a[1], a02 = a[2], a12 = a[5];
+ out[1] = a[3];
+ out[2] = a[6];
+ out[3] = a01;
+ out[5] = a[7];
+ out[6] = a02;
+ out[7] = a12;
+ } else {
+ out[0] = a[0];
+ out[1] = a[3];
+ out[2] = a[6];
+ out[3] = a[1];
+ out[4] = a[4];
+ out[5] = a[7];
+ out[6] = a[2];
+ out[7] = a[5];
+ out[8] = a[8];
+ }
+
+ return out;
+ };
+
+ /**
+ * Inverts a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+ mat3.invert = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+
+ b01 = a22 * a11 - a12 * a21,
+ b11 = -a22 * a10 + a12 * a20,
+ b21 = a21 * a10 - a11 * a20,
+
+ // Calculate the determinant
+ det = a00 * b01 + a01 * b11 + a02 * b21;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = b01 * det;
+ out[1] = (-a22 * a01 + a02 * a21) * det;
+ out[2] = (a12 * a01 - a02 * a11) * det;
+ out[3] = b11 * det;
+ out[4] = (a22 * a00 - a02 * a20) * det;
+ out[5] = (-a12 * a00 + a02 * a10) * det;
+ out[6] = b21 * det;
+ out[7] = (-a21 * a00 + a01 * a20) * det;
+ out[8] = (a11 * a00 - a01 * a10) * det;
+ return out;
+ };
+
+ /**
+ * Calculates the adjugate of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+ mat3.adjoint = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8];
+
+ out[0] = (a11 * a22 - a12 * a21);
+ out[1] = (a02 * a21 - a01 * a22);
+ out[2] = (a01 * a12 - a02 * a11);
+ out[3] = (a12 * a20 - a10 * a22);
+ out[4] = (a00 * a22 - a02 * a20);
+ out[5] = (a02 * a10 - a00 * a12);
+ out[6] = (a10 * a21 - a11 * a20);
+ out[7] = (a01 * a20 - a00 * a21);
+ out[8] = (a00 * a11 - a01 * a10);
+ return out;
+ };
+
+ /**
+ * Calculates the determinant of a mat3
+ *
+ * @param {mat3} a the source matrix
+ * @returns {Number} determinant of a
+ */
+ mat3.determinant = function (a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8];
+
+ return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20);
+ };
+
+ /**
+ * Multiplies two mat3's
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the first operand
+ * @param {mat3} b the second operand
+ * @returns {mat3} out
+ */
+ mat3.multiply = function (out, a, b) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+
+ b00 = b[0], b01 = b[1], b02 = b[2],
+ b10 = b[3], b11 = b[4], b12 = b[5],
+ b20 = b[6], b21 = b[7], b22 = b[8];
+
+ out[0] = b00 * a00 + b01 * a10 + b02 * a20;
+ out[1] = b00 * a01 + b01 * a11 + b02 * a21;
+ out[2] = b00 * a02 + b01 * a12 + b02 * a22;
+
+ out[3] = b10 * a00 + b11 * a10 + b12 * a20;
+ out[4] = b10 * a01 + b11 * a11 + b12 * a21;
+ out[5] = b10 * a02 + b11 * a12 + b12 * a22;
+
+ out[6] = b20 * a00 + b21 * a10 + b22 * a20;
+ out[7] = b20 * a01 + b21 * a11 + b22 * a21;
+ out[8] = b20 * a02 + b21 * a12 + b22 * a22;
+ return out;
+ };
+
+ /**
+ * Alias for {@link mat3.multiply}
+ * @function
+ */
+ mat3.mul = mat3.multiply;
+
+ /**
+ * Translate a mat3 by the given vector
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to translate
+ * @param {vec2} v vector to translate by
+ * @returns {mat3} out
+ */
+ mat3.translate = function(out, a, v) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+ x = v[0], y = v[1];
+
+ out[0] = a00;
+ out[1] = a01;
+ out[2] = a02;
+
+ out[3] = a10;
+ out[4] = a11;
+ out[5] = a12;
+
+ out[6] = x * a00 + y * a10 + a20;
+ out[7] = x * a01 + y * a11 + a21;
+ out[8] = x * a02 + y * a12 + a22;
+ return out;
+ };
+
+ /**
+ * Rotates a mat3 by the given angle
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat3} out
+ */
+ mat3.rotate = function (out, a, rad) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+
+ s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ out[0] = c * a00 + s * a10;
+ out[1] = c * a01 + s * a11;
+ out[2] = c * a02 + s * a12;
+
+ out[3] = c * a10 - s * a00;
+ out[4] = c * a11 - s * a01;
+ out[5] = c * a12 - s * a02;
+
+ out[6] = a20;
+ out[7] = a21;
+ out[8] = a22;
+ return out;
+ };
+
+ /**
+ * Scales the mat3 by the dimensions in the given vec2
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat3} out
+ **/
+ mat3.scale = function(out, a, v) {
+ var x = v[0], y = v[1];
+
+ out[0] = x * a[0];
+ out[1] = x * a[1];
+ out[2] = x * a[2];
+
+ out[3] = y * a[3];
+ out[4] = y * a[4];
+ out[5] = y * a[5];
+
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ return out;
+ };
+
+ /**
+ * Creates a matrix from a vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat3.identity(dest);
+ * mat3.translate(dest, dest, vec);
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {vec2} v Translation vector
+ * @returns {mat3} out
+ */
+ mat3.fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 1;
+ out[5] = 0;
+ out[6] = v[0];
+ out[7] = v[1];
+ out[8] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a given angle
+ * This is equivalent to (but much faster than):
+ *
+ * mat3.identity(dest);
+ * mat3.rotate(dest, dest, rad);
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat3} out
+ */
+ mat3.fromRotation = function(out, rad) {
+ var s = Math.sin(rad), c = Math.cos(rad);
+
+ out[0] = c;
+ out[1] = s;
+ out[2] = 0;
+
+ out[3] = -s;
+ out[4] = c;
+ out[5] = 0;
+
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat3.identity(dest);
+ * mat3.scale(dest, dest, vec);
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {vec2} v Scaling vector
+ * @returns {mat3} out
+ */
+ mat3.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+
+ out[3] = 0;
+ out[4] = v[1];
+ out[5] = 0;
+
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+ }
+
+ /**
+ * Copies the values from a mat2d into a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat2d} a the matrix to copy
+ * @returns {mat3} out
+ **/
+ mat3.fromMat2d = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = 0;
+
+ out[3] = a[2];
+ out[4] = a[3];
+ out[5] = 0;
+
+ out[6] = a[4];
+ out[7] = a[5];
+ out[8] = 1;
+ return out;
+ };
+
+ /**
+ * Calculates a 3x3 matrix from the given quaternion
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {quat} q Quaternion to create matrix from
+ *
+ * @returns {mat3} out
+ */
+ mat3.fromQuat = function (out, q) {
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ yx = y * x2,
+ yy = y * y2,
+ zx = z * x2,
+ zy = z * y2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2;
+
+ out[0] = 1 - yy - zz;
+ out[3] = yx - wz;
+ out[6] = zx + wy;
+
+ out[1] = yx + wz;
+ out[4] = 1 - xx - zz;
+ out[7] = zy - wx;
+
+ out[2] = zx - wy;
+ out[5] = zy + wx;
+ out[8] = 1 - xx - yy;
+
+ return out;
+ };
+
+ /**
+ * Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {mat4} a Mat4 to derive the normal matrix from
+ *
+ * @returns {mat3} out
+ */
+ mat3.normalFromMat4 = function (out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+ b00 = a00 * a11 - a01 * a10,
+ b01 = a00 * a12 - a02 * a10,
+ b02 = a00 * a13 - a03 * a10,
+ b03 = a01 * a12 - a02 * a11,
+ b04 = a01 * a13 - a03 * a11,
+ b05 = a02 * a13 - a03 * a12,
+ b06 = a20 * a31 - a21 * a30,
+ b07 = a20 * a32 - a22 * a30,
+ b08 = a20 * a33 - a23 * a30,
+ b09 = a21 * a32 - a22 * a31,
+ b10 = a21 * a33 - a23 * a31,
+ b11 = a22 * a33 - a23 * a32,
+
+ // Calculate the determinant
+ det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+ out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+ out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+
+ out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+ out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+ out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+
+ out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+ out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+ out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+
+ return out;
+ };
+
+ /**
+ * Returns a string representation of a mat3
+ *
+ * @param {mat3} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+ mat3.str = function (a) {
+ return 'mat3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' +
+ a[3] + ', ' + a[4] + ', ' + a[5] + ', ' +
+ a[6] + ', ' + a[7] + ', ' + a[8] + ')';
+ };
+
+ /**
+ * Returns Frobenius norm of a mat3
+ *
+ * @param {mat3} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+ mat3.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2)))
+ };
+
+
+ module.exports = mat3;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 4x4 Matrix
+ * @name mat4
+ */
+ var mat4 = {};
+
+ /**
+ * Creates a new identity mat4
+ *
+ * @returns {mat4} a new 4x4 matrix
+ */
+ mat4.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(16);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ };
+
+ /**
+ * Creates a new mat4 initialized with values from an existing matrix
+ *
+ * @param {mat4} a matrix to clone
+ * @returns {mat4} a new 4x4 matrix
+ */
+ mat4.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(16);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ out[9] = a[9];
+ out[10] = a[10];
+ out[11] = a[11];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+ };
+
+ /**
+ * Copy the values from one mat4 to another
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+ mat4.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ out[9] = a[9];
+ out[10] = a[10];
+ out[11] = a[11];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+ };
+
+ /**
+ * Set a mat4 to the identity matrix
+ *
+ * @param {mat4} out the receiving matrix
+ * @returns {mat4} out
+ */
+ mat4.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ };
+
+ /**
+ * Transpose the values of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+ mat4.transpose = function(out, a) {
+ // If we are transposing ourselves we can skip a few steps but have to cache some values
+ if (out === a) {
+ var a01 = a[1], a02 = a[2], a03 = a[3],
+ a12 = a[6], a13 = a[7],
+ a23 = a[11];
+
+ out[1] = a[4];
+ out[2] = a[8];
+ out[3] = a[12];
+ out[4] = a01;
+ out[6] = a[9];
+ out[7] = a[13];
+ out[8] = a02;
+ out[9] = a12;
+ out[11] = a[14];
+ out[12] = a03;
+ out[13] = a13;
+ out[14] = a23;
+ } else {
+ out[0] = a[0];
+ out[1] = a[4];
+ out[2] = a[8];
+ out[3] = a[12];
+ out[4] = a[1];
+ out[5] = a[5];
+ out[6] = a[9];
+ out[7] = a[13];
+ out[8] = a[2];
+ out[9] = a[6];
+ out[10] = a[10];
+ out[11] = a[14];
+ out[12] = a[3];
+ out[13] = a[7];
+ out[14] = a[11];
+ out[15] = a[15];
+ }
+
+ return out;
+ };
+
+ /**
+ * Inverts a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+ mat4.invert = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+ b00 = a00 * a11 - a01 * a10,
+ b01 = a00 * a12 - a02 * a10,
+ b02 = a00 * a13 - a03 * a10,
+ b03 = a01 * a12 - a02 * a11,
+ b04 = a01 * a13 - a03 * a11,
+ b05 = a02 * a13 - a03 * a12,
+ b06 = a20 * a31 - a21 * a30,
+ b07 = a20 * a32 - a22 * a30,
+ b08 = a20 * a33 - a23 * a30,
+ b09 = a21 * a32 - a22 * a31,
+ b10 = a21 * a33 - a23 * a31,
+ b11 = a22 * a33 - a23 * a32,
+
+ // Calculate the determinant
+ det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
+
+ return out;
+ };
+
+ /**
+ * Calculates the adjugate of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+ mat4.adjoint = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+ out[0] = (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22));
+ out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22));
+ out[2] = (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12));
+ out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12));
+ out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22));
+ out[5] = (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22));
+ out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12));
+ out[7] = (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12));
+ out[8] = (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21));
+ out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21));
+ out[10] = (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11));
+ out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11));
+ out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21));
+ out[13] = (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21));
+ out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11));
+ out[15] = (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11));
+ return out;
+ };
+
+ /**
+ * Calculates the determinant of a mat4
+ *
+ * @param {mat4} a the source matrix
+ * @returns {Number} determinant of a
+ */
+ mat4.determinant = function (a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+ b00 = a00 * a11 - a01 * a10,
+ b01 = a00 * a12 - a02 * a10,
+ b02 = a00 * a13 - a03 * a10,
+ b03 = a01 * a12 - a02 * a11,
+ b04 = a01 * a13 - a03 * a11,
+ b05 = a02 * a13 - a03 * a12,
+ b06 = a20 * a31 - a21 * a30,
+ b07 = a20 * a32 - a22 * a30,
+ b08 = a20 * a33 - a23 * a30,
+ b09 = a21 * a32 - a22 * a31,
+ b10 = a21 * a33 - a23 * a31,
+ b11 = a22 * a33 - a23 * a32;
+
+ // Calculate the determinant
+ return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+ };
+
+ /**
+ * Multiplies two mat4's
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the first operand
+ * @param {mat4} b the second operand
+ * @returns {mat4} out
+ */
+ mat4.multiply = function (out, a, b) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+ // Cache only the current line of the second matrix
+ var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+ out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
+ out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
+ out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
+ out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+ return out;
+ };
+
+ /**
+ * Alias for {@link mat4.multiply}
+ * @function
+ */
+ mat4.mul = mat4.multiply;
+
+ /**
+ * Translate a mat4 by the given vector
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to translate
+ * @param {vec3} v vector to translate by
+ * @returns {mat4} out
+ */
+ mat4.translate = function (out, a, v) {
+ var x = v[0], y = v[1], z = v[2],
+ a00, a01, a02, a03,
+ a10, a11, a12, a13,
+ a20, a21, a22, a23;
+
+ if (a === out) {
+ out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
+ out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
+ out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
+ out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
+ } else {
+ a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+ a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+ a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+ out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03;
+ out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13;
+ out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23;
+
+ out[12] = a00 * x + a10 * y + a20 * z + a[12];
+ out[13] = a01 * x + a11 * y + a21 * z + a[13];
+ out[14] = a02 * x + a12 * y + a22 * z + a[14];
+ out[15] = a03 * x + a13 * y + a23 * z + a[15];
+ }
+
+ return out;
+ };
+
+ /**
+ * Scales the mat4 by the dimensions in the given vec3
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to scale
+ * @param {vec3} v the vec3 to scale the matrix by
+ * @returns {mat4} out
+ **/
+ mat4.scale = function(out, a, v) {
+ var x = v[0], y = v[1], z = v[2];
+
+ out[0] = a[0] * x;
+ out[1] = a[1] * x;
+ out[2] = a[2] * x;
+ out[3] = a[3] * x;
+ out[4] = a[4] * y;
+ out[5] = a[5] * y;
+ out[6] = a[6] * y;
+ out[7] = a[7] * y;
+ out[8] = a[8] * z;
+ out[9] = a[9] * z;
+ out[10] = a[10] * z;
+ out[11] = a[11] * z;
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+ };
+
+ /**
+ * Rotates a mat4 by the given angle around the given axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @param {vec3} axis the axis to rotate around
+ * @returns {mat4} out
+ */
+ mat4.rotate = function (out, a, rad, axis) {
+ var x = axis[0], y = axis[1], z = axis[2],
+ len = Math.sqrt(x * x + y * y + z * z),
+ s, c, t,
+ a00, a01, a02, a03,
+ a10, a11, a12, a13,
+ a20, a21, a22, a23,
+ b00, b01, b02,
+ b10, b11, b12,
+ b20, b21, b22;
+
+ if (Math.abs(len) < glMatrix.EPSILON) { return null; }
+
+ len = 1 / len;
+ x *= len;
+ y *= len;
+ z *= len;
+
+ s = Math.sin(rad);
+ c = Math.cos(rad);
+ t = 1 - c;
+
+ a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+ a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+ a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+ // Construct the elements of the rotation matrix
+ b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s;
+ b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s;
+ b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c;
+
+ // Perform rotation-specific matrix multiplication
+ out[0] = a00 * b00 + a10 * b01 + a20 * b02;
+ out[1] = a01 * b00 + a11 * b01 + a21 * b02;
+ out[2] = a02 * b00 + a12 * b01 + a22 * b02;
+ out[3] = a03 * b00 + a13 * b01 + a23 * b02;
+ out[4] = a00 * b10 + a10 * b11 + a20 * b12;
+ out[5] = a01 * b10 + a11 * b11 + a21 * b12;
+ out[6] = a02 * b10 + a12 * b11 + a22 * b12;
+ out[7] = a03 * b10 + a13 * b11 + a23 * b12;
+ out[8] = a00 * b20 + a10 * b21 + a20 * b22;
+ out[9] = a01 * b20 + a11 * b21 + a21 * b22;
+ out[10] = a02 * b20 + a12 * b21 + a22 * b22;
+ out[11] = a03 * b20 + a13 * b21 + a23 * b22;
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged last row
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+ return out;
+ };
+
+ /**
+ * Rotates a matrix by the given angle around the X axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+ mat4.rotateX = function (out, a, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad),
+ a10 = a[4],
+ a11 = a[5],
+ a12 = a[6],
+ a13 = a[7],
+ a20 = a[8],
+ a21 = a[9],
+ a22 = a[10],
+ a23 = a[11];
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged rows
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+
+ // Perform axis-specific matrix multiplication
+ out[4] = a10 * c + a20 * s;
+ out[5] = a11 * c + a21 * s;
+ out[6] = a12 * c + a22 * s;
+ out[7] = a13 * c + a23 * s;
+ out[8] = a20 * c - a10 * s;
+ out[9] = a21 * c - a11 * s;
+ out[10] = a22 * c - a12 * s;
+ out[11] = a23 * c - a13 * s;
+ return out;
+ };
+
+ /**
+ * Rotates a matrix by the given angle around the Y axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+ mat4.rotateY = function (out, a, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad),
+ a00 = a[0],
+ a01 = a[1],
+ a02 = a[2],
+ a03 = a[3],
+ a20 = a[8],
+ a21 = a[9],
+ a22 = a[10],
+ a23 = a[11];
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged rows
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+
+ // Perform axis-specific matrix multiplication
+ out[0] = a00 * c - a20 * s;
+ out[1] = a01 * c - a21 * s;
+ out[2] = a02 * c - a22 * s;
+ out[3] = a03 * c - a23 * s;
+ out[8] = a00 * s + a20 * c;
+ out[9] = a01 * s + a21 * c;
+ out[10] = a02 * s + a22 * c;
+ out[11] = a03 * s + a23 * c;
+ return out;
+ };
+
+ /**
+ * Rotates a matrix by the given angle around the Z axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+ mat4.rotateZ = function (out, a, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad),
+ a00 = a[0],
+ a01 = a[1],
+ a02 = a[2],
+ a03 = a[3],
+ a10 = a[4],
+ a11 = a[5],
+ a12 = a[6],
+ a13 = a[7];
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged last row
+ out[8] = a[8];
+ out[9] = a[9];
+ out[10] = a[10];
+ out[11] = a[11];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+
+ // Perform axis-specific matrix multiplication
+ out[0] = a00 * c + a10 * s;
+ out[1] = a01 * c + a11 * s;
+ out[2] = a02 * c + a12 * s;
+ out[3] = a03 * c + a13 * s;
+ out[4] = a10 * c - a00 * s;
+ out[5] = a11 * c - a01 * s;
+ out[6] = a12 * c - a02 * s;
+ out[7] = a13 * c - a03 * s;
+ return out;
+ };
+
+ /**
+ * Creates a matrix from a vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, dest, vec);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {vec3} v Translation vector
+ * @returns {mat4} out
+ */
+ mat4.fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.scale(dest, dest, vec);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {vec3} v Scaling vector
+ * @returns {mat4} out
+ */
+ mat4.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = v[1];
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = v[2];
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a given angle around a given axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotate(dest, dest, rad, axis);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @param {vec3} axis the axis to rotate around
+ * @returns {mat4} out
+ */
+ mat4.fromRotation = function(out, rad, axis) {
+ var x = axis[0], y = axis[1], z = axis[2],
+ len = Math.sqrt(x * x + y * y + z * z),
+ s, c, t;
+
+ if (Math.abs(len) < glMatrix.EPSILON) { return null; }
+
+ len = 1 / len;
+ x *= len;
+ y *= len;
+ z *= len;
+
+ s = Math.sin(rad);
+ c = Math.cos(rad);
+ t = 1 - c;
+
+ // Perform rotation-specific matrix multiplication
+ out[0] = x * x * t + c;
+ out[1] = y * x * t + z * s;
+ out[2] = z * x * t - y * s;
+ out[3] = 0;
+ out[4] = x * y * t - z * s;
+ out[5] = y * y * t + c;
+ out[6] = z * y * t + x * s;
+ out[7] = 0;
+ out[8] = x * z * t + y * s;
+ out[9] = y * z * t - x * s;
+ out[10] = z * z * t + c;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from the given angle around the X axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotateX(dest, dest, rad);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+ mat4.fromXRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ // Perform axis-specific matrix multiplication
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = c;
+ out[6] = s;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = -s;
+ out[10] = c;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from the given angle around the Y axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotateY(dest, dest, rad);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+ mat4.fromYRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ // Perform axis-specific matrix multiplication
+ out[0] = c;
+ out[1] = 0;
+ out[2] = -s;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = s;
+ out[9] = 0;
+ out[10] = c;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from the given angle around the Z axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotateZ(dest, dest, rad);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+ mat4.fromZRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ // Perform axis-specific matrix multiplication
+ out[0] = c;
+ out[1] = s;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = -s;
+ out[5] = c;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ }
+
+ /**
+ * Creates a matrix from a quaternion rotation and vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, vec);
+ * var quatMat = mat4.create();
+ * quat4.toMat4(quat, quatMat);
+ * mat4.multiply(dest, quatMat);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @returns {mat4} out
+ */
+ mat4.fromRotationTranslation = function (out, q, v) {
+ // Quaternion math
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ xy = x * y2,
+ xz = x * z2,
+ yy = y * y2,
+ yz = y * z2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2;
+
+ out[0] = 1 - (yy + zz);
+ out[1] = xy + wz;
+ out[2] = xz - wy;
+ out[3] = 0;
+ out[4] = xy - wz;
+ out[5] = 1 - (xx + zz);
+ out[6] = yz + wx;
+ out[7] = 0;
+ out[8] = xz + wy;
+ out[9] = yz - wx;
+ out[10] = 1 - (xx + yy);
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+
+ return out;
+ };
+
+ /**
+ * Creates a matrix from a quaternion rotation, vector translation and vector scale
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, vec);
+ * var quatMat = mat4.create();
+ * quat4.toMat4(quat, quatMat);
+ * mat4.multiply(dest, quatMat);
+ * mat4.scale(dest, scale)
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @param {vec3} s Scaling vector
+ * @returns {mat4} out
+ */
+ mat4.fromRotationTranslationScale = function (out, q, v, s) {
+ // Quaternion math
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ xy = x * y2,
+ xz = x * z2,
+ yy = y * y2,
+ yz = y * z2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2,
+ sx = s[0],
+ sy = s[1],
+ sz = s[2];
+
+ out[0] = (1 - (yy + zz)) * sx;
+ out[1] = (xy + wz) * sx;
+ out[2] = (xz - wy) * sx;
+ out[3] = 0;
+ out[4] = (xy - wz) * sy;
+ out[5] = (1 - (xx + zz)) * sy;
+ out[6] = (yz + wx) * sy;
+ out[7] = 0;
+ out[8] = (xz + wy) * sz;
+ out[9] = (yz - wx) * sz;
+ out[10] = (1 - (xx + yy)) * sz;
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+
+ return out;
+ };
+
+ /**
+ * Creates a matrix from a quaternion rotation, vector translation and vector scale, rotating and scaling around the given origin
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, vec);
+ * mat4.translate(dest, origin);
+ * var quatMat = mat4.create();
+ * quat4.toMat4(quat, quatMat);
+ * mat4.multiply(dest, quatMat);
+ * mat4.scale(dest, scale)
+ * mat4.translate(dest, negativeOrigin);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @param {vec3} s Scaling vector
+ * @param {vec3} o The origin vector around which to scale and rotate
+ * @returns {mat4} out
+ */
+ mat4.fromRotationTranslationScaleOrigin = function (out, q, v, s, o) {
+ // Quaternion math
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ xy = x * y2,
+ xz = x * z2,
+ yy = y * y2,
+ yz = y * z2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2,
+
+ sx = s[0],
+ sy = s[1],
+ sz = s[2],
+
+ ox = o[0],
+ oy = o[1],
+ oz = o[2];
+
+ out[0] = (1 - (yy + zz)) * sx;
+ out[1] = (xy + wz) * sx;
+ out[2] = (xz - wy) * sx;
+ out[3] = 0;
+ out[4] = (xy - wz) * sy;
+ out[5] = (1 - (xx + zz)) * sy;
+ out[6] = (yz + wx) * sy;
+ out[7] = 0;
+ out[8] = (xz + wy) * sz;
+ out[9] = (yz - wx) * sz;
+ out[10] = (1 - (xx + yy)) * sz;
+ out[11] = 0;
+ out[12] = v[0] + ox - (out[0] * ox + out[4] * oy + out[8] * oz);
+ out[13] = v[1] + oy - (out[1] * ox + out[5] * oy + out[9] * oz);
+ out[14] = v[2] + oz - (out[2] * ox + out[6] * oy + out[10] * oz);
+ out[15] = 1;
+
+ return out;
+ };
+
+ mat4.fromQuat = function (out, q) {
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ yx = y * x2,
+ yy = y * y2,
+ zx = z * x2,
+ zy = z * y2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2;
+
+ out[0] = 1 - yy - zz;
+ out[1] = yx + wz;
+ out[2] = zx - wy;
+ out[3] = 0;
+
+ out[4] = yx - wz;
+ out[5] = 1 - xx - zz;
+ out[6] = zy + wx;
+ out[7] = 0;
+
+ out[8] = zx + wy;
+ out[9] = zy - wx;
+ out[10] = 1 - xx - yy;
+ out[11] = 0;
+
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+
+ return out;
+ };
+
+ /**
+ * Generates a frustum matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {Number} left Left bound of the frustum
+ * @param {Number} right Right bound of the frustum
+ * @param {Number} bottom Bottom bound of the frustum
+ * @param {Number} top Top bound of the frustum
+ * @param {Number} near Near bound of the frustum
+ * @param {Number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+ mat4.frustum = function (out, left, right, bottom, top, near, far) {
+ var rl = 1 / (right - left),
+ tb = 1 / (top - bottom),
+ nf = 1 / (near - far);
+ out[0] = (near * 2) * rl;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = (near * 2) * tb;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = (right + left) * rl;
+ out[9] = (top + bottom) * tb;
+ out[10] = (far + near) * nf;
+ out[11] = -1;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = (far * near * 2) * nf;
+ out[15] = 0;
+ return out;
+ };
+
+ /**
+ * Generates a perspective projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} fovy Vertical field of view in radians
+ * @param {number} aspect Aspect ratio. typically viewport width/height
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+ mat4.perspective = function (out, fovy, aspect, near, far) {
+ var f = 1.0 / Math.tan(fovy / 2),
+ nf = 1 / (near - far);
+ out[0] = f / aspect;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = f;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = (far + near) * nf;
+ out[11] = -1;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = (2 * far * near) * nf;
+ out[15] = 0;
+ return out;
+ };
+
+ /**
+ * Generates a perspective projection matrix with the given field of view.
+ * This is primarily useful for generating projection matrices to be used
+ * with the still experiemental WebVR API.
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} fov Object containing the following values: upDegrees, downDegrees, leftDegrees, rightDegrees
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+ mat4.perspectiveFromFieldOfView = function (out, fov, near, far) {
+ var upTan = Math.tan(fov.upDegrees * Math.PI/180.0),
+ downTan = Math.tan(fov.downDegrees * Math.PI/180.0),
+ leftTan = Math.tan(fov.leftDegrees * Math.PI/180.0),
+ rightTan = Math.tan(fov.rightDegrees * Math.PI/180.0),
+ xScale = 2.0 / (leftTan + rightTan),
+ yScale = 2.0 / (upTan + downTan);
+
+ out[0] = xScale;
+ out[1] = 0.0;
+ out[2] = 0.0;
+ out[3] = 0.0;
+ out[4] = 0.0;
+ out[5] = yScale;
+ out[6] = 0.0;
+ out[7] = 0.0;
+ out[8] = -((leftTan - rightTan) * xScale * 0.5);
+ out[9] = ((upTan - downTan) * yScale * 0.5);
+ out[10] = far / (near - far);
+ out[11] = -1.0;
+ out[12] = 0.0;
+ out[13] = 0.0;
+ out[14] = (far * near) / (near - far);
+ out[15] = 0.0;
+ return out;
+ }
+
+ /**
+ * Generates a orthogonal projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} left Left bound of the frustum
+ * @param {number} right Right bound of the frustum
+ * @param {number} bottom Bottom bound of the frustum
+ * @param {number} top Top bound of the frustum
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+ mat4.ortho = function (out, left, right, bottom, top, near, far) {
+ var lr = 1 / (left - right),
+ bt = 1 / (bottom - top),
+ nf = 1 / (near - far);
+ out[0] = -2 * lr;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = -2 * bt;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 2 * nf;
+ out[11] = 0;
+ out[12] = (left + right) * lr;
+ out[13] = (top + bottom) * bt;
+ out[14] = (far + near) * nf;
+ out[15] = 1;
+ return out;
+ };
+
+ /**
+ * Generates a look-at matrix with the given eye position, focal point, and up axis
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {vec3} eye Position of the viewer
+ * @param {vec3} center Point the viewer is looking at
+ * @param {vec3} up vec3 pointing up
+ * @returns {mat4} out
+ */
+ mat4.lookAt = function (out, eye, center, up) {
+ var x0, x1, x2, y0, y1, y2, z0, z1, z2, len,
+ eyex = eye[0],
+ eyey = eye[1],
+ eyez = eye[2],
+ upx = up[0],
+ upy = up[1],
+ upz = up[2],
+ centerx = center[0],
+ centery = center[1],
+ centerz = center[2];
+
+ if (Math.abs(eyex - centerx) < glMatrix.EPSILON &&
+ Math.abs(eyey - centery) < glMatrix.EPSILON &&
+ Math.abs(eyez - centerz) < glMatrix.EPSILON) {
+ return mat4.identity(out);
+ }
+
+ z0 = eyex - centerx;
+ z1 = eyey - centery;
+ z2 = eyez - centerz;
+
+ len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
+ z0 *= len;
+ z1 *= len;
+ z2 *= len;
+
+ x0 = upy * z2 - upz * z1;
+ x1 = upz * z0 - upx * z2;
+ x2 = upx * z1 - upy * z0;
+ len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
+ if (!len) {
+ x0 = 0;
+ x1 = 0;
+ x2 = 0;
+ } else {
+ len = 1 / len;
+ x0 *= len;
+ x1 *= len;
+ x2 *= len;
+ }
+
+ y0 = z1 * x2 - z2 * x1;
+ y1 = z2 * x0 - z0 * x2;
+ y2 = z0 * x1 - z1 * x0;
+
+ len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
+ if (!len) {
+ y0 = 0;
+ y1 = 0;
+ y2 = 0;
+ } else {
+ len = 1 / len;
+ y0 *= len;
+ y1 *= len;
+ y2 *= len;
+ }
+
+ out[0] = x0;
+ out[1] = y0;
+ out[2] = z0;
+ out[3] = 0;
+ out[4] = x1;
+ out[5] = y1;
+ out[6] = z1;
+ out[7] = 0;
+ out[8] = x2;
+ out[9] = y2;
+ out[10] = z2;
+ out[11] = 0;
+ out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
+ out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
+ out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
+ out[15] = 1;
+
+ return out;
+ };
+
+ /**
+ * Returns a string representation of a mat4
+ *
+ * @param {mat4} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+ mat4.str = function (a) {
+ return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' +
+ a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' +
+ a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' +
+ a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')';
+ };
+
+ /**
+ * Returns Frobenius norm of a mat4
+ *
+ * @param {mat4} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+ mat4.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2) + Math.pow(a[9], 2) + Math.pow(a[10], 2) + Math.pow(a[11], 2) + Math.pow(a[12], 2) + Math.pow(a[13], 2) + Math.pow(a[14], 2) + Math.pow(a[15], 2) ))
+ };
+
+
+ module.exports = mat4;
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+ var mat3 = __webpack_require__(4);
+ var vec3 = __webpack_require__(7);
+ var vec4 = __webpack_require__(8);
+
+ /**
+ * @class Quaternion
+ * @name quat
+ */
+ var quat = {};
+
+ /**
+ * Creates a new identity quat
+ *
+ * @returns {quat} a new quaternion
+ */
+ quat.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+ };
+
+ /**
+ * Sets a quaternion to represent the shortest rotation from one
+ * vector to another.
+ *
+ * Both vectors are assumed to be unit length.
+ *
+ * @param {quat} out the receiving quaternion.
+ * @param {vec3} a the initial vector
+ * @param {vec3} b the destination vector
+ * @returns {quat} out
+ */
+ quat.rotationTo = (function() {
+ var tmpvec3 = vec3.create();
+ var xUnitVec3 = vec3.fromValues(1,0,0);
+ var yUnitVec3 = vec3.fromValues(0,1,0);
+
+ return function(out, a, b) {
+ var dot = vec3.dot(a, b);
+ if (dot < -0.999999) {
+ vec3.cross(tmpvec3, xUnitVec3, a);
+ if (vec3.length(tmpvec3) < 0.000001)
+ vec3.cross(tmpvec3, yUnitVec3, a);
+ vec3.normalize(tmpvec3, tmpvec3);
+ quat.setAxisAngle(out, tmpvec3, Math.PI);
+ return out;
+ } else if (dot > 0.999999) {
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+ } else {
+ vec3.cross(tmpvec3, a, b);
+ out[0] = tmpvec3[0];
+ out[1] = tmpvec3[1];
+ out[2] = tmpvec3[2];
+ out[3] = 1 + dot;
+ return quat.normalize(out, out);
+ }
+ };
+ })();
+
+ /**
+ * Sets the specified quaternion with values corresponding to the given
+ * axes. Each axis is a vec3 and is expected to be unit length and
+ * perpendicular to all other specified axes.
+ *
+ * @param {vec3} view the vector representing the viewing direction
+ * @param {vec3} right the vector representing the local "right" direction
+ * @param {vec3} up the vector representing the local "up" direction
+ * @returns {quat} out
+ */
+ quat.setAxes = (function() {
+ var matr = mat3.create();
+
+ return function(out, view, right, up) {
+ matr[0] = right[0];
+ matr[3] = right[1];
+ matr[6] = right[2];
+
+ matr[1] = up[0];
+ matr[4] = up[1];
+ matr[7] = up[2];
+
+ matr[2] = -view[0];
+ matr[5] = -view[1];
+ matr[8] = -view[2];
+
+ return quat.normalize(out, quat.fromMat3(out, matr));
+ };
+ })();
+
+ /**
+ * Creates a new quat initialized with values from an existing quaternion
+ *
+ * @param {quat} a quaternion to clone
+ * @returns {quat} a new quaternion
+ * @function
+ */
+ quat.clone = vec4.clone;
+
+ /**
+ * Creates a new quat initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} a new quaternion
+ * @function
+ */
+ quat.fromValues = vec4.fromValues;
+
+ /**
+ * Copy the values from one quat to another
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the source quaternion
+ * @returns {quat} out
+ * @function
+ */
+ quat.copy = vec4.copy;
+
+ /**
+ * Set the components of a quat to the given values
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} out
+ * @function
+ */
+ quat.set = vec4.set;
+
+ /**
+ * Set a quat to the identity quaternion
+ *
+ * @param {quat} out the receiving quaternion
+ * @returns {quat} out
+ */
+ quat.identity = function(out) {
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+ };
+
+ /**
+ * Sets a quat from the given angle and rotation axis,
+ * then returns it.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {vec3} axis the axis around which to rotate
+ * @param {Number} rad the angle in radians
+ * @returns {quat} out
+ **/
+ quat.setAxisAngle = function(out, axis, rad) {
+ rad = rad * 0.5;
+ var s = Math.sin(rad);
+ out[0] = s * axis[0];
+ out[1] = s * axis[1];
+ out[2] = s * axis[2];
+ out[3] = Math.cos(rad);
+ return out;
+ };
+
+ /**
+ * Adds two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ * @function
+ */
+ quat.add = vec4.add;
+
+ /**
+ * Multiplies two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ */
+ quat.multiply = function(out, a, b) {
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+ out[0] = ax * bw + aw * bx + ay * bz - az * by;
+ out[1] = ay * bw + aw * by + az * bx - ax * bz;
+ out[2] = az * bw + aw * bz + ax * by - ay * bx;
+ out[3] = aw * bw - ax * bx - ay * by - az * bz;
+ return out;
+ };
+
+ /**
+ * Alias for {@link quat.multiply}
+ * @function
+ */
+ quat.mul = quat.multiply;
+
+ /**
+ * Scales a quat by a scalar number
+ *
+ * @param {quat} out the receiving vector
+ * @param {quat} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {quat} out
+ * @function
+ */
+ quat.scale = vec4.scale;
+
+ /**
+ * Rotates a quaternion by the given angle about the X axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+ quat.rotateX = function (out, a, rad) {
+ rad *= 0.5;
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bx = Math.sin(rad), bw = Math.cos(rad);
+
+ out[0] = ax * bw + aw * bx;
+ out[1] = ay * bw + az * bx;
+ out[2] = az * bw - ay * bx;
+ out[3] = aw * bw - ax * bx;
+ return out;
+ };
+
+ /**
+ * Rotates a quaternion by the given angle about the Y axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+ quat.rotateY = function (out, a, rad) {
+ rad *= 0.5;
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ by = Math.sin(rad), bw = Math.cos(rad);
+
+ out[0] = ax * bw - az * by;
+ out[1] = ay * bw + aw * by;
+ out[2] = az * bw + ax * by;
+ out[3] = aw * bw - ay * by;
+ return out;
+ };
+
+ /**
+ * Rotates a quaternion by the given angle about the Z axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+ quat.rotateZ = function (out, a, rad) {
+ rad *= 0.5;
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bz = Math.sin(rad), bw = Math.cos(rad);
+
+ out[0] = ax * bw + ay * bz;
+ out[1] = ay * bw - ax * bz;
+ out[2] = az * bw + aw * bz;
+ out[3] = aw * bw - az * bz;
+ return out;
+ };
+
+ /**
+ * Calculates the W component of a quat from the X, Y, and Z components.
+ * Assumes that quaternion is 1 unit in length.
+ * Any existing W component will be ignored.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate W component of
+ * @returns {quat} out
+ */
+ quat.calculateW = function (out, a) {
+ var x = a[0], y = a[1], z = a[2];
+
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ out[3] = Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z));
+ return out;
+ };
+
+ /**
+ * Calculates the dot product of two quat's
+ *
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {Number} dot product of a and b
+ * @function
+ */
+ quat.dot = vec4.dot;
+
+ /**
+ * Performs a linear interpolation between two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ * @function
+ */
+ quat.lerp = vec4.lerp;
+
+ /**
+ * Performs a spherical linear interpolation between two quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ */
+ quat.slerp = function (out, a, b, t) {
+ // benchmarks:
+ // http://jsperf.com/quaternion-slerp-implementations
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+ var omega, cosom, sinom, scale0, scale1;
+
+ // calc cosine
+ cosom = ax * bx + ay * by + az * bz + aw * bw;
+ // adjust signs (if necessary)
+ if ( cosom < 0.0 ) {
+ cosom = -cosom;
+ bx = - bx;
+ by = - by;
+ bz = - bz;
+ bw = - bw;
+ }
+ // calculate coefficients
+ if ( (1.0 - cosom) > 0.000001 ) {
+ // standard case (slerp)
+ omega = Math.acos(cosom);
+ sinom = Math.sin(omega);
+ scale0 = Math.sin((1.0 - t) * omega) / sinom;
+ scale1 = Math.sin(t * omega) / sinom;
+ } else {
+ // "from" and "to" quaternions are very close
+ // ... so we can do a linear interpolation
+ scale0 = 1.0 - t;
+ scale1 = t;
+ }
+ // calculate final values
+ out[0] = scale0 * ax + scale1 * bx;
+ out[1] = scale0 * ay + scale1 * by;
+ out[2] = scale0 * az + scale1 * bz;
+ out[3] = scale0 * aw + scale1 * bw;
+
+ return out;
+ };
+
+ /**
+ * Performs a spherical linear interpolation with two control points
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {quat} c the third operand
+ * @param {quat} d the fourth operand
+ * @param {Number} t interpolation amount
+ * @returns {quat} out
+ */
+ quat.sqlerp = (function () {
+ var temp1 = quat.create();
+ var temp2 = quat.create();
+
+ return function (out, a, b, c, d, t) {
+ quat.slerp(temp1, a, d, t);
+ quat.slerp(temp2, b, c, t);
+ quat.slerp(out, temp1, temp2, 2 * t * (1 - t));
+
+ return out;
+ };
+ }());
+
+ /**
+ * Calculates the inverse of a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate inverse of
+ * @returns {quat} out
+ */
+ quat.invert = function(out, a) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+ dot = a0*a0 + a1*a1 + a2*a2 + a3*a3,
+ invDot = dot ? 1.0/dot : 0;
+
+ // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0
+
+ out[0] = -a0*invDot;
+ out[1] = -a1*invDot;
+ out[2] = -a2*invDot;
+ out[3] = a3*invDot;
+ return out;
+ };
+
+ /**
+ * Calculates the conjugate of a quat
+ * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate conjugate of
+ * @returns {quat} out
+ */
+ quat.conjugate = function (out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ out[3] = a[3];
+ return out;
+ };
+
+ /**
+ * Calculates the length of a quat
+ *
+ * @param {quat} a vector to calculate length of
+ * @returns {Number} length of a
+ * @function
+ */
+ quat.length = vec4.length;
+
+ /**
+ * Alias for {@link quat.length}
+ * @function
+ */
+ quat.len = quat.length;
+
+ /**
+ * Calculates the squared length of a quat
+ *
+ * @param {quat} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ * @function
+ */
+ quat.squaredLength = vec4.squaredLength;
+
+ /**
+ * Alias for {@link quat.squaredLength}
+ * @function
+ */
+ quat.sqrLen = quat.squaredLength;
+
+ /**
+ * Normalize a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quaternion to normalize
+ * @returns {quat} out
+ * @function
+ */
+ quat.normalize = vec4.normalize;
+
+ /**
+ * Creates a quaternion from the given 3x3 rotation matrix.
+ *
+ * NOTE: The resultant quaternion is not normalized, so you should be sure
+ * to renormalize the quaternion yourself where necessary.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {mat3} m rotation matrix
+ * @returns {quat} out
+ * @function
+ */
+ quat.fromMat3 = function(out, m) {
+ // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes
+ // article "Quaternion Calculus and Fast Animation".
+ var fTrace = m[0] + m[4] + m[8];
+ var fRoot;
+
+ if ( fTrace > 0.0 ) {
+ // |w| > 1/2, may as well choose w > 1/2
+ fRoot = Math.sqrt(fTrace + 1.0); // 2w
+ out[3] = 0.5 * fRoot;
+ fRoot = 0.5/fRoot; // 1/(4w)
+ out[0] = (m[5]-m[7])*fRoot;
+ out[1] = (m[6]-m[2])*fRoot;
+ out[2] = (m[1]-m[3])*fRoot;
+ } else {
+ // |w| <= 1/2
+ var i = 0;
+ if ( m[4] > m[0] )
+ i = 1;
+ if ( m[8] > m[i*3+i] )
+ i = 2;
+ var j = (i+1)%3;
+ var k = (i+2)%3;
+
+ fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0);
+ out[i] = 0.5 * fRoot;
+ fRoot = 0.5 / fRoot;
+ out[3] = (m[j*3+k] - m[k*3+j]) * fRoot;
+ out[j] = (m[j*3+i] + m[i*3+j]) * fRoot;
+ out[k] = (m[k*3+i] + m[i*3+k]) * fRoot;
+ }
+
+ return out;
+ };
+
+ /**
+ * Returns a string representation of a quatenion
+ *
+ * @param {quat} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+ quat.str = function (a) {
+ return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+ };
+
+ module.exports = quat;
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 3 Dimensional Vector
+ * @name vec3
+ */
+ var vec3 = {};
+
+ /**
+ * Creates a new, empty vec3
+ *
+ * @returns {vec3} a new 3D vector
+ */
+ vec3.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(3);
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ return out;
+ };
+
+ /**
+ * Creates a new vec3 initialized with values from an existing vector
+ *
+ * @param {vec3} a vector to clone
+ * @returns {vec3} a new 3D vector
+ */
+ vec3.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(3);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ return out;
+ };
+
+ /**
+ * Creates a new vec3 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} a new 3D vector
+ */
+ vec3.fromValues = function(x, y, z) {
+ var out = new glMatrix.ARRAY_TYPE(3);
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ return out;
+ };
+
+ /**
+ * Copy the values from one vec3 to another
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the source vector
+ * @returns {vec3} out
+ */
+ vec3.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ return out;
+ };
+
+ /**
+ * Set the components of a vec3 to the given values
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} out
+ */
+ vec3.set = function(out, x, y, z) {
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ return out;
+ };
+
+ /**
+ * Adds two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.add = function(out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ out[2] = a[2] + b[2];
+ return out;
+ };
+
+ /**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.subtract = function(out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ out[2] = a[2] - b[2];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec3.subtract}
+ * @function
+ */
+ vec3.sub = vec3.subtract;
+
+ /**
+ * Multiplies two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.multiply = function(out, a, b) {
+ out[0] = a[0] * b[0];
+ out[1] = a[1] * b[1];
+ out[2] = a[2] * b[2];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec3.multiply}
+ * @function
+ */
+ vec3.mul = vec3.multiply;
+
+ /**
+ * Divides two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.divide = function(out, a, b) {
+ out[0] = a[0] / b[0];
+ out[1] = a[1] / b[1];
+ out[2] = a[2] / b[2];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec3.divide}
+ * @function
+ */
+ vec3.div = vec3.divide;
+
+ /**
+ * Returns the minimum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.min = function(out, a, b) {
+ out[0] = Math.min(a[0], b[0]);
+ out[1] = Math.min(a[1], b[1]);
+ out[2] = Math.min(a[2], b[2]);
+ return out;
+ };
+
+ /**
+ * Returns the maximum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.max = function(out, a, b) {
+ out[0] = Math.max(a[0], b[0]);
+ out[1] = Math.max(a[1], b[1]);
+ out[2] = Math.max(a[2], b[2]);
+ return out;
+ };
+
+ /**
+ * Scales a vec3 by a scalar number
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec3} out
+ */
+ vec3.scale = function(out, a, b) {
+ out[0] = a[0] * b;
+ out[1] = a[1] * b;
+ out[2] = a[2] * b;
+ return out;
+ };
+
+ /**
+ * Adds two vec3's after scaling the second operand by a scalar value
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec3} out
+ */
+ vec3.scaleAndAdd = function(out, a, b, scale) {
+ out[0] = a[0] + (b[0] * scale);
+ out[1] = a[1] + (b[1] * scale);
+ out[2] = a[2] + (b[2] * scale);
+ return out;
+ };
+
+ /**
+ * Calculates the euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} distance between a and b
+ */
+ vec3.distance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2];
+ return Math.sqrt(x*x + y*y + z*z);
+ };
+
+ /**
+ * Alias for {@link vec3.distance}
+ * @function
+ */
+ vec3.dist = vec3.distance;
+
+ /**
+ * Calculates the squared euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+ vec3.squaredDistance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2];
+ return x*x + y*y + z*z;
+ };
+
+ /**
+ * Alias for {@link vec3.squaredDistance}
+ * @function
+ */
+ vec3.sqrDist = vec3.squaredDistance;
+
+ /**
+ * Calculates the length of a vec3
+ *
+ * @param {vec3} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+ vec3.length = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2];
+ return Math.sqrt(x*x + y*y + z*z);
+ };
+
+ /**
+ * Alias for {@link vec3.length}
+ * @function
+ */
+ vec3.len = vec3.length;
+
+ /**
+ * Calculates the squared length of a vec3
+ *
+ * @param {vec3} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+ vec3.squaredLength = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2];
+ return x*x + y*y + z*z;
+ };
+
+ /**
+ * Alias for {@link vec3.squaredLength}
+ * @function
+ */
+ vec3.sqrLen = vec3.squaredLength;
+
+ /**
+ * Negates the components of a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to negate
+ * @returns {vec3} out
+ */
+ vec3.negate = function(out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ return out;
+ };
+
+ /**
+ * Returns the inverse of the components of a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to invert
+ * @returns {vec3} out
+ */
+ vec3.inverse = function(out, a) {
+ out[0] = 1.0 / a[0];
+ out[1] = 1.0 / a[1];
+ out[2] = 1.0 / a[2];
+ return out;
+ };
+
+ /**
+ * Normalize a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to normalize
+ * @returns {vec3} out
+ */
+ vec3.normalize = function(out, a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2];
+ var len = x*x + y*y + z*z;
+ if (len > 0) {
+ //TODO: evaluate use of glm_invsqrt here?
+ len = 1 / Math.sqrt(len);
+ out[0] = a[0] * len;
+ out[1] = a[1] * len;
+ out[2] = a[2] * len;
+ }
+ return out;
+ };
+
+ /**
+ * Calculates the dot product of two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+ vec3.dot = function (a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ };
+
+ /**
+ * Computes the cross product of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+ vec3.cross = function(out, a, b) {
+ var ax = a[0], ay = a[1], az = a[2],
+ bx = b[0], by = b[1], bz = b[2];
+
+ out[0] = ay * bz - az * by;
+ out[1] = az * bx - ax * bz;
+ out[2] = ax * by - ay * bx;
+ return out;
+ };
+
+ /**
+ * Performs a linear interpolation between two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+ vec3.lerp = function (out, a, b, t) {
+ var ax = a[0],
+ ay = a[1],
+ az = a[2];
+ out[0] = ax + t * (b[0] - ax);
+ out[1] = ay + t * (b[1] - ay);
+ out[2] = az + t * (b[2] - az);
+ return out;
+ };
+
+ /**
+ * Performs a hermite interpolation with two control points
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {vec3} c the third operand
+ * @param {vec3} d the fourth operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+ vec3.hermite = function (out, a, b, c, d, t) {
+ var factorTimes2 = t * t,
+ factor1 = factorTimes2 * (2 * t - 3) + 1,
+ factor2 = factorTimes2 * (t - 2) + t,
+ factor3 = factorTimes2 * (t - 1),
+ factor4 = factorTimes2 * (3 - 2 * t);
+
+ out[0] = a[0] * factor1 + b[0] * factor2 + c[0] * factor3 + d[0] * factor4;
+ out[1] = a[1] * factor1 + b[1] * factor2 + c[1] * factor3 + d[1] * factor4;
+ out[2] = a[2] * factor1 + b[2] * factor2 + c[2] * factor3 + d[2] * factor4;
+
+ return out;
+ };
+
+ /**
+ * Performs a bezier interpolation with two control points
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {vec3} c the third operand
+ * @param {vec3} d the fourth operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+ vec3.bezier = function (out, a, b, c, d, t) {
+ var inverseFactor = 1 - t,
+ inverseFactorTimesTwo = inverseFactor * inverseFactor,
+ factorTimes2 = t * t,
+ factor1 = inverseFactorTimesTwo * inverseFactor,
+ factor2 = 3 * t * inverseFactorTimesTwo,
+ factor3 = 3 * factorTimes2 * inverseFactor,
+ factor4 = factorTimes2 * t;
+
+ out[0] = a[0] * factor1 + b[0] * factor2 + c[0] * factor3 + d[0] * factor4;
+ out[1] = a[1] * factor1 + b[1] * factor2 + c[1] * factor3 + d[1] * factor4;
+ out[2] = a[2] * factor1 + b[2] * factor2 + c[2] * factor3 + d[2] * factor4;
+
+ return out;
+ };
+
+ /**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec3} out
+ */
+ vec3.random = function (out, scale) {
+ scale = scale || 1.0;
+
+ var r = glMatrix.RANDOM() * 2.0 * Math.PI;
+ var z = (glMatrix.RANDOM() * 2.0) - 1.0;
+ var zScale = Math.sqrt(1.0-z*z) * scale;
+
+ out[0] = Math.cos(r) * zScale;
+ out[1] = Math.sin(r) * zScale;
+ out[2] = z * scale;
+ return out;
+ };
+
+ /**
+ * Transforms the vec3 with a mat4.
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec3} out
+ */
+ vec3.transformMat4 = function(out, a, m) {
+ var x = a[0], y = a[1], z = a[2],
+ w = m[3] * x + m[7] * y + m[11] * z + m[15];
+ w = w || 1.0;
+ out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
+ out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
+ out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
+ return out;
+ };
+
+ /**
+ * Transforms the vec3 with a mat3.
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m the 3x3 matrix to transform with
+ * @returns {vec3} out
+ */
+ vec3.transformMat3 = function(out, a, m) {
+ var x = a[0], y = a[1], z = a[2];
+ out[0] = x * m[0] + y * m[3] + z * m[6];
+ out[1] = x * m[1] + y * m[4] + z * m[7];
+ out[2] = x * m[2] + y * m[5] + z * m[8];
+ return out;
+ };
+
+ /**
+ * Transforms the vec3 with a quat
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec3} out
+ */
+ vec3.transformQuat = function(out, a, q) {
+ // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations
+
+ var x = a[0], y = a[1], z = a[2],
+ qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+ // calculate quat * vec
+ ix = qw * x + qy * z - qz * y,
+ iy = qw * y + qz * x - qx * z,
+ iz = qw * z + qx * y - qy * x,
+ iw = -qx * x - qy * y - qz * z;
+
+ // calculate result * inverse quat
+ out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+ return out;
+ };
+
+ /**
+ * Rotate a 3D vector around the x-axis
+ * @param {vec3} out The receiving vec3
+ * @param {vec3} a The vec3 point to rotate
+ * @param {vec3} b The origin of the rotation
+ * @param {Number} c The angle of rotation
+ * @returns {vec3} out
+ */
+ vec3.rotateX = function(out, a, b, c){
+ var p = [], r=[];
+ //Translate point to the origin
+ p[0] = a[0] - b[0];
+ p[1] = a[1] - b[1];
+ p[2] = a[2] - b[2];
+
+ //perform rotation
+ r[0] = p[0];
+ r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c);
+ r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c);
+
+ //translate to correct position
+ out[0] = r[0] + b[0];
+ out[1] = r[1] + b[1];
+ out[2] = r[2] + b[2];
+
+ return out;
+ };
+
+ /**
+ * Rotate a 3D vector around the y-axis
+ * @param {vec3} out The receiving vec3
+ * @param {vec3} a The vec3 point to rotate
+ * @param {vec3} b The origin of the rotation
+ * @param {Number} c The angle of rotation
+ * @returns {vec3} out
+ */
+ vec3.rotateY = function(out, a, b, c){
+ var p = [], r=[];
+ //Translate point to the origin
+ p[0] = a[0] - b[0];
+ p[1] = a[1] - b[1];
+ p[2] = a[2] - b[2];
+
+ //perform rotation
+ r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c);
+ r[1] = p[1];
+ r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c);
+
+ //translate to correct position
+ out[0] = r[0] + b[0];
+ out[1] = r[1] + b[1];
+ out[2] = r[2] + b[2];
+
+ return out;
+ };
+
+ /**
+ * Rotate a 3D vector around the z-axis
+ * @param {vec3} out The receiving vec3
+ * @param {vec3} a The vec3 point to rotate
+ * @param {vec3} b The origin of the rotation
+ * @param {Number} c The angle of rotation
+ * @returns {vec3} out
+ */
+ vec3.rotateZ = function(out, a, b, c){
+ var p = [], r=[];
+ //Translate point to the origin
+ p[0] = a[0] - b[0];
+ p[1] = a[1] - b[1];
+ p[2] = a[2] - b[2];
+
+ //perform rotation
+ r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c);
+ r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c);
+ r[2] = p[2];
+
+ //translate to correct position
+ out[0] = r[0] + b[0];
+ out[1] = r[1] + b[1];
+ out[2] = r[2] + b[2];
+
+ return out;
+ };
+
+ /**
+ * Perform some operation over an array of vec3s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+ vec3.forEach = (function() {
+ var vec = vec3.create();
+
+ return function(a, stride, offset, count, fn, arg) {
+ var i, l;
+ if(!stride) {
+ stride = 3;
+ }
+
+ if(!offset) {
+ offset = 0;
+ }
+
+ if(count) {
+ l = Math.min((count * stride) + offset, a.length);
+ } else {
+ l = a.length;
+ }
+
+ for(i = offset; i < l; i += stride) {
+ vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2];
+ fn(vec, vec, arg);
+ a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2];
+ }
+
+ return a;
+ };
+ })();
+
+ /**
+ * Get the angle between two 3D vectors
+ * @param {vec3} a The first operand
+ * @param {vec3} b The second operand
+ * @returns {Number} The angle in radians
+ */
+ vec3.angle = function(a, b) {
+
+ var tempA = vec3.fromValues(a[0], a[1], a[2]);
+ var tempB = vec3.fromValues(b[0], b[1], b[2]);
+
+ vec3.normalize(tempA, tempA);
+ vec3.normalize(tempB, tempB);
+
+ var cosine = vec3.dot(tempA, tempB);
+
+ if(cosine > 1.0){
+ return 0;
+ } else {
+ return Math.acos(cosine);
+ }
+ };
+
+ /**
+ * Returns a string representation of a vector
+ *
+ * @param {vec3} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+ vec3.str = function (a) {
+ return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')';
+ };
+
+ module.exports = vec3;
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 4 Dimensional Vector
+ * @name vec4
+ */
+ var vec4 = {};
+
+ /**
+ * Creates a new, empty vec4
+ *
+ * @returns {vec4} a new 4D vector
+ */
+ vec4.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ return out;
+ };
+
+ /**
+ * Creates a new vec4 initialized with values from an existing vector
+ *
+ * @param {vec4} a vector to clone
+ * @returns {vec4} a new 4D vector
+ */
+ vec4.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+ };
+
+ /**
+ * Creates a new vec4 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} a new 4D vector
+ */
+ vec4.fromValues = function(x, y, z, w) {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ out[3] = w;
+ return out;
+ };
+
+ /**
+ * Copy the values from one vec4 to another
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the source vector
+ * @returns {vec4} out
+ */
+ vec4.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+ };
+
+ /**
+ * Set the components of a vec4 to the given values
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} out
+ */
+ vec4.set = function(out, x, y, z, w) {
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ out[3] = w;
+ return out;
+ };
+
+ /**
+ * Adds two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+ vec4.add = function(out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ out[2] = a[2] + b[2];
+ out[3] = a[3] + b[3];
+ return out;
+ };
+
+ /**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+ vec4.subtract = function(out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ out[2] = a[2] - b[2];
+ out[3] = a[3] - b[3];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec4.subtract}
+ * @function
+ */
+ vec4.sub = vec4.subtract;
+
+ /**
+ * Multiplies two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+ vec4.multiply = function(out, a, b) {
+ out[0] = a[0] * b[0];
+ out[1] = a[1] * b[1];
+ out[2] = a[2] * b[2];
+ out[3] = a[3] * b[3];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec4.multiply}
+ * @function
+ */
+ vec4.mul = vec4.multiply;
+
+ /**
+ * Divides two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+ vec4.divide = function(out, a, b) {
+ out[0] = a[0] / b[0];
+ out[1] = a[1] / b[1];
+ out[2] = a[2] / b[2];
+ out[3] = a[3] / b[3];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec4.divide}
+ * @function
+ */
+ vec4.div = vec4.divide;
+
+ /**
+ * Returns the minimum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+ vec4.min = function(out, a, b) {
+ out[0] = Math.min(a[0], b[0]);
+ out[1] = Math.min(a[1], b[1]);
+ out[2] = Math.min(a[2], b[2]);
+ out[3] = Math.min(a[3], b[3]);
+ return out;
+ };
+
+ /**
+ * Returns the maximum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+ vec4.max = function(out, a, b) {
+ out[0] = Math.max(a[0], b[0]);
+ out[1] = Math.max(a[1], b[1]);
+ out[2] = Math.max(a[2], b[2]);
+ out[3] = Math.max(a[3], b[3]);
+ return out;
+ };
+
+ /**
+ * Scales a vec4 by a scalar number
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec4} out
+ */
+ vec4.scale = function(out, a, b) {
+ out[0] = a[0] * b;
+ out[1] = a[1] * b;
+ out[2] = a[2] * b;
+ out[3] = a[3] * b;
+ return out;
+ };
+
+ /**
+ * Adds two vec4's after scaling the second operand by a scalar value
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec4} out
+ */
+ vec4.scaleAndAdd = function(out, a, b, scale) {
+ out[0] = a[0] + (b[0] * scale);
+ out[1] = a[1] + (b[1] * scale);
+ out[2] = a[2] + (b[2] * scale);
+ out[3] = a[3] + (b[3] * scale);
+ return out;
+ };
+
+ /**
+ * Calculates the euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} distance between a and b
+ */
+ vec4.distance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2],
+ w = b[3] - a[3];
+ return Math.sqrt(x*x + y*y + z*z + w*w);
+ };
+
+ /**
+ * Alias for {@link vec4.distance}
+ * @function
+ */
+ vec4.dist = vec4.distance;
+
+ /**
+ * Calculates the squared euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+ vec4.squaredDistance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2],
+ w = b[3] - a[3];
+ return x*x + y*y + z*z + w*w;
+ };
+
+ /**
+ * Alias for {@link vec4.squaredDistance}
+ * @function
+ */
+ vec4.sqrDist = vec4.squaredDistance;
+
+ /**
+ * Calculates the length of a vec4
+ *
+ * @param {vec4} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+ vec4.length = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2],
+ w = a[3];
+ return Math.sqrt(x*x + y*y + z*z + w*w);
+ };
+
+ /**
+ * Alias for {@link vec4.length}
+ * @function
+ */
+ vec4.len = vec4.length;
+
+ /**
+ * Calculates the squared length of a vec4
+ *
+ * @param {vec4} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+ vec4.squaredLength = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2],
+ w = a[3];
+ return x*x + y*y + z*z + w*w;
+ };
+
+ /**
+ * Alias for {@link vec4.squaredLength}
+ * @function
+ */
+ vec4.sqrLen = vec4.squaredLength;
+
+ /**
+ * Negates the components of a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to negate
+ * @returns {vec4} out
+ */
+ vec4.negate = function(out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ out[3] = -a[3];
+ return out;
+ };
+
+ /**
+ * Returns the inverse of the components of a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to invert
+ * @returns {vec4} out
+ */
+ vec4.inverse = function(out, a) {
+ out[0] = 1.0 / a[0];
+ out[1] = 1.0 / a[1];
+ out[2] = 1.0 / a[2];
+ out[3] = 1.0 / a[3];
+ return out;
+ };
+
+ /**
+ * Normalize a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to normalize
+ * @returns {vec4} out
+ */
+ vec4.normalize = function(out, a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2],
+ w = a[3];
+ var len = x*x + y*y + z*z + w*w;
+ if (len > 0) {
+ len = 1 / Math.sqrt(len);
+ out[0] = x * len;
+ out[1] = y * len;
+ out[2] = z * len;
+ out[3] = w * len;
+ }
+ return out;
+ };
+
+ /**
+ * Calculates the dot product of two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+ vec4.dot = function (a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+ };
+
+ /**
+ * Performs a linear interpolation between two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec4} out
+ */
+ vec4.lerp = function (out, a, b, t) {
+ var ax = a[0],
+ ay = a[1],
+ az = a[2],
+ aw = a[3];
+ out[0] = ax + t * (b[0] - ax);
+ out[1] = ay + t * (b[1] - ay);
+ out[2] = az + t * (b[2] - az);
+ out[3] = aw + t * (b[3] - aw);
+ return out;
+ };
+
+ /**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec4} out
+ */
+ vec4.random = function (out, scale) {
+ scale = scale || 1.0;
+
+ //TODO: This is a pretty awful way of doing this. Find something better.
+ out[0] = glMatrix.RANDOM();
+ out[1] = glMatrix.RANDOM();
+ out[2] = glMatrix.RANDOM();
+ out[3] = glMatrix.RANDOM();
+ vec4.normalize(out, out);
+ vec4.scale(out, out, scale);
+ return out;
+ };
+
+ /**
+ * Transforms the vec4 with a mat4.
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec4} out
+ */
+ vec4.transformMat4 = function(out, a, m) {
+ var x = a[0], y = a[1], z = a[2], w = a[3];
+ out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
+ out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
+ out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
+ out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
+ return out;
+ };
+
+ /**
+ * Transforms the vec4 with a quat
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec4} out
+ */
+ vec4.transformQuat = function(out, a, q) {
+ var x = a[0], y = a[1], z = a[2],
+ qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+ // calculate quat * vec
+ ix = qw * x + qy * z - qz * y,
+ iy = qw * y + qz * x - qx * z,
+ iz = qw * z + qx * y - qy * x,
+ iw = -qx * x - qy * y - qz * z;
+
+ // calculate result * inverse quat
+ out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+ out[3] = a[3];
+ return out;
+ };
+
+ /**
+ * Perform some operation over an array of vec4s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec4s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+ vec4.forEach = (function() {
+ var vec = vec4.create();
+
+ return function(a, stride, offset, count, fn, arg) {
+ var i, l;
+ if(!stride) {
+ stride = 4;
+ }
+
+ if(!offset) {
+ offset = 0;
+ }
+
+ if(count) {
+ l = Math.min((count * stride) + offset, a.length);
+ } else {
+ l = a.length;
+ }
+
+ for(i = offset; i < l; i += stride) {
+ vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3];
+ fn(vec, vec, arg);
+ a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3];
+ }
+
+ return a;
+ };
+ })();
+
+ /**
+ * Returns a string representation of a vector
+ *
+ * @param {vec4} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+ vec4.str = function (a) {
+ return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+ };
+
+ module.exports = vec4;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE. */
+
+ var glMatrix = __webpack_require__(1);
+
+ /**
+ * @class 2 Dimensional Vector
+ * @name vec2
+ */
+ var vec2 = {};
+
+ /**
+ * Creates a new, empty vec2
+ *
+ * @returns {vec2} a new 2D vector
+ */
+ vec2.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(2);
+ out[0] = 0;
+ out[1] = 0;
+ return out;
+ };
+
+ /**
+ * Creates a new vec2 initialized with values from an existing vector
+ *
+ * @param {vec2} a vector to clone
+ * @returns {vec2} a new 2D vector
+ */
+ vec2.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(2);
+ out[0] = a[0];
+ out[1] = a[1];
+ return out;
+ };
+
+ /**
+ * Creates a new vec2 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} a new 2D vector
+ */
+ vec2.fromValues = function(x, y) {
+ var out = new glMatrix.ARRAY_TYPE(2);
+ out[0] = x;
+ out[1] = y;
+ return out;
+ };
+
+ /**
+ * Copy the values from one vec2 to another
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the source vector
+ * @returns {vec2} out
+ */
+ vec2.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ return out;
+ };
+
+ /**
+ * Set the components of a vec2 to the given values
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} out
+ */
+ vec2.set = function(out, x, y) {
+ out[0] = x;
+ out[1] = y;
+ return out;
+ };
+
+ /**
+ * Adds two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+ vec2.add = function(out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ return out;
+ };
+
+ /**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+ vec2.subtract = function(out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec2.subtract}
+ * @function
+ */
+ vec2.sub = vec2.subtract;
+
+ /**
+ * Multiplies two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+ vec2.multiply = function(out, a, b) {
+ out[0] = a[0] * b[0];
+ out[1] = a[1] * b[1];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec2.multiply}
+ * @function
+ */
+ vec2.mul = vec2.multiply;
+
+ /**
+ * Divides two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+ vec2.divide = function(out, a, b) {
+ out[0] = a[0] / b[0];
+ out[1] = a[1] / b[1];
+ return out;
+ };
+
+ /**
+ * Alias for {@link vec2.divide}
+ * @function
+ */
+ vec2.div = vec2.divide;
+
+ /**
+ * Returns the minimum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+ vec2.min = function(out, a, b) {
+ out[0] = Math.min(a[0], b[0]);
+ out[1] = Math.min(a[1], b[1]);
+ return out;
+ };
+
+ /**
+ * Returns the maximum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+ vec2.max = function(out, a, b) {
+ out[0] = Math.max(a[0], b[0]);
+ out[1] = Math.max(a[1], b[1]);
+ return out;
+ };
+
+ /**
+ * Scales a vec2 by a scalar number
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec2} out
+ */
+ vec2.scale = function(out, a, b) {
+ out[0] = a[0] * b;
+ out[1] = a[1] * b;
+ return out;
+ };
+
+ /**
+ * Adds two vec2's after scaling the second operand by a scalar value
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec2} out
+ */
+ vec2.scaleAndAdd = function(out, a, b, scale) {
+ out[0] = a[0] + (b[0] * scale);
+ out[1] = a[1] + (b[1] * scale);
+ return out;
+ };
+
+ /**
+ * Calculates the euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} distance between a and b
+ */
+ vec2.distance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1];
+ return Math.sqrt(x*x + y*y);
+ };
+
+ /**
+ * Alias for {@link vec2.distance}
+ * @function
+ */
+ vec2.dist = vec2.distance;
+
+ /**
+ * Calculates the squared euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+ vec2.squaredDistance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1];
+ return x*x + y*y;
+ };
+
+ /**
+ * Alias for {@link vec2.squaredDistance}
+ * @function
+ */
+ vec2.sqrDist = vec2.squaredDistance;
+
+ /**
+ * Calculates the length of a vec2
+ *
+ * @param {vec2} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+ vec2.length = function (a) {
+ var x = a[0],
+ y = a[1];
+ return Math.sqrt(x*x + y*y);
+ };
+
+ /**
+ * Alias for {@link vec2.length}
+ * @function
+ */
+ vec2.len = vec2.length;
+
+ /**
+ * Calculates the squared length of a vec2
+ *
+ * @param {vec2} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+ vec2.squaredLength = function (a) {
+ var x = a[0],
+ y = a[1];
+ return x*x + y*y;
+ };
+
+ /**
+ * Alias for {@link vec2.squaredLength}
+ * @function
+ */
+ vec2.sqrLen = vec2.squaredLength;
+
+ /**
+ * Negates the components of a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to negate
+ * @returns {vec2} out
+ */
+ vec2.negate = function(out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ return out;
+ };
+
+ /**
+ * Returns the inverse of the components of a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to invert
+ * @returns {vec2} out
+ */
+ vec2.inverse = function(out, a) {
+ out[0] = 1.0 / a[0];
+ out[1] = 1.0 / a[1];
+ return out;
+ };
+
+ /**
+ * Normalize a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to normalize
+ * @returns {vec2} out
+ */
+ vec2.normalize = function(out, a) {
+ var x = a[0],
+ y = a[1];
+ var len = x*x + y*y;
+ if (len > 0) {
+ //TODO: evaluate use of glm_invsqrt here?
+ len = 1 / Math.sqrt(len);
+ out[0] = a[0] * len;
+ out[1] = a[1] * len;
+ }
+ return out;
+ };
+
+ /**
+ * Calculates the dot product of two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+ vec2.dot = function (a, b) {
+ return a[0] * b[0] + a[1] * b[1];
+ };
+
+ /**
+ * Computes the cross product of two vec2's
+ * Note that the cross product must by definition produce a 3D vector
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec3} out
+ */
+ vec2.cross = function(out, a, b) {
+ var z = a[0] * b[1] - a[1] * b[0];
+ out[0] = out[1] = 0;
+ out[2] = z;
+ return out;
+ };
+
+ /**
+ * Performs a linear interpolation between two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec2} out
+ */
+ vec2.lerp = function (out, a, b, t) {
+ var ax = a[0],
+ ay = a[1];
+ out[0] = ax + t * (b[0] - ax);
+ out[1] = ay + t * (b[1] - ay);
+ return out;
+ };
+
+ /**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec2} out
+ */
+ vec2.random = function (out, scale) {
+ scale = scale || 1.0;
+ var r = glMatrix.RANDOM() * 2.0 * Math.PI;
+ out[0] = Math.cos(r) * scale;
+ out[1] = Math.sin(r) * scale;
+ return out;
+ };
+
+ /**
+ * Transforms the vec2 with a mat2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2} m matrix to transform with
+ * @returns {vec2} out
+ */
+ vec2.transformMat2 = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[2] * y;
+ out[1] = m[1] * x + m[3] * y;
+ return out;
+ };
+
+ /**
+ * Transforms the vec2 with a mat2d
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2d} m matrix to transform with
+ * @returns {vec2} out
+ */
+ vec2.transformMat2d = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[2] * y + m[4];
+ out[1] = m[1] * x + m[3] * y + m[5];
+ return out;
+ };
+
+ /**
+ * Transforms the vec2 with a mat3
+ * 3rd vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat3} m matrix to transform with
+ * @returns {vec2} out
+ */
+ vec2.transformMat3 = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[3] * y + m[6];
+ out[1] = m[1] * x + m[4] * y + m[7];
+ return out;
+ };
+
+ /**
+ * Transforms the vec2 with a mat4
+ * 3rd vector component is implicitly '0'
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec2} out
+ */
+ vec2.transformMat4 = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[4] * y + m[12];
+ out[1] = m[1] * x + m[5] * y + m[13];
+ return out;
+ };
+
+ /**
+ * Perform some operation over an array of vec2s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+ vec2.forEach = (function() {
+ var vec = vec2.create();
+
+ return function(a, stride, offset, count, fn, arg) {
+ var i, l;
+ if(!stride) {
+ stride = 2;
+ }
+
+ if(!offset) {
+ offset = 0;
+ }
+
+ if(count) {
+ l = Math.min((count * stride) + offset, a.length);
+ } else {
+ l = a.length;
+ }
+
+ for(i = offset; i < l; i += stride) {
+ vec[0] = a[i]; vec[1] = a[i+1];
+ fn(vec, vec, arg);
+ a[i] = vec[0]; a[i+1] = vec[1];
+ }
+
+ return a;
+ };
+ })();
+
+ /**
+ * Returns a string representation of a vector
+ *
+ * @param {vec2} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+ vec2.str = function (a) {
+ return 'vec2(' + a[0] + ', ' + a[1] + ')';
+ };
+
+ module.exports = vec2;
+
+
+/***/ }
+/******/ ])
+});
+; \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allclasses.tmpl b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allclasses.tmpl
new file mode 100644
index 00000000000..e89da024d82
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allclasses.tmpl
@@ -0,0 +1,14 @@
+<div align="center">{+new Link().toFile("index.html").withText("Class Index")+}
+| {+new Link().toFile("files.html").withText("File Index")+}</div>
+<hr />
+<h2>Classes</h2>
+<ul class="classList">
+ <for each="thisClass" in="data">
+ <li>{!
+ if (thisClass.alias != "_global_") {
+ output += new Link().toClass(thisClass.alias);
+ }
+ !}</li>
+ </for>
+</ul>
+<hr /> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allfiles.tmpl b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allfiles.tmpl
new file mode 100644
index 00000000000..c6e40c9a4fe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/allfiles.tmpl
@@ -0,0 +1,65 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset={+IO.encoding+}" />
+ {! Link.base = ""; /* all generated links will be relative to this */ !}
+ <title>glMatrix - File Index</title>
+ <meta name="generator" content="JsDoc Toolkit" />
+
+ <style type="text/css">
+ {+include("static/default.css")+}
+ </style>
+ </head>
+
+ <body>
+ {+include("static/header.html")+}
+
+ <div class="wrapper">
+
+ <header id="index">
+ {+publish.classesIndex+}
+ </header>
+
+ <section id="content">
+ <h1 class="classTitle">File Index</h1>
+
+ <for each="item" in="data">
+ <div>
+ <h2>{+new Link().toSrc(item.alias).withText(item.name)+}</h2>
+ <if test="item.desc">{+resolveLinks(item.desc)+}</if>
+ <dl>
+ <if test="item.author">
+ <dt class="heading">Author:</dt>
+ <dd>{+item.author+}</dd>
+ </if>
+ <if test="item.version">
+ <dt class="heading">Version:</dt>
+ <dd>{+item.version+}</dd>
+ </if>
+ {! var locations = item.comment.getTag('location').map(function($){return $.toString().replace(/(^\$ ?| ?\$$)/g, '').replace(/^HeadURL: https:/g, 'http:');}) !}
+ <if test="locations.length">
+ <dt class="heading">Location:</dt>
+ <for each="location" in="locations">
+ <dd><a href="{+location+}">{+location+}</a></dd>
+ </for>
+ </if>
+ </dl>
+ </div>
+ <hr />
+ </for>
+
+ </section>
+
+ <footer>
+ <small>
+ <if test="JSDOC.opt.D.copyright">&copy;{+JSDOC.opt.D.copyright+}<br /></if>
+ Documentation generated by <a href="http://code.google.com/p/jsdoc-toolkit/" target="_blank">JsDoc Toolkit</a> {+JSDOC.VERSION+} on {+new Date()+}
+ <br/><br/>
+ Theme based on Github Pages template by <a href="https://github.com/orderedlist">orderedlist</a>
+ </small>
+ </footer>
+
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/class.tmpl b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/class.tmpl
new file mode 100644
index 00000000000..cb1d2b0f3d6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/class.tmpl
@@ -0,0 +1,340 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset={+IO.encoding+}" />
+ <meta name="generator" content="JsDoc Toolkit" />
+ {! Link.base = "../"; /* all generated links will be relative to this */ !}
+ <title>glMatrix - {+data.alias+}</title>
+
+ <style type="text/css">
+ {+include("static/default.css")+}
+ </style>
+ </head>
+
+ <body>
+<!-- ============================== header ================================= -->
+ <!-- begin static/header.html -->
+ {+include("static/header.html")+}
+ <!-- end static/header.html -->
+
+ <div class="wrapper">
+<!-- ============================== classes index ============================ -->
+ <header id="index">
+ <!-- begin publish.classesIndex -->
+ {+publish.classesIndex+}
+ <!-- end publish.classesIndex -->
+ </header>
+
+ <section id="content">
+<!-- ============================== class title ============================ -->
+ <h1 class="classTitle">
+ {!
+ var classType = "";
+
+ if (data.isBuiltin()) {
+ classType += "Built-In ";
+ }
+
+ if (data.isNamespace) {
+ if (data.is('FUNCTION')) {
+ classType += "Function ";
+ }
+ classType += "Namespace ";
+ }
+ else {
+ classType += "Class ";
+ }
+ !}
+ {+classType+}{+data.alias+}
+ </h1>
+
+<!-- ============================== class summary ========================== -->
+ <p class="description">
+ <if test="data.version"><br />Version
+ {+ data.version +}.<br />
+ </if>
+ <if test="data.augments.length"><br />Extends
+ {+
+ data.augments
+ .sort()
+ .map(
+ function($) { return new Link().toSymbol($); }
+ )
+ .join(", ")
+ +}.<br />
+ </if>
+
+ {+resolveLinks(data.classDesc)+}
+
+ <if test="data.desc">
+ <div class="description">{+resolveLinks(summarize(data.desc))+}</div>
+ </if>
+
+ <if test="!data.isBuiltin()">{# isn't defined in any file #}
+ <br /><i>Defined in: </i> {+new Link().toSrc(data.srcFile)+}.
+ </if>
+ </p>
+
+<!-- ============================== properties summary ===================== -->
+ <if test="data.properties.length">
+ {! var ownProperties = data.properties.filter(function($){return $.memberOf == data.alias && !$.isNamespace}).sort(makeSortby("name")); !}
+ <if test="ownProperties.length">
+ <table class="summaryTable" cellspacing="0" summary="A summary of the fields documented in the class {+data.alias+}.">
+ <caption>Field Summary</caption>
+ <thead>
+ <tr>
+ <th scope="col">Field Attributes</th>
+ <th scope="col">Field Name and Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <for each="member" in="ownProperties">
+ <tr>
+ <td class="attributes">{!
+ if (member.isPrivate) output += "&lt;private&gt; ";
+ if (member.isInner) output += "&lt;inner&gt; ";
+ if (member.isStatic) output += "&lt;static&gt; ";
+ if (member.isConstant) output += "&lt;constant&gt; ";
+ !}&nbsp;</td>
+ <td class="nameDescription">
+ <div class="fixedFont">
+ <if test="member.isStatic && member.memberOf != '_global_'">{+member.memberOf+}.</if><b>{+new Link().toSymbol(member.alias).withText(member.name)+}</b>
+ </div>
+ <div class="description">{+resolveLinks(summarize(member.desc))+}</div>
+ </td>
+ </tr>
+ </for>
+ </tbody>
+ </table>
+ </if>
+
+ <if test="data.inheritsFrom.length">
+ <dl class="inheritsList">
+ {!
+ var borrowedMembers = data.properties.filter(function($) {return $.memberOf != data.alias});
+
+ var contributers = [];
+ borrowedMembers.map(function($) {if (contributers.indexOf($.memberOf) < 0) contributers.push($.memberOf)});
+ for (var i = 0, l = contributers.length; i < l; i++) {
+ output +=
+ "<dt>Fields borrowed from class "+new Link().toSymbol(contributers[i])+": </dt>"
+ +
+ "<dd>" +
+ borrowedMembers
+ .filter(
+ function($) { return $.memberOf == contributers[i] }
+ )
+ .sort(makeSortby("name"))
+ .map(
+ function($) { return new Link().toSymbol($.alias).withText($.name) }
+ )
+ .join(", ")
+ +
+ "</dd>";
+ }
+ !}
+ </dl>
+ </if>
+ </if>
+
+<!-- ============================== methods summary ======================== -->
+ <if test="data.methods.length">
+ {! var ownMethods = data.methods.filter(function($){return $.memberOf == data.alias && !$.isNamespace}).sort(makeSortby("name")); !}
+ <if test="ownMethods.length">
+ <h2>Methods</h2>
+ <table class="summaryTable" cellspacing="0" summary="A summary of the methods documented in the class {+data.alias+}.">
+ <tbody>
+ <for each="member" in="ownMethods">
+ <tr>
+ <td class="nameDescription">
+ <code class="fixedFont"><if test="member.isStatic && member.memberOf != '_global_'">{+member.memberOf+}.</if><b>{+new Link().toSymbol(member.alias).withText(member.name.replace(/\^\d+$/, ''))+}</b>{+makeSignature(member.params)+}
+ </code>
+ <div class="description">{+resolveLinks(summarize(member.desc))+}</div>
+ </td>
+ </tr>
+ </for>
+ </tbody>
+ </table>
+ </if>
+ </if>
+
+<!-- ============================== field details ========================== -->
+ <if test="defined(ownProperties) && ownProperties.length">
+ <div class="sectionTitle">
+ Field Detail
+ </div>
+ <for each="member" in="ownProperties">
+ <a name="{+Link.symbolNameToLinkName(member)+}"> </a>
+ <div class="fixedFont">{!
+ if (member.isPrivate) output += "&lt;private&gt; ";
+ if (member.isInner) output += "&lt;inner&gt; ";
+ if (member.isStatic) output += "&lt;static&gt; ";
+ if (member.isConstant) output += "&lt;constant&gt; ";
+ !}
+
+ <if test="member.type"><span class="light">{{+new Link().toSymbol(member.type)+}}</span></if>
+ <if test="member.isStatic && member.memberOf != '_global_'"><span class="light">{+member.memberOf+}.</span></if><b>{+member.name+}</b>
+
+ </div>
+ <div class="description">
+ {+resolveLinks(member.desc)+}
+ <if test="member.srcFile != data.srcFile">
+ <br />
+ <i>Defined in: </i> {+new Link().toSrc(member.srcFile)+}.
+ </if>
+ <if test="member.author"><br /><i>Author: </i>{+member.author+}.</if>
+ </div>
+
+ <if test="member.example.length">
+ <for each="example" in="member.example">
+ <pre class="code">{+example+}</pre>
+ </for>
+ </if>
+
+ <if test="member.deprecated">
+ <dl class="detailList">
+ <dt class="heading">Deprecated:</dt>
+ <dt>
+ {+ resolveLinks(member.deprecated) +}
+ </dt>
+ </dl>
+ </if>
+ <if test="member.since">
+ <dl class="detailList">
+ <dt class="heading">Since:</dt>
+ <dd>{+ member.since +}</dd>
+ </dl>
+ </if>
+ <if test="member.see.length">
+ <dl class="detailList">
+ <dt class="heading">See:</dt>
+ <for each="item" in="member.see">
+ <dd>{+ new Link().toSymbol(item) +}</dd>
+ </for>
+ </dl>
+ </if>
+ <if test="member.defaultValue">
+ <dl class="detailList">
+ <dt class="heading">Default Value:</dt>
+ <dd>
+ {+resolveLinks(member.defaultValue)+}
+ </dd>
+ </dl>
+ </if>
+
+ <if test="!$member_last"><hr /></if>
+ </for>
+ </if>
+
+<!-- ============================== method details ========================= -->
+ <if test="defined(ownMethods) && ownMethods.length">
+ <h2>Method Detail</h2>
+ <for each="member" in="ownMethods">
+ <a name="{+Link.symbolNameToLinkName(member)+}"> </a>
+ <h3 class="fixedFont">
+ <if test="member.type"><span class="light">{{+new Link().toSymbol(member.type)+}}</span></if>
+ <if test="member.isStatic && member.memberOf != '_global_'"><span class="light">{+member.memberOf+}.</span></if><b>{+member.name.replace(/\^\d+$/, '')+}</b>{+makeSignature(member.params)+}
+ </h3>
+
+ <div style="margin-left: 1em;">
+ <p class="description">
+ {+resolveLinks(member.desc)+}
+ <if test="member.srcFile != data.srcFile">
+ <br />
+ <i>Defined in: </i> {+new Link().toSrc(member.srcFile)+}.
+ </if>
+ <if test="member.author"><br /><i>Author: </i>{+member.author+}.</if>
+ </p>
+
+ <if test="member.example.length">
+ <for each="example" in="member.example">
+ <pre class="code">{+example+}</pre>
+ </for>
+ </if>
+
+ <if test="member.params.length">
+ <dl class="detailList">
+ <dt class="heading">Parameters:</dt>
+ <for each="item" in="member.params">
+ <dt>
+ {+((item.type)?"<span class=\"light fixedFont\">{"+(new Link().toSymbol(item.type))+"}</span> " : "")+}<b>{+item.name+}</b>
+ <if test="item.isOptional"><i>Optional<if test="item.defaultValue">, Default: {+item.defaultValue+}</if></i></if>
+ </dt>
+ <dd>{+resolveLinks(item.desc)+}</dd>
+ </for>
+ </dl>
+ </if>
+ <if test="member.deprecated">
+ <dl class="detailList">
+ <dt class="heading">Deprecated:</dt>
+ <dt>
+ {+ resolveLinks(member.deprecated) +}
+ </dt>
+ </dl>
+ </if>
+ <if test="member.since">
+ <dl class="detailList">
+ <dt class="heading">Since:</dt>
+ <dd>{+ member.since +}</dd>
+ </dl>
+ </dl>
+ </if>
+ <if test="member.exceptions.length">
+ <dl class="detailList">
+ <dt class="heading">Throws:</dt>
+ <for each="item" in="member.exceptions">
+ <dt>
+ {+((item.type)?"<span class=\"light fixedFont\">{"+(new Link().toSymbol(item.type))+"}</span> " : "")+} <b>{+item.name+}</b>
+ </dt>
+ <dd>{+resolveLinks(item.desc)+}</dd>
+ </for>
+ </dl>
+ </if>
+ <if test="member.returns.length">
+ <dl class="detailList">
+ <dt class="heading">Returns:</dt>
+ <for each="item" in="member.returns">
+ <dd>{+((item.type)?"<span class=\"light fixedFont\">{"+(new Link().toSymbol(item.type))+"}</span> " : "")+}{+resolveLinks(item.desc)+}</dd>
+ </for>
+ </dl>
+ </if>
+ <if test="member.requires.length">
+ <dl class="detailList">
+ <dt class="heading">Requires:</dt>
+ <for each="item" in="member.requires">
+ <dd>{+ resolveLinks(item) +}</dd>
+ </for>
+ </dl>
+ </if>
+ <if test="member.see.length">
+ <dl class="detailList">
+ <dt class="heading">See:</dt>
+ <for each="item" in="member.see">
+ <dd>{+ new Link().toSymbol(item) +}</dd>
+ </for>
+ </dl>
+ </if>
+
+ <if test="!$member_last"><hr /></if>
+ </div>
+ </for>
+ </if>
+
+ <hr />
+ </section>
+
+
+<!-- ============================== footer ================================= -->
+ <footer>
+ <small>
+ <if test="JSDOC.opt.D.copyright">&copy;{+JSDOC.opt.D.copyright+}<br /></if>
+ Documentation generated by <a href="http://code.google.com/p/jsdoc-toolkit/" target="_blank">JsDoc Toolkit</a> {+JSDOC.VERSION+} on {+new Date()+}
+ <br/><br/>
+ Theme based on Github Pages template by <a href="https://github.com/orderedlist">orderedlist</a>
+ </small>
+ </footer>
+
+ </div>
+ </body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/index.tmpl b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/index.tmpl
new file mode 100644
index 00000000000..7c92dd40ccd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/index.tmpl
@@ -0,0 +1,52 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset={+IO.encoding+}" />
+
+ <title>glMatrix - Index</title>
+ <meta name="generator" content="JsDoc Toolkit" />
+
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+
+ <style type="text/css">
+ {+include("static/default.css")+}
+ </style>
+ </head>
+
+ <body>
+ {+include("static/header.html")+}
+
+ <div class="wrapper">
+
+ <header id="index">
+ {+publish.classesIndex+}
+ </header>
+
+ <section id="content">
+ <h1 class="classTitle">Class Index</h1>
+
+ <for each="thisClass" in="data">
+ <if test="thisClass.alias != '_global_'">
+ <div>
+ <h2>{+(new Link().toSymbol(thisClass.alias))+}</h2>
+ {+resolveLinks(summarize(thisClass.classDesc))+}
+ </div>
+ <hr />
+ </if>
+ </for>
+
+ </section>
+
+ <footer>
+ <small>
+ <if test="JSDOC.opt.D.copyright">&copy;{+JSDOC.opt.D.copyright+}<br /></if>
+ Documentation generated by <a href="http://code.google.com/p/jsdoc-toolkit/" target="_blank">JsDoc Toolkit</a> {+JSDOC.VERSION+} on {+new Date()+}
+ <br/><br/>
+ Theme based on Github Pages template by <a href="https://github.com/orderedlist">orderedlist</a>
+ </small>
+ </footer>
+
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/publish.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/publish.js
new file mode 100644
index 00000000000..82c20045e94
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/publish.js
@@ -0,0 +1,201 @@
+/** Called automatically by JsDoc Toolkit. */
+function publish(symbolSet) {
+ publish.conf = { // trailing slash expected for dirs
+ ext: ".html",
+ outDir: JSDOC.opt.d || SYS.pwd+"../out/jsdoc/",
+ templatesDir: JSDOC.opt.t || SYS.pwd+"../jsdoc-template/",
+ symbolsDir: "symbols/",
+ srcDir: "symbols/src/"
+ };
+
+ // is source output is suppressed, just display the links to the source file
+ if (JSDOC.opt.s && defined(Link) && Link.prototype._makeSrcLink) {
+ Link.prototype._makeSrcLink = function(srcFilePath) {
+ return "&lt;"+srcFilePath+"&gt;";
+ }
+ }
+
+ // create the folders and subfolders to hold the output
+ IO.mkPath((publish.conf.outDir+"symbols/src").split("/"));
+
+ // used to allow Link to check the details of things being linked to
+ Link.symbolSet = symbolSet;
+
+ // create the required templates
+ try {
+ var classTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"class.tmpl");
+ var classesTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"allclasses.tmpl");
+ }
+ catch(e) {
+ print("Couldn't create the required templates: "+e);
+ quit();
+ }
+
+ // some ustility filters
+ function hasNoParent($) {return ($.memberOf == "")}
+ function isaFile($) {return ($.is("FILE"))}
+ function isaClass($) {return ($.is("CONSTRUCTOR") || $.isNamespace)}
+
+ // get an array version of the symbolset, useful for filtering
+ var symbols = symbolSet.toArray();
+
+ // create the hilited source code files
+ var files = JSDOC.opt.srcFiles;
+ for (var i = 0, l = files.length; i < l; i++) {
+ var file = files[i];
+ var srcDir = publish.conf.outDir + "symbols/src/";
+ makeSrcFile(file, srcDir);
+ }
+
+ // get a list of all the classes in the symbolset
+ var classes = symbols.filter(isaClass).sort(makeSortby("alias"));
+
+ // create a filemap in which outfiles must be to be named uniquely, ignoring case
+ if (JSDOC.opt.u) {
+ var filemapCounts = {};
+ Link.filemap = {};
+ for (var i = 0, l = classes.length; i < l; i++) {
+ var lcAlias = classes[i].alias.toLowerCase();
+
+ if (!filemapCounts[lcAlias]) filemapCounts[lcAlias] = 1;
+ else filemapCounts[lcAlias]++;
+
+ Link.filemap[classes[i].alias] =
+ (filemapCounts[lcAlias] > 1)?
+ lcAlias+"_"+filemapCounts[lcAlias] : lcAlias;
+ }
+ }
+
+ // create a class index, displayed in the left-hand column of every class page
+ Link.base = "../";
+ publish.classesIndex = classesTemplate.process(classes); // kept in memory
+
+ // create each of the class pages
+ for (var i = 0, l = classes.length; i < l; i++) {
+ var symbol = classes[i];
+
+ symbol.events = symbol.getEvents(); // 1 order matters
+ symbol.methods = symbol.getMethods(); // 2
+
+ Link.currentSymbol= symbol;
+ var output = "";
+ output = classTemplate.process(symbol);
+
+ IO.saveFile(publish.conf.outDir+"symbols/", ((JSDOC.opt.u)? Link.filemap[symbol.alias] : symbol.alias) + publish.conf.ext, output);
+ }
+
+ // regenerate the index with different relative links, used in the index pages
+ Link.base = "";
+ publish.classesIndex = classesTemplate.process(classes);
+
+ // create the class index page
+ try {
+ var classesindexTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"index.tmpl");
+ }
+ catch(e) { print(e.message); quit(); }
+
+ var classesIndex = classesindexTemplate.process(classes);
+ IO.saveFile(publish.conf.outDir, "index"+publish.conf.ext, classesIndex);
+ classesindexTemplate = classesIndex = classes = null;
+
+ // create the file index page
+ try {
+ var fileindexTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"allfiles.tmpl");
+ }
+ catch(e) { print(e.message); quit(); }
+
+ var documentedFiles = symbols.filter(isaFile); // files that have file-level docs
+ var allFiles = []; // not all files have file-level docs, but we need to list every one
+
+ for (var i = 0; i < files.length; i++) {
+ allFiles.push(new JSDOC.Symbol(files[i], [], "FILE", new JSDOC.DocComment("/** */")));
+ }
+
+ for (var i = 0; i < documentedFiles.length; i++) {
+ var offset = files.indexOf(documentedFiles[i].alias);
+ allFiles[offset] = documentedFiles[i];
+ }
+
+ allFiles = allFiles.sort(makeSortby("name"));
+
+ // output the file index page
+ var filesIndex = fileindexTemplate.process(allFiles);
+ IO.saveFile(publish.conf.outDir, "files"+publish.conf.ext, filesIndex);
+ fileindexTemplate = filesIndex = files = null;
+}
+
+
+/** Just the first sentence (up to a full stop). Should not break on dotted variable names. */
+function summarize(desc) {
+ if (typeof desc != "undefined")
+ return desc.match(/([\w\W]+?\.)[^a-z0-9_$]/i)? RegExp.$1 : desc;
+}
+
+/** Make a symbol sorter by some attribute. */
+function makeSortby(attribute) {
+ return function(a, b) {
+ if (a[attribute] != undefined && b[attribute] != undefined) {
+ a = a[attribute].toLowerCase();
+ b = b[attribute].toLowerCase();
+ if (a < b) return -1;
+ if (a > b) return 1;
+ return 0;
+ }
+ }
+}
+
+/** Pull in the contents of an external file at the given path. */
+function include(path) {
+ var path = publish.conf.templatesDir+path;
+ return IO.readFile(path);
+}
+
+/** Turn a raw source file into a code-hilited page in the docs. */
+function makeSrcFile(path, srcDir, name) {
+ if (JSDOC.opt.s) return;
+
+ if (!name) {
+ name = path.replace(/\.\.?[\\\/]/g, "").replace(/[\\\/]/g, "_");
+ name = name.replace(/\:/g, "_");
+ }
+
+ var src = {path: path, name:name, charset: IO.encoding, hilited: ""};
+
+ if (defined(JSDOC.PluginManager)) {
+ JSDOC.PluginManager.run("onPublishSrc", src);
+ }
+
+ if (src.hilited) {
+ IO.saveFile(srcDir, name+publish.conf.ext, src.hilited);
+ }
+}
+
+/** Build output for displaying function parameters. */
+function makeSignature(params) {
+ if (!params) return "()";
+ var signature = "("
+ +
+ params.filter(
+ function($) {
+ return $.name.indexOf(".") == -1; // don't show config params in signature
+ }
+ ).map(
+ function($) {
+ return $.name;
+ }
+ ).join(", ")
+ +
+ ")";
+ return signature;
+}
+
+/** Find symbol {@link ...} strings in text and turn into html links */
+function resolveLinks(str, from) {
+ str = str.replace(/\{@link ([^} ]+) ?\}/gi,
+ function(match, symbolName) {
+ return new Link().toSymbol(symbolName);
+ }
+ );
+
+ return str;
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/default.css b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/default.css
new file mode 100644
index 00000000000..efcf7827133
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/default.css
@@ -0,0 +1,428 @@
+/* default.css */
+
+/*
+
+body
+{
+ font: 12px "Lucida Grande", Tahoma, Arial, Helvetica, sans-serif;
+ width: 800px;
+}
+
+.header
+{
+ clear: both;
+ background-color: #ccc;
+ padding: 8px;
+}
+
+h1
+{
+ font-size: 150%;
+ font-weight: bold;
+ padding: 0;
+ margin: 1em 0 0 .3em;
+}
+
+hr
+{
+ border: none 0;
+ border-top: 1px solid #7F8FB1;
+ height: 1px;
+}
+
+pre.code
+{
+ display: block;
+ padding: 8px;
+ border: 1px dashed #ccc;
+}
+
+#index
+{
+ margin-top: 24px;
+ float: left;
+ width: 160px;
+ position: absolute;
+ left: 8px;
+ background-color: #F3F3F3;
+ padding: 8px;
+}
+
+#content
+{
+ margin-left: 190px;
+ width: 600px;
+}
+
+.classList
+{
+ list-style-type: none;
+ padding: 0;
+ margin: 0 0 0 8px;
+ font-family: arial, sans-serif;
+ font-size: 1em;
+ overflow: auto;
+}
+
+.classList li
+{
+ padding: 0;
+ margin: 0 0 8px 0;
+}
+
+.summaryTable { width: 100%; }
+
+h1.classTitle
+{
+ font-size:170%;
+ line-height:130%;
+}
+
+h2 { font-size: 110%; }
+caption, div.sectionTitle
+{
+ background-color: #7F8FB1;
+ color: #fff;
+ font-size:130%;
+ text-align: left;
+ padding: 2px 6px 2px 6px;
+ border: 1px #7F8FB1 solid;
+}
+
+div.sectionTitle { margin-bottom: 8px; }
+.summaryTable thead { display: none; }
+
+.summaryTable td
+{
+ vertical-align: top;
+ padding: 4px;
+ border-bottom: 1px #7F8FB1 solid;
+ border-right: 1px #7F8FB1 solid;
+ border-left: 1px #7F8FB1 solid;
+}
+
+.summaryTable td.attributes
+{
+ border-left: 1px #7F8FB1 solid;
+ width: 140px;
+ text-align: right;
+}
+
+td.attributes, .fixedFont
+{
+ line-height: 15px;
+ color: #002EBE;
+ font-family: "Courier New",Courier,monospace;
+ font-size: 13px;
+}
+
+.summaryTable td.nameDescription
+{
+ text-align: left;
+ font-size: 13px;
+ line-height: 15px;
+}
+
+.summaryTable td.nameDescription, .description
+{
+ line-height: 15px;
+ padding: 4px;
+ padding-left: 4px;
+}
+
+.summaryTable { margin-bottom: 8px; }
+
+ul.inheritsList
+{
+ list-style: square;
+ margin-left: 20px;
+ padding-left: 0;
+}
+
+.detailList {
+ margin-left: 20px;
+ line-height: 15px;
+}
+.detailList dt { margin-left: 20px; }
+
+.detailList .heading
+{
+ font-weight: bold;
+ padding-bottom: 6px;
+ margin-left: 0;
+}
+
+.light, td.attributes, .light a:link, .light a:visited
+{
+ color: #777;
+ font-style: italic;
+}
+
+.fineprint
+{
+ text-align: right;
+ font-size: 10px;
+}
+
+*/
+
+/* Copied from styles.css generated by Github Pages */
+
+@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700);
+
+body {
+ padding:50px;
+ font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color:#777;
+ font-weight:300;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color:#222;
+ margin:0 0 20px;
+}
+
+p, ul, ol, table, pre, dl {
+ margin:0 0 20px;
+}
+
+h1, h2, h3 {
+ line-height:1.1;
+}
+
+h1 {
+ font-size:28px;
+}
+
+h2 {
+ color:#393939;
+}
+
+h3, h4, h5, h6 {
+ color:#494949;
+}
+
+a {
+ color:#39c;
+ font-weight:400;
+ text-decoration:none;
+}
+
+a small {
+ font-size:11px;
+ color:#777;
+ margin-top:-0.6em;
+ display:block;
+}
+
+.wrapper {
+ width:860px;
+ margin:0 auto;
+}
+
+blockquote {
+ border-left:1px solid #e5e5e5;
+ margin:0;
+ padding:0 0 0 20px;
+ font-style:italic;
+}
+
+code, pre {
+ font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;
+ color:#333;
+ font-size:12px;
+}
+
+pre {
+ padding:8px 15px;
+ background: #f8f8f8;
+ border-radius:5px;
+ border:1px solid #e5e5e5;
+ overflow-x: auto;
+}
+
+table {
+ width:100%;
+ border-collapse:collapse;
+}
+
+th, td {
+ text-align:left;
+ padding:5px 10px;
+ border-bottom:1px solid #e5e5e5;
+}
+
+dt {
+ color:#444;
+ font-weight:700;
+}
+
+th {
+ color:#444;
+}
+
+img {
+ max-width:100%;
+}
+
+header {
+ width:270px;
+ float:left;
+ position:fixed;
+}
+
+header ul {
+ list-style:none;
+ padding:0;
+
+ /*background: #eee;
+ background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd));
+ background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%);
+ background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%);
+ background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%);
+ background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%);
+
+ border-radius:5px;
+ border:1px solid #d2d2d2;
+ box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0;*/
+ width:270px;
+}
+
+header li {
+ width:89px;
+ /*float:left;
+ border-right:1px solid #d2d2d2;
+ height:40px;*/
+}
+
+header ul a {
+ line-height:1;
+ font-size:11px;
+ color:#999;
+ display:block;
+ text-align:center;
+ padding-top:6px;
+ /*height:40px;*/
+}
+
+strong {
+ color:#222;
+ font-weight:700;
+}
+
+/*header ul li + li {
+ width:88px;
+ border-left:1px solid #fff;
+}
+
+header ul li + li + li {
+ border-right:none;
+ width:89px;
+}*/
+
+header ul a strong {
+ font-size:14px;
+ display:block;
+ color:#222;
+}
+
+section {
+ width:500px;
+ float:right;
+ padding-bottom:50px;
+}
+
+small {
+ font-size:11px;
+}
+
+hr {
+ border:0;
+ background:#e5e5e5;
+ height:1px;
+ margin:0 0 20px;
+}
+
+footer {
+ width:270px;
+ float:left;
+ position:fixed;
+ bottom:50px;
+}
+
+@media print, screen and (max-width: 960px) {
+
+ div.wrapper {
+ width:auto;
+ margin:0;
+ }
+
+ header, section, footer {
+ float:none;
+ position:static;
+ width:auto;
+ }
+
+ header {
+ /*padding-right:320px;*/
+ padding: 0;
+ }
+
+ section {
+ border:1px solid #e5e5e5;
+ border-width:1px 0;
+ padding:20px 0;
+ margin:0 0 20px;
+ }
+
+ header a small {
+ display:inline;
+ }
+
+ header ul {
+ position:static;
+ height:40px;
+ width: auto;
+ }
+
+ header ul li {
+ float: left;
+ }
+}
+
+@media print, screen and (max-width: 720px) {
+ body {
+ word-wrap:break-word;
+ }
+
+ header {
+ padding:0;
+ }
+
+ header ul, header p.view {
+ position:static;
+ }
+
+ pre, code {
+ word-wrap:normal;
+ }
+}
+
+@media print, screen and (max-width: 480px) {
+ body {
+ padding:15px;
+ }
+
+ /*header ul {
+ display:none;
+ }*/
+}
+
+@media print {
+ body {
+ padding:0.4in;
+ font-size:12pt;
+ color:#444;
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/header.html b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/header.html
new file mode 100644
index 00000000000..353b735a4ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/header.html
@@ -0,0 +1,2 @@
+<div id="header">
+</div> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/index.html b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/index.html
new file mode 100644
index 00000000000..d51d4efaa15
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/static/index.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>glMatrix Documentation</title>
+</head>
+<frameset cols="20%,80%">
+ <frame src="allclasses-frame.html" name="packageFrame" />
+ <frame src="splash.html" name="classFrame" />
+ <noframes>
+ <body>
+ <p>
+ This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client.
+ </p>
+ </body>
+ </noframes>
+</frameset>
+</html> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/symbol.tmpl b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/symbol.tmpl
new file mode 100644
index 00000000000..f8f4bd1f6f2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/jsdoc-template/symbol.tmpl
@@ -0,0 +1,35 @@
+<symbol alias="{+data.alias+}">
+ <name>{+data.name+}</name>
+ <memberOf>{+data.memberOf+}</memberOf>
+ <isStatic>{+data.isStatic+}</isStatic>
+ <isa>{+data.isa+}</isa>
+ <desc>{+data.desc+}</desc>
+ <classDesc>{+data.classDesc+}</classDesc>
+
+ <methods><for each="method" in="data.methods">
+ <method>
+ <name>{+method.name+}</name>
+ <memberOf>{+method.memberOf+}</memberOf>
+ <isStatic>{+method.isStatic+}</isStatic>
+ <desc>{+method.desc+}</desc>
+ <params><for each="param" in="method.params">
+ <param>
+ <type>{+param.type+}</type>
+ <name>{+param.name+}</name>
+ <desc>{+param.desc+}</desc>
+ <defaultValue>{+param.defaultValue+}</defaultValue>
+ </param></for>
+ </params>
+ </method></for>
+ </methods>
+
+ <properties><for each="property" in="data.properties">
+ <property>
+ <name>{+property.name+}</name>
+ <memberOf>{+property.memberOf+}</memberOf>
+ <isStatic>{+property.isStatic+}</isStatic>
+ <desc>{+property.desc+}</desc>
+ <type>{+property.type+}</type>
+ </property></for>
+ </properties>
+</symbol>
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/package.json b/chromium/third_party/catapult/tracing/third_party/gl-matrix/package.json
new file mode 100644
index 00000000000..f92ab7d2083
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "gl-matrix",
+ "description": "Javascript Matrix and Vector library for High Performance WebGL apps",
+ "version": "2.3.1",
+ "main": "src/gl-matrix.js",
+ "homepage": "http://glmatrix.net",
+ "bugs": {
+ "url": "https://github.com/toji/gl-matrix/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/toji/gl-matrix.git"
+ },
+ "contributors": [
+ {
+ "name": "Brandon Jones",
+ "email": "tojiro@gmail.com"
+ },
+ {
+ "name": "Colin MacKenzie IV",
+ "email": "sinisterchipmunk@gmail.com"
+ }
+ ],
+ "devDependencies": {
+ "jasmine-node": "1.2.2",
+ "node-libs-browser": "^0.5.2",
+ "webpack": "^1.9.10"
+ },
+ "license": "MIT"
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/common-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/common-spec.js
new file mode 100644
index 00000000000..b6649d09587
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/common-spec.js
@@ -0,0 +1,14 @@
+/*
+* common.js unit test
+*/
+
+describe("glMatrix", function(){
+ var result;
+
+ var glMatrix = require("../../src/gl-matrix/common.js");
+
+ describe("toRadian", function(){
+ beforeEach(function(){ result = glMatrix.toRadian(180); });
+ it("should return a value of 3.141592654(Math.PI)", function(){ expect(result).toBeEqualish(Math.PI); });
+ });
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2-spec.js
new file mode 100644
index 00000000000..e901d332a6c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2-spec.js
@@ -0,0 +1,210 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("mat2", function() {
+ var mat2 = require("../../src/gl-matrix/mat2.js");
+
+ var out, matA, matB, identity, result;
+
+ beforeEach(function() {
+ matA = [1, 2,
+ 3, 4];
+
+ matB = [5, 6,
+ 7, 8];
+
+ out = [0, 0,
+ 0, 0];
+
+ identity = [1, 0,
+ 0, 1];
+ });
+
+ describe("create", function() {
+ beforeEach(function() { result = mat2.create(); });
+ it("should return a 4 element array initialized to a 2x2 identity matrix", function() { expect(result).toBeEqualish(identity); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = mat2.clone(matA); });
+ it("should return a 4 element array initialized to the values in matA", function() { expect(result).toBeEqualish(matA); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = mat2.copy(out, matA); });
+ it("should place values into out", function() { expect(out).toBeEqualish(matA); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("identity", function() {
+ beforeEach(function() { result = mat2.identity(out); });
+ it("should place values into out", function() { expect(result).toBeEqualish(identity); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("transpose", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2.transpose(out, matA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 3, 2, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2.transpose(matA, matA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([1, 3, 2, 4]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("invert", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2.invert(out, matA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-2, 1, 1.5, -0.5]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2.invert(matA, matA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([-2, 1, 1.5, -0.5]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("adjoint", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2.adjoint(out, matA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([4, -2, -3, 1]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2.adjoint(matA, matA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([4, -2, -3, 1]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("determinant", function() {
+ beforeEach(function() { result = mat2.determinant(matA); });
+
+ it("should return the determinant", function() { expect(result).toEqual(-2); });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(mat2.mul).toEqual(mat2.multiply); });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2.multiply(out, matA, matB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([23, 34, 31, 46]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify matB", function() { expect(matB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2.multiply(matA, matA, matB); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([23, 34, 31, 46]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ it("should not modify matB", function() { expect(matB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when matB is the output matrix", function() {
+ beforeEach(function() { result = mat2.multiply(matB, matA, matB); });
+
+ it("should place values into matB", function() { expect(matB).toBeEqualish([23, 34, 31, 46]); });
+ it("should return matB", function() { expect(result).toBe(matB); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("rotate", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2.rotate(out, matA, Math.PI * 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4, -1, -2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2.rotate(matA, matA, Math.PI * 0.5); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([3, 4, -1, -2]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("scale", function() {
+ var vecA;
+ beforeEach(function() { vecA = [2, 3]; });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2.scale(out, matA, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 4, 9, 12]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2.scale(matA, matA, vecA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([2, 4, 9, 12]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = mat2.str(matA); });
+
+ it("should return a string representation of the matrix", function() { expect(result).toEqual("mat2(1, 2, 3, 4)"); });
+ });
+
+ describe("frob", function() {
+ beforeEach(function() { result = mat2.frob(matA); });
+ it("should return the Frobenius Norm of the matrix", function() { expect(result).toEqual( Math.sqrt(Math.pow(1, 2) + Math.pow(2, 2) + Math.pow(3, 2) + Math.pow(4, 2))); });
+ });
+
+ describe("LDU", function() {
+ beforeEach(function() {L = mat2.create(); D = mat2.create(); U = mat2.create(); result = mat2.LDU(L, D, U, [4,3,6,3]);
+ L_result = mat2.create(); L_result[2] = 1.5;
+ D_result = mat2.create();
+ U_result = mat2.create();
+ U_result[0] = 4; U_result[1] = 3; U_result[3] = -1.5;
+ });
+ it("should return a lower triangular, a diagonal and an upper triangular matrix", function() {
+ expect(result[0]).toBeEqualish(L_result);
+ expect(result[1]).toBeEqualish(D_result);
+ expect(result[2]).toBeEqualish(U_result);
+ });
+ });
+
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2d-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2d-spec.js
new file mode 100644
index 00000000000..dda0ddd6bc2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat2d-spec.js
@@ -0,0 +1,194 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("mat2d", function() {
+ var mat2d = require("../../src/gl-matrix/mat2d.js");
+
+ var out, matA, matB, identity, result;
+
+ beforeEach(function() {
+ matA = [1, 2,
+ 3, 4,
+ 5, 6];
+
+ oldA = [1, 2,
+ 3, 4,
+ 5, 6];
+
+ matB = [7, 8,
+ 9, 10,
+ 11, 12];
+
+ oldB = [7, 8,
+ 9, 10,
+ 11, 12];
+
+ out = [0, 0,
+ 0, 0,
+ 0, 0];
+
+ identity = [1, 0,
+ 0, 1,
+ 0, 0];
+ });
+
+ describe("create", function() {
+ beforeEach(function() { result = mat2d.create(); });
+ it("should return a 6 element array initialized to a 2x3 identity matrix", function() { expect(result).toBeEqualish(identity); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = mat2d.clone(matA); });
+ it("should return a 6 element array initialized to the values in matA", function() { expect(result).toBeEqualish(matA); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = mat2d.copy(out, matA); });
+ it("should place values into out", function() { expect(out).toBeEqualish(matA); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("identity", function() {
+ beforeEach(function() { result = mat2d.identity(out); });
+ it("should place values into out", function() { expect(result).toBeEqualish(identity); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("invert", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2d.invert(out, matA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([ -2, 1, 1.5, -0.5, 1, -2 ]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish(oldA); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2d.invert(matA, matA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([ -2, 1, 1.5, -0.5, 1, -2 ]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("determinant", function() {
+ beforeEach(function() { result = mat2d.determinant(matA); });
+
+ it("should return the determinant", function() { expect(result).toEqual(-2); });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(mat2d.mul).toEqual(mat2d.multiply); });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2d.multiply(out, matA, matB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([31, 46, 39, 58, 52, 76]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish(oldA); });
+ it("should not modify matB", function() { expect(matB).toBeEqualish(oldB); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2d.multiply(matA, matA, matB); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([31, 46, 39, 58, 52, 76]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ it("should not modify matB", function() { expect(matB).toBeEqualish(oldB); });
+ });
+
+ describe("when matB is the output matrix", function() {
+ beforeEach(function() { result = mat2d.multiply(matB, matA, matB); });
+
+ it("should place values into matB", function() { expect(matB).toBeEqualish([31, 46, 39, 58, 52, 76]); });
+ it("should return matB", function() { expect(result).toBe(matB); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish(oldA); });
+ });
+ });
+
+ describe("rotate", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2d.rotate(out, matA, Math.PI * 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4, -1, -2, 5, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish(oldA); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2d.rotate(matA, matA, Math.PI * 0.5); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([3, 4, -1, -2, 5, 6]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("scale", function() {
+ var vecA;
+ beforeEach(function() { vecA = [2, 3]; });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2d.scale(out, matA, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 4, 9, 12, 5, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish(oldA); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2d.scale(matA, matA, vecA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([2, 4, 9, 12, 5, 6]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("translate", function() {
+ var vecA;
+ beforeEach(function() { vecA = [2, 3]; });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat2d.translate(out, matA, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3, 4, 16, 22]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish(oldA); });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat2d.translate(matA, matA, vecA); });
+
+ it("should place values into matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4, 16, 22]); });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = mat2d.str(matA); });
+
+ it("should return a string representation of the matrix", function() { expect(result).toEqual("mat2d(1, 2, 3, 4, 5, 6)"); });
+ });
+
+ describe("frob", function() {
+ beforeEach(function() { result = mat2d.frob(matA); });
+ it("should return the Frobenius Norm of the matrix", function() { expect(result).toEqual( Math.sqrt(Math.pow(1, 2) + Math.pow(2, 2) + Math.pow(3, 2) + Math.pow(4, 2) + Math.pow(5, 2) + Math.pow(6, 2) + 1)); });
+ });
+
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat3-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat3-spec.js
new file mode 100644
index 00000000000..9a9d55a71de
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat3-spec.js
@@ -0,0 +1,347 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("mat3", function() {
+ var mat3 = require("../../src/gl-matrix/mat3.js");
+ var mat4 = require("../../src/gl-matrix/mat4.js");
+ var vec3 = require("../../src/gl-matrix/vec3.js");
+
+ var out, matA, matB, identity, result;
+
+ beforeEach(function() {
+ matA = [1, 0, 0,
+ 0, 1, 0,
+ 1, 2, 1];
+
+ matB = [1, 0, 0,
+ 0, 1, 0,
+ 3, 4, 1];
+
+ out = [0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0];
+
+ identity = [1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 1];
+ });
+
+ describe("normalFromMat4", function() {
+ beforeEach(function() {
+ matA = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1];
+ result = mat3.normalFromMat4(out, matA);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ describe("with translation and rotation", function() {
+ beforeEach(function() {
+ mat4.translate(matA, matA, [2, 4, 6]);
+ mat4.rotateX(matA, matA, Math.PI / 2);
+
+ result = mat3.normalFromMat4(out, matA);
+ });
+
+ it("should give rotated matrix", function() {
+ expect(result).toBeEqualish([1, 0, 0,
+ 0, 0, 1,
+ 0,-1, 0]);
+ });
+
+ describe("and scale", function() {
+ beforeEach(function() {
+ mat4.scale(matA, matA, [2, 3, 4]);
+
+ result = mat3.normalFromMat4(out, matA);
+ });
+
+ it("should give rotated matrix", function() {
+ expect(result).toBeEqualish([0.5, 0, 0,
+ 0, 0, 0.333333,
+ 0, -0.25, 0]);
+ });
+ });
+ });
+ });
+
+ describe("fromQuat", function() {
+ var q;
+
+ beforeEach(function() {
+ q = [ 0, -0.7071067811865475, 0, 0.7071067811865475 ];
+ result = mat3.fromQuat(out, q);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should rotate a vector the same as the original quat", function() {
+ expect(vec3.transformMat3([], [0,0,-1], out)).toBeEqualish(vec3.transformQuat([], [0,0,-1], q));
+ });
+
+ it("should rotate a vector by PI/2 radians", function() {
+ expect(vec3.transformMat3([], [0,0,-1], out)).toBeEqualish([1,0,0]);
+ });
+ });
+
+ describe("fromMat4", function() {
+ beforeEach(function() {
+ result = mat3.fromMat4(out, [ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 9,10,11,12,
+ 13,14,15,16]); });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should calculate proper mat3", function() {
+ expect(out).toBeEqualish([ 1, 2, 3,
+ 5, 6, 7,
+ 9,10,11]);
+ });
+ });
+
+ describe("scale", function() {
+ beforeEach(function() { result = mat3.scale(out, matA, [2,2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it('should place proper values in out', function() {
+ expect(out).toBeEqualish([ 2, 0, 0,
+ 0, 2, 0,
+ 1, 2, 1 ]);
+ });
+ });
+
+ describe("create", function() {
+ beforeEach(function() { result = mat3.create(); });
+ it("should return a 9 element array initialized to a 3x3 identity matrix", function() { expect(result).toBeEqualish(identity); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = mat3.clone(matA); });
+ it("should return a 9 element array initialized to the values in matA", function() { expect(result).toBeEqualish(matA); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = mat3.copy(out, matA); });
+ it("should place values into out", function() { expect(out).toBeEqualish(matA); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("identity", function() {
+ beforeEach(function() { result = mat3.identity(out); });
+ it("should place values into out", function() { expect(result).toBeEqualish(identity); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("transpose", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat3.transpose(out, matA); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 1,
+ 0, 1, 2,
+ 0, 0, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 1, 2, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat3.transpose(matA, matA); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 1,
+ 0, 1, 2,
+ 0, 0, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("invert", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat3.invert(out, matA); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ -1, -2, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 1, 2, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat3.invert(matA, matA); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ -1, -2, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("adjoint", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat3.adjoint(out, matA); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ -1, -2, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 1, 2, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat3.adjoint(matA, matA); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ -1, -2, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("determinant", function() {
+ beforeEach(function() { result = mat3.determinant(matA); });
+
+ it("should return the determinant", function() { expect(result).toEqual(1); });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(mat3.mul).toEqual(mat3.multiply); });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat3.multiply(out, matA, matB); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 4, 6, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 1, 2, 1
+ ]);
+ });
+ it("should not modify matB", function() {
+ expect(matB).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 3, 4, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat3.multiply(matA, matA, matB); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 4, 6, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ it("should not modify matB", function() {
+ expect(matB).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 3, 4, 1
+ ]);
+ });
+ });
+
+ describe("when matB is the output matrix", function() {
+ beforeEach(function() { result = mat3.multiply(matB, matA, matB); });
+
+ it("should place values into matB", function() {
+ expect(matB).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 4, 6, 1
+ ]);
+ });
+ it("should return matB", function() { expect(result).toBe(matB); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0,
+ 0, 1, 0,
+ 1, 2, 1
+ ]);
+ });
+ });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = mat3.str(matA); });
+
+ it("should return a string representation of the matrix", function() { expect(result).toEqual("mat3(1, 0, 0, 0, 1, 0, 1, 2, 1)"); });
+ });
+
+ describe("frob", function() {
+ beforeEach(function() { result = mat3.frob(matA); });
+ it("should return the Frobenius Norm of the matrix", function() { expect(result).toEqual( Math.sqrt(Math.pow(1, 2) + Math.pow(0, 2) + Math.pow(0, 2) + Math.pow(0, 2) + Math.pow(1, 2) + Math.pow(0, 2) + Math.pow(1, 2) + Math.pow(2, 2) + Math.pow(1, 2))); });
+ });
+
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat4-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat4-spec.js
new file mode 100644
index 00000000000..9b7a67f4ee6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/mat4-spec.js
@@ -0,0 +1,637 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("mat4", function() {
+ var mat4 = require("../../src/gl-matrix/mat4.js");
+ var vec3 = require("../../src/gl-matrix/vec3.js");
+
+ var out, matA, matB, identity, result;
+
+ beforeEach(function() {
+ // Attempting to portray a semi-realistic transform matrix
+ matA = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1];
+
+ matB = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 4, 5, 6, 1];
+
+ out = [0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0];
+
+ identity = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1];
+ });
+
+ describe("create", function() {
+ beforeEach(function() { result = mat4.create(); });
+ it("should return a 16 element array initialized to a 4x4 identity matrix", function() { expect(result).toBeEqualish(identity); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = mat4.clone(matA); });
+ it("should return a 16 element array initialized to the values in matA", function() { expect(result).toBeEqualish(matA); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = mat4.copy(out, matA); });
+ it("should place values into out", function() { expect(out).toBeEqualish(matA); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("identity", function() {
+ beforeEach(function() { result = mat4.identity(out); });
+ it("should place values into out", function() { expect(result).toBeEqualish(identity); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("transpose", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.transpose(out, matA); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 1,
+ 0, 1, 0, 2,
+ 0, 0, 1, 3,
+ 0, 0, 0, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.transpose(matA, matA); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 1,
+ 0, 1, 0, 2,
+ 0, 0, 1, 3,
+ 0, 0, 0, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("invert", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.invert(out, matA); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ -1, -2, -3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.invert(matA, matA); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ -1, -2, -3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("adjoint", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.adjoint(out, matA); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ -1, -2, -3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.adjoint(matA, matA); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ -1, -2, -3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("determinant", function() {
+ beforeEach(function() { result = mat4.determinant(matA); });
+
+ it("should return the determinant", function() { expect(result).toEqual(1); });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(mat4.mul).toEqual(mat4.multiply); });
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.multiply(out, matA, matB); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 5, 7, 9, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should not modify matB", function() {
+ expect(matB).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 4, 5, 6, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.multiply(matA, matA, matB); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 5, 7, 9, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ it("should not modify matB", function() {
+ expect(matB).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 4, 5, 6, 1
+ ]);
+ });
+ });
+
+ describe("when matB is the output matrix", function() {
+ beforeEach(function() { result = mat4.multiply(matB, matA, matB); });
+
+ it("should place values into matB", function() {
+ expect(matB).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 5, 7, 9, 1
+ ]);
+ });
+ it("should return matB", function() { expect(result).toBe(matB); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+ });
+
+ describe("translate", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.translate(out, matA, [4, 5, 6]); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 5, 7, 9, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.translate(matA, matA, [4, 5, 6]); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 5, 7, 9, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("scale", function() {
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.scale(out, matA, [4, 5, 6]); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 4, 0, 0, 0,
+ 0, 5, 0, 0,
+ 0, 0, 6, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.scale(matA, matA, [4, 5, 6]); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 4, 0, 0, 0,
+ 0, 5, 0, 0,
+ 0, 0, 6, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("rotate", function() {
+ var rad = Math.PI * 0.5;
+ var axis = [1, 0, 0];
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.rotate(out, matA, rad, axis); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, Math.cos(rad), Math.sin(rad), 0,
+ 0, -Math.sin(rad), Math.cos(rad), 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.rotate(matA, matA, rad, axis); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, Math.cos(rad), Math.sin(rad), 0,
+ 0, -Math.sin(rad), Math.cos(rad), 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("rotateX", function() {
+ var rad = Math.PI * 0.5;
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.rotateX(out, matA, rad); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, Math.cos(rad), Math.sin(rad), 0,
+ 0, -Math.sin(rad), Math.cos(rad), 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.rotateX(matA, matA, rad); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, Math.cos(rad), Math.sin(rad), 0,
+ 0, -Math.sin(rad), Math.cos(rad), 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("rotateY", function() {
+ var rad = Math.PI * 0.5;
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.rotateY(out, matA, rad); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ Math.cos(rad), 0, -Math.sin(rad), 0,
+ 0, 1, 0, 0,
+ Math.sin(rad), 0, Math.cos(rad), 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.rotateY(matA, matA, rad); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ Math.cos(rad), 0, -Math.sin(rad), 0,
+ 0, 1, 0, 0,
+ Math.sin(rad), 0, Math.cos(rad), 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ describe("rotateZ", function() {
+ var rad = Math.PI * 0.5;
+
+ describe("with a separate output matrix", function() {
+ beforeEach(function() { result = mat4.rotateZ(out, matA, rad); });
+
+ it("should place values into out", function() {
+ expect(out).toBeEqualish([
+ Math.cos(rad), Math.sin(rad), 0, 0,
+ -Math.sin(rad), Math.cos(rad), 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify matA", function() {
+ expect(matA).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ });
+
+ describe("when matA is the output matrix", function() {
+ beforeEach(function() { result = mat4.rotateZ(matA, matA, rad); });
+
+ it("should place values into matA", function() {
+ expect(matA).toBeEqualish([
+ Math.cos(rad), Math.sin(rad), 0, 0,
+ -Math.sin(rad), Math.cos(rad), 0, 0,
+ 0, 0, 1, 0,
+ 1, 2, 3, 1
+ ]);
+ });
+ it("should return matA", function() { expect(result).toBe(matA); });
+ });
+ });
+
+ // TODO: fromRotationTranslation
+
+ describe("frustum", function() {
+ beforeEach(function() { result = mat4.frustum(out, -1, 1, -1, 1, -1, 1); });
+ it("should place values into out", function() { expect(result).toBeEqualish([
+ -1, 0, 0, 0,
+ 0, -1, 0, 0,
+ 0, 0, 0, -1,
+ 0, 0, 1, 0
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("perspective", function() {
+ var fovy = Math.PI * 0.5;
+ beforeEach(function() { result = mat4.perspective(out, fovy, 1, 0, 1); });
+ it("should place values into out", function() { expect(result).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, -1, -1,
+ 0, 0, 0, 0
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+
+ describe("with nonzero near, 45deg fovy, and realistic aspect ratio", function() {
+ beforeEach(function() { result = mat4.perspective(out, 45 * Math.PI / 180.0, 640/480, 0.1, 200); });
+ it("should calculate correct matrix", function() { expect(result).toBeEqualish([
+ 1.81066, 0, 0, 0,
+ 0, 2.414213, 0, 0,
+ 0, 0, -1.001, -1,
+ 0, 0, -0.2001, 0
+ ]); });
+ });
+ });
+
+ describe("ortho", function() {
+ beforeEach(function() { result = mat4.ortho(out, -1, 1, -1, 1, -1, 1); });
+ it("should place values into out", function() { expect(result).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, -1, 0,
+ 0, 0, 0, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("lookAt", function() {
+ var eye = [0, 0, 1];
+ var center = [0, 0, -1];
+ var up = [0, 1, 0];
+ var view, up, right;
+
+ describe("looking down", function() {
+ beforeEach(function() {
+ view = [0, -1, 0];
+ up = [0, 0, -1];
+ right= [1, 0, 0];
+ result = mat4.lookAt(out, [0, 0, 0], view, up);
+ });
+
+ it("should transform view into local -Z", function() {
+ result = vec3.transformMat4([], view, out);
+ expect(result).toBeEqualish([0, 0, -1]);
+ });
+
+ it("should transform up into local +Y", function() {
+ result = vec3.transformMat4([], up, out);
+ expect(result).toBeEqualish([0, 1, 0]);
+ });
+
+ it("should transform right into local +X", function() {
+ result = vec3.transformMat4([], right, out);
+ expect(result).toBeEqualish([1, 0, 0]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("#74", function() {
+ beforeEach(function() {
+ mat4.lookAt(out, [0,2,0], [0,0.6,0], [0,0,-1]);
+ });
+
+ it("should transform a point 'above' into local +Y", function() {
+ result = vec3.transformMat4([], [0, 2, -1], out);
+ expect(result).toBeEqualish([0, 1, 0]);
+ });
+
+ it("should transform a point 'right of' into local +X", function() {
+ result = vec3.transformMat4([], [1, 2, 0], out);
+ expect(result).toBeEqualish([1, 0, 0]);
+ });
+
+ it("should transform a point 'in front of' into local -Z", function() {
+ result = vec3.transformMat4([], [0, 1, 0], out);
+ expect(result).toBeEqualish([0, 0, -1]);
+ });
+ });
+
+ beforeEach(function() {
+ eye = [0, 0, 1];
+ center = [0, 0, -1];
+ up = [0, 1, 0];
+ result = mat4.lookAt(out, eye, center, up);
+ });
+ it("should place values into out", function() { expect(result).toBeEqualish([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -1, 1
+ ]);
+ });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = mat4.str(matA); });
+
+ it("should return a string representation of the matrix", function() { expect(result).toEqual("mat4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1)"); });
+ });
+
+ describe("frob", function() {
+ beforeEach(function() { result = mat4.frob(matA); });
+ it("should return the Frobenius Norm of the matrix", function() { expect(result).toEqual( Math.sqrt(Math.pow(1, 2) + Math.pow(1, 2) + Math.pow(1, 2) + Math.pow(1, 2) + Math.pow(1, 2) + Math.pow(2, 2) + Math.pow(3, 2) )); });
+ });
+
+
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/quat-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/quat-spec.js
new file mode 100644
index 00000000000..4f5150014d0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/quat-spec.js
@@ -0,0 +1,559 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("quat", function() {
+ var mat3 = require("../../src/gl-matrix/mat3.js");
+ var mat4 = require("../../src/gl-matrix/mat4.js");
+ var quat = require("../../src/gl-matrix/quat.js");
+ var vec3 = require("../../src/gl-matrix/vec3.js");
+
+ var out, quatA, quatB, result;
+ var vec, id, deg90;
+
+ beforeEach(function() {
+ quatA = [1, 2, 3, 4];
+ quatB = [5, 6, 7, 8];
+ out = [0, 0, 0, 0];
+ vec = [1, 1, -1];
+ id = [0, 0, 0, 1];
+ deg90 = Math.PI / 2;
+ });
+
+ describe("slerp", function() {
+ describe("the normal case", function() {
+ beforeEach(function() {
+ result = quat.slerp(out, [0, 0, 0, 1], [0, 1, 0, 0], 0.5);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should calculate proper quat", function() {
+ expect(result).toBeEqualish([0, 0.707106, 0, 0.707106]);
+ });
+ });
+
+ describe("where a == b", function() {
+ beforeEach(function() {
+ result = quat.slerp(out, [0, 0, 0, 1], [0, 0, 0, 1], 0.5);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should calculate proper quat", function() {
+ expect(result).toBeEqualish([0, 0, 0, 1]);
+ });
+ });
+
+ describe("where theta == 180deg", function() {
+ beforeEach(function() {
+ quat.rotateX(quatA, [1,0,0,0], Math.PI); // 180 deg
+ result = quat.slerp(out, [1,0,0,0], quatA, 1);
+ });
+
+ it("should calculate proper quat", function() {
+ expect(result).toBeEqualish([0,0,0,-1]);
+ });
+ });
+
+ describe("where a == -b", function() {
+ beforeEach(function() {
+ result = quat.slerp(out, [1, 0, 0, 0], [-1, 0, 0, 0], 0.5);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should calculate proper quat", function() {
+ expect(result).toBeEqualish([1, 0, 0, 0]);
+ });
+ });
+ });
+
+ describe("rotateX", function() {
+ beforeEach(function() {
+ result = quat.rotateX(out, id, deg90);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should transform vec accordingly", function() {
+ vec3.transformQuat(vec, [0,0,-1], out);
+ expect(vec).toBeEqualish([0, 1, 0]);
+ });
+ });
+
+ describe("rotateY", function() {
+ beforeEach(function() {
+ result = quat.rotateY(out, id, deg90);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should transform vec accordingly", function() {
+ vec3.transformQuat(vec, [0,0,-1], out);
+ expect(vec).toBeEqualish([-1, 0, 0]);
+ });
+ });
+
+ describe("rotateZ", function() {
+ beforeEach(function() {
+ result = quat.rotateZ(out, id, deg90);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should transform vec accordingly", function() {
+ vec3.transformQuat(vec, [0,1,0], out);
+ expect(vec).toBeEqualish([-1, 0, 0]);
+ });
+ });
+
+ describe("fromMat3", function() {
+ var matr;
+
+ describe("legacy", function() {
+ beforeEach(function() {
+ matr = [ 1, 0, 0,
+ 0, 0, -1,
+ 0, 1, 0 ];
+ result = quat.fromMat3(out, matr);
+ });
+
+ it("should set dest to the correct value", function() {
+ expect(result).toBeEqualish([-0.707106, 0, 0, 0.707106]);
+ });
+ });
+
+ describe("where trace > 0", function() {
+ beforeEach(function() {
+ matr = [ 1, 0, 0,
+ 0, 0, -1,
+ 0, 1, 0 ];
+ result = quat.fromMat3(out, matr);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should produce the correct transformation", function() {
+ expect(vec3.transformQuat([], [0,1,0], out)).toBeEqualish([0,0,-1]);
+ });
+ });
+
+ describe("from a normal matrix looking 'backward'", function() {
+ beforeEach(function() {
+ matr = mat3.create();
+ mat3.transpose(matr, mat3.invert(matr, mat3.fromMat4(matr, mat4.lookAt(mat4.create(), [0, 0, 0], [0, 0, 1], [0, 1, 0]))));
+ result = quat.fromMat3(out, matr);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should produce the same transformation as the given matrix", function() {
+ expect(vec3.transformQuat([], [3,2,-1], quat.normalize(out, out))).toBeEqualish(vec3.transformMat3([], [3,2,-1], matr));
+ });
+ });
+
+ describe("from a normal matrix looking 'left' and 'upside down'", function() {
+ beforeEach(function() {
+ matr = mat3.create();
+ mat3.transpose(matr, mat3.invert(matr, mat3.fromMat4(matr, mat4.lookAt(mat4.create(), [0, 0, 0], [-1, 0, 0], [0, -1, 0]))));
+ result = quat.fromMat3(out, matr);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should produce the same transformation as the given matrix", function() {
+ expect(vec3.transformQuat([], [3,2,-1], quat.normalize(out, out))).toBeEqualish(vec3.transformMat3([], [3,2,-1], matr));
+ });
+ });
+
+ describe("from a normal matrix looking 'upside down'", function() {
+ beforeEach(function() {
+ matr = mat3.create();
+ mat3.transpose(matr, mat3.invert(matr, mat3.fromMat4(matr, mat4.lookAt(mat4.create(), [0, 0, 0], [0, 0, -1], [0, -1, 0]))));
+ result = quat.fromMat3(out, matr);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should produce the same transformation as the given matrix", function() {
+ expect(vec3.transformQuat([], [3,2,-1], quat.normalize(out, out))).toBeEqualish(vec3.transformMat3([], [3,2,-1], matr));
+ });
+ });
+ });
+
+ describe("setAxes", function() {
+ var r;
+ beforeEach(function() { r = vec3.create(); });
+
+ describe("looking left", function() {
+ var view, up, right;
+ beforeEach(function() {
+ view = [-1, 0, 0];
+ up = [ 0, 1, 0];
+ right= [ 0, 0,-1];
+ result = quat.setAxes([], view, right, up);
+ });
+
+ it("should transform local view into world left", function() {
+ r = vec3.transformQuat([], [0,0,-1], result);
+ expect(r).toBeEqualish([1, 0, 0]);
+ });
+
+ it("should transform local right into world front", function() {
+ r = vec3.transformQuat([], [1,0,0], result);
+ expect(r).toBeEqualish([0, 0, 1]);
+ });
+ });
+
+ describe("given opengl defaults", function() {
+ var view, up, right;
+ beforeEach(function() {
+ view = [0, 0, -1];
+ up = [0, 1, 0];
+ right= [1, 0, 0];
+ result = quat.setAxes(out, view, right, up);
+ });
+
+ it("should return out", function() {
+ expect(result).toBe(out);
+ });
+
+ it("should produce identity", function() {
+ expect(out).toBeEqualish([0, 0, 0, 1]);
+ });
+ });
+
+ describe("legacy example", function() {
+ var view, up, right;
+ beforeEach(function() {
+ right= [1, 0, 0];
+ up = [0, 0, 1];
+ view = [0, -1, 0];
+ result = quat.setAxes(out, view, right, up);
+ });
+
+ xit("should set correct quat4 values", function() {
+ expect(result).toBeEqualish([0.707106, 0, 0, 0.707106]);
+ });
+ });
+ });
+
+ describe("rotationTo", function() {
+ var r;
+ beforeEach(function() { r = vec3.create(); });
+
+ describe("at right angle", function() {
+ beforeEach(function() {
+ result = quat.rotationTo(out, [0, 1, 0], [1, 0, 0]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("should calculate proper quaternion", function() {
+ expect(out).toBeEqualish([0, 0, -0.707106, 0.707106]);
+ });
+ });
+
+ describe("when vectors are parallel", function() {
+ beforeEach(function() {
+ result = quat.rotationTo(out, [0, 1, 0], [0, 1, 0]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("multiplying A should produce B", function() {
+ expect(vec3.transformQuat(r, [0, 1, 0], out)).toBeEqualish([0, 1, 0]);
+ });
+ });
+
+ describe("when vectors are opposed X", function() {
+ beforeEach(function() {
+ result = quat.rotationTo(out, [1, 0, 0], [-1, 0, 0]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("multiplying A should produce B", function() {
+ expect(vec3.transformQuat(r, [1, 0, 0], out)).toBeEqualish([-1, 0, 0]);
+ });
+ });
+
+ describe("when vectors are opposed Y", function() {
+ beforeEach(function() {
+ result = quat.rotationTo(out, [0, 1, 0], [0, -1, 0]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("multiplying A should produce B", function() {
+ expect(vec3.transformQuat(r, [0, 1, 0], out)).toBeEqualish([0, -1, 0]);
+ });
+ });
+
+ describe("when vectors are opposed Z", function() {
+ beforeEach(function() {
+ result = quat.rotationTo(out, [0, 0, 1], [0, 0, -1]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+
+ it("multiplying A should produce B", function() {
+ expect(vec3.transformQuat(r, [0, 0, 1], out)).toBeEqualish([0, 0, -1]);
+ });
+ });
+ });
+
+ describe("create", function() {
+ beforeEach(function() { result = quat.create(); });
+ it("should return a 4 element array initialized to an identity quaternion", function() { expect(result).toBeEqualish([0, 0, 0, 1]); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = quat.clone(quatA); });
+ it("should return a 4 element array initialized to the values in quatA", function() { expect(result).toBeEqualish(quatA); });
+ });
+
+ describe("fromValues", function() {
+ beforeEach(function() { result = quat.fromValues(1, 2, 3, 4); });
+ it("should return a 4 element array initialized to the values passed", function() { expect(result).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = quat.copy(out, quatA); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("set", function() {
+ beforeEach(function() { result = quat.set(out, 1, 2, 3, 4); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("identity", function() {
+ beforeEach(function() { result = quat.identity(out); });
+ it("should place values into out", function() { expect(result).toBeEqualish([0, 0, 0, 1]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("setAxisAngle", function() {
+ beforeEach(function() { result = quat.setAxisAngle(out, [1, 0, 0], Math.PI * 0.5); });
+ it("should place values into out", function() { expect(result).toBeEqualish([0.707106, 0, 0, 0.707106]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("add", function() {
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.add(out, quatA, quatB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([6, 8, 10, 12]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.add(quatA, quatA, quatB); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([6, 8, 10, 12]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatB is the output quaternion", function() {
+ beforeEach(function() { result = quat.add(quatB, quatA, quatB); });
+
+ it("should place values into quatB", function() { expect(quatB).toBeEqualish([6, 8, 10, 12]); });
+ it("should return quatB", function() { expect(result).toBe(quatB); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(quat.mul).toEqual(quat.multiply); });
+
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.multiply(out, quatA, quatB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([24, 48, 48, -6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.multiply(quatA, quatA, quatB); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([24, 48, 48, -6]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatB is the output quaternion", function() {
+ beforeEach(function() { result = quat.multiply(quatB, quatA, quatB); });
+
+ it("should place values into quatB", function() { expect(quatB).toBeEqualish([24, 48, 48, -6]); });
+ it("should return quatB", function() { expect(result).toBe(quatB); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("scale", function() {
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.scale(out, quatA, 2); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 4, 6, 8]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.scale(quatA, quatA, 2); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([2, 4, 6, 8]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ });
+ });
+
+ describe("length", function() {
+ it("should have an alias called 'len'", function() { expect(quat.len).toEqual(quat.length); });
+
+ beforeEach(function() { result = quat.length(quatA); });
+
+ it("should return the length", function() { expect(result).toBeCloseTo(5.477225); });
+ });
+
+ describe("squaredLength", function() {
+ it("should have an alias called 'sqrLen'", function() { expect(quat.sqrLen).toEqual(quat.squaredLength); });
+
+ beforeEach(function() { result = quat.squaredLength(quatA); });
+
+ it("should return the squared length", function() { expect(result).toEqual(30); });
+ });
+
+ describe("normalize", function() {
+ beforeEach(function() { quatA = [5, 0, 0, 0]; });
+
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.normalize(out, quatA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 0, 0, 0]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([5, 0, 0, 0]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.normalize(quatA, quatA); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([1, 0, 0, 0]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ });
+ });
+
+ describe("lerp", function() {
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.lerp(out, quatA, quatB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4, 5, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.lerp(quatA, quatA, quatB, 0.5); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([3, 4, 5, 6]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatB is the output quaternion", function() {
+ beforeEach(function() { result = quat.lerp(quatB, quatA, quatB, 0.5); });
+
+ it("should place values into quatB", function() { expect(quatB).toBeEqualish([3, 4, 5, 6]); });
+ it("should return quatB", function() { expect(result).toBe(quatB); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ /*describe("slerp", function() {
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.slerp(out, quatA, quatB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4, 5, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.slerp(quatA, quatA, quatB, 0.5); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([3, 4, 5, 6]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ it("should not modify quatB", function() { expect(quatB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when quatB is the output quaternion", function() {
+ beforeEach(function() { result = quat.slerp(quatB, quatA, quatB, 0.5); });
+
+ it("should place values into quatB", function() { expect(quatB).toBeEqualish([3, 4, 5, 6]); });
+ it("should return quatB", function() { expect(result).toBe(quatB); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });*/
+
+ // TODO: slerp, calcuateW, rotateX, rotateY, rotateZ
+
+ describe("invert", function() {
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.invert(out, quatA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-0.033333, -0.066666, -0.1, 0.133333]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.invert(quatA, quatA); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([-0.033333, -0.066666, -0.1, 0.133333]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ });
+ });
+
+ describe("conjugate", function() {
+ describe("with a separate output quaternion", function() {
+ beforeEach(function() { result = quat.conjugate(out, quatA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-1, -2, -3, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify quatA", function() { expect(quatA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when quatA is the output quaternion", function() {
+ beforeEach(function() { result = quat.conjugate(quatA, quatA); });
+
+ it("should place values into quatA", function() { expect(quatA).toBeEqualish([-1, -2, -3, 4]); });
+ it("should return quatA", function() { expect(result).toBe(quatA); });
+ });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = quat.str(quatA); });
+
+ it("should return a string representation of the quaternion", function() { expect(result).toEqual("quat(1, 2, 3, 4)"); });
+ });
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec2-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec2-spec.js
new file mode 100644
index 00000000000..254c52739c3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec2-spec.js
@@ -0,0 +1,549 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("vec2", function() {
+ var vec2 = require("../../src/gl-matrix/vec2.js");
+
+ var out, vecA, vecB, result;
+
+ beforeEach(function() { vecA = [1, 2]; vecB = [3, 4]; out = [0, 0]; });
+
+ describe("create", function() {
+ beforeEach(function() { result = vec2.create(); });
+ it("should return a 2 element array initialized to 0s", function() { expect(result).toBeEqualish([0, 0]); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = vec2.clone(vecA); });
+ it("should return a 2 element array initialized to the values in vecA", function() { expect(result).toBeEqualish(vecA); });
+ });
+
+ describe("fromValues", function() {
+ beforeEach(function() { result = vec2.fromValues(1, 2); });
+ it("should return a 2 element array initialized to the values passed", function() { expect(result).toBeEqualish([1, 2]); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = vec2.copy(out, vecA); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("set", function() {
+ beforeEach(function() { result = vec2.set(out, 1, 2); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("add", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.add(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([4, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.add(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([4, 6]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.add(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([4, 6]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+ });
+
+ describe("subtract", function() {
+ it("should have an alias called 'sub'", function() { expect(vec2.sub).toEqual(vec2.subtract); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.subtract(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-2, -2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.subtract(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-2, -2]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.subtract(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([-2, -2]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(vec2.mul).toEqual(vec2.multiply); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.multiply(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 8]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.multiply(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3, 8]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.multiply(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3, 8]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+ });
+
+ describe("divide", function() {
+ it("should have an alias called 'div'", function() { expect(vec2.div).toEqual(vec2.divide); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.divide(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([0.3333333, 0.5]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.divide(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([0.3333333, 0.5]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.divide(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([0.3333333, 0.5]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+ });
+
+ describe("min", function() {
+ beforeEach(function() { vecA = [1, 4]; vecB = [3, 2]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.min(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 2]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.min(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 2]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.min(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([1, 2]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 4]); });
+ });
+ });
+
+ describe("max", function() {
+ beforeEach(function() { vecA = [1, 4]; vecB = [3, 2]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.max(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 2]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.max(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3, 4]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 2]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.max(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 4]); });
+ });
+ });
+
+ describe("scale", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.scale(out, vecA, 2); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.scale(vecA, vecA, 2); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([2, 4]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("scaleAndAdd", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.scaleAndAdd(out, vecA, vecB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2.5, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.scaleAndAdd(vecA, vecA, vecB, 0.5); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([2.5, 4]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.scaleAndAdd(vecB, vecA, vecB, 0.5); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([2.5, 4]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+ });
+
+ describe("distance", function() {
+ it("should have an alias called 'dist'", function() { expect(vec2.dist).toEqual(vec2.distance); });
+
+ beforeEach(function() { result = vec2.distance(vecA, vecB); });
+
+ it("should return the distance", function() { expect(result).toBeCloseTo(2.828427); });
+ });
+
+ describe("squaredDistance", function() {
+ it("should have an alias called 'sqrDist'", function() { expect(vec2.sqrDist).toEqual(vec2.squaredDistance); });
+
+ beforeEach(function() { result = vec2.squaredDistance(vecA, vecB); });
+
+ it("should return the squared distance", function() { expect(result).toEqual(8); });
+ });
+
+ describe("length", function() {
+ it("should have an alias called 'len'", function() { expect(vec2.len).toEqual(vec2.length); });
+
+ beforeEach(function() { result = vec2.length(vecA); });
+
+ it("should return the length", function() { expect(result).toBeCloseTo(2.236067); });
+ });
+
+ describe("squaredLength", function() {
+ it("should have an alias called 'sqrLen'", function() { expect(vec2.sqrLen).toEqual(vec2.squaredLength); });
+
+ beforeEach(function() { result = vec2.squaredLength(vecA); });
+
+ it("should return the squared length", function() { expect(result).toEqual(5); });
+ });
+
+ describe("negate", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.negate(out, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-1, -2]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.negate(vecA, vecA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-1, -2]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("normalize", function() {
+ beforeEach(function() { vecA = [5, 0]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.normalize(out, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 0]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([5, 0]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.normalize(vecA, vecA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([1, 0]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("dot", function() {
+ beforeEach(function() { result = vec2.dot(vecA, vecB); });
+
+ it("should return the dot product", function() { expect(result).toEqual(11); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("cross", function() {
+ var out3;
+
+ beforeEach(function() {
+ out3 = [0, 0, 0];
+ result = vec2.cross(out3, vecA, vecB);
+ });
+
+ it("should place values into out", function() { expect(out3).toBeEqualish([0, 0, -2]); });
+ it("should return out", function() { expect(result).toBe(out3); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("lerp", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.lerp(out, vecA, vecB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.lerp(vecA, vecA, vecB, 0.5); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([2, 3]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 4]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec2.lerp(vecB, vecA, vecB, 0.5); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([2, 3]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+ });
+
+ describe("random", function() {
+ describe("with no scale", function() {
+ beforeEach(function() { result = vec2.random(out); });
+
+ it("should result in a unit length vector", function() { expect(vec2.length(out)).toBeCloseTo(1.0); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("with a scale", function() {
+ beforeEach(function() { result = vec2.random(out, 5.0); });
+
+ it("should result in a unit length vector", function() { expect(vec2.length(out)).toBeCloseTo(5.0); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+ });
+
+ describe("transformMat2", function() {
+ var matA;
+ beforeEach(function() { matA = [1, 2, 3, 4]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.transformMat2(out, vecA, matA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([7, 10]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.transformMat2(vecA, vecA, matA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([7, 10]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("transformMat2d", function() {
+ var matA;
+ beforeEach(function() { matA = [1, 2, 3, 4, 5, 6]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec2.transformMat2d(out, vecA, matA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([12, 16]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec2.transformMat2d(vecA, vecA, matA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([12, 16]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify matA", function() { expect(matA).toBeEqualish([1, 2, 3, 4, 5, 6]); });
+ });
+ });
+
+ describe("forEach", function() {
+ var vecArray;
+
+ beforeEach(function() {
+ vecArray = [
+ 1, 2,
+ 3, 4,
+ 0, 0
+ ];
+ });
+
+ describe("when performing operations that take no extra arguments", function() {
+ beforeEach(function() { result = vec2.forEach(vecArray, 0, 0, 0, vec2.normalize); });
+
+ it("should update all values", function() {
+ expect(vecArray).toBeEqualish([
+ 0.447214, 0.894427,
+ 0.6, 0.8,
+ 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ });
+
+ describe("when performing operations that takes one extra arguments", function() {
+ beforeEach(function() { result = vec2.forEach(vecArray, 0, 0, 0, vec2.add, vecA); });
+
+ it("should update all values", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4,
+ 4, 6,
+ 1, 2
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+
+ describe("when specifying an offset", function() {
+ beforeEach(function() { result = vec2.forEach(vecArray, 0, 2, 0, vec2.add, vecA); });
+
+ it("should update all values except the first vector", function() {
+ expect(vecArray).toBeEqualish([
+ 1, 2,
+ 4, 6,
+ 1, 2
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+
+ describe("when specifying a count", function() {
+ beforeEach(function() { result = vec2.forEach(vecArray, 0, 0, 2, vec2.add, vecA); });
+
+ it("should update all values except the last vector", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4,
+ 4, 6,
+ 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+
+ describe("when specifying a stride", function() {
+ beforeEach(function() { result = vec2.forEach(vecArray, 4, 0, 0, vec2.add, vecA); });
+
+ it("should update all values except the second vector", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4,
+ 3, 4,
+ 1, 2
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2]); });
+ });
+
+ describe("when calling a function that does not modify the out variable", function() {
+ beforeEach(function() {
+ result = vec2.forEach(vecArray, 0, 0, 0, function(out, vec) {});
+ });
+
+ it("values should remain unchanged", function() {
+ expect(vecArray).toBeEqualish([
+ 1, 2,
+ 3, 4,
+ 0, 0,
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = vec2.str(vecA); });
+
+ it("should return a string representation of the vector", function() { expect(result).toEqual("vec2(1, 2)"); });
+ });
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec3-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec3-spec.js
new file mode 100644
index 00000000000..8ef944f1a24
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec3-spec.js
@@ -0,0 +1,661 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("vec3", function() {
+ var mat3 = require("../../src/gl-matrix/mat3.js");
+ var mat4 = require("../../src/gl-matrix/mat4.js");
+ var vec3 = require("../../src/gl-matrix/vec3.js");
+
+ var out, vecA, vecB, result;
+
+ beforeEach(function() { vecA = [1, 2, 3]; vecB = [4, 5, 6]; out = [0, 0, 0]; });
+
+ describe('rotateX', function(){
+ describe('rotation around world origin [0, 0, 0]', function(){
+ beforeEach(function(){ vecA = [0, 1, 0]; vecB = [0, 0, 0]; result = vec3.rotateX(out, vecA, vecB, Math.PI); });
+ it("should return the rotated vector", function(){ expect(result).toBeEqualish([0, -1, 0]); });
+ });
+ describe('rotation around an arbitrary origin', function(){
+ beforeEach(function(){ vecA = [2, 7, 0]; vecB = [2, 5, 0]; result = vec3.rotateX(out, vecA, vecB, Math.PI); });
+ it("should return the rotated vector", function(){ expect(result).toBeEqualish([2, 3, 0]); });
+ });
+ });
+
+ describe('rotateY', function(){
+ describe('rotation around world origin [0, 0, 0]', function(){
+ beforeEach(function(){ vecA = [1, 0, 0]; vecB = [0, 0, 0]; result = vec3.rotateY(out, vecA, vecB, Math.PI); });
+ it("should return the rotated vector", function(){ expect(result).toBeEqualish([-1, 0, 0]); });
+ });
+ describe('rotation around an arbitrary origin', function(){
+ beforeEach(function(){ vecA = [-2, 3, 10]; vecB = [-4, 3, 10]; result = vec3.rotateY(out, vecA, vecB, Math.PI); });
+ it("should return the rotated vector", function(){ expect(result).toBeEqualish([-6, 3, 10]); });
+ });
+ });
+
+ describe('rotateZ', function(){
+ describe('rotation around world origin [0, 0, 0]', function(){
+ beforeEach(function(){ vecA = [0, 1, 0]; vecB = [0, 0, 0]; result = vec3.rotateZ(out, vecA, vecB, Math.PI); });
+ it("should return the rotated vector", function(){ expect(result).toBeEqualish([0, -1, 0]); });
+ });
+ describe('rotation around an arbitrary origin', function(){
+ beforeEach(function(){ vecA = [0, 6, -5]; vecB = [0, 0, -5]; result = vec3.rotateZ(out, vecA, vecB, Math.PI); });
+ it("should return the rotated vector", function(){ expect(result).toBeEqualish([0, -6, -5]); });
+ });
+ });
+
+ describe('transformMat4', function() {
+ var matr;
+ describe("with an identity", function() {
+ beforeEach(function() { matr = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] });
+
+ beforeEach(function() { result = vec3.transformMat4(out, vecA, matr); });
+
+ it("should produce the input", function() {
+ expect(out).toBeEqualish([1, 2, 3]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("with a lookAt", function() {
+ beforeEach(function() { matr = mat4.lookAt(mat4.create(), [5, 6, 7], [2, 6, 7], [0, 1, 0]); });
+
+ beforeEach(function() { result = vec3.transformMat4(out, vecA, matr); });
+
+ it("should rotate and translate the input", function() {
+ expect(out).toBeEqualish([ 4, -4, -4 ]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("with a perspective matrix (#92)", function() {
+ it("should transform a point from perspective(pi/2, 4/3, 1, 100)", function() {
+ matr = [0.750, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, -1.02, -1,
+ 0, 0, -2.02, 0];
+ result = vec3.transformMat4([], [10, 20, 30], matr);
+ expect(result).toBeEqualish([-0.25, -0.666666, 1.087333]);
+ });
+ });
+
+ });
+
+ describe('transformMat3', function() {
+ var matr;
+ describe("with an identity", function() {
+ beforeEach(function() { matr = [1, 0, 0, 0, 1, 0, 0, 0, 1 ] });
+
+ beforeEach(function() { result = vec3.transformMat3(out, vecA, matr); });
+
+ it("should produce the input", function() {
+ expect(out).toBeEqualish([1, 2, 3]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("with 90deg about X", function() {
+ beforeEach(function() {
+ result = vec3.transformMat3(out, [0,1,0], [1,0,0,0,0,1,0,-1,0]);
+ });
+
+ it("should produce correct output", function() {
+ expect(out).toBeEqualish([0,0,1]);
+ });
+ });
+
+ describe("with 90deg about Y", function() {
+ beforeEach(function() {
+ result = vec3.transformMat3(out, [1,0,0], [0,0,-1,0,1,0,1,0,0]);
+ });
+
+ it("should produce correct output", function() {
+ expect(out).toBeEqualish([0,0,-1]);
+ });
+ });
+
+ describe("with 90deg about Z", function() {
+ beforeEach(function() {
+ result = vec3.transformMat3(out, [1,0,0], [0,1,0,-1,0,0,0,0,1]);
+ });
+
+ it("should produce correct output", function() {
+ expect(out).toBeEqualish([0,1,0]);
+ });
+ });
+
+ describe("with a lookAt normal matrix", function() {
+ beforeEach(function() {
+ matr = mat4.lookAt(mat4.create(), [5, 6, 7], [2, 6, 7], [0, 1, 0]);
+ var n = mat3.create();
+ matr = mat3.transpose(n, mat3.invert(n, mat3.fromMat4(n, matr)));
+ });
+
+ beforeEach(function() { result = vec3.transformMat3(out, [1,0,0], matr); });
+
+ it("should rotate the input", function() {
+ expect(out).toBeEqualish([ 0,0,1 ]);
+ });
+
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+ });
+
+ describe("create", function() {
+ beforeEach(function() { result = vec3.create(); });
+ it("should return a 3 element array initialized to 0s", function() { expect(result).toBeEqualish([0, 0, 0]); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = vec3.clone(vecA); });
+ it("should return a 3 element array initialized to the values in vecA", function() { expect(result).toBeEqualish(vecA); });
+ });
+
+ describe("fromValues", function() {
+ beforeEach(function() { result = vec3.fromValues(1, 2, 3); });
+ it("should return a 3 element array initialized to the values passed", function() { expect(result).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = vec3.copy(out, vecA); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("set", function() {
+ beforeEach(function() { result = vec3.set(out, 1, 2, 3); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("add", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.add(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([5, 7, 9]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.add(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([5, 7, 9]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.add(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([5, 7, 9]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("subtract", function() {
+ it("should have an alias called 'sub'", function() { expect(vec3.sub).toEqual(vec3.subtract); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.subtract(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-3, -3, -3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.subtract(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-3, -3, -3]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.subtract(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([-3, -3, -3]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(vec3.mul).toEqual(vec3.multiply); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.multiply(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([4, 10, 18]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.multiply(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([4, 10, 18]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.multiply(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([4, 10, 18]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("divide", function() {
+ it("should have an alias called 'div'", function() { expect(vec3.div).toEqual(vec3.divide); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.divide(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([0.25, 0.4, 0.5]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.divide(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([0.25, 0.4, 0.5]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.divide(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([0.25, 0.4, 0.5]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("min", function() {
+ beforeEach(function() { vecA = [1, 3, 1]; vecB = [3, 1, 3]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.min(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 1, 1]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.min(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([1, 1, 1]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.min(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([1, 1, 1]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1]); });
+ });
+ });
+
+ describe("max", function() {
+ beforeEach(function() { vecA = [1, 3, 1]; vecB = [3, 1, 3]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.max(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 3, 3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.max(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3, 3, 3]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.max(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3, 3, 3]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1]); });
+ });
+ });
+
+ describe("scale", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.scale(out, vecA, 2); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 4, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.scale(vecA, vecA, 2); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([2, 4, 6]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("scaleAndAdd", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.scaleAndAdd(out, vecA, vecB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4.5, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.scaleAndAdd(vecA, vecA, vecB, 0.5); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3, 4.5, 6]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.scaleAndAdd(vecB, vecA, vecB, 0.5); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3, 4.5, 6]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("distance", function() {
+ it("should have an alias called 'dist'", function() { expect(vec3.dist).toEqual(vec3.distance); });
+
+ beforeEach(function() { result = vec3.distance(vecA, vecB); });
+
+ it("should return the distance", function() { expect(result).toBeCloseTo(5.196152); });
+ });
+
+ describe("squaredDistance", function() {
+ it("should have an alias called 'sqrDist'", function() { expect(vec3.sqrDist).toEqual(vec3.squaredDistance); });
+
+ beforeEach(function() { result = vec3.squaredDistance(vecA, vecB); });
+
+ it("should return the squared distance", function() { expect(result).toEqual(27); });
+ });
+
+ describe("length", function() {
+ it("should have an alias called 'len'", function() { expect(vec3.len).toEqual(vec3.length); });
+
+ beforeEach(function() { result = vec3.length(vecA); });
+
+ it("should return the length", function() { expect(result).toBeCloseTo(3.741657); });
+ });
+
+ describe("squaredLength", function() {
+ it("should have an alias called 'sqrLen'", function() { expect(vec3.sqrLen).toEqual(vec3.squaredLength); });
+
+ beforeEach(function() { result = vec3.squaredLength(vecA); });
+
+ it("should return the squared length", function() { expect(result).toEqual(14); });
+ });
+
+ describe("negate", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.negate(out, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-1, -2, -3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.negate(vecA, vecA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-1, -2, -3]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("normalize", function() {
+ beforeEach(function() { vecA = [5, 0, 0]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.normalize(out, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 0, 0]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([5, 0, 0]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.normalize(vecA, vecA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([1, 0, 0]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("dot", function() {
+ beforeEach(function() { result = vec3.dot(vecA, vecB); });
+
+ it("should return the dot product", function() { expect(result).toEqual(32); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("cross", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.cross(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-3, 6, -3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.cross(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-3, 6, -3]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.cross(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([-3, 6, -3]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("lerp", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec3.lerp(out, vecA, vecB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2.5, 3.5, 4.5]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec3.lerp(vecA, vecA, vecB, 0.5); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([2.5, 3.5, 4.5]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec3.lerp(vecB, vecA, vecB, 0.5); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([2.5, 3.5, 4.5]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+ });
+
+ describe("random", function() {
+ describe("with no scale", function() {
+ beforeEach(function() { result = vec3.random(out); });
+
+ it("should result in a unit length vector", function() { expect(vec3.length(out)).toBeCloseTo(1.0); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("with a scale", function() {
+ beforeEach(function() { result = vec3.random(out, 5.0); });
+
+ it("should result in a unit length vector", function() { expect(vec3.length(out)).toBeCloseTo(5.0); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+ });
+
+ describe("forEach", function() {
+ var vecArray;
+
+ beforeEach(function() {
+ vecArray = [
+ 1, 2, 3,
+ 4, 5, 6,
+ 0, 0, 0
+ ];
+ });
+
+ describe("when performing operations that take no extra arguments", function() {
+ beforeEach(function() { result = vec3.forEach(vecArray, 0, 0, 0, vec3.normalize); });
+
+ it("should update all values", function() {
+ expect(vecArray).toBeEqualish([
+ 0.267261, 0.534522, 0.801783,
+ 0.455842, 0.569802, 0.683763,
+ 0, 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ });
+
+ describe("when performing operations that takes one extra arguments", function() {
+ beforeEach(function() { result = vec3.forEach(vecArray, 0, 0, 0, vec3.add, vecA); });
+
+ it("should update all values", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4, 6,
+ 5, 7, 9,
+ 1, 2, 3
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("when specifying an offset", function() {
+ beforeEach(function() { result = vec3.forEach(vecArray, 0, 3, 0, vec3.add, vecA); });
+
+ it("should update all values except the first vector", function() {
+ expect(vecArray).toBeEqualish([
+ 1, 2, 3,
+ 5, 7, 9,
+ 1, 2, 3
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("when specifying a count", function() {
+ beforeEach(function() { result = vec3.forEach(vecArray, 0, 0, 2, vec3.add, vecA); });
+
+ it("should update all values except the last vector", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4, 6,
+ 5, 7, 9,
+ 0, 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("when specifying a stride", function() {
+ beforeEach(function() { result = vec3.forEach(vecArray, 6, 0, 0, vec3.add, vecA); });
+
+ it("should update all values except the second vector", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4, 6,
+ 4, 5, 6,
+ 1, 2, 3
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ });
+
+ describe("when calling a function that does not modify the out variable", function() {
+ beforeEach(function() {
+ result = vec3.forEach(vecArray, 0, 0, 0, function(out, vec) {});
+ });
+
+ it("values should remain unchanged", function() {
+ expect(vecArray).toBeEqualish([
+ 1, 2, 3,
+ 4, 5, 6,
+ 0, 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ });
+ });
+
+ describe("angle", function() {
+ beforeEach(function() { result = vec3.angle(vecA, vecB); });
+
+ it("should return the angle", function() { expect(result).toBeEqualish(0.225726); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([4, 5, 6]); });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = vec3.str(vecA); });
+
+ it("should return a string representation of the vector", function() { expect(result).toEqual("vec3(1, 2, 3)"); });
+ });
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec4-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec4-spec.js
new file mode 100644
index 00000000000..0c65908cb4a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/vec4-spec.js
@@ -0,0 +1,492 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+describe("vec4", function() {
+ var vec3 = require("../../src/gl-matrix/vec3.js");
+ var vec4 = require("../../src/gl-matrix/vec4.js");
+
+ var out, vecA, vecB, result;
+
+ beforeEach(function() { vecA = [1, 2, 3, 4]; vecB = [5, 6, 7, 8]; out = [0, 0, 0, 0]; });
+
+ describe("create", function() {
+ beforeEach(function() { result = vec4.create(); });
+ it("should return a 4 element array initialized to 0s", function() { expect(result).toBeEqualish([0, 0, 0, 0]); });
+ });
+
+ describe("clone", function() {
+ beforeEach(function() { result = vec4.clone(vecA); });
+ it("should return a 4 element array initialized to the values in vecA", function() { expect(result).toBeEqualish(vecA); });
+ });
+
+ describe("fromValues", function() {
+ beforeEach(function() { result = vec4.fromValues(1, 2, 3, 4); });
+ it("should return a 4 element array initialized to the values passed", function() { expect(result).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("copy", function() {
+ beforeEach(function() { result = vec4.copy(out, vecA); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("set", function() {
+ beforeEach(function() { result = vec4.set(out, 1, 2, 3, 4); });
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 2, 3, 4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("add", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.add(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([6, 8, 10, 12]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.add(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([6, 8, 10, 12]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.add(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([6, 8, 10, 12]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("subtract", function() {
+ it("should have an alias called 'sub'", function() { expect(vec4.sub).toEqual(vec4.subtract); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.subtract(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-4, -4, -4, -4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.subtract(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-4, -4, -4, -4]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.subtract(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([-4, -4, -4, -4]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("multiply", function() {
+ it("should have an alias called 'mul'", function() { expect(vec4.mul).toEqual(vec4.multiply); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.multiply(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([5, 12, 21, 32]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.multiply(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([5, 12, 21, 32]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.multiply(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([5, 12, 21, 32]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("divide", function() {
+ it("should have an alias called 'div'", function() { expect(vec4.div).toEqual(vec4.divide); });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.divide(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([0.2, 0.333333, 0.428571, 0.5]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.divide(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([0.2, 0.333333, 0.428571, 0.5]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.divide(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([0.2, 0.333333, 0.428571, 0.5]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("min", function() {
+ beforeEach(function() { vecA = [1, 3, 1, 3]; vecB = [3, 1, 3, 1]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.min(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 1, 1, 1]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3, 1]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.min(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([1, 1, 1, 1]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3, 1]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.min(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([1, 1, 1, 1]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1, 3]); });
+ });
+ });
+
+ describe("max", function() {
+ beforeEach(function() { vecA = [1, 3, 1, 3]; vecB = [3, 1, 3, 1]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.max(out, vecA, vecB); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 3, 3, 3]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1, 3]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3, 1]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.max(vecA, vecA, vecB); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3, 3, 3, 3]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([3, 1, 3, 1]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.max(vecB, vecA, vecB); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3, 3, 3, 3]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 3, 1, 3]); });
+ });
+ });
+
+ describe("scale", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.scale(out, vecA, 2); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([2, 4, 6, 8]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.scale(vecA, vecA, 2); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([2, 4, 6, 8]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("scaleAndAdd", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.scaleAndAdd(out, vecA, vecB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3.5, 5, 6.5, 8]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.scaleAndAdd(vecA, vecA, vecB, 0.5); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3.5, 5, 6.5, 8]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.scaleAndAdd(vecB, vecA, vecB, 0.5); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3.5, 5, 6.5, 8]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("distance", function() {
+ it("should have an alias called 'dist'", function() { expect(vec4.dist).toEqual(vec4.distance); });
+
+ beforeEach(function() { result = vec4.distance(vecA, vecB); });
+
+ it("should return the distance", function() { expect(result).toBeCloseTo(8); });
+ });
+
+ describe("squaredDistance", function() {
+ it("should have an alias called 'sqrDist'", function() { expect(vec4.sqrDist).toEqual(vec4.squaredDistance); });
+
+ beforeEach(function() { result = vec4.squaredDistance(vecA, vecB); });
+
+ it("should return the squared distance", function() { expect(result).toEqual(64); });
+ });
+
+ describe("length", function() {
+ it("should have an alias called 'len'", function() { expect(vec4.len).toEqual(vec4.length); });
+
+ beforeEach(function() { result = vec4.length(vecA); });
+
+ it("should return the length", function() { expect(result).toBeCloseTo(5.477225); });
+ });
+
+ describe("squaredLength", function() {
+ it("should have an alias called 'sqrLen'", function() { expect(vec4.sqrLen).toEqual(vec4.squaredLength); });
+
+ beforeEach(function() { result = vec4.squaredLength(vecA); });
+
+ it("should return the squared length", function() { expect(result).toEqual(30); });
+ });
+
+ describe("negate", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.negate(out, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([-1, -2, -3, -4]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.negate(vecA, vecA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([-1, -2, -3, -4]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("normalize", function() {
+ beforeEach(function() { vecA = [5, 0, 0, 0]; });
+
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.normalize(out, vecA); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([1, 0, 0, 0]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([5, 0, 0, 0]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.normalize(vecA, vecA); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([1, 0, 0, 0]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ });
+ });
+
+ describe("dot", function() {
+ beforeEach(function() { result = vec4.dot(vecA, vecB); });
+
+ it("should return the dot product", function() { expect(result).toEqual(70); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("lerp", function() {
+ describe("with a separate output vector", function() {
+ beforeEach(function() { result = vec4.lerp(out, vecA, vecB, 0.5); });
+
+ it("should place values into out", function() { expect(out).toBeEqualish([3, 4, 5, 6]); });
+ it("should return out", function() { expect(result).toBe(out); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecA is the output vector", function() {
+ beforeEach(function() { result = vec4.lerp(vecA, vecA, vecB, 0.5); });
+
+ it("should place values into vecA", function() { expect(vecA).toBeEqualish([3, 4, 5, 6]); });
+ it("should return vecA", function() { expect(result).toBe(vecA); });
+ it("should not modify vecB", function() { expect(vecB).toBeEqualish([5, 6, 7, 8]); });
+ });
+
+ describe("when vecB is the output vector", function() {
+ beforeEach(function() { result = vec4.lerp(vecB, vecA, vecB, 0.5); });
+
+ it("should place values into vecB", function() { expect(vecB).toBeEqualish([3, 4, 5, 6]); });
+ it("should return vecB", function() { expect(result).toBe(vecB); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+ });
+
+ describe("random", function() {
+ describe("with no scale", function() {
+ beforeEach(function() { result = vec4.random(out); });
+
+ it("should result in a unit length vector", function() { expect(vec4.length(out)).toBeCloseTo(1.0); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+
+ describe("with a scale", function() {
+ beforeEach(function() { result = vec4.random(out, 5.0); });
+
+ it("should result in a unit length vector", function() { expect(vec4.length(out)).toBeCloseTo(5.0); });
+ it("should return out", function() { expect(result).toBe(out); });
+ });
+ });
+
+ describe("forEach", function() {
+ var vecArray;
+
+ beforeEach(function() {
+ vecArray = [
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 0, 0, 0, 0
+ ];
+ });
+
+ describe("when performing operations that take no extra arguments", function() {
+ beforeEach(function() { result = vec4.forEach(vecArray, 0, 0, 0, vec4.normalize); });
+
+ it("should update all values", function() {
+ expect(vecArray).toBeEqualish([
+ 0.182574, 0.365148, 0.547722, 0.730296,
+ 0.379049, 0.454858, 0.530668, 0.606478,
+ 0, 0, 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ });
+
+ describe("when performing operations that takes one extra arguments", function() {
+ beforeEach(function() { result = vec4.forEach(vecArray, 0, 0, 0, vec4.add, vecA); });
+
+ it("should update all values", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4, 6, 8,
+ 6, 8, 10, 12,
+ 1, 2, 3, 4
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when specifying an offset", function() {
+ beforeEach(function() { result = vec4.forEach(vecArray, 0, 4, 0, vec4.add, vecA); });
+
+ it("should update all values except the first vector", function() {
+ expect(vecArray).toBeEqualish([
+ 1, 2, 3, 4,
+ 6, 8, 10, 12,
+ 1, 2, 3, 4
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when specifying a count", function() {
+ beforeEach(function() { result = vec4.forEach(vecArray, 0, 0, 2, vec4.add, vecA); });
+
+ it("should update all values except the last vector", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4, 6, 8,
+ 6, 8, 10, 12,
+ 0, 0, 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when specifying a stride", function() {
+ beforeEach(function() { result = vec4.forEach(vecArray, 8, 0, 0, vec4.add, vecA); });
+
+ it("should update all values except the second vector", function() {
+ expect(vecArray).toBeEqualish([
+ 2, 4, 6, 8,
+ 5, 6, 7, 8,
+ 1, 2, 3, 4
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ it("should not modify vecA", function() { expect(vecA).toBeEqualish([1, 2, 3, 4]); });
+ });
+
+ describe("when calling a function that does not modify the out variable", function() {
+ beforeEach(function() {
+ result = vec3.forEach(vecArray, 0, 0, 0, function(out, vec) {});
+ });
+
+ it("values should remain unchanged", function() {
+ expect(vecArray).toBeEqualish([
+ 1, 2, 3, 4,
+ 5, 6, 7, 8,
+ 0, 0, 0, 0
+ ]);
+ });
+ it("should return vecArray", function() { expect(result).toBe(vecArray); });
+ });
+ });
+
+ describe("str", function() {
+ beforeEach(function() { result = vec4.str(vecA); });
+
+ it("should return a string representation of the vector", function() { expect(result).toEqual("vec4(1, 2, 3, 4)"); });
+ });
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/worker-spec.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/worker-spec.js
new file mode 100644
index 00000000000..8b1754f4db1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/gl-matrix/worker-spec.js
@@ -0,0 +1,44 @@
+/* spec tests gl-matrix when embedded into a Web Worker */
+
+// only test with workers if workers are available
+if (typeof(Worker) !== 'undefined') {
+ describe("Embedded within Web Workers", function() {
+ it("should initialize successfully", function() {
+ var xhr = new XMLHttpRequest();
+ var source = null;
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ if (this.status == 200) {
+ source = this.responseText;
+ }
+ }
+ };
+ xhr.open("GET", "/dist/gl-matrix-min.js");
+ xhr.send();
+
+ var result = null;
+
+ waitsFor(function() {
+ if (!source) return false;
+ var blob = new Blob([
+ source,
+ "self.postMessage(vec3.create());"
+ ],
+ {type: "application/javascript"}
+ );
+
+ var worker = new Worker(URL.createObjectURL(blob));
+ worker.onmessage = function(e) {
+ result = e.data;
+ };
+ return true;
+ });
+
+ waitsFor(function() {
+ if (!result) return false;
+ expect(result).toBeEqualish([0, 0, 0]);
+ return true;
+ });
+ });
+ });
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/helpers/spec-helper.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/helpers/spec-helper.js
new file mode 100644
index 00000000000..58e36714505
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/helpers/spec-helper.js
@@ -0,0 +1,32 @@
+var HELPER_MATCHERS = (function() {
+ var EPSILON = 0.00001;
+
+ return {
+ /*
+ Returns true if `actual` has the same length as `expected`, and
+ if each element of both arrays is within 0.000001 of each other.
+ This is a way to check for "equal enough" conditions, as a way
+ of working around floating point imprecision.
+ */
+ toBeEqualish: function(expected) {
+ if (typeof(this.actual) == 'number')
+ return Math.abs(this.actual - expected) < EPSILON;
+
+ if (this.actual.length != expected.length) return false;
+ for (var i = 0; i < this.actual.length; i++) {
+ if (isNaN(this.actual[i]) !== isNaN(expected[i]))
+ return false;
+ if (Math.abs(this.actual[i] - expected[i]) >= EPSILON)
+ return false;
+ }
+ return true;
+ }
+ };
+})();
+
+beforeEach(function() {
+ this.addMatchers(HELPER_MATCHERS);
+});
+
+if (typeof(global) != 'undefined')
+ global.HELPER_MATCHERS = HELPER_MATCHERS;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/jasmine.yml b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/jasmine.yml
new file mode 100644
index 00000000000..afb56ec62c9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/spec/jasmine.yml
@@ -0,0 +1,74 @@
+# src_files
+#
+# Return an array of filepaths relative to src_dir to include before jasmine specs.
+# Default: []
+#
+# EXAMPLE:
+#
+# src_files:
+# - lib/source1.js
+# - lib/source2.js
+# - dist/**/*.js
+#
+src_files:
+ - src/gl-matrix.js
+ - src/gl-matrix/common.js
+
+# stylesheets
+#
+# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
+# Default: []
+#
+# EXAMPLE:
+#
+# stylesheets:
+# - css/style.css
+# - stylesheets/*.css
+#
+stylesheets:
+
+# helpers
+#
+# Return an array of filepaths relative to spec_dir to include before jasmine specs.
+# Default: ["helpers/**/*.js"]
+#
+# EXAMPLE:
+#
+# helpers:
+# - helpers/**/*.js
+#
+helpers:
+
+# spec_files
+#
+# Return an array of filepaths relative to spec_dir to include.
+# Default: ["**/*[sS]pec.js"]
+#
+# EXAMPLE:
+#
+# spec_files:
+# - **/*[sS]pec.js
+#
+spec_files:
+
+# src_dir
+#
+# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
+# Default: project root
+#
+# EXAMPLE:
+#
+# src_dir: public
+#
+src_dir:
+
+# spec_dir
+#
+# Spec directory path. Your spec_files must be returned relative to this path.
+# Default: spec/javascripts
+#
+# EXAMPLE:
+#
+# spec_dir: spec/javascripts
+#
+spec_dir: spec
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix.js
new file mode 100644
index 00000000000..da4fe60dfd2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix.js
@@ -0,0 +1,37 @@
+/**
+ * @fileoverview gl-matrix - High performance matrix and vector operations
+ * @author Brandon Jones
+ * @author Colin MacKenzie IV
+ * @version 2.3.1
+ */
+
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+// END HEADER
+
+exports.glMatrix = require("./gl-matrix/common.js");
+exports.mat2 = require("./gl-matrix/mat2.js");
+exports.mat2d = require("./gl-matrix/mat2d.js");
+exports.mat3 = require("./gl-matrix/mat3.js");
+exports.mat4 = require("./gl-matrix/mat4.js");
+exports.quat = require("./gl-matrix/quat.js");
+exports.vec2 = require("./gl-matrix/vec2.js");
+exports.vec3 = require("./gl-matrix/vec3.js");
+exports.vec4 = require("./gl-matrix/vec4.js"); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/common.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/common.js
new file mode 100644
index 00000000000..5253421532e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/common.js
@@ -0,0 +1,52 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+/**
+ * @class Common utilities
+ * @name glMatrix
+ */
+var glMatrix = {};
+
+// Constants
+glMatrix.EPSILON = 0.000001;
+glMatrix.ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array;
+glMatrix.RANDOM = Math.random;
+
+/**
+ * Sets the type of array used when creating new vectors and matrices
+ *
+ * @param {Type} type Array type, such as Float32Array or Array
+ */
+glMatrix.setMatrixArrayType = function(type) {
+ GLMAT_ARRAY_TYPE = type;
+}
+
+var degree = Math.PI / 180;
+
+/**
+* Convert Degree To Radian
+*
+* @param {Number} Angle in Degrees
+*/
+glMatrix.toRadian = function(a){
+ return a * degree;
+}
+
+module.exports = glMatrix;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2.js
new file mode 100644
index 00000000000..012a5c1af6b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2.js
@@ -0,0 +1,302 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 2x2 Matrix
+ * @name mat2
+ */
+var mat2 = {};
+
+/**
+ * Creates a new identity mat2
+ *
+ * @returns {mat2} a new 2x2 matrix
+ */
+mat2.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+};
+
+/**
+ * Creates a new mat2 initialized with values from an existing matrix
+ *
+ * @param {mat2} a matrix to clone
+ * @returns {mat2} a new 2x2 matrix
+ */
+mat2.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+};
+
+/**
+ * Copy the values from one mat2 to another
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+};
+
+/**
+ * Set a mat2 to the identity matrix
+ *
+ * @param {mat2} out the receiving matrix
+ * @returns {mat2} out
+ */
+mat2.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+};
+
+/**
+ * Transpose the values of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.transpose = function(out, a) {
+ // If we are transposing ourselves we can skip a few steps but have to cache some values
+ if (out === a) {
+ var a1 = a[1];
+ out[1] = a[2];
+ out[2] = a1;
+ } else {
+ out[0] = a[0];
+ out[1] = a[2];
+ out[2] = a[1];
+ out[3] = a[3];
+ }
+
+ return out;
+};
+
+/**
+ * Inverts a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.invert = function(out, a) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+
+ // Calculate the determinant
+ det = a0 * a3 - a2 * a1;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = a3 * det;
+ out[1] = -a1 * det;
+ out[2] = -a2 * det;
+ out[3] = a0 * det;
+
+ return out;
+};
+
+/**
+ * Calculates the adjugate of a mat2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the source matrix
+ * @returns {mat2} out
+ */
+mat2.adjoint = function(out, a) {
+ // Caching this value is nessecary if out == a
+ var a0 = a[0];
+ out[0] = a[3];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ out[3] = a0;
+
+ return out;
+};
+
+/**
+ * Calculates the determinant of a mat2
+ *
+ * @param {mat2} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat2.determinant = function (a) {
+ return a[0] * a[3] - a[2] * a[1];
+};
+
+/**
+ * Multiplies two mat2's
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the first operand
+ * @param {mat2} b the second operand
+ * @returns {mat2} out
+ */
+mat2.multiply = function (out, a, b) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3];
+ var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+ out[0] = a0 * b0 + a2 * b1;
+ out[1] = a1 * b0 + a3 * b1;
+ out[2] = a0 * b2 + a2 * b3;
+ out[3] = a1 * b2 + a3 * b3;
+ return out;
+};
+
+/**
+ * Alias for {@link mat2.multiply}
+ * @function
+ */
+mat2.mul = mat2.multiply;
+
+/**
+ * Rotates a mat2 by the given angle
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2} out
+ */
+mat2.rotate = function (out, a, rad) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+ s = Math.sin(rad),
+ c = Math.cos(rad);
+ out[0] = a0 * c + a2 * s;
+ out[1] = a1 * c + a3 * s;
+ out[2] = a0 * -s + a2 * c;
+ out[3] = a1 * -s + a3 * c;
+ return out;
+};
+
+/**
+ * Scales the mat2 by the dimensions in the given vec2
+ *
+ * @param {mat2} out the receiving matrix
+ * @param {mat2} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2} out
+ **/
+mat2.scale = function(out, a, v) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+ v0 = v[0], v1 = v[1];
+ out[0] = a0 * v0;
+ out[1] = a1 * v0;
+ out[2] = a2 * v1;
+ out[3] = a3 * v1;
+ return out;
+};
+
+/**
+ * Creates a matrix from a given angle
+ * This is equivalent to (but much faster than):
+ *
+ * mat2.identity(dest);
+ * mat2.rotate(dest, dest, rad);
+ *
+ * @param {mat2} out mat2 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2} out
+ */
+mat2.fromRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+ out[0] = c;
+ out[1] = s;
+ out[2] = -s;
+ out[3] = c;
+ return out;
+}
+
+/**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat2.identity(dest);
+ * mat2.scale(dest, dest, vec);
+ *
+ * @param {mat2} out mat2 receiving operation result
+ * @param {vec2} v Scaling vector
+ * @returns {mat2} out
+ */
+mat2.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = v[1];
+ return out;
+}
+
+/**
+ * Returns a string representation of a mat2
+ *
+ * @param {mat2} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat2.str = function (a) {
+ return 'mat2(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat2
+ *
+ * @param {mat2} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat2.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2)))
+};
+
+/**
+ * Returns L, D and U matrices (Lower triangular, Diagonal and Upper triangular) by factorizing the input matrix
+ * @param {mat2} L the lower triangular matrix
+ * @param {mat2} D the diagonal matrix
+ * @param {mat2} U the upper triangular matrix
+ * @param {mat2} a the input matrix to factorize
+ */
+
+mat2.LDU = function (L, D, U, a) {
+ L[2] = a[2]/a[0];
+ U[0] = a[0];
+ U[1] = a[1];
+ U[3] = a[3] - L[2] * U[1];
+ return [L, D, U];
+};
+
+
+module.exports = mat2;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2d.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2d.js
new file mode 100644
index 00000000000..df3a0e6a0b3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat2d.js
@@ -0,0 +1,317 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 2x3 Matrix
+ * @name mat2d
+ *
+ * @description
+ * A mat2d contains six elements defined as:
+ * <pre>
+ * [a, c, tx,
+ * b, d, ty]
+ * </pre>
+ * This is a short form for the 3x3 matrix:
+ * <pre>
+ * [a, c, tx,
+ * b, d, ty,
+ * 0, 0, 1]
+ * </pre>
+ * The last row is ignored so the array is shorter and operations are faster.
+ */
+var mat2d = {};
+
+/**
+ * Creates a new identity mat2d
+ *
+ * @returns {mat2d} a new 2x3 matrix
+ */
+mat2d.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(6);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+};
+
+/**
+ * Creates a new mat2d initialized with values from an existing matrix
+ *
+ * @param {mat2d} a matrix to clone
+ * @returns {mat2d} a new 2x3 matrix
+ */
+mat2d.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(6);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ return out;
+};
+
+/**
+ * Copy the values from one mat2d to another
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+mat2d.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ return out;
+};
+
+/**
+ * Set a mat2d to the identity matrix
+ *
+ * @param {mat2d} out the receiving matrix
+ * @returns {mat2d} out
+ */
+mat2d.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+};
+
+/**
+ * Inverts a mat2d
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the source matrix
+ * @returns {mat2d} out
+ */
+mat2d.invert = function(out, a) {
+ var aa = a[0], ab = a[1], ac = a[2], ad = a[3],
+ atx = a[4], aty = a[5];
+
+ var det = aa * ad - ab * ac;
+ if(!det){
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = ad * det;
+ out[1] = -ab * det;
+ out[2] = -ac * det;
+ out[3] = aa * det;
+ out[4] = (ac * aty - ad * atx) * det;
+ out[5] = (ab * atx - aa * aty) * det;
+ return out;
+};
+
+/**
+ * Calculates the determinant of a mat2d
+ *
+ * @param {mat2d} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat2d.determinant = function (a) {
+ return a[0] * a[3] - a[1] * a[2];
+};
+
+/**
+ * Multiplies two mat2d's
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the first operand
+ * @param {mat2d} b the second operand
+ * @returns {mat2d} out
+ */
+mat2d.multiply = function (out, a, b) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
+ out[0] = a0 * b0 + a2 * b1;
+ out[1] = a1 * b0 + a3 * b1;
+ out[2] = a0 * b2 + a2 * b3;
+ out[3] = a1 * b2 + a3 * b3;
+ out[4] = a0 * b4 + a2 * b5 + a4;
+ out[5] = a1 * b4 + a3 * b5 + a5;
+ return out;
+};
+
+/**
+ * Alias for {@link mat2d.multiply}
+ * @function
+ */
+mat2d.mul = mat2d.multiply;
+
+/**
+ * Rotates a mat2d by the given angle
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2d} out
+ */
+mat2d.rotate = function (out, a, rad) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ s = Math.sin(rad),
+ c = Math.cos(rad);
+ out[0] = a0 * c + a2 * s;
+ out[1] = a1 * c + a3 * s;
+ out[2] = a0 * -s + a2 * c;
+ out[3] = a1 * -s + a3 * c;
+ out[4] = a4;
+ out[5] = a5;
+ return out;
+};
+
+/**
+ * Scales the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat2d} out
+ **/
+mat2d.scale = function(out, a, v) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ v0 = v[0], v1 = v[1];
+ out[0] = a0 * v0;
+ out[1] = a1 * v0;
+ out[2] = a2 * v1;
+ out[3] = a3 * v1;
+ out[4] = a4;
+ out[5] = a5;
+ return out;
+};
+
+/**
+ * Translates the mat2d by the dimensions in the given vec2
+ *
+ * @param {mat2d} out the receiving matrix
+ * @param {mat2d} a the matrix to translate
+ * @param {vec2} v the vec2 to translate the matrix by
+ * @returns {mat2d} out
+ **/
+mat2d.translate = function(out, a, v) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5],
+ v0 = v[0], v1 = v[1];
+ out[0] = a0;
+ out[1] = a1;
+ out[2] = a2;
+ out[3] = a3;
+ out[4] = a0 * v0 + a2 * v1 + a4;
+ out[5] = a1 * v0 + a3 * v1 + a5;
+ return out;
+};
+
+/**
+ * Creates a matrix from a given angle
+ * This is equivalent to (but much faster than):
+ *
+ * mat2d.identity(dest);
+ * mat2d.rotate(dest, dest, rad);
+ *
+ * @param {mat2d} out mat2d receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat2d} out
+ */
+mat2d.fromRotation = function(out, rad) {
+ var s = Math.sin(rad), c = Math.cos(rad);
+ out[0] = c;
+ out[1] = s;
+ out[2] = -s;
+ out[3] = c;
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+}
+
+/**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat2d.identity(dest);
+ * mat2d.scale(dest, dest, vec);
+ *
+ * @param {mat2d} out mat2d receiving operation result
+ * @param {vec2} v Scaling vector
+ * @returns {mat2d} out
+ */
+mat2d.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = v[1];
+ out[4] = 0;
+ out[5] = 0;
+ return out;
+}
+
+/**
+ * Creates a matrix from a vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat2d.identity(dest);
+ * mat2d.translate(dest, dest, vec);
+ *
+ * @param {mat2d} out mat2d receiving operation result
+ * @param {vec2} v Translation vector
+ * @returns {mat2d} out
+ */
+mat2d.fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ out[4] = v[0];
+ out[5] = v[1];
+ return out;
+}
+
+/**
+ * Returns a string representation of a mat2d
+ *
+ * @param {mat2d} a matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat2d.str = function (a) {
+ return 'mat2d(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' +
+ a[3] + ', ' + a[4] + ', ' + a[5] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat2d
+ *
+ * @param {mat2d} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat2d.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + 1))
+};
+
+module.exports = mat2d;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat3.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat3.js
new file mode 100644
index 00000000000..bdda05ef020
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat3.js
@@ -0,0 +1,565 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 3x3 Matrix
+ * @name mat3
+ */
+var mat3 = {};
+
+/**
+ * Creates a new identity mat3
+ *
+ * @returns {mat3} a new 3x3 matrix
+ */
+mat3.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(9);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 1;
+ out[5] = 0;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+};
+
+/**
+ * Copies the upper-left 3x3 values into the given mat3.
+ *
+ * @param {mat3} out the receiving 3x3 matrix
+ * @param {mat4} a the source 4x4 matrix
+ * @returns {mat3} out
+ */
+mat3.fromMat4 = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[4];
+ out[4] = a[5];
+ out[5] = a[6];
+ out[6] = a[8];
+ out[7] = a[9];
+ out[8] = a[10];
+ return out;
+};
+
+/**
+ * Creates a new mat3 initialized with values from an existing matrix
+ *
+ * @param {mat3} a matrix to clone
+ * @returns {mat3} a new 3x3 matrix
+ */
+mat3.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(9);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ return out;
+};
+
+/**
+ * Copy the values from one mat3 to another
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ return out;
+};
+
+/**
+ * Set a mat3 to the identity matrix
+ *
+ * @param {mat3} out the receiving matrix
+ * @returns {mat3} out
+ */
+mat3.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 1;
+ out[5] = 0;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+};
+
+/**
+ * Transpose the values of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.transpose = function(out, a) {
+ // If we are transposing ourselves we can skip a few steps but have to cache some values
+ if (out === a) {
+ var a01 = a[1], a02 = a[2], a12 = a[5];
+ out[1] = a[3];
+ out[2] = a[6];
+ out[3] = a01;
+ out[5] = a[7];
+ out[6] = a02;
+ out[7] = a12;
+ } else {
+ out[0] = a[0];
+ out[1] = a[3];
+ out[2] = a[6];
+ out[3] = a[1];
+ out[4] = a[4];
+ out[5] = a[7];
+ out[6] = a[2];
+ out[7] = a[5];
+ out[8] = a[8];
+ }
+
+ return out;
+};
+
+/**
+ * Inverts a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.invert = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+
+ b01 = a22 * a11 - a12 * a21,
+ b11 = -a22 * a10 + a12 * a20,
+ b21 = a21 * a10 - a11 * a20,
+
+ // Calculate the determinant
+ det = a00 * b01 + a01 * b11 + a02 * b21;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = b01 * det;
+ out[1] = (-a22 * a01 + a02 * a21) * det;
+ out[2] = (a12 * a01 - a02 * a11) * det;
+ out[3] = b11 * det;
+ out[4] = (a22 * a00 - a02 * a20) * det;
+ out[5] = (-a12 * a00 + a02 * a10) * det;
+ out[6] = b21 * det;
+ out[7] = (-a21 * a00 + a01 * a20) * det;
+ out[8] = (a11 * a00 - a01 * a10) * det;
+ return out;
+};
+
+/**
+ * Calculates the adjugate of a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the source matrix
+ * @returns {mat3} out
+ */
+mat3.adjoint = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8];
+
+ out[0] = (a11 * a22 - a12 * a21);
+ out[1] = (a02 * a21 - a01 * a22);
+ out[2] = (a01 * a12 - a02 * a11);
+ out[3] = (a12 * a20 - a10 * a22);
+ out[4] = (a00 * a22 - a02 * a20);
+ out[5] = (a02 * a10 - a00 * a12);
+ out[6] = (a10 * a21 - a11 * a20);
+ out[7] = (a01 * a20 - a00 * a21);
+ out[8] = (a00 * a11 - a01 * a10);
+ return out;
+};
+
+/**
+ * Calculates the determinant of a mat3
+ *
+ * @param {mat3} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat3.determinant = function (a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8];
+
+ return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20);
+};
+
+/**
+ * Multiplies two mat3's
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the first operand
+ * @param {mat3} b the second operand
+ * @returns {mat3} out
+ */
+mat3.multiply = function (out, a, b) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+
+ b00 = b[0], b01 = b[1], b02 = b[2],
+ b10 = b[3], b11 = b[4], b12 = b[5],
+ b20 = b[6], b21 = b[7], b22 = b[8];
+
+ out[0] = b00 * a00 + b01 * a10 + b02 * a20;
+ out[1] = b00 * a01 + b01 * a11 + b02 * a21;
+ out[2] = b00 * a02 + b01 * a12 + b02 * a22;
+
+ out[3] = b10 * a00 + b11 * a10 + b12 * a20;
+ out[4] = b10 * a01 + b11 * a11 + b12 * a21;
+ out[5] = b10 * a02 + b11 * a12 + b12 * a22;
+
+ out[6] = b20 * a00 + b21 * a10 + b22 * a20;
+ out[7] = b20 * a01 + b21 * a11 + b22 * a21;
+ out[8] = b20 * a02 + b21 * a12 + b22 * a22;
+ return out;
+};
+
+/**
+ * Alias for {@link mat3.multiply}
+ * @function
+ */
+mat3.mul = mat3.multiply;
+
+/**
+ * Translate a mat3 by the given vector
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to translate
+ * @param {vec2} v vector to translate by
+ * @returns {mat3} out
+ */
+mat3.translate = function(out, a, v) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+ x = v[0], y = v[1];
+
+ out[0] = a00;
+ out[1] = a01;
+ out[2] = a02;
+
+ out[3] = a10;
+ out[4] = a11;
+ out[5] = a12;
+
+ out[6] = x * a00 + y * a10 + a20;
+ out[7] = x * a01 + y * a11 + a21;
+ out[8] = x * a02 + y * a12 + a22;
+ return out;
+};
+
+/**
+ * Rotates a mat3 by the given angle
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat3} out
+ */
+mat3.rotate = function (out, a, rad) {
+ var a00 = a[0], a01 = a[1], a02 = a[2],
+ a10 = a[3], a11 = a[4], a12 = a[5],
+ a20 = a[6], a21 = a[7], a22 = a[8],
+
+ s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ out[0] = c * a00 + s * a10;
+ out[1] = c * a01 + s * a11;
+ out[2] = c * a02 + s * a12;
+
+ out[3] = c * a10 - s * a00;
+ out[4] = c * a11 - s * a01;
+ out[5] = c * a12 - s * a02;
+
+ out[6] = a20;
+ out[7] = a21;
+ out[8] = a22;
+ return out;
+};
+
+/**
+ * Scales the mat3 by the dimensions in the given vec2
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat3} a the matrix to rotate
+ * @param {vec2} v the vec2 to scale the matrix by
+ * @returns {mat3} out
+ **/
+mat3.scale = function(out, a, v) {
+ var x = v[0], y = v[1];
+
+ out[0] = x * a[0];
+ out[1] = x * a[1];
+ out[2] = x * a[2];
+
+ out[3] = y * a[3];
+ out[4] = y * a[4];
+ out[5] = y * a[5];
+
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ return out;
+};
+
+/**
+ * Creates a matrix from a vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat3.identity(dest);
+ * mat3.translate(dest, dest, vec);
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {vec2} v Translation vector
+ * @returns {mat3} out
+ */
+mat3.fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 1;
+ out[5] = 0;
+ out[6] = v[0];
+ out[7] = v[1];
+ out[8] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from a given angle
+ * This is equivalent to (but much faster than):
+ *
+ * mat3.identity(dest);
+ * mat3.rotate(dest, dest, rad);
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat3} out
+ */
+mat3.fromRotation = function(out, rad) {
+ var s = Math.sin(rad), c = Math.cos(rad);
+
+ out[0] = c;
+ out[1] = s;
+ out[2] = 0;
+
+ out[3] = -s;
+ out[4] = c;
+ out[5] = 0;
+
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat3.identity(dest);
+ * mat3.scale(dest, dest, vec);
+ *
+ * @param {mat3} out mat3 receiving operation result
+ * @param {vec2} v Scaling vector
+ * @returns {mat3} out
+ */
+mat3.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+
+ out[3] = 0;
+ out[4] = v[1];
+ out[5] = 0;
+
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 1;
+ return out;
+}
+
+/**
+ * Copies the values from a mat2d into a mat3
+ *
+ * @param {mat3} out the receiving matrix
+ * @param {mat2d} a the matrix to copy
+ * @returns {mat3} out
+ **/
+mat3.fromMat2d = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = 0;
+
+ out[3] = a[2];
+ out[4] = a[3];
+ out[5] = 0;
+
+ out[6] = a[4];
+ out[7] = a[5];
+ out[8] = 1;
+ return out;
+};
+
+/**
+* Calculates a 3x3 matrix from the given quaternion
+*
+* @param {mat3} out mat3 receiving operation result
+* @param {quat} q Quaternion to create matrix from
+*
+* @returns {mat3} out
+*/
+mat3.fromQuat = function (out, q) {
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ yx = y * x2,
+ yy = y * y2,
+ zx = z * x2,
+ zy = z * y2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2;
+
+ out[0] = 1 - yy - zz;
+ out[3] = yx - wz;
+ out[6] = zx + wy;
+
+ out[1] = yx + wz;
+ out[4] = 1 - xx - zz;
+ out[7] = zy - wx;
+
+ out[2] = zx - wy;
+ out[5] = zy + wx;
+ out[8] = 1 - xx - yy;
+
+ return out;
+};
+
+/**
+* Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix
+*
+* @param {mat3} out mat3 receiving operation result
+* @param {mat4} a Mat4 to derive the normal matrix from
+*
+* @returns {mat3} out
+*/
+mat3.normalFromMat4 = function (out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+ b00 = a00 * a11 - a01 * a10,
+ b01 = a00 * a12 - a02 * a10,
+ b02 = a00 * a13 - a03 * a10,
+ b03 = a01 * a12 - a02 * a11,
+ b04 = a01 * a13 - a03 * a11,
+ b05 = a02 * a13 - a03 * a12,
+ b06 = a20 * a31 - a21 * a30,
+ b07 = a20 * a32 - a22 * a30,
+ b08 = a20 * a33 - a23 * a30,
+ b09 = a21 * a32 - a22 * a31,
+ b10 = a21 * a33 - a23 * a31,
+ b11 = a22 * a33 - a23 * a32,
+
+ // Calculate the determinant
+ det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+ out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+ out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+
+ out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+ out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+ out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+
+ out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+ out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+ out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+
+ return out;
+};
+
+/**
+ * Returns a string representation of a mat3
+ *
+ * @param {mat3} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat3.str = function (a) {
+ return 'mat3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' +
+ a[3] + ', ' + a[4] + ', ' + a[5] + ', ' +
+ a[6] + ', ' + a[7] + ', ' + a[8] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat3
+ *
+ * @param {mat3} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat3.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2)))
+};
+
+
+module.exports = mat3;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat4.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat4.js
new file mode 100644
index 00000000000..b82526d1365
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/mat4.js
@@ -0,0 +1,1283 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 4x4 Matrix
+ * @name mat4
+ */
+var mat4 = {};
+
+/**
+ * Creates a new identity mat4
+ *
+ * @returns {mat4} a new 4x4 matrix
+ */
+mat4.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(16);
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+};
+
+/**
+ * Creates a new mat4 initialized with values from an existing matrix
+ *
+ * @param {mat4} a matrix to clone
+ * @returns {mat4} a new 4x4 matrix
+ */
+mat4.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(16);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ out[9] = a[9];
+ out[10] = a[10];
+ out[11] = a[11];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+};
+
+/**
+ * Copy the values from one mat4 to another
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[8] = a[8];
+ out[9] = a[9];
+ out[10] = a[10];
+ out[11] = a[11];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+};
+
+/**
+ * Set a mat4 to the identity matrix
+ *
+ * @param {mat4} out the receiving matrix
+ * @returns {mat4} out
+ */
+mat4.identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+};
+
+/**
+ * Transpose the values of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.transpose = function(out, a) {
+ // If we are transposing ourselves we can skip a few steps but have to cache some values
+ if (out === a) {
+ var a01 = a[1], a02 = a[2], a03 = a[3],
+ a12 = a[6], a13 = a[7],
+ a23 = a[11];
+
+ out[1] = a[4];
+ out[2] = a[8];
+ out[3] = a[12];
+ out[4] = a01;
+ out[6] = a[9];
+ out[7] = a[13];
+ out[8] = a02;
+ out[9] = a12;
+ out[11] = a[14];
+ out[12] = a03;
+ out[13] = a13;
+ out[14] = a23;
+ } else {
+ out[0] = a[0];
+ out[1] = a[4];
+ out[2] = a[8];
+ out[3] = a[12];
+ out[4] = a[1];
+ out[5] = a[5];
+ out[6] = a[9];
+ out[7] = a[13];
+ out[8] = a[2];
+ out[9] = a[6];
+ out[10] = a[10];
+ out[11] = a[14];
+ out[12] = a[3];
+ out[13] = a[7];
+ out[14] = a[11];
+ out[15] = a[15];
+ }
+
+ return out;
+};
+
+/**
+ * Inverts a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.invert = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+ b00 = a00 * a11 - a01 * a10,
+ b01 = a00 * a12 - a02 * a10,
+ b02 = a00 * a13 - a03 * a10,
+ b03 = a01 * a12 - a02 * a11,
+ b04 = a01 * a13 - a03 * a11,
+ b05 = a02 * a13 - a03 * a12,
+ b06 = a20 * a31 - a21 * a30,
+ b07 = a20 * a32 - a22 * a30,
+ b08 = a20 * a33 - a23 * a30,
+ b09 = a21 * a32 - a22 * a31,
+ b10 = a21 * a33 - a23 * a31,
+ b11 = a22 * a33 - a23 * a32,
+
+ // Calculate the determinant
+ det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
+
+ return out;
+};
+
+/**
+ * Calculates the adjugate of a mat4
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the source matrix
+ * @returns {mat4} out
+ */
+mat4.adjoint = function(out, a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+ out[0] = (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22));
+ out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22));
+ out[2] = (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12));
+ out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12));
+ out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22));
+ out[5] = (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22));
+ out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12));
+ out[7] = (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12));
+ out[8] = (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21));
+ out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21));
+ out[10] = (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11));
+ out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11));
+ out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21));
+ out[13] = (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21));
+ out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11));
+ out[15] = (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11));
+ return out;
+};
+
+/**
+ * Calculates the determinant of a mat4
+ *
+ * @param {mat4} a the source matrix
+ * @returns {Number} determinant of a
+ */
+mat4.determinant = function (a) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
+
+ b00 = a00 * a11 - a01 * a10,
+ b01 = a00 * a12 - a02 * a10,
+ b02 = a00 * a13 - a03 * a10,
+ b03 = a01 * a12 - a02 * a11,
+ b04 = a01 * a13 - a03 * a11,
+ b05 = a02 * a13 - a03 * a12,
+ b06 = a20 * a31 - a21 * a30,
+ b07 = a20 * a32 - a22 * a30,
+ b08 = a20 * a33 - a23 * a30,
+ b09 = a21 * a32 - a22 * a31,
+ b10 = a21 * a33 - a23 * a31,
+ b11 = a22 * a33 - a23 * a32;
+
+ // Calculate the determinant
+ return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+};
+
+/**
+ * Multiplies two mat4's
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the first operand
+ * @param {mat4} b the second operand
+ * @returns {mat4} out
+ */
+mat4.multiply = function (out, a, b) {
+ var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
+ a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
+ a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
+ a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
+
+ // Cache only the current line of the second matrix
+ var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+ out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
+ out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
+ out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
+ out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+ return out;
+};
+
+/**
+ * Alias for {@link mat4.multiply}
+ * @function
+ */
+mat4.mul = mat4.multiply;
+
+/**
+ * Translate a mat4 by the given vector
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to translate
+ * @param {vec3} v vector to translate by
+ * @returns {mat4} out
+ */
+mat4.translate = function (out, a, v) {
+ var x = v[0], y = v[1], z = v[2],
+ a00, a01, a02, a03,
+ a10, a11, a12, a13,
+ a20, a21, a22, a23;
+
+ if (a === out) {
+ out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
+ out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
+ out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
+ out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
+ } else {
+ a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+ a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+ a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+ out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03;
+ out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13;
+ out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23;
+
+ out[12] = a00 * x + a10 * y + a20 * z + a[12];
+ out[13] = a01 * x + a11 * y + a21 * z + a[13];
+ out[14] = a02 * x + a12 * y + a22 * z + a[14];
+ out[15] = a03 * x + a13 * y + a23 * z + a[15];
+ }
+
+ return out;
+};
+
+/**
+ * Scales the mat4 by the dimensions in the given vec3
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to scale
+ * @param {vec3} v the vec3 to scale the matrix by
+ * @returns {mat4} out
+ **/
+mat4.scale = function(out, a, v) {
+ var x = v[0], y = v[1], z = v[2];
+
+ out[0] = a[0] * x;
+ out[1] = a[1] * x;
+ out[2] = a[2] * x;
+ out[3] = a[3] * x;
+ out[4] = a[4] * y;
+ out[5] = a[5] * y;
+ out[6] = a[6] * y;
+ out[7] = a[7] * y;
+ out[8] = a[8] * z;
+ out[9] = a[9] * z;
+ out[10] = a[10] * z;
+ out[11] = a[11] * z;
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+};
+
+/**
+ * Rotates a mat4 by the given angle around the given axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @param {vec3} axis the axis to rotate around
+ * @returns {mat4} out
+ */
+mat4.rotate = function (out, a, rad, axis) {
+ var x = axis[0], y = axis[1], z = axis[2],
+ len = Math.sqrt(x * x + y * y + z * z),
+ s, c, t,
+ a00, a01, a02, a03,
+ a10, a11, a12, a13,
+ a20, a21, a22, a23,
+ b00, b01, b02,
+ b10, b11, b12,
+ b20, b21, b22;
+
+ if (Math.abs(len) < glMatrix.EPSILON) { return null; }
+
+ len = 1 / len;
+ x *= len;
+ y *= len;
+ z *= len;
+
+ s = Math.sin(rad);
+ c = Math.cos(rad);
+ t = 1 - c;
+
+ a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
+ a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
+ a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
+
+ // Construct the elements of the rotation matrix
+ b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s;
+ b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s;
+ b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c;
+
+ // Perform rotation-specific matrix multiplication
+ out[0] = a00 * b00 + a10 * b01 + a20 * b02;
+ out[1] = a01 * b00 + a11 * b01 + a21 * b02;
+ out[2] = a02 * b00 + a12 * b01 + a22 * b02;
+ out[3] = a03 * b00 + a13 * b01 + a23 * b02;
+ out[4] = a00 * b10 + a10 * b11 + a20 * b12;
+ out[5] = a01 * b10 + a11 * b11 + a21 * b12;
+ out[6] = a02 * b10 + a12 * b11 + a22 * b12;
+ out[7] = a03 * b10 + a13 * b11 + a23 * b12;
+ out[8] = a00 * b20 + a10 * b21 + a20 * b22;
+ out[9] = a01 * b20 + a11 * b21 + a21 * b22;
+ out[10] = a02 * b20 + a12 * b21 + a22 * b22;
+ out[11] = a03 * b20 + a13 * b21 + a23 * b22;
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged last row
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+ return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the X axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateX = function (out, a, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad),
+ a10 = a[4],
+ a11 = a[5],
+ a12 = a[6],
+ a13 = a[7],
+ a20 = a[8],
+ a21 = a[9],
+ a22 = a[10],
+ a23 = a[11];
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged rows
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+
+ // Perform axis-specific matrix multiplication
+ out[4] = a10 * c + a20 * s;
+ out[5] = a11 * c + a21 * s;
+ out[6] = a12 * c + a22 * s;
+ out[7] = a13 * c + a23 * s;
+ out[8] = a20 * c - a10 * s;
+ out[9] = a21 * c - a11 * s;
+ out[10] = a22 * c - a12 * s;
+ out[11] = a23 * c - a13 * s;
+ return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the Y axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateY = function (out, a, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad),
+ a00 = a[0],
+ a01 = a[1],
+ a02 = a[2],
+ a03 = a[3],
+ a20 = a[8],
+ a21 = a[9],
+ a22 = a[10],
+ a23 = a[11];
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged rows
+ out[4] = a[4];
+ out[5] = a[5];
+ out[6] = a[6];
+ out[7] = a[7];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+
+ // Perform axis-specific matrix multiplication
+ out[0] = a00 * c - a20 * s;
+ out[1] = a01 * c - a21 * s;
+ out[2] = a02 * c - a22 * s;
+ out[3] = a03 * c - a23 * s;
+ out[8] = a00 * s + a20 * c;
+ out[9] = a01 * s + a21 * c;
+ out[10] = a02 * s + a22 * c;
+ out[11] = a03 * s + a23 * c;
+ return out;
+};
+
+/**
+ * Rotates a matrix by the given angle around the Z axis
+ *
+ * @param {mat4} out the receiving matrix
+ * @param {mat4} a the matrix to rotate
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.rotateZ = function (out, a, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad),
+ a00 = a[0],
+ a01 = a[1],
+ a02 = a[2],
+ a03 = a[3],
+ a10 = a[4],
+ a11 = a[5],
+ a12 = a[6],
+ a13 = a[7];
+
+ if (a !== out) { // If the source and destination differ, copy the unchanged last row
+ out[8] = a[8];
+ out[9] = a[9];
+ out[10] = a[10];
+ out[11] = a[11];
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ }
+
+ // Perform axis-specific matrix multiplication
+ out[0] = a00 * c + a10 * s;
+ out[1] = a01 * c + a11 * s;
+ out[2] = a02 * c + a12 * s;
+ out[3] = a03 * c + a13 * s;
+ out[4] = a10 * c - a00 * s;
+ out[5] = a11 * c - a01 * s;
+ out[6] = a12 * c - a02 * s;
+ out[7] = a13 * c - a03 * s;
+ return out;
+};
+
+/**
+ * Creates a matrix from a vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, dest, vec);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {vec3} v Translation vector
+ * @returns {mat4} out
+ */
+mat4.fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from a vector scaling
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.scale(dest, dest, vec);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {vec3} v Scaling vector
+ * @returns {mat4} out
+ */
+mat4.fromScaling = function(out, v) {
+ out[0] = v[0];
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = v[1];
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = v[2];
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from a given angle around a given axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotate(dest, dest, rad, axis);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @param {vec3} axis the axis to rotate around
+ * @returns {mat4} out
+ */
+mat4.fromRotation = function(out, rad, axis) {
+ var x = axis[0], y = axis[1], z = axis[2],
+ len = Math.sqrt(x * x + y * y + z * z),
+ s, c, t;
+
+ if (Math.abs(len) < glMatrix.EPSILON) { return null; }
+
+ len = 1 / len;
+ x *= len;
+ y *= len;
+ z *= len;
+
+ s = Math.sin(rad);
+ c = Math.cos(rad);
+ t = 1 - c;
+
+ // Perform rotation-specific matrix multiplication
+ out[0] = x * x * t + c;
+ out[1] = y * x * t + z * s;
+ out[2] = z * x * t - y * s;
+ out[3] = 0;
+ out[4] = x * y * t - z * s;
+ out[5] = y * y * t + c;
+ out[6] = z * y * t + x * s;
+ out[7] = 0;
+ out[8] = x * z * t + y * s;
+ out[9] = y * z * t - x * s;
+ out[10] = z * z * t + c;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from the given angle around the X axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotateX(dest, dest, rad);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.fromXRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ // Perform axis-specific matrix multiplication
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = c;
+ out[6] = s;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = -s;
+ out[10] = c;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from the given angle around the Y axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotateY(dest, dest, rad);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.fromYRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ // Perform axis-specific matrix multiplication
+ out[0] = c;
+ out[1] = 0;
+ out[2] = -s;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = s;
+ out[9] = 0;
+ out[10] = c;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from the given angle around the Z axis
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.rotateZ(dest, dest, rad);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {Number} rad the angle to rotate the matrix by
+ * @returns {mat4} out
+ */
+mat4.fromZRotation = function(out, rad) {
+ var s = Math.sin(rad),
+ c = Math.cos(rad);
+
+ // Perform axis-specific matrix multiplication
+ out[0] = c;
+ out[1] = s;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = -s;
+ out[5] = c;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+}
+
+/**
+ * Creates a matrix from a quaternion rotation and vector translation
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, vec);
+ * var quatMat = mat4.create();
+ * quat4.toMat4(quat, quatMat);
+ * mat4.multiply(dest, quatMat);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @returns {mat4} out
+ */
+mat4.fromRotationTranslation = function (out, q, v) {
+ // Quaternion math
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ xy = x * y2,
+ xz = x * z2,
+ yy = y * y2,
+ yz = y * z2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2;
+
+ out[0] = 1 - (yy + zz);
+ out[1] = xy + wz;
+ out[2] = xz - wy;
+ out[3] = 0;
+ out[4] = xy - wz;
+ out[5] = 1 - (xx + zz);
+ out[6] = yz + wx;
+ out[7] = 0;
+ out[8] = xz + wy;
+ out[9] = yz - wx;
+ out[10] = 1 - (xx + yy);
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+
+ return out;
+};
+
+/**
+ * Creates a matrix from a quaternion rotation, vector translation and vector scale
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, vec);
+ * var quatMat = mat4.create();
+ * quat4.toMat4(quat, quatMat);
+ * mat4.multiply(dest, quatMat);
+ * mat4.scale(dest, scale)
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @param {vec3} s Scaling vector
+ * @returns {mat4} out
+ */
+mat4.fromRotationTranslationScale = function (out, q, v, s) {
+ // Quaternion math
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ xy = x * y2,
+ xz = x * z2,
+ yy = y * y2,
+ yz = y * z2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2,
+ sx = s[0],
+ sy = s[1],
+ sz = s[2];
+
+ out[0] = (1 - (yy + zz)) * sx;
+ out[1] = (xy + wz) * sx;
+ out[2] = (xz - wy) * sx;
+ out[3] = 0;
+ out[4] = (xy - wz) * sy;
+ out[5] = (1 - (xx + zz)) * sy;
+ out[6] = (yz + wx) * sy;
+ out[7] = 0;
+ out[8] = (xz + wy) * sz;
+ out[9] = (yz - wx) * sz;
+ out[10] = (1 - (xx + yy)) * sz;
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+
+ return out;
+};
+
+/**
+ * Creates a matrix from a quaternion rotation, vector translation and vector scale, rotating and scaling around the given origin
+ * This is equivalent to (but much faster than):
+ *
+ * mat4.identity(dest);
+ * mat4.translate(dest, vec);
+ * mat4.translate(dest, origin);
+ * var quatMat = mat4.create();
+ * quat4.toMat4(quat, quatMat);
+ * mat4.multiply(dest, quatMat);
+ * mat4.scale(dest, scale)
+ * mat4.translate(dest, negativeOrigin);
+ *
+ * @param {mat4} out mat4 receiving operation result
+ * @param {quat4} q Rotation quaternion
+ * @param {vec3} v Translation vector
+ * @param {vec3} s Scaling vector
+ * @param {vec3} o The origin vector around which to scale and rotate
+ * @returns {mat4} out
+ */
+mat4.fromRotationTranslationScaleOrigin = function (out, q, v, s, o) {
+ // Quaternion math
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ xy = x * y2,
+ xz = x * z2,
+ yy = y * y2,
+ yz = y * z2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2,
+
+ sx = s[0],
+ sy = s[1],
+ sz = s[2],
+
+ ox = o[0],
+ oy = o[1],
+ oz = o[2];
+
+ out[0] = (1 - (yy + zz)) * sx;
+ out[1] = (xy + wz) * sx;
+ out[2] = (xz - wy) * sx;
+ out[3] = 0;
+ out[4] = (xy - wz) * sy;
+ out[5] = (1 - (xx + zz)) * sy;
+ out[6] = (yz + wx) * sy;
+ out[7] = 0;
+ out[8] = (xz + wy) * sz;
+ out[9] = (yz - wx) * sz;
+ out[10] = (1 - (xx + yy)) * sz;
+ out[11] = 0;
+ out[12] = v[0] + ox - (out[0] * ox + out[4] * oy + out[8] * oz);
+ out[13] = v[1] + oy - (out[1] * ox + out[5] * oy + out[9] * oz);
+ out[14] = v[2] + oz - (out[2] * ox + out[6] * oy + out[10] * oz);
+ out[15] = 1;
+
+ return out;
+};
+
+mat4.fromQuat = function (out, q) {
+ var x = q[0], y = q[1], z = q[2], w = q[3],
+ x2 = x + x,
+ y2 = y + y,
+ z2 = z + z,
+
+ xx = x * x2,
+ yx = y * x2,
+ yy = y * y2,
+ zx = z * x2,
+ zy = z * y2,
+ zz = z * z2,
+ wx = w * x2,
+ wy = w * y2,
+ wz = w * z2;
+
+ out[0] = 1 - yy - zz;
+ out[1] = yx + wz;
+ out[2] = zx - wy;
+ out[3] = 0;
+
+ out[4] = yx - wz;
+ out[5] = 1 - xx - zz;
+ out[6] = zy + wx;
+ out[7] = 0;
+
+ out[8] = zx + wy;
+ out[9] = zy - wx;
+ out[10] = 1 - xx - yy;
+ out[11] = 0;
+
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+
+ return out;
+};
+
+/**
+ * Generates a frustum matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {Number} left Left bound of the frustum
+ * @param {Number} right Right bound of the frustum
+ * @param {Number} bottom Bottom bound of the frustum
+ * @param {Number} top Top bound of the frustum
+ * @param {Number} near Near bound of the frustum
+ * @param {Number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.frustum = function (out, left, right, bottom, top, near, far) {
+ var rl = 1 / (right - left),
+ tb = 1 / (top - bottom),
+ nf = 1 / (near - far);
+ out[0] = (near * 2) * rl;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = (near * 2) * tb;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = (right + left) * rl;
+ out[9] = (top + bottom) * tb;
+ out[10] = (far + near) * nf;
+ out[11] = -1;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = (far * near * 2) * nf;
+ out[15] = 0;
+ return out;
+};
+
+/**
+ * Generates a perspective projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} fovy Vertical field of view in radians
+ * @param {number} aspect Aspect ratio. typically viewport width/height
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.perspective = function (out, fovy, aspect, near, far) {
+ var f = 1.0 / Math.tan(fovy / 2),
+ nf = 1 / (near - far);
+ out[0] = f / aspect;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = f;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = (far + near) * nf;
+ out[11] = -1;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = (2 * far * near) * nf;
+ out[15] = 0;
+ return out;
+};
+
+/**
+ * Generates a perspective projection matrix with the given field of view.
+ * This is primarily useful for generating projection matrices to be used
+ * with the still experiemental WebVR API.
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} fov Object containing the following values: upDegrees, downDegrees, leftDegrees, rightDegrees
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.perspectiveFromFieldOfView = function (out, fov, near, far) {
+ var upTan = Math.tan(fov.upDegrees * Math.PI/180.0),
+ downTan = Math.tan(fov.downDegrees * Math.PI/180.0),
+ leftTan = Math.tan(fov.leftDegrees * Math.PI/180.0),
+ rightTan = Math.tan(fov.rightDegrees * Math.PI/180.0),
+ xScale = 2.0 / (leftTan + rightTan),
+ yScale = 2.0 / (upTan + downTan);
+
+ out[0] = xScale;
+ out[1] = 0.0;
+ out[2] = 0.0;
+ out[3] = 0.0;
+ out[4] = 0.0;
+ out[5] = yScale;
+ out[6] = 0.0;
+ out[7] = 0.0;
+ out[8] = -((leftTan - rightTan) * xScale * 0.5);
+ out[9] = ((upTan - downTan) * yScale * 0.5);
+ out[10] = far / (near - far);
+ out[11] = -1.0;
+ out[12] = 0.0;
+ out[13] = 0.0;
+ out[14] = (far * near) / (near - far);
+ out[15] = 0.0;
+ return out;
+}
+
+/**
+ * Generates a orthogonal projection matrix with the given bounds
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {number} left Left bound of the frustum
+ * @param {number} right Right bound of the frustum
+ * @param {number} bottom Bottom bound of the frustum
+ * @param {number} top Top bound of the frustum
+ * @param {number} near Near bound of the frustum
+ * @param {number} far Far bound of the frustum
+ * @returns {mat4} out
+ */
+mat4.ortho = function (out, left, right, bottom, top, near, far) {
+ var lr = 1 / (left - right),
+ bt = 1 / (bottom - top),
+ nf = 1 / (near - far);
+ out[0] = -2 * lr;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = -2 * bt;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 2 * nf;
+ out[11] = 0;
+ out[12] = (left + right) * lr;
+ out[13] = (top + bottom) * bt;
+ out[14] = (far + near) * nf;
+ out[15] = 1;
+ return out;
+};
+
+/**
+ * Generates a look-at matrix with the given eye position, focal point, and up axis
+ *
+ * @param {mat4} out mat4 frustum matrix will be written into
+ * @param {vec3} eye Position of the viewer
+ * @param {vec3} center Point the viewer is looking at
+ * @param {vec3} up vec3 pointing up
+ * @returns {mat4} out
+ */
+mat4.lookAt = function (out, eye, center, up) {
+ var x0, x1, x2, y0, y1, y2, z0, z1, z2, len,
+ eyex = eye[0],
+ eyey = eye[1],
+ eyez = eye[2],
+ upx = up[0],
+ upy = up[1],
+ upz = up[2],
+ centerx = center[0],
+ centery = center[1],
+ centerz = center[2];
+
+ if (Math.abs(eyex - centerx) < glMatrix.EPSILON &&
+ Math.abs(eyey - centery) < glMatrix.EPSILON &&
+ Math.abs(eyez - centerz) < glMatrix.EPSILON) {
+ return mat4.identity(out);
+ }
+
+ z0 = eyex - centerx;
+ z1 = eyey - centery;
+ z2 = eyez - centerz;
+
+ len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
+ z0 *= len;
+ z1 *= len;
+ z2 *= len;
+
+ x0 = upy * z2 - upz * z1;
+ x1 = upz * z0 - upx * z2;
+ x2 = upx * z1 - upy * z0;
+ len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
+ if (!len) {
+ x0 = 0;
+ x1 = 0;
+ x2 = 0;
+ } else {
+ len = 1 / len;
+ x0 *= len;
+ x1 *= len;
+ x2 *= len;
+ }
+
+ y0 = z1 * x2 - z2 * x1;
+ y1 = z2 * x0 - z0 * x2;
+ y2 = z0 * x1 - z1 * x0;
+
+ len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
+ if (!len) {
+ y0 = 0;
+ y1 = 0;
+ y2 = 0;
+ } else {
+ len = 1 / len;
+ y0 *= len;
+ y1 *= len;
+ y2 *= len;
+ }
+
+ out[0] = x0;
+ out[1] = y0;
+ out[2] = z0;
+ out[3] = 0;
+ out[4] = x1;
+ out[5] = y1;
+ out[6] = z1;
+ out[7] = 0;
+ out[8] = x2;
+ out[9] = y2;
+ out[10] = z2;
+ out[11] = 0;
+ out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
+ out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
+ out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
+ out[15] = 1;
+
+ return out;
+};
+
+/**
+ * Returns a string representation of a mat4
+ *
+ * @param {mat4} mat matrix to represent as a string
+ * @returns {String} string representation of the matrix
+ */
+mat4.str = function (a) {
+ return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' +
+ a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' +
+ a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' +
+ a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')';
+};
+
+/**
+ * Returns Frobenius norm of a mat4
+ *
+ * @param {mat4} a the matrix to calculate Frobenius norm of
+ * @returns {Number} Frobenius norm
+ */
+mat4.frob = function (a) {
+ return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2) + Math.pow(a[9], 2) + Math.pow(a[10], 2) + Math.pow(a[11], 2) + Math.pow(a[12], 2) + Math.pow(a[13], 2) + Math.pow(a[14], 2) + Math.pow(a[15], 2) ))
+};
+
+
+module.exports = mat4;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/quat.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/quat.js
new file mode 100644
index 00000000000..2990f7ba52f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/quat.js
@@ -0,0 +1,553 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+var mat3 = require("./mat3.js");
+var vec3 = require("./vec3.js");
+var vec4 = require("./vec4.js");
+
+/**
+ * @class Quaternion
+ * @name quat
+ */
+var quat = {};
+
+/**
+ * Creates a new identity quat
+ *
+ * @returns {quat} a new quaternion
+ */
+quat.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+};
+
+/**
+ * Sets a quaternion to represent the shortest rotation from one
+ * vector to another.
+ *
+ * Both vectors are assumed to be unit length.
+ *
+ * @param {quat} out the receiving quaternion.
+ * @param {vec3} a the initial vector
+ * @param {vec3} b the destination vector
+ * @returns {quat} out
+ */
+quat.rotationTo = (function() {
+ var tmpvec3 = vec3.create();
+ var xUnitVec3 = vec3.fromValues(1,0,0);
+ var yUnitVec3 = vec3.fromValues(0,1,0);
+
+ return function(out, a, b) {
+ var dot = vec3.dot(a, b);
+ if (dot < -0.999999) {
+ vec3.cross(tmpvec3, xUnitVec3, a);
+ if (vec3.length(tmpvec3) < 0.000001)
+ vec3.cross(tmpvec3, yUnitVec3, a);
+ vec3.normalize(tmpvec3, tmpvec3);
+ quat.setAxisAngle(out, tmpvec3, Math.PI);
+ return out;
+ } else if (dot > 0.999999) {
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+ } else {
+ vec3.cross(tmpvec3, a, b);
+ out[0] = tmpvec3[0];
+ out[1] = tmpvec3[1];
+ out[2] = tmpvec3[2];
+ out[3] = 1 + dot;
+ return quat.normalize(out, out);
+ }
+ };
+})();
+
+/**
+ * Sets the specified quaternion with values corresponding to the given
+ * axes. Each axis is a vec3 and is expected to be unit length and
+ * perpendicular to all other specified axes.
+ *
+ * @param {vec3} view the vector representing the viewing direction
+ * @param {vec3} right the vector representing the local "right" direction
+ * @param {vec3} up the vector representing the local "up" direction
+ * @returns {quat} out
+ */
+quat.setAxes = (function() {
+ var matr = mat3.create();
+
+ return function(out, view, right, up) {
+ matr[0] = right[0];
+ matr[3] = right[1];
+ matr[6] = right[2];
+
+ matr[1] = up[0];
+ matr[4] = up[1];
+ matr[7] = up[2];
+
+ matr[2] = -view[0];
+ matr[5] = -view[1];
+ matr[8] = -view[2];
+
+ return quat.normalize(out, quat.fromMat3(out, matr));
+ };
+})();
+
+/**
+ * Creates a new quat initialized with values from an existing quaternion
+ *
+ * @param {quat} a quaternion to clone
+ * @returns {quat} a new quaternion
+ * @function
+ */
+quat.clone = vec4.clone;
+
+/**
+ * Creates a new quat initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} a new quaternion
+ * @function
+ */
+quat.fromValues = vec4.fromValues;
+
+/**
+ * Copy the values from one quat to another
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the source quaternion
+ * @returns {quat} out
+ * @function
+ */
+quat.copy = vec4.copy;
+
+/**
+ * Set the components of a quat to the given values
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {quat} out
+ * @function
+ */
+quat.set = vec4.set;
+
+/**
+ * Set a quat to the identity quaternion
+ *
+ * @param {quat} out the receiving quaternion
+ * @returns {quat} out
+ */
+quat.identity = function(out) {
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 1;
+ return out;
+};
+
+/**
+ * Sets a quat from the given angle and rotation axis,
+ * then returns it.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {vec3} axis the axis around which to rotate
+ * @param {Number} rad the angle in radians
+ * @returns {quat} out
+ **/
+quat.setAxisAngle = function(out, axis, rad) {
+ rad = rad * 0.5;
+ var s = Math.sin(rad);
+ out[0] = s * axis[0];
+ out[1] = s * axis[1];
+ out[2] = s * axis[2];
+ out[3] = Math.cos(rad);
+ return out;
+};
+
+/**
+ * Adds two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ * @function
+ */
+quat.add = vec4.add;
+
+/**
+ * Multiplies two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {quat} out
+ */
+quat.multiply = function(out, a, b) {
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+ out[0] = ax * bw + aw * bx + ay * bz - az * by;
+ out[1] = ay * bw + aw * by + az * bx - ax * bz;
+ out[2] = az * bw + aw * bz + ax * by - ay * bx;
+ out[3] = aw * bw - ax * bx - ay * by - az * bz;
+ return out;
+};
+
+/**
+ * Alias for {@link quat.multiply}
+ * @function
+ */
+quat.mul = quat.multiply;
+
+/**
+ * Scales a quat by a scalar number
+ *
+ * @param {quat} out the receiving vector
+ * @param {quat} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {quat} out
+ * @function
+ */
+quat.scale = vec4.scale;
+
+/**
+ * Rotates a quaternion by the given angle about the X axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateX = function (out, a, rad) {
+ rad *= 0.5;
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bx = Math.sin(rad), bw = Math.cos(rad);
+
+ out[0] = ax * bw + aw * bx;
+ out[1] = ay * bw + az * bx;
+ out[2] = az * bw - ay * bx;
+ out[3] = aw * bw - ax * bx;
+ return out;
+};
+
+/**
+ * Rotates a quaternion by the given angle about the Y axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateY = function (out, a, rad) {
+ rad *= 0.5;
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ by = Math.sin(rad), bw = Math.cos(rad);
+
+ out[0] = ax * bw - az * by;
+ out[1] = ay * bw + aw * by;
+ out[2] = az * bw + ax * by;
+ out[3] = aw * bw - ay * by;
+ return out;
+};
+
+/**
+ * Rotates a quaternion by the given angle about the Z axis
+ *
+ * @param {quat} out quat receiving operation result
+ * @param {quat} a quat to rotate
+ * @param {number} rad angle (in radians) to rotate
+ * @returns {quat} out
+ */
+quat.rotateZ = function (out, a, rad) {
+ rad *= 0.5;
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bz = Math.sin(rad), bw = Math.cos(rad);
+
+ out[0] = ax * bw + ay * bz;
+ out[1] = ay * bw - ax * bz;
+ out[2] = az * bw + aw * bz;
+ out[3] = aw * bw - az * bz;
+ return out;
+};
+
+/**
+ * Calculates the W component of a quat from the X, Y, and Z components.
+ * Assumes that quaternion is 1 unit in length.
+ * Any existing W component will be ignored.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate W component of
+ * @returns {quat} out
+ */
+quat.calculateW = function (out, a) {
+ var x = a[0], y = a[1], z = a[2];
+
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ out[3] = Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z));
+ return out;
+};
+
+/**
+ * Calculates the dot product of two quat's
+ *
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @returns {Number} dot product of a and b
+ * @function
+ */
+quat.dot = vec4.dot;
+
+/**
+ * Performs a linear interpolation between two quat's
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ * @function
+ */
+quat.lerp = vec4.lerp;
+
+/**
+ * Performs a spherical linear interpolation between two quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {quat} out
+ */
+quat.slerp = function (out, a, b, t) {
+ // benchmarks:
+ // http://jsperf.com/quaternion-slerp-implementations
+
+ var ax = a[0], ay = a[1], az = a[2], aw = a[3],
+ bx = b[0], by = b[1], bz = b[2], bw = b[3];
+
+ var omega, cosom, sinom, scale0, scale1;
+
+ // calc cosine
+ cosom = ax * bx + ay * by + az * bz + aw * bw;
+ // adjust signs (if necessary)
+ if ( cosom < 0.0 ) {
+ cosom = -cosom;
+ bx = - bx;
+ by = - by;
+ bz = - bz;
+ bw = - bw;
+ }
+ // calculate coefficients
+ if ( (1.0 - cosom) > 0.000001 ) {
+ // standard case (slerp)
+ omega = Math.acos(cosom);
+ sinom = Math.sin(omega);
+ scale0 = Math.sin((1.0 - t) * omega) / sinom;
+ scale1 = Math.sin(t * omega) / sinom;
+ } else {
+ // "from" and "to" quaternions are very close
+ // ... so we can do a linear interpolation
+ scale0 = 1.0 - t;
+ scale1 = t;
+ }
+ // calculate final values
+ out[0] = scale0 * ax + scale1 * bx;
+ out[1] = scale0 * ay + scale1 * by;
+ out[2] = scale0 * az + scale1 * bz;
+ out[3] = scale0 * aw + scale1 * bw;
+
+ return out;
+};
+
+/**
+ * Performs a spherical linear interpolation with two control points
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a the first operand
+ * @param {quat} b the second operand
+ * @param {quat} c the third operand
+ * @param {quat} d the fourth operand
+ * @param {Number} t interpolation amount
+ * @returns {quat} out
+ */
+quat.sqlerp = (function () {
+ var temp1 = quat.create();
+ var temp2 = quat.create();
+
+ return function (out, a, b, c, d, t) {
+ quat.slerp(temp1, a, d, t);
+ quat.slerp(temp2, b, c, t);
+ quat.slerp(out, temp1, temp2, 2 * t * (1 - t));
+
+ return out;
+ };
+}());
+
+/**
+ * Calculates the inverse of a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate inverse of
+ * @returns {quat} out
+ */
+quat.invert = function(out, a) {
+ var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
+ dot = a0*a0 + a1*a1 + a2*a2 + a3*a3,
+ invDot = dot ? 1.0/dot : 0;
+
+ // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0
+
+ out[0] = -a0*invDot;
+ out[1] = -a1*invDot;
+ out[2] = -a2*invDot;
+ out[3] = a3*invDot;
+ return out;
+};
+
+/**
+ * Calculates the conjugate of a quat
+ * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quat to calculate conjugate of
+ * @returns {quat} out
+ */
+quat.conjugate = function (out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ out[3] = a[3];
+ return out;
+};
+
+/**
+ * Calculates the length of a quat
+ *
+ * @param {quat} a vector to calculate length of
+ * @returns {Number} length of a
+ * @function
+ */
+quat.length = vec4.length;
+
+/**
+ * Alias for {@link quat.length}
+ * @function
+ */
+quat.len = quat.length;
+
+/**
+ * Calculates the squared length of a quat
+ *
+ * @param {quat} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ * @function
+ */
+quat.squaredLength = vec4.squaredLength;
+
+/**
+ * Alias for {@link quat.squaredLength}
+ * @function
+ */
+quat.sqrLen = quat.squaredLength;
+
+/**
+ * Normalize a quat
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {quat} a quaternion to normalize
+ * @returns {quat} out
+ * @function
+ */
+quat.normalize = vec4.normalize;
+
+/**
+ * Creates a quaternion from the given 3x3 rotation matrix.
+ *
+ * NOTE: The resultant quaternion is not normalized, so you should be sure
+ * to renormalize the quaternion yourself where necessary.
+ *
+ * @param {quat} out the receiving quaternion
+ * @param {mat3} m rotation matrix
+ * @returns {quat} out
+ * @function
+ */
+quat.fromMat3 = function(out, m) {
+ // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes
+ // article "Quaternion Calculus and Fast Animation".
+ var fTrace = m[0] + m[4] + m[8];
+ var fRoot;
+
+ if ( fTrace > 0.0 ) {
+ // |w| > 1/2, may as well choose w > 1/2
+ fRoot = Math.sqrt(fTrace + 1.0); // 2w
+ out[3] = 0.5 * fRoot;
+ fRoot = 0.5/fRoot; // 1/(4w)
+ out[0] = (m[5]-m[7])*fRoot;
+ out[1] = (m[6]-m[2])*fRoot;
+ out[2] = (m[1]-m[3])*fRoot;
+ } else {
+ // |w| <= 1/2
+ var i = 0;
+ if ( m[4] > m[0] )
+ i = 1;
+ if ( m[8] > m[i*3+i] )
+ i = 2;
+ var j = (i+1)%3;
+ var k = (i+2)%3;
+
+ fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0);
+ out[i] = 0.5 * fRoot;
+ fRoot = 0.5 / fRoot;
+ out[3] = (m[j*3+k] - m[k*3+j]) * fRoot;
+ out[j] = (m[j*3+i] + m[i*3+j]) * fRoot;
+ out[k] = (m[k*3+i] + m[i*3+k]) * fRoot;
+ }
+
+ return out;
+};
+
+/**
+ * Returns a string representation of a quatenion
+ *
+ * @param {quat} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+quat.str = function (a) {
+ return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+module.exports = quat;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec2.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec2.js
new file mode 100644
index 00000000000..9e790563af5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec2.js
@@ -0,0 +1,523 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 2 Dimensional Vector
+ * @name vec2
+ */
+var vec2 = {};
+
+/**
+ * Creates a new, empty vec2
+ *
+ * @returns {vec2} a new 2D vector
+ */
+vec2.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(2);
+ out[0] = 0;
+ out[1] = 0;
+ return out;
+};
+
+/**
+ * Creates a new vec2 initialized with values from an existing vector
+ *
+ * @param {vec2} a vector to clone
+ * @returns {vec2} a new 2D vector
+ */
+vec2.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(2);
+ out[0] = a[0];
+ out[1] = a[1];
+ return out;
+};
+
+/**
+ * Creates a new vec2 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} a new 2D vector
+ */
+vec2.fromValues = function(x, y) {
+ var out = new glMatrix.ARRAY_TYPE(2);
+ out[0] = x;
+ out[1] = y;
+ return out;
+};
+
+/**
+ * Copy the values from one vec2 to another
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the source vector
+ * @returns {vec2} out
+ */
+vec2.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ return out;
+};
+
+/**
+ * Set the components of a vec2 to the given values
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @returns {vec2} out
+ */
+vec2.set = function(out, x, y) {
+ out[0] = x;
+ out[1] = y;
+ return out;
+};
+
+/**
+ * Adds two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.add = function(out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.subtract = function(out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ return out;
+};
+
+/**
+ * Alias for {@link vec2.subtract}
+ * @function
+ */
+vec2.sub = vec2.subtract;
+
+/**
+ * Multiplies two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.multiply = function(out, a, b) {
+ out[0] = a[0] * b[0];
+ out[1] = a[1] * b[1];
+ return out;
+};
+
+/**
+ * Alias for {@link vec2.multiply}
+ * @function
+ */
+vec2.mul = vec2.multiply;
+
+/**
+ * Divides two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.divide = function(out, a, b) {
+ out[0] = a[0] / b[0];
+ out[1] = a[1] / b[1];
+ return out;
+};
+
+/**
+ * Alias for {@link vec2.divide}
+ * @function
+ */
+vec2.div = vec2.divide;
+
+/**
+ * Returns the minimum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.min = function(out, a, b) {
+ out[0] = Math.min(a[0], b[0]);
+ out[1] = Math.min(a[1], b[1]);
+ return out;
+};
+
+/**
+ * Returns the maximum of two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec2} out
+ */
+vec2.max = function(out, a, b) {
+ out[0] = Math.max(a[0], b[0]);
+ out[1] = Math.max(a[1], b[1]);
+ return out;
+};
+
+/**
+ * Scales a vec2 by a scalar number
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec2} out
+ */
+vec2.scale = function(out, a, b) {
+ out[0] = a[0] * b;
+ out[1] = a[1] * b;
+ return out;
+};
+
+/**
+ * Adds two vec2's after scaling the second operand by a scalar value
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec2} out
+ */
+vec2.scaleAndAdd = function(out, a, b, scale) {
+ out[0] = a[0] + (b[0] * scale);
+ out[1] = a[1] + (b[1] * scale);
+ return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec2.distance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1];
+ return Math.sqrt(x*x + y*y);
+};
+
+/**
+ * Alias for {@link vec2.distance}
+ * @function
+ */
+vec2.dist = vec2.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec2.squaredDistance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1];
+ return x*x + y*y;
+};
+
+/**
+ * Alias for {@link vec2.squaredDistance}
+ * @function
+ */
+vec2.sqrDist = vec2.squaredDistance;
+
+/**
+ * Calculates the length of a vec2
+ *
+ * @param {vec2} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec2.length = function (a) {
+ var x = a[0],
+ y = a[1];
+ return Math.sqrt(x*x + y*y);
+};
+
+/**
+ * Alias for {@link vec2.length}
+ * @function
+ */
+vec2.len = vec2.length;
+
+/**
+ * Calculates the squared length of a vec2
+ *
+ * @param {vec2} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec2.squaredLength = function (a) {
+ var x = a[0],
+ y = a[1];
+ return x*x + y*y;
+};
+
+/**
+ * Alias for {@link vec2.squaredLength}
+ * @function
+ */
+vec2.sqrLen = vec2.squaredLength;
+
+/**
+ * Negates the components of a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to negate
+ * @returns {vec2} out
+ */
+vec2.negate = function(out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ return out;
+};
+
+/**
+ * Returns the inverse of the components of a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to invert
+ * @returns {vec2} out
+ */
+vec2.inverse = function(out, a) {
+ out[0] = 1.0 / a[0];
+ out[1] = 1.0 / a[1];
+ return out;
+};
+
+/**
+ * Normalize a vec2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a vector to normalize
+ * @returns {vec2} out
+ */
+vec2.normalize = function(out, a) {
+ var x = a[0],
+ y = a[1];
+ var len = x*x + y*y;
+ if (len > 0) {
+ //TODO: evaluate use of glm_invsqrt here?
+ len = 1 / Math.sqrt(len);
+ out[0] = a[0] * len;
+ out[1] = a[1] * len;
+ }
+ return out;
+};
+
+/**
+ * Calculates the dot product of two vec2's
+ *
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec2.dot = function (a, b) {
+ return a[0] * b[0] + a[1] * b[1];
+};
+
+/**
+ * Computes the cross product of two vec2's
+ * Note that the cross product must by definition produce a 3D vector
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @returns {vec3} out
+ */
+vec2.cross = function(out, a, b) {
+ var z = a[0] * b[1] - a[1] * b[0];
+ out[0] = out[1] = 0;
+ out[2] = z;
+ return out;
+};
+
+/**
+ * Performs a linear interpolation between two vec2's
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the first operand
+ * @param {vec2} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec2} out
+ */
+vec2.lerp = function (out, a, b, t) {
+ var ax = a[0],
+ ay = a[1];
+ out[0] = ax + t * (b[0] - ax);
+ out[1] = ay + t * (b[1] - ay);
+ return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec2} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec2} out
+ */
+vec2.random = function (out, scale) {
+ scale = scale || 1.0;
+ var r = glMatrix.RANDOM() * 2.0 * Math.PI;
+ out[0] = Math.cos(r) * scale;
+ out[1] = Math.sin(r) * scale;
+ return out;
+};
+
+/**
+ * Transforms the vec2 with a mat2
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat2 = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[2] * y;
+ out[1] = m[1] * x + m[3] * y;
+ return out;
+};
+
+/**
+ * Transforms the vec2 with a mat2d
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat2d} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat2d = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[2] * y + m[4];
+ out[1] = m[1] * x + m[3] * y + m[5];
+ return out;
+};
+
+/**
+ * Transforms the vec2 with a mat3
+ * 3rd vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat3} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat3 = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[3] * y + m[6];
+ out[1] = m[1] * x + m[4] * y + m[7];
+ return out;
+};
+
+/**
+ * Transforms the vec2 with a mat4
+ * 3rd vector component is implicitly '0'
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec2} out the receiving vector
+ * @param {vec2} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec2} out
+ */
+vec2.transformMat4 = function(out, a, m) {
+ var x = a[0],
+ y = a[1];
+ out[0] = m[0] * x + m[4] * y + m[12];
+ out[1] = m[1] * x + m[5] * y + m[13];
+ return out;
+};
+
+/**
+ * Perform some operation over an array of vec2s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec2.forEach = (function() {
+ var vec = vec2.create();
+
+ return function(a, stride, offset, count, fn, arg) {
+ var i, l;
+ if(!stride) {
+ stride = 2;
+ }
+
+ if(!offset) {
+ offset = 0;
+ }
+
+ if(count) {
+ l = Math.min((count * stride) + offset, a.length);
+ } else {
+ l = a.length;
+ }
+
+ for(i = offset; i < l; i += stride) {
+ vec[0] = a[i]; vec[1] = a[i+1];
+ fn(vec, vec, arg);
+ a[i] = vec[0]; a[i+1] = vec[1];
+ }
+
+ return a;
+ };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec2} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec2.str = function (a) {
+ return 'vec2(' + a[0] + ', ' + a[1] + ')';
+};
+
+module.exports = vec2;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec3.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec3.js
new file mode 100644
index 00000000000..c0676feefcd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec3.js
@@ -0,0 +1,709 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 3 Dimensional Vector
+ * @name vec3
+ */
+var vec3 = {};
+
+/**
+ * Creates a new, empty vec3
+ *
+ * @returns {vec3} a new 3D vector
+ */
+vec3.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(3);
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ return out;
+};
+
+/**
+ * Creates a new vec3 initialized with values from an existing vector
+ *
+ * @param {vec3} a vector to clone
+ * @returns {vec3} a new 3D vector
+ */
+vec3.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(3);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ return out;
+};
+
+/**
+ * Creates a new vec3 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} a new 3D vector
+ */
+vec3.fromValues = function(x, y, z) {
+ var out = new glMatrix.ARRAY_TYPE(3);
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ return out;
+};
+
+/**
+ * Copy the values from one vec3 to another
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the source vector
+ * @returns {vec3} out
+ */
+vec3.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ return out;
+};
+
+/**
+ * Set the components of a vec3 to the given values
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @returns {vec3} out
+ */
+vec3.set = function(out, x, y, z) {
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ return out;
+};
+
+/**
+ * Adds two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.add = function(out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ out[2] = a[2] + b[2];
+ return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.subtract = function(out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ out[2] = a[2] - b[2];
+ return out;
+};
+
+/**
+ * Alias for {@link vec3.subtract}
+ * @function
+ */
+vec3.sub = vec3.subtract;
+
+/**
+ * Multiplies two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.multiply = function(out, a, b) {
+ out[0] = a[0] * b[0];
+ out[1] = a[1] * b[1];
+ out[2] = a[2] * b[2];
+ return out;
+};
+
+/**
+ * Alias for {@link vec3.multiply}
+ * @function
+ */
+vec3.mul = vec3.multiply;
+
+/**
+ * Divides two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.divide = function(out, a, b) {
+ out[0] = a[0] / b[0];
+ out[1] = a[1] / b[1];
+ out[2] = a[2] / b[2];
+ return out;
+};
+
+/**
+ * Alias for {@link vec3.divide}
+ * @function
+ */
+vec3.div = vec3.divide;
+
+/**
+ * Returns the minimum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.min = function(out, a, b) {
+ out[0] = Math.min(a[0], b[0]);
+ out[1] = Math.min(a[1], b[1]);
+ out[2] = Math.min(a[2], b[2]);
+ return out;
+};
+
+/**
+ * Returns the maximum of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.max = function(out, a, b) {
+ out[0] = Math.max(a[0], b[0]);
+ out[1] = Math.max(a[1], b[1]);
+ out[2] = Math.max(a[2], b[2]);
+ return out;
+};
+
+/**
+ * Scales a vec3 by a scalar number
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec3} out
+ */
+vec3.scale = function(out, a, b) {
+ out[0] = a[0] * b;
+ out[1] = a[1] * b;
+ out[2] = a[2] * b;
+ return out;
+};
+
+/**
+ * Adds two vec3's after scaling the second operand by a scalar value
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec3} out
+ */
+vec3.scaleAndAdd = function(out, a, b, scale) {
+ out[0] = a[0] + (b[0] * scale);
+ out[1] = a[1] + (b[1] * scale);
+ out[2] = a[2] + (b[2] * scale);
+ return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec3.distance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2];
+ return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Alias for {@link vec3.distance}
+ * @function
+ */
+vec3.dist = vec3.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec3.squaredDistance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2];
+ return x*x + y*y + z*z;
+};
+
+/**
+ * Alias for {@link vec3.squaredDistance}
+ * @function
+ */
+vec3.sqrDist = vec3.squaredDistance;
+
+/**
+ * Calculates the length of a vec3
+ *
+ * @param {vec3} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec3.length = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2];
+ return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Alias for {@link vec3.length}
+ * @function
+ */
+vec3.len = vec3.length;
+
+/**
+ * Calculates the squared length of a vec3
+ *
+ * @param {vec3} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec3.squaredLength = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2];
+ return x*x + y*y + z*z;
+};
+
+/**
+ * Alias for {@link vec3.squaredLength}
+ * @function
+ */
+vec3.sqrLen = vec3.squaredLength;
+
+/**
+ * Negates the components of a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to negate
+ * @returns {vec3} out
+ */
+vec3.negate = function(out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ return out;
+};
+
+/**
+ * Returns the inverse of the components of a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to invert
+ * @returns {vec3} out
+ */
+vec3.inverse = function(out, a) {
+ out[0] = 1.0 / a[0];
+ out[1] = 1.0 / a[1];
+ out[2] = 1.0 / a[2];
+ return out;
+};
+
+/**
+ * Normalize a vec3
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a vector to normalize
+ * @returns {vec3} out
+ */
+vec3.normalize = function(out, a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2];
+ var len = x*x + y*y + z*z;
+ if (len > 0) {
+ //TODO: evaluate use of glm_invsqrt here?
+ len = 1 / Math.sqrt(len);
+ out[0] = a[0] * len;
+ out[1] = a[1] * len;
+ out[2] = a[2] * len;
+ }
+ return out;
+};
+
+/**
+ * Calculates the dot product of two vec3's
+ *
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec3.dot = function (a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+};
+
+/**
+ * Computes the cross product of two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @returns {vec3} out
+ */
+vec3.cross = function(out, a, b) {
+ var ax = a[0], ay = a[1], az = a[2],
+ bx = b[0], by = b[1], bz = b[2];
+
+ out[0] = ay * bz - az * by;
+ out[1] = az * bx - ax * bz;
+ out[2] = ax * by - ay * bx;
+ return out;
+};
+
+/**
+ * Performs a linear interpolation between two vec3's
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+vec3.lerp = function (out, a, b, t) {
+ var ax = a[0],
+ ay = a[1],
+ az = a[2];
+ out[0] = ax + t * (b[0] - ax);
+ out[1] = ay + t * (b[1] - ay);
+ out[2] = az + t * (b[2] - az);
+ return out;
+};
+
+/**
+ * Performs a hermite interpolation with two control points
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {vec3} c the third operand
+ * @param {vec3} d the fourth operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+vec3.hermite = function (out, a, b, c, d, t) {
+ var factorTimes2 = t * t,
+ factor1 = factorTimes2 * (2 * t - 3) + 1,
+ factor2 = factorTimes2 * (t - 2) + t,
+ factor3 = factorTimes2 * (t - 1),
+ factor4 = factorTimes2 * (3 - 2 * t);
+
+ out[0] = a[0] * factor1 + b[0] * factor2 + c[0] * factor3 + d[0] * factor4;
+ out[1] = a[1] * factor1 + b[1] * factor2 + c[1] * factor3 + d[1] * factor4;
+ out[2] = a[2] * factor1 + b[2] * factor2 + c[2] * factor3 + d[2] * factor4;
+
+ return out;
+};
+
+/**
+ * Performs a bezier interpolation with two control points
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the first operand
+ * @param {vec3} b the second operand
+ * @param {vec3} c the third operand
+ * @param {vec3} d the fourth operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec3} out
+ */
+vec3.bezier = function (out, a, b, c, d, t) {
+ var inverseFactor = 1 - t,
+ inverseFactorTimesTwo = inverseFactor * inverseFactor,
+ factorTimes2 = t * t,
+ factor1 = inverseFactorTimesTwo * inverseFactor,
+ factor2 = 3 * t * inverseFactorTimesTwo,
+ factor3 = 3 * factorTimes2 * inverseFactor,
+ factor4 = factorTimes2 * t;
+
+ out[0] = a[0] * factor1 + b[0] * factor2 + c[0] * factor3 + d[0] * factor4;
+ out[1] = a[1] * factor1 + b[1] * factor2 + c[1] * factor3 + d[1] * factor4;
+ out[2] = a[2] * factor1 + b[2] * factor2 + c[2] * factor3 + d[2] * factor4;
+
+ return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec3} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec3} out
+ */
+vec3.random = function (out, scale) {
+ scale = scale || 1.0;
+
+ var r = glMatrix.RANDOM() * 2.0 * Math.PI;
+ var z = (glMatrix.RANDOM() * 2.0) - 1.0;
+ var zScale = Math.sqrt(1.0-z*z) * scale;
+
+ out[0] = Math.cos(r) * zScale;
+ out[1] = Math.sin(r) * zScale;
+ out[2] = z * scale;
+ return out;
+};
+
+/**
+ * Transforms the vec3 with a mat4.
+ * 4th vector component is implicitly '1'
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec3} out
+ */
+vec3.transformMat4 = function(out, a, m) {
+ var x = a[0], y = a[1], z = a[2],
+ w = m[3] * x + m[7] * y + m[11] * z + m[15];
+ w = w || 1.0;
+ out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
+ out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
+ out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
+ return out;
+};
+
+/**
+ * Transforms the vec3 with a mat3.
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {mat4} m the 3x3 matrix to transform with
+ * @returns {vec3} out
+ */
+vec3.transformMat3 = function(out, a, m) {
+ var x = a[0], y = a[1], z = a[2];
+ out[0] = x * m[0] + y * m[3] + z * m[6];
+ out[1] = x * m[1] + y * m[4] + z * m[7];
+ out[2] = x * m[2] + y * m[5] + z * m[8];
+ return out;
+};
+
+/**
+ * Transforms the vec3 with a quat
+ *
+ * @param {vec3} out the receiving vector
+ * @param {vec3} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec3} out
+ */
+vec3.transformQuat = function(out, a, q) {
+ // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations
+
+ var x = a[0], y = a[1], z = a[2],
+ qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+ // calculate quat * vec
+ ix = qw * x + qy * z - qz * y,
+ iy = qw * y + qz * x - qx * z,
+ iz = qw * z + qx * y - qy * x,
+ iw = -qx * x - qy * y - qz * z;
+
+ // calculate result * inverse quat
+ out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+ return out;
+};
+
+/**
+ * Rotate a 3D vector around the x-axis
+ * @param {vec3} out The receiving vec3
+ * @param {vec3} a The vec3 point to rotate
+ * @param {vec3} b The origin of the rotation
+ * @param {Number} c The angle of rotation
+ * @returns {vec3} out
+ */
+vec3.rotateX = function(out, a, b, c){
+ var p = [], r=[];
+ //Translate point to the origin
+ p[0] = a[0] - b[0];
+ p[1] = a[1] - b[1];
+ p[2] = a[2] - b[2];
+
+ //perform rotation
+ r[0] = p[0];
+ r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c);
+ r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c);
+
+ //translate to correct position
+ out[0] = r[0] + b[0];
+ out[1] = r[1] + b[1];
+ out[2] = r[2] + b[2];
+
+ return out;
+};
+
+/**
+ * Rotate a 3D vector around the y-axis
+ * @param {vec3} out The receiving vec3
+ * @param {vec3} a The vec3 point to rotate
+ * @param {vec3} b The origin of the rotation
+ * @param {Number} c The angle of rotation
+ * @returns {vec3} out
+ */
+vec3.rotateY = function(out, a, b, c){
+ var p = [], r=[];
+ //Translate point to the origin
+ p[0] = a[0] - b[0];
+ p[1] = a[1] - b[1];
+ p[2] = a[2] - b[2];
+
+ //perform rotation
+ r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c);
+ r[1] = p[1];
+ r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c);
+
+ //translate to correct position
+ out[0] = r[0] + b[0];
+ out[1] = r[1] + b[1];
+ out[2] = r[2] + b[2];
+
+ return out;
+};
+
+/**
+ * Rotate a 3D vector around the z-axis
+ * @param {vec3} out The receiving vec3
+ * @param {vec3} a The vec3 point to rotate
+ * @param {vec3} b The origin of the rotation
+ * @param {Number} c The angle of rotation
+ * @returns {vec3} out
+ */
+vec3.rotateZ = function(out, a, b, c){
+ var p = [], r=[];
+ //Translate point to the origin
+ p[0] = a[0] - b[0];
+ p[1] = a[1] - b[1];
+ p[2] = a[2] - b[2];
+
+ //perform rotation
+ r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c);
+ r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c);
+ r[2] = p[2];
+
+ //translate to correct position
+ out[0] = r[0] + b[0];
+ out[1] = r[1] + b[1];
+ out[2] = r[2] + b[2];
+
+ return out;
+};
+
+/**
+ * Perform some operation over an array of vec3s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec3.forEach = (function() {
+ var vec = vec3.create();
+
+ return function(a, stride, offset, count, fn, arg) {
+ var i, l;
+ if(!stride) {
+ stride = 3;
+ }
+
+ if(!offset) {
+ offset = 0;
+ }
+
+ if(count) {
+ l = Math.min((count * stride) + offset, a.length);
+ } else {
+ l = a.length;
+ }
+
+ for(i = offset; i < l; i += stride) {
+ vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2];
+ fn(vec, vec, arg);
+ a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2];
+ }
+
+ return a;
+ };
+})();
+
+/**
+ * Get the angle between two 3D vectors
+ * @param {vec3} a The first operand
+ * @param {vec3} b The second operand
+ * @returns {Number} The angle in radians
+ */
+vec3.angle = function(a, b) {
+
+ var tempA = vec3.fromValues(a[0], a[1], a[2]);
+ var tempB = vec3.fromValues(b[0], b[1], b[2]);
+
+ vec3.normalize(tempA, tempA);
+ vec3.normalize(tempB, tempB);
+
+ var cosine = vec3.dot(tempA, tempB);
+
+ if(cosine > 1.0){
+ return 0;
+ } else {
+ return Math.acos(cosine);
+ }
+};
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec3} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec3.str = function (a) {
+ return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')';
+};
+
+module.exports = vec3;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec4.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec4.js
new file mode 100644
index 00000000000..f18d17440ae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/src/gl-matrix/vec4.js
@@ -0,0 +1,537 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var glMatrix = require("./common.js");
+
+/**
+ * @class 4 Dimensional Vector
+ * @name vec4
+ */
+var vec4 = {};
+
+/**
+ * Creates a new, empty vec4
+ *
+ * @returns {vec4} a new 4D vector
+ */
+vec4.create = function() {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = 0;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ return out;
+};
+
+/**
+ * Creates a new vec4 initialized with values from an existing vector
+ *
+ * @param {vec4} a vector to clone
+ * @returns {vec4} a new 4D vector
+ */
+vec4.clone = function(a) {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+};
+
+/**
+ * Creates a new vec4 initialized with the given values
+ *
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} a new 4D vector
+ */
+vec4.fromValues = function(x, y, z, w) {
+ var out = new glMatrix.ARRAY_TYPE(4);
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ out[3] = w;
+ return out;
+};
+
+/**
+ * Copy the values from one vec4 to another
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the source vector
+ * @returns {vec4} out
+ */
+vec4.copy = function(out, a) {
+ out[0] = a[0];
+ out[1] = a[1];
+ out[2] = a[2];
+ out[3] = a[3];
+ return out;
+};
+
+/**
+ * Set the components of a vec4 to the given values
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} x X component
+ * @param {Number} y Y component
+ * @param {Number} z Z component
+ * @param {Number} w W component
+ * @returns {vec4} out
+ */
+vec4.set = function(out, x, y, z, w) {
+ out[0] = x;
+ out[1] = y;
+ out[2] = z;
+ out[3] = w;
+ return out;
+};
+
+/**
+ * Adds two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.add = function(out, a, b) {
+ out[0] = a[0] + b[0];
+ out[1] = a[1] + b[1];
+ out[2] = a[2] + b[2];
+ out[3] = a[3] + b[3];
+ return out;
+};
+
+/**
+ * Subtracts vector b from vector a
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.subtract = function(out, a, b) {
+ out[0] = a[0] - b[0];
+ out[1] = a[1] - b[1];
+ out[2] = a[2] - b[2];
+ out[3] = a[3] - b[3];
+ return out;
+};
+
+/**
+ * Alias for {@link vec4.subtract}
+ * @function
+ */
+vec4.sub = vec4.subtract;
+
+/**
+ * Multiplies two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.multiply = function(out, a, b) {
+ out[0] = a[0] * b[0];
+ out[1] = a[1] * b[1];
+ out[2] = a[2] * b[2];
+ out[3] = a[3] * b[3];
+ return out;
+};
+
+/**
+ * Alias for {@link vec4.multiply}
+ * @function
+ */
+vec4.mul = vec4.multiply;
+
+/**
+ * Divides two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.divide = function(out, a, b) {
+ out[0] = a[0] / b[0];
+ out[1] = a[1] / b[1];
+ out[2] = a[2] / b[2];
+ out[3] = a[3] / b[3];
+ return out;
+};
+
+/**
+ * Alias for {@link vec4.divide}
+ * @function
+ */
+vec4.div = vec4.divide;
+
+/**
+ * Returns the minimum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.min = function(out, a, b) {
+ out[0] = Math.min(a[0], b[0]);
+ out[1] = Math.min(a[1], b[1]);
+ out[2] = Math.min(a[2], b[2]);
+ out[3] = Math.min(a[3], b[3]);
+ return out;
+};
+
+/**
+ * Returns the maximum of two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {vec4} out
+ */
+vec4.max = function(out, a, b) {
+ out[0] = Math.max(a[0], b[0]);
+ out[1] = Math.max(a[1], b[1]);
+ out[2] = Math.max(a[2], b[2]);
+ out[3] = Math.max(a[3], b[3]);
+ return out;
+};
+
+/**
+ * Scales a vec4 by a scalar number
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to scale
+ * @param {Number} b amount to scale the vector by
+ * @returns {vec4} out
+ */
+vec4.scale = function(out, a, b) {
+ out[0] = a[0] * b;
+ out[1] = a[1] * b;
+ out[2] = a[2] * b;
+ out[3] = a[3] * b;
+ return out;
+};
+
+/**
+ * Adds two vec4's after scaling the second operand by a scalar value
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} scale the amount to scale b by before adding
+ * @returns {vec4} out
+ */
+vec4.scaleAndAdd = function(out, a, b, scale) {
+ out[0] = a[0] + (b[0] * scale);
+ out[1] = a[1] + (b[1] * scale);
+ out[2] = a[2] + (b[2] * scale);
+ out[3] = a[3] + (b[3] * scale);
+ return out;
+};
+
+/**
+ * Calculates the euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} distance between a and b
+ */
+vec4.distance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2],
+ w = b[3] - a[3];
+ return Math.sqrt(x*x + y*y + z*z + w*w);
+};
+
+/**
+ * Alias for {@link vec4.distance}
+ * @function
+ */
+vec4.dist = vec4.distance;
+
+/**
+ * Calculates the squared euclidian distance between two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} squared distance between a and b
+ */
+vec4.squaredDistance = function(a, b) {
+ var x = b[0] - a[0],
+ y = b[1] - a[1],
+ z = b[2] - a[2],
+ w = b[3] - a[3];
+ return x*x + y*y + z*z + w*w;
+};
+
+/**
+ * Alias for {@link vec4.squaredDistance}
+ * @function
+ */
+vec4.sqrDist = vec4.squaredDistance;
+
+/**
+ * Calculates the length of a vec4
+ *
+ * @param {vec4} a vector to calculate length of
+ * @returns {Number} length of a
+ */
+vec4.length = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2],
+ w = a[3];
+ return Math.sqrt(x*x + y*y + z*z + w*w);
+};
+
+/**
+ * Alias for {@link vec4.length}
+ * @function
+ */
+vec4.len = vec4.length;
+
+/**
+ * Calculates the squared length of a vec4
+ *
+ * @param {vec4} a vector to calculate squared length of
+ * @returns {Number} squared length of a
+ */
+vec4.squaredLength = function (a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2],
+ w = a[3];
+ return x*x + y*y + z*z + w*w;
+};
+
+/**
+ * Alias for {@link vec4.squaredLength}
+ * @function
+ */
+vec4.sqrLen = vec4.squaredLength;
+
+/**
+ * Negates the components of a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to negate
+ * @returns {vec4} out
+ */
+vec4.negate = function(out, a) {
+ out[0] = -a[0];
+ out[1] = -a[1];
+ out[2] = -a[2];
+ out[3] = -a[3];
+ return out;
+};
+
+/**
+ * Returns the inverse of the components of a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to invert
+ * @returns {vec4} out
+ */
+vec4.inverse = function(out, a) {
+ out[0] = 1.0 / a[0];
+ out[1] = 1.0 / a[1];
+ out[2] = 1.0 / a[2];
+ out[3] = 1.0 / a[3];
+ return out;
+};
+
+/**
+ * Normalize a vec4
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a vector to normalize
+ * @returns {vec4} out
+ */
+vec4.normalize = function(out, a) {
+ var x = a[0],
+ y = a[1],
+ z = a[2],
+ w = a[3];
+ var len = x*x + y*y + z*z + w*w;
+ if (len > 0) {
+ len = 1 / Math.sqrt(len);
+ out[0] = x * len;
+ out[1] = y * len;
+ out[2] = z * len;
+ out[3] = w * len;
+ }
+ return out;
+};
+
+/**
+ * Calculates the dot product of two vec4's
+ *
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @returns {Number} dot product of a and b
+ */
+vec4.dot = function (a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+};
+
+/**
+ * Performs a linear interpolation between two vec4's
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the first operand
+ * @param {vec4} b the second operand
+ * @param {Number} t interpolation amount between the two inputs
+ * @returns {vec4} out
+ */
+vec4.lerp = function (out, a, b, t) {
+ var ax = a[0],
+ ay = a[1],
+ az = a[2],
+ aw = a[3];
+ out[0] = ax + t * (b[0] - ax);
+ out[1] = ay + t * (b[1] - ay);
+ out[2] = az + t * (b[2] - az);
+ out[3] = aw + t * (b[3] - aw);
+ return out;
+};
+
+/**
+ * Generates a random vector with the given scale
+ *
+ * @param {vec4} out the receiving vector
+ * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned
+ * @returns {vec4} out
+ */
+vec4.random = function (out, scale) {
+ scale = scale || 1.0;
+
+ //TODO: This is a pretty awful way of doing this. Find something better.
+ out[0] = glMatrix.RANDOM();
+ out[1] = glMatrix.RANDOM();
+ out[2] = glMatrix.RANDOM();
+ out[3] = glMatrix.RANDOM();
+ vec4.normalize(out, out);
+ vec4.scale(out, out, scale);
+ return out;
+};
+
+/**
+ * Transforms the vec4 with a mat4.
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {mat4} m matrix to transform with
+ * @returns {vec4} out
+ */
+vec4.transformMat4 = function(out, a, m) {
+ var x = a[0], y = a[1], z = a[2], w = a[3];
+ out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
+ out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
+ out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
+ out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
+ return out;
+};
+
+/**
+ * Transforms the vec4 with a quat
+ *
+ * @param {vec4} out the receiving vector
+ * @param {vec4} a the vector to transform
+ * @param {quat} q quaternion to transform with
+ * @returns {vec4} out
+ */
+vec4.transformQuat = function(out, a, q) {
+ var x = a[0], y = a[1], z = a[2],
+ qx = q[0], qy = q[1], qz = q[2], qw = q[3],
+
+ // calculate quat * vec
+ ix = qw * x + qy * z - qz * y,
+ iy = qw * y + qz * x - qx * z,
+ iz = qw * z + qx * y - qy * x,
+ iw = -qx * x - qy * y - qz * z;
+
+ // calculate result * inverse quat
+ out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+ out[3] = a[3];
+ return out;
+};
+
+/**
+ * Perform some operation over an array of vec4s.
+ *
+ * @param {Array} a the array of vectors to iterate over
+ * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed
+ * @param {Number} offset Number of elements to skip at the beginning of the array
+ * @param {Number} count Number of vec4s to iterate over. If 0 iterates over entire array
+ * @param {Function} fn Function to call for each vector in the array
+ * @param {Object} [arg] additional argument to pass to fn
+ * @returns {Array} a
+ * @function
+ */
+vec4.forEach = (function() {
+ var vec = vec4.create();
+
+ return function(a, stride, offset, count, fn, arg) {
+ var i, l;
+ if(!stride) {
+ stride = 4;
+ }
+
+ if(!offset) {
+ offset = 0;
+ }
+
+ if(count) {
+ l = Math.min((count * stride) + offset, a.length);
+ } else {
+ l = a.length;
+ }
+
+ for(i = offset; i < l; i += stride) {
+ vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3];
+ fn(vec, vec, arg);
+ a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3];
+ }
+
+ return a;
+ };
+})();
+
+/**
+ * Returns a string representation of a vector
+ *
+ * @param {vec4} vec vector to represent as a string
+ * @returns {String} string representation of the vector
+ */
+vec4.str = function (a) {
+ return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')';
+};
+
+module.exports = vec4;
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build.rake b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build.rake
new file mode 100644
index 00000000000..a1783aabc7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build.rake
@@ -0,0 +1,2 @@
+desc "compile & minify sources into a single file"
+task :build => ['build:compile', 'build:minify']
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/compile.rake b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/compile.rake
new file mode 100644
index 00000000000..905e393d889
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/compile.rake
@@ -0,0 +1,5 @@
+namespace :build do
+ task :compile do
+ compile
+ end
+end
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/minify.rake b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/minify.rake
new file mode 100644
index 00000000000..ea5fec42af5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/build/minify.rake
@@ -0,0 +1,5 @@
+namespace :build do
+ task :minify do
+ minify
+ end
+end
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/default.rake b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/default.rake
new file mode 100644
index 00000000000..c057922037d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/default.rake
@@ -0,0 +1 @@
+task :default => ['test:node', 'test:ci']
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/release.rake b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/release.rake
new file mode 100644
index 00000000000..c70f70cfc13
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/release.rake
@@ -0,0 +1,21 @@
+desc "tag and release gl-matrix v#{GLMatrix::VERSION}"
+task :release do
+ require 'thor'
+ Bundler.ui = Bundler::UI::Shell.new(Thor::Shell::Basic.new)
+ Bundler.ui.debug! if ENV['DEBUG']
+
+ # Sanity check: rebuild files just in case dev forgot to.
+ # If so, files will change and release will abort since changes
+ # were not checked in.
+ Rake::Task['build'].invoke
+
+ release do
+ # Put other release-related stuff here, such as publishing docs;
+ # if anything fails, gl-matrix will be untagged and not pushed.
+ #
+ # Example:
+ #
+ # Rake::Task['doc:publish'].invoke
+ #
+ end
+end
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix.rb b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix.rb
new file mode 100644
index 00000000000..cecdc615a97
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix.rb
@@ -0,0 +1,84 @@
+# Copyright (c) 2013 Brandon Jones, Colin MacKenzie IV
+#
+# This software is provided 'as-is', without any express or implied
+# warranty. In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+# claim that you wrote the original software. If you use this software
+# in a product, an acknowledgment in the product documentation would be
+# appreciated but is not required.
+#
+# 2. Altered source versions must be plainly marked as such, and must not
+# be misrepresented as being the original software.
+#
+# 3. This notice may not be removed or altered from any source distribution.
+#
+$:.unshift File.expand_path('.', File.dirname(__FILE__))
+require 'sprockets'
+require 'jasmine'
+
+class Jasmine::Config
+ def simple_config_file
+ File.expand_path GLMatrix.base_path.join('spec/jasmine.yml')
+ end
+end
+
+class Rack::Jasmine::Runner
+ alias_method :jasmine_call, :call
+ def call(env)
+ GLMatrix.compile
+ jasmine_call env
+ end
+end
+
+module GLMatrix
+ autoload :ReleaseHelper, 'gl-matrix/release_helper'
+ autoload :Version, 'gl-matrix/version'
+ autoload :VERSION, 'gl-matrix/version'
+
+ module_function
+
+ def release(&block)
+ GLMatrix::ReleaseHelper.release &block
+ end
+
+ def sprockets
+ env = Sprockets::Environment.new base_path
+ env.append_path base_path.join('src')
+ env
+ end
+
+ def base_path
+ Pathname.new File.expand_path('../..', File.dirname(__FILE__))
+ end
+
+ # Compiles the source file to the dest file. If a block
+ # is given, the source file is yielded and replaced with
+ # the result. Returns the destination as a Pathname.
+ def compile(source = 'gl-matrix.js', dest = 'dist/gl-matrix.js')
+ dest = base_path.join dest
+ js = sprockets[source]
+ js = yield js if block_given?
+
+ File.open dest, "w" do |f|
+ f.puts js
+ end
+
+ puts "compiled #{source} to #{dest.relative_path_from base_path}"
+ dest
+ end
+
+ def minify(source = 'gl-matrix.js', dest = 'dist/gl-matrix-min.js')
+ dest = compile source, dest do |js|
+ Uglifier.compile js
+ end
+
+ puts "minified #{source} to #{dest.relative_path_from base_path}"
+ end
+
+end
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/release_helper.rb b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/release_helper.rb
new file mode 100644
index 00000000000..4aca02baf88
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/release_helper.rb
@@ -0,0 +1,104 @@
+# Copyright (c) 2013 Brandon Jones, Colin MacKenzie IV
+#
+# This software is provided 'as-is', without any express or implied
+# warranty. In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+# claim that you wrote the original software. If you use this software
+# in a product, an acknowledgment in the product documentation would be
+# appreciated but is not required.
+#
+# 2. Altered source versions must be plainly marked as such, and must not
+# be misrepresented as being the original software.
+#
+# 3. This notice may not be removed or altered from any source distribution.
+
+# Pretty much everything here was ripped from Bundler.
+# https://github.com/carlhuda/bundler/blob/master/lib/bundler/gem_helper.rb
+module GLMatrix::ReleaseHelper
+ module_function
+
+ def release
+ guard_clean
+ guard_already_tagged
+ tag_version {
+ yield if block_given?
+ git_push
+ }
+ end
+
+ def base
+ GLMatrix.base_path.to_s
+ end
+
+ def git_push
+ perform_git_push
+ perform_git_push ' --tags'
+ Bundler.ui.confirm "Pushed git commits and tags"
+ end
+
+ def perform_git_push(options = '')
+ cmd = "git push #{options}"
+ out, code = sh_with_code(cmd)
+ raise "Couldn't git push. `#{cmd}' failed with the following output:\n\n#{out}\n" unless code == 0
+ end
+
+ def guard_already_tagged
+ if sh('git tag').split(/\n/).include?(version_tag)
+ raise("This tag has already been committed to the repo.")
+ end
+ end
+
+ def guard_clean
+ clean? or raise("There are files that need to be committed first.")
+ end
+
+ def clean?
+ sh_with_code("git diff --exit-code")[1] == 0
+ end
+
+ def tag_version
+ sh "git tag -a -m \"Version #{version}\" #{version_tag}"
+ Bundler.ui.confirm "Tagged #{version_tag}"
+ yield if block_given?
+ rescue
+ Bundler.ui.error "Untagged #{version_tag} due to error"
+ sh_with_code "git tag -d #{version_tag}"
+ raise
+ end
+
+ def version
+ GLMatrix::VERSION
+ end
+
+ def version_tag
+ "v#{version}"
+ end
+
+ def name
+ "gl-matrix"
+ end
+
+ def sh(cmd, &block)
+ out, code = sh_with_code(cmd, &block)
+ code == 0 ? out : raise(out.empty? ? "Running `#{cmd}' failed. Run this command directly for more detailed output." : out)
+ end
+
+ def sh_with_code(cmd, &block)
+ cmd << " 2>&1"
+ outbuf = ''
+ Bundler.ui.debug(cmd)
+ Dir.chdir(base) {
+ outbuf = `#{cmd}`
+ if $? == 0
+ block.call(outbuf) if block
+ end
+ }
+ [outbuf, $?]
+ end
+end
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/version.rb b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/version.rb
new file mode 100644
index 00000000000..73bb9d2889d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/tasks/support/gl-matrix/version.rb
@@ -0,0 +1,28 @@
+# Copyright (c) 2013 Brandon Jones, Colin MacKenzie IV
+#
+# This software is provided 'as-is', without any express or implied
+# warranty. In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+# claim that you wrote the original software. If you use this software
+# in a product, an acknowledgment in the product documentation would be
+# appreciated but is not required.
+#
+# 2. Altered source versions must be plainly marked as such, and must not
+# be misrepresented as being the original software.
+#
+# 3. This notice may not be removed or altered from any source distribution.
+
+module GLMatrix
+ module Version
+ MAJOR, MINOR, PATCH, REL = *File.read(base_path.join 'VERSION').split(".")
+ STRING = [MAJOR, MINOR, PATCH, REL].compact.join '.'
+ end
+
+ VERSION = Version::STRING
+end
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.js
new file mode 100644
index 00000000000..b9eace3a5ad
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.js
@@ -0,0 +1,47 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var fs = require('fs');
+var webpack = require('webpack');
+
+var entryFile = './src/gl-matrix.js';
+
+// Read the comments from the top of the main gl-matrix file and append them to
+// the minified version.
+var header = '';
+var mainFile = fs.readFileSync(entryFile, { encoding: 'utf8' });
+if (mainFile) {
+ var headerIndex = mainFile.indexOf('\/\/ END HEADER');
+ if (headerIndex >= 0) {
+ header = mainFile.substr(0, headerIndex);
+ }
+}
+
+module.exports = {
+ entry: entryFile,
+ output: {
+ path: __dirname + '/dist',
+ filename: 'gl-matrix.js',
+ libraryTarget: 'umd'
+ },
+ plugins: [
+ new webpack.BannerPlugin(header, { raw: true }),
+ ]
+}; \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.min.js b/chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.min.js
new file mode 100644
index 00000000000..c7f91092d1b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/gl-matrix/webpack.config.min.js
@@ -0,0 +1,28 @@
+/* Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+var webpack = require('webpack');
+module.exports = require('./webpack.config.js');
+
+module.exports.plugins.unshift(
+ new webpack.optimize.UglifyJsPlugin()
+);
+
+module.exports.output.filename = 'gl-matrix-min.js'; \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/jszip/LICENSE.markdown b/chromium/third_party/catapult/tracing/third_party/jszip/LICENSE.markdown
new file mode 100644
index 00000000000..77b40ad819e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/jszip/LICENSE.markdown
@@ -0,0 +1,651 @@
+JSZip is dual licensed. You may use it under the MIT license *or* the GPLv3
+license.
+
+The MIT License
+===============
+
+Copyright (c) 2009-2012 Stuart Knightley, David Duponchel, Franz Buchinger, António Afonso
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+GPL version 3
+=============
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
diff --git a/chromium/third_party/catapult/tracing/third_party/jszip/README.chromium b/chromium/third_party/catapult/tracing/third_party/jszip/README.chromium
new file mode 100644
index 00000000000..ab23a109aae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/jszip/README.chromium
@@ -0,0 +1,16 @@
+Name: JSZip
+Short Name: JSZip
+URL: https://github.com/Stuk/jszip
+Date: Wed Aug 28 23:10:31 EST 2013
+Revision: 1b5ef3c66bd676c24dd7337ca02ecc99e119e205
+License: LICENSE.markdown
+License File: MIT
+Security Critical: no
+
+Description:
+Create, read and edit .zip files with Javascript
+
+Local Modifications:
+I deleted the below comment from jszip-inflate.js in order to make things load
+correctly.
+ //@ sourceMappingURL=rawinflate.min.js.map
diff --git a/chromium/third_party/catapult/tracing/third_party/jszip/README.markdown b/chromium/third_party/catapult/tracing/third_party/jszip/README.markdown
new file mode 100644
index 00000000000..537c7adff7e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/jszip/README.markdown
@@ -0,0 +1,32 @@
+JSZip
+=====
+
+A library for creating, reading and editing .zip files with Javascript, with a
+lovely and simple API.
+
+See http://stuartk.com/jszip for all the documentation
+
+```javascript
+var zip = new JSZip();
+
+zip.file("Hello.txt", "Hello World\n");
+
+var img = zip.folder("images");
+img.file("smile.gif", imgData, {base64: true});
+
+var content = zip.generate();
+
+location.href = "data:application/zip;base64," + content;
+/*
+Results in a zip containing
+Hello.txt
+images/
+ smile.gif
+*/
+```
+
+License
+=======
+
+JSZip is dual-licensed. You may use it under the MIT license *or* the GPLv3
+license. See LICENSE.markdown.
diff --git a/chromium/third_party/catapult/tracing/third_party/jszip/jszip.min.js b/chromium/third_party/catapult/tracing/third_party/jszip/jszip.min.js
new file mode 100644
index 00000000000..767d8c11d27
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/jszip/jszip.min.js
@@ -0,0 +1,14 @@
+/*!
+
+JSZip - A Javascript class for generating and reading zip files
+<http://stuartk.com/jszip>
+
+(c) 2009-2014 Stuart Knightley <stuart [at] stuartk.com>
+Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown.
+
+JSZip uses the library pako released under the MIT license :
+https://github.com/nodeca/pako/blob/master/LICENSE
+*/
+!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;"undefined"!=typeof window?b=window:"undefined"!=typeof global?b=global:"undefined"!=typeof self&&(b=self),b.JSZip=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){"use strict";var d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";c.encode=function(a){for(var b,c,e,f,g,h,i,j="",k=0;k<a.length;)b=a.charCodeAt(k++),c=a.charCodeAt(k++),e=a.charCodeAt(k++),f=b>>2,g=(3&b)<<4|c>>4,h=(15&c)<<2|e>>6,i=63&e,isNaN(c)?h=i=64:isNaN(e)&&(i=64),j=j+d.charAt(f)+d.charAt(g)+d.charAt(h)+d.charAt(i);return j},c.decode=function(a){var b,c,e,f,g,h,i,j="",k=0;for(a=a.replace(/[^A-Za-z0-9\+\/\=]/g,"");k<a.length;)f=d.indexOf(a.charAt(k++)),g=d.indexOf(a.charAt(k++)),h=d.indexOf(a.charAt(k++)),i=d.indexOf(a.charAt(k++)),b=f<<2|g>>4,c=(15&g)<<4|h>>2,e=(3&h)<<6|i,j+=String.fromCharCode(b),64!=h&&(j+=String.fromCharCode(c)),64!=i&&(j+=String.fromCharCode(e));return j}},{}],2:[function(a,b){"use strict";function c(){this.compressedSize=0,this.uncompressedSize=0,this.crc32=0,this.compressionMethod=null,this.compressedContent=null}c.prototype={getContent:function(){return null},getCompressedContent:function(){return null}},b.exports=c},{}],3:[function(a,b,c){"use strict";c.STORE={magic:"\x00\x00",compress:function(a){return a},uncompress:function(a){return a},compressInputType:null,uncompressInputType:null},c.DEFLATE=a("./flate")},{"./flate":8}],4:[function(a,b){"use strict";var c=a("./utils"),d=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,936918e3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117];b.exports=function(a,b){if("undefined"==typeof a||!a.length)return 0;var e="string"!==c.getTypeOf(a);"undefined"==typeof b&&(b=0);var f=0,g=0,h=0;b=-1^b;for(var i=0,j=a.length;j>i;i++)h=e?a[i]:a.charCodeAt(i),g=255&(b^h),f=d[g],b=b>>>8^f;return-1^b}},{"./utils":21}],5:[function(a,b){"use strict";function c(){this.data=null,this.length=0,this.index=0}var d=a("./utils");c.prototype={checkOffset:function(a){this.checkIndex(this.index+a)},checkIndex:function(a){if(this.length<a||0>a)throw new Error("End of data reached (data length = "+this.length+", asked index = "+a+"). Corrupted zip ?")},setIndex:function(a){this.checkIndex(a),this.index=a},skip:function(a){this.setIndex(this.index+a)},byteAt:function(){},readInt:function(a){var b,c=0;for(this.checkOffset(a),b=this.index+a-1;b>=this.index;b--)c=(c<<8)+this.byteAt(b);return this.index+=a,c},readString:function(a){return d.transformTo("string",this.readData(a))},readData:function(){},lastIndexOfSignature:function(){},readDate:function(){var a=this.readInt(4);return new Date((a>>25&127)+1980,(a>>21&15)-1,a>>16&31,a>>11&31,a>>5&63,(31&a)<<1)}},b.exports=c},{"./utils":21}],6:[function(a,b,c){"use strict";c.base64=!1,c.binary=!1,c.dir=!1,c.createFolders=!1,c.date=null,c.compression=null,c.comment=null},{}],7:[function(a,b,c){"use strict";var d=a("./utils");c.string2binary=function(a){return d.string2binary(a)},c.string2Uint8Array=function(a){return d.transformTo("uint8array",a)},c.uint8Array2String=function(a){return d.transformTo("string",a)},c.string2Blob=function(a){var b=d.transformTo("arraybuffer",a);return d.arrayBuffer2Blob(b)},c.arrayBuffer2Blob=function(a){return d.arrayBuffer2Blob(a)},c.transformTo=function(a,b){return d.transformTo(a,b)},c.getTypeOf=function(a){return d.getTypeOf(a)},c.checkSupport=function(a){return d.checkSupport(a)},c.MAX_VALUE_16BITS=d.MAX_VALUE_16BITS,c.MAX_VALUE_32BITS=d.MAX_VALUE_32BITS,c.pretty=function(a){return d.pretty(a)},c.findCompression=function(a){return d.findCompression(a)},c.isRegExp=function(a){return d.isRegExp(a)}},{"./utils":21}],8:[function(a,b,c){"use strict";var d="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,e=a("pako");c.uncompressInputType=d?"uint8array":"array",c.compressInputType=d?"uint8array":"array",c.magic="\b\x00",c.compress=function(a){return e.deflateRaw(a)},c.uncompress=function(a){return e.inflateRaw(a)}},{pako:24}],9:[function(a,b){"use strict";function c(a,b){return this instanceof c?(this.files={},this.comment=null,this.root="",a&&this.load(a,b),void(this.clone=function(){var a=new c;for(var b in this)"function"!=typeof this[b]&&(a[b]=this[b]);return a})):new c(a,b)}var d=a("./base64");c.prototype=a("./object"),c.prototype.load=a("./load"),c.support=a("./support"),c.defaults=a("./defaults"),c.utils=a("./deprecatedPublicUtils"),c.base64={encode:function(a){return d.encode(a)},decode:function(a){return d.decode(a)}},c.compressions=a("./compressions"),b.exports=c},{"./base64":1,"./compressions":3,"./defaults":6,"./deprecatedPublicUtils":7,"./load":10,"./object":13,"./support":17}],10:[function(a,b){"use strict";var c=a("./base64"),d=a("./zipEntries");b.exports=function(a,b){var e,f,g,h;for(b=b||{},b.base64&&(a=c.decode(a)),f=new d(a,b),e=f.files,g=0;g<e.length;g++)h=e[g],this.file(h.fileName,h.decompressed,{binary:!0,optimizedBinaryString:!0,date:h.date,dir:h.dir,comment:h.fileComment.length?h.fileComment:null,createFolders:b.createFolders});return f.zipComment.length&&(this.comment=f.zipComment),this}},{"./base64":1,"./zipEntries":22}],11:[function(a,b){(function(a){"use strict";b.exports=function(b,c){return new a(b,c)},b.exports.test=function(b){return a.isBuffer(b)}}).call(this,"undefined"!=typeof Buffer?Buffer:void 0)},{}],12:[function(a,b){"use strict";function c(a){this.data=a,this.length=this.data.length,this.index=0}var d=a("./uint8ArrayReader");c.prototype=new d,c.prototype.readData=function(a){this.checkOffset(a);var b=this.data.slice(this.index,this.index+a);return this.index+=a,b},b.exports=c},{"./uint8ArrayReader":18}],13:[function(a,b){"use strict";var c=a("./support"),d=a("./utils"),e=a("./crc32"),f=a("./signature"),g=a("./defaults"),h=a("./base64"),i=a("./compressions"),j=a("./compressedObject"),k=a("./nodeBuffer"),l=a("./utf8"),m=a("./stringWriter"),n=a("./uint8ArrayWriter"),o=function(a){if(a._data instanceof j&&(a._data=a._data.getContent(),a.options.binary=!0,a.options.base64=!1,"uint8array"===d.getTypeOf(a._data))){var b=a._data;a._data=new Uint8Array(b.length),0!==b.length&&a._data.set(b,0)}return a._data},p=function(a){var b=o(a),e=d.getTypeOf(b);return"string"===e?!a.options.binary&&c.nodebuffer?k(b,"utf-8"):a.asBinary():b},q=function(a){var b=o(this);return null===b||"undefined"==typeof b?"":(this.options.base64&&(b=h.decode(b)),b=a&&this.options.binary?A.utf8decode(b):d.transformTo("string",b),a||this.options.binary||(b=d.transformTo("string",A.utf8encode(b))),b)},r=function(a,b,c){this.name=a,this.dir=c.dir,this.date=c.date,this.comment=c.comment,this._data=b,this.options=c,this._initialMetadata={dir:c.dir,date:c.date}};r.prototype={asText:function(){return q.call(this,!0)},asBinary:function(){return q.call(this,!1)},asNodeBuffer:function(){var a=p(this);return d.transformTo("nodebuffer",a)},asUint8Array:function(){var a=p(this);return d.transformTo("uint8array",a)},asArrayBuffer:function(){return this.asUint8Array().buffer}};var s=function(a,b){var c,d="";for(c=0;b>c;c++)d+=String.fromCharCode(255&a),a>>>=8;return d},t=function(){var a,b,c={};for(a=0;a<arguments.length;a++)for(b in arguments[a])arguments[a].hasOwnProperty(b)&&"undefined"==typeof c[b]&&(c[b]=arguments[a][b]);return c},u=function(a){return a=a||{},a.base64!==!0||null!==a.binary&&void 0!==a.binary||(a.binary=!0),a=t(a,g),a.date=a.date||new Date,null!==a.compression&&(a.compression=a.compression.toUpperCase()),a},v=function(a,b,c){var e,f=d.getTypeOf(b);if(c=u(c),c.createFolders&&(e=w(a))&&x.call(this,e,!0),c.dir||null===b||"undefined"==typeof b)c.base64=!1,c.binary=!1,b=null;else if("string"===f)c.binary&&!c.base64&&c.optimizedBinaryString!==!0&&(b=d.string2binary(b));else{if(c.base64=!1,c.binary=!0,!(f||b instanceof j))throw new Error("The data of '"+a+"' is in an unsupported format !");"arraybuffer"===f&&(b=d.transformTo("uint8array",b))}var g=new r(a,b,c);return this.files[a]=g,g},w=function(a){"/"==a.slice(-1)&&(a=a.substring(0,a.length-1));var b=a.lastIndexOf("/");return b>0?a.substring(0,b):""},x=function(a,b){return"/"!=a.slice(-1)&&(a+="/"),b="undefined"!=typeof b?b:!1,this.files[a]||v.call(this,a,null,{dir:!0,createFolders:b}),this.files[a]},y=function(a,b){var c,f=new j;return a._data instanceof j?(f.uncompressedSize=a._data.uncompressedSize,f.crc32=a._data.crc32,0===f.uncompressedSize||a.dir?(b=i.STORE,f.compressedContent="",f.crc32=0):a._data.compressionMethod===b.magic?f.compressedContent=a._data.getCompressedContent():(c=a._data.getContent(),f.compressedContent=b.compress(d.transformTo(b.compressInputType,c)))):(c=p(a),(!c||0===c.length||a.dir)&&(b=i.STORE,c=""),f.uncompressedSize=c.length,f.crc32=e(c),f.compressedContent=b.compress(d.transformTo(b.compressInputType,c))),f.compressedSize=f.compressedContent.length,f.compressionMethod=b.magic,f},z=function(a,b,c,g){var h,i,j,k,m=(c.compressedContent,d.transformTo("string",l.utf8encode(b.name))),n=b.comment||"",o=d.transformTo("string",l.utf8encode(n)),p=m.length!==b.name.length,q=o.length!==n.length,r=b.options,t="",u="",v="";j=b._initialMetadata.dir!==b.dir?b.dir:r.dir,k=b._initialMetadata.date!==b.date?b.date:r.date,h=k.getHours(),h<<=6,h|=k.getMinutes(),h<<=5,h|=k.getSeconds()/2,i=k.getFullYear()-1980,i<<=4,i|=k.getMonth()+1,i<<=5,i|=k.getDate(),p&&(u=s(1,1)+s(e(m),4)+m,t+="up"+s(u.length,2)+u),q&&(v=s(1,1)+s(this.crc32(o),4)+o,t+="uc"+s(v.length,2)+v);var w="";w+="\n\x00",w+=p||q?"\x00\b":"\x00\x00",w+=c.compressionMethod,w+=s(h,2),w+=s(i,2),w+=s(c.crc32,4),w+=s(c.compressedSize,4),w+=s(c.uncompressedSize,4),w+=s(m.length,2),w+=s(t.length,2);var x=f.LOCAL_FILE_HEADER+w+m+t,y=f.CENTRAL_FILE_HEADER+"\x00"+w+s(o.length,2)+"\x00\x00\x00\x00"+(j===!0?"\x00\x00\x00":"\x00\x00\x00\x00")+s(g,4)+m+t+o;return{fileRecord:x,dirRecord:y,compressedObject:c}},A={load:function(){throw new Error("Load method is not defined. Is the file jszip-load.js included ?")},filter:function(a){var b,c,d,e,f=[];for(b in this.files)this.files.hasOwnProperty(b)&&(d=this.files[b],e=new r(d.name,d._data,t(d.options)),c=b.slice(this.root.length,b.length),b.slice(0,this.root.length)===this.root&&a(c,e)&&f.push(e));return f},file:function(a,b,c){if(1===arguments.length){if(d.isRegExp(a)){var e=a;return this.filter(function(a,b){return!b.dir&&e.test(a)})}return this.filter(function(b,c){return!c.dir&&b===a})[0]||null}return a=this.root+a,v.call(this,a,b,c),this},folder:function(a){if(!a)return this;if(d.isRegExp(a))return this.filter(function(b,c){return c.dir&&a.test(b)});var b=this.root+a,c=x.call(this,b),e=this.clone();return e.root=c.name,e},remove:function(a){a=this.root+a;var b=this.files[a];if(b||("/"!=a.slice(-1)&&(a+="/"),b=this.files[a]),b&&!b.dir)delete this.files[a];else for(var c=this.filter(function(b,c){return c.name.slice(0,a.length)===a}),d=0;d<c.length;d++)delete this.files[c[d].name];return this},generate:function(a){a=t(a||{},{base64:!0,compression:"STORE",type:"base64",comment:null}),d.checkSupport(a.type);var b,c,e=[],g=0,j=0,k=d.transformTo("string",this.utf8encode(a.comment||this.comment||""));for(var l in this.files)if(this.files.hasOwnProperty(l)){var o=this.files[l],p=o.options.compression||a.compression.toUpperCase(),q=i[p];if(!q)throw new Error(p+" is not a valid compression method !");var r=y.call(this,o,q),u=z.call(this,l,o,r,g);g+=u.fileRecord.length+r.compressedSize,j+=u.dirRecord.length,e.push(u)}var v="";v=f.CENTRAL_DIRECTORY_END+"\x00\x00\x00\x00"+s(e.length,2)+s(e.length,2)+s(j,4)+s(g,4)+s(k.length,2)+k;var w=a.type.toLowerCase();for(b="uint8array"===w||"arraybuffer"===w||"blob"===w||"nodebuffer"===w?new n(g+j+v.length):new m(g+j+v.length),c=0;c<e.length;c++)b.append(e[c].fileRecord),b.append(e[c].compressedObject.compressedContent);for(c=0;c<e.length;c++)b.append(e[c].dirRecord);b.append(v);var x=b.finalize();switch(a.type.toLowerCase()){case"uint8array":case"arraybuffer":case"nodebuffer":return d.transformTo(a.type.toLowerCase(),x);case"blob":return d.arrayBuffer2Blob(d.transformTo("arraybuffer",x));case"base64":return a.base64?h.encode(x):x;default:return x}},crc32:function(a,b){return e(a,b)},utf8encode:function(a){return d.transformTo("string",l.utf8encode(a))},utf8decode:function(a){return l.utf8decode(a)}};b.exports=A},{"./base64":1,"./compressedObject":2,"./compressions":3,"./crc32":4,"./defaults":6,"./nodeBuffer":11,"./signature":14,"./stringWriter":16,"./support":17,"./uint8ArrayWriter":19,"./utf8":20,"./utils":21}],14:[function(a,b,c){"use strict";c.LOCAL_FILE_HEADER="PK",c.CENTRAL_FILE_HEADER="PK",c.CENTRAL_DIRECTORY_END="PK",c.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",c.ZIP64_CENTRAL_DIRECTORY_END="PK",c.DATA_DESCRIPTOR="PK\b"},{}],15:[function(a,b){"use strict";function c(a,b){this.data=a,b||(this.data=e.string2binary(this.data)),this.length=this.data.length,this.index=0}var d=a("./dataReader"),e=a("./utils");c.prototype=new d,c.prototype.byteAt=function(a){return this.data.charCodeAt(a)},c.prototype.lastIndexOfSignature=function(a){return this.data.lastIndexOf(a)},c.prototype.readData=function(a){this.checkOffset(a);var b=this.data.slice(this.index,this.index+a);return this.index+=a,b},b.exports=c},{"./dataReader":5,"./utils":21}],16:[function(a,b){"use strict";var c=a("./utils"),d=function(){this.data=[]};d.prototype={append:function(a){a=c.transformTo("string",a),this.data.push(a)},finalize:function(){return this.data.join("")}},b.exports=d},{"./utils":21}],17:[function(a,b,c){(function(a){"use strict";if(c.base64=!0,c.array=!0,c.string=!0,c.arraybuffer="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array,c.nodebuffer="undefined"!=typeof a,c.uint8array="undefined"!=typeof Uint8Array,"undefined"==typeof ArrayBuffer)c.blob=!1;else{var b=new ArrayBuffer(0);try{c.blob=0===new Blob([b],{type:"application/zip"}).size}catch(d){try{var e=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder||window.MSBlobBuilder,f=new e;f.append(b),c.blob=0===f.getBlob("application/zip").size}catch(d){c.blob=!1}}}}).call(this,"undefined"!=typeof Buffer?Buffer:void 0)},{}],18:[function(a,b){"use strict";function c(a){a&&(this.data=a,this.length=this.data.length,this.index=0)}var d=a("./dataReader");c.prototype=new d,c.prototype.byteAt=function(a){return this.data[a]},c.prototype.lastIndexOfSignature=function(a){for(var b=a.charCodeAt(0),c=a.charCodeAt(1),d=a.charCodeAt(2),e=a.charCodeAt(3),f=this.length-4;f>=0;--f)if(this.data[f]===b&&this.data[f+1]===c&&this.data[f+2]===d&&this.data[f+3]===e)return f;return-1},c.prototype.readData=function(a){if(this.checkOffset(a),0===a)return new Uint8Array(0);var b=this.data.subarray(this.index,this.index+a);return this.index+=a,b},b.exports=c},{"./dataReader":5}],19:[function(a,b){"use strict";var c=a("./utils"),d=function(a){this.data=new Uint8Array(a),this.index=0};d.prototype={append:function(a){0!==a.length&&(a=c.transformTo("uint8array",a),this.data.set(a,this.index),this.index+=a.length)},finalize:function(){return this.data}},b.exports=d},{"./utils":21}],20:[function(a,b,c){"use strict";for(var d=a("./utils"),e=a("./support"),f=a("./nodeBuffer"),g=new Array(256),h=0;256>h;h++)g[h]=h>=252?6:h>=248?5:h>=240?4:h>=224?3:h>=192?2:1;g[254]=g[254]=1;var i=function(a){var b,c,d,f,g,h=a.length,i=0;for(f=0;h>f;f++)c=a.charCodeAt(f),55296===(64512&c)&&h>f+1&&(d=a.charCodeAt(f+1),56320===(64512&d)&&(c=65536+(c-55296<<10)+(d-56320),f++)),i+=128>c?1:2048>c?2:65536>c?3:4;for(b=e.uint8array?new Uint8Array(i):new Array(i),g=0,f=0;i>g;f++)c=a.charCodeAt(f),55296===(64512&c)&&h>f+1&&(d=a.charCodeAt(f+1),56320===(64512&d)&&(c=65536+(c-55296<<10)+(d-56320),f++)),128>c?b[g++]=c:2048>c?(b[g++]=192|c>>>6,b[g++]=128|63&c):65536>c?(b[g++]=224|c>>>12,b[g++]=128|c>>>6&63,b[g++]=128|63&c):(b[g++]=240|c>>>18,b[g++]=128|c>>>12&63,b[g++]=128|c>>>6&63,b[g++]=128|63&c);return b},j=function(a,b){var c;for(b=b||a.length,b>a.length&&(b=a.length),c=b-1;c>=0&&128===(192&a[c]);)c--;return 0>c?b:0===c?b:c+g[a[c]]>b?c:b},k=function(a){var b,c,e,f,h=a.length,i=new Array(2*h);for(c=0,b=0;h>b;)if(e=a[b++],128>e)i[c++]=e;else if(f=g[e],f>4)i[c++]=65533,b+=f-1;else{for(e&=2===f?31:3===f?15:7;f>1&&h>b;)e=e<<6|63&a[b++],f--;f>1?i[c++]=65533:65536>e?i[c++]=e:(e-=65536,i[c++]=55296|e>>10&1023,i[c++]=56320|1023&e)}return i.length!==c&&(i.subarray?i=i.subarray(0,c):i.length=c),d.applyFromCharCode(i)};c.utf8encode=function(a){return e.nodebuffer?f(a,"utf-8"):i(a)},c.utf8decode=function(a){if(e.nodebuffer)return d.transformTo("nodebuffer",a).toString("utf-8");a=d.transformTo(e.uint8array?"uint8array":"array",a);for(var b=[],c=0,f=a.length,g=65536;f>c;){var h=j(a,Math.min(c+g,f));b.push(e.uint8array?k(a.subarray(c,h)):k(a.slice(c,h))),c=h}return b.join("")}},{"./nodeBuffer":11,"./support":17,"./utils":21}],21:[function(a,b,c){"use strict";function d(a){return a}function e(a,b){for(var c=0;c<a.length;++c)b[c]=255&a.charCodeAt(c);return b}function f(a){var b=65536,d=[],e=a.length,f=c.getTypeOf(a),g=0,h=!0;try{switch(f){case"uint8array":String.fromCharCode.apply(null,new Uint8Array(0));break;case"nodebuffer":String.fromCharCode.apply(null,j(0))}}catch(i){h=!1}if(!h){for(var k="",l=0;l<a.length;l++)k+=String.fromCharCode(a[l]);return k}for(;e>g&&b>1;)try{d.push("array"===f||"nodebuffer"===f?String.fromCharCode.apply(null,a.slice(g,Math.min(g+b,e))):String.fromCharCode.apply(null,a.subarray(g,Math.min(g+b,e)))),g+=b}catch(i){b=Math.floor(b/2)}return d.join("")}function g(a,b){for(var c=0;c<a.length;c++)b[c]=a[c];return b}var h=a("./support"),i=a("./compressions"),j=a("./nodeBuffer");c.string2binary=function(a){for(var b="",c=0;c<a.length;c++)b+=String.fromCharCode(255&a.charCodeAt(c));return b},c.arrayBuffer2Blob=function(a){c.checkSupport("blob");try{return new Blob([a],{type:"application/zip"})}catch(b){try{var d=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder||window.MSBlobBuilder,e=new d;return e.append(a),e.getBlob("application/zip")}catch(b){throw new Error("Bug : can't construct the Blob.")}}},c.applyFromCharCode=f;var k={};k.string={string:d,array:function(a){return e(a,new Array(a.length))},arraybuffer:function(a){return k.string.uint8array(a).buffer},uint8array:function(a){return e(a,new Uint8Array(a.length))},nodebuffer:function(a){return e(a,j(a.length))}},k.array={string:f,array:d,arraybuffer:function(a){return new Uint8Array(a).buffer},uint8array:function(a){return new Uint8Array(a)},nodebuffer:function(a){return j(a)}},k.arraybuffer={string:function(a){return f(new Uint8Array(a))},array:function(a){return g(new Uint8Array(a),new Array(a.byteLength))},arraybuffer:d,uint8array:function(a){return new Uint8Array(a)},nodebuffer:function(a){return j(new Uint8Array(a))}},k.uint8array={string:f,array:function(a){return g(a,new Array(a.length))},arraybuffer:function(a){return a.buffer},uint8array:d,nodebuffer:function(a){return j(a)}},k.nodebuffer={string:f,array:function(a){return g(a,new Array(a.length))},arraybuffer:function(a){return k.nodebuffer.uint8array(a).buffer},uint8array:function(a){return g(a,new Uint8Array(a.length))},nodebuffer:d},c.transformTo=function(a,b){if(b||(b=""),!a)return b;c.checkSupport(a);var d=c.getTypeOf(b),e=k[d][a](b);return e},c.getTypeOf=function(a){return"string"==typeof a?"string":"[object Array]"===Object.prototype.toString.call(a)?"array":h.nodebuffer&&j.test(a)?"nodebuffer":h.uint8array&&a instanceof Uint8Array?"uint8array":h.arraybuffer&&a instanceof ArrayBuffer?"arraybuffer":void 0},c.checkSupport=function(a){var b=h[a.toLowerCase()];if(!b)throw new Error(a+" is not supported by this browser")},c.MAX_VALUE_16BITS=65535,c.MAX_VALUE_32BITS=-1,c.pretty=function(a){var b,c,d="";for(c=0;c<(a||"").length;c++)b=a.charCodeAt(c),d+="\\x"+(16>b?"0":"")+b.toString(16).toUpperCase();return d},c.findCompression=function(a){for(var b in i)if(i.hasOwnProperty(b)&&i[b].magic===a)return i[b];return null},c.isRegExp=function(a){return"[object RegExp]"===Object.prototype.toString.call(a)}},{"./compressions":3,"./nodeBuffer":11,"./support":17}],22:[function(a,b){"use strict";function c(a,b){this.files=[],this.loadOptions=b,a&&this.load(a)}var d=a("./stringReader"),e=a("./nodeBufferReader"),f=a("./uint8ArrayReader"),g=a("./utils"),h=a("./signature"),i=a("./zipEntry"),j=a("./support"),k=a("./object");c.prototype={checkSignature:function(a){var b=this.reader.readString(4);if(b!==a)throw new Error("Corrupted zip or bug : unexpected signature ("+g.pretty(b)+", expected "+g.pretty(a)+")")},readBlockEndOfCentral:function(){this.diskNumber=this.reader.readInt(2),this.diskWithCentralDirStart=this.reader.readInt(2),this.centralDirRecordsOnThisDisk=this.reader.readInt(2),this.centralDirRecords=this.reader.readInt(2),this.centralDirSize=this.reader.readInt(4),this.centralDirOffset=this.reader.readInt(4),this.zipCommentLength=this.reader.readInt(2),this.zipComment=this.reader.readString(this.zipCommentLength),this.zipComment=k.utf8decode(this.zipComment)},readBlockZip64EndOfCentral:function(){this.zip64EndOfCentralSize=this.reader.readInt(8),this.versionMadeBy=this.reader.readString(2),this.versionNeeded=this.reader.readInt(2),this.diskNumber=this.reader.readInt(4),this.diskWithCentralDirStart=this.reader.readInt(4),this.centralDirRecordsOnThisDisk=this.reader.readInt(8),this.centralDirRecords=this.reader.readInt(8),this.centralDirSize=this.reader.readInt(8),this.centralDirOffset=this.reader.readInt(8),this.zip64ExtensibleData={};for(var a,b,c,d=this.zip64EndOfCentralSize-44,e=0;d>e;)a=this.reader.readInt(2),b=this.reader.readInt(4),c=this.reader.readString(b),this.zip64ExtensibleData[a]={id:a,length:b,value:c}},readBlockZip64EndOfCentralLocator:function(){if(this.diskWithZip64CentralDirStart=this.reader.readInt(4),this.relativeOffsetEndOfZip64CentralDir=this.reader.readInt(8),this.disksCount=this.reader.readInt(4),this.disksCount>1)throw new Error("Multi-volumes zip are not supported")},readLocalFiles:function(){var a,b;for(a=0;a<this.files.length;a++)b=this.files[a],this.reader.setIndex(b.localHeaderOffset),this.checkSignature(h.LOCAL_FILE_HEADER),b.readLocalPart(this.reader),b.handleUTF8()},readCentralDir:function(){var a;for(this.reader.setIndex(this.centralDirOffset);this.reader.readString(4)===h.CENTRAL_FILE_HEADER;)a=new i({zip64:this.zip64},this.loadOptions),a.readCentralPart(this.reader),this.files.push(a)},readEndOfCentral:function(){var a=this.reader.lastIndexOfSignature(h.CENTRAL_DIRECTORY_END);if(-1===a)throw new Error("Corrupted zip : can't find end of central directory");if(this.reader.setIndex(a),this.checkSignature(h.CENTRAL_DIRECTORY_END),this.readBlockEndOfCentral(),this.diskNumber===g.MAX_VALUE_16BITS||this.diskWithCentralDirStart===g.MAX_VALUE_16BITS||this.centralDirRecordsOnThisDisk===g.MAX_VALUE_16BITS||this.centralDirRecords===g.MAX_VALUE_16BITS||this.centralDirSize===g.MAX_VALUE_32BITS||this.centralDirOffset===g.MAX_VALUE_32BITS){if(this.zip64=!0,a=this.reader.lastIndexOfSignature(h.ZIP64_CENTRAL_DIRECTORY_LOCATOR),-1===a)throw new Error("Corrupted zip : can't find the ZIP64 end of central directory locator");this.reader.setIndex(a),this.checkSignature(h.ZIP64_CENTRAL_DIRECTORY_LOCATOR),this.readBlockZip64EndOfCentralLocator(),this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir),this.checkSignature(h.ZIP64_CENTRAL_DIRECTORY_END),this.readBlockZip64EndOfCentral()}},prepareReader:function(a){var b=g.getTypeOf(a);this.reader="string"!==b||j.uint8array?"nodebuffer"===b?new e(a):new f(g.transformTo("uint8array",a)):new d(a,this.loadOptions.optimizedBinaryString)},load:function(a){this.prepareReader(a),this.readEndOfCentral(),this.readCentralDir(),this.readLocalFiles()}},b.exports=c},{"./nodeBufferReader":12,"./object":13,"./signature":14,"./stringReader":15,"./support":17,"./uint8ArrayReader":18,"./utils":21,"./zipEntry":23}],23:[function(a,b){"use strict";function c(a,b){this.options=a,this.loadOptions=b}var d=a("./stringReader"),e=a("./utils"),f=a("./compressedObject"),g=a("./object");c.prototype={isEncrypted:function(){return 1===(1&this.bitFlag)},useUTF8:function(){return 2048===(2048&this.bitFlag)},prepareCompressedContent:function(a,b,c){return function(){var d=a.index;a.setIndex(b);var e=a.readData(c);return a.setIndex(d),e}},prepareContent:function(a,b,c,d,f){return function(){var a=e.transformTo(d.uncompressInputType,this.getCompressedContent()),b=d.uncompress(a);if(b.length!==f)throw new Error("Bug : uncompressed data size mismatch");return b}},readLocalPart:function(a){var b,c;if(a.skip(22),this.fileNameLength=a.readInt(2),c=a.readInt(2),this.fileName=a.readString(this.fileNameLength),a.skip(c),-1==this.compressedSize||-1==this.uncompressedSize)throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory (compressedSize == -1 || uncompressedSize == -1)");if(b=e.findCompression(this.compressionMethod),null===b)throw new Error("Corrupted zip : compression "+e.pretty(this.compressionMethod)+" unknown (inner file : "+this.fileName+")");if(this.decompressed=new f,this.decompressed.compressedSize=this.compressedSize,this.decompressed.uncompressedSize=this.uncompressedSize,this.decompressed.crc32=this.crc32,this.decompressed.compressionMethod=this.compressionMethod,this.decompressed.getCompressedContent=this.prepareCompressedContent(a,a.index,this.compressedSize,b),this.decompressed.getContent=this.prepareContent(a,a.index,this.compressedSize,b,this.uncompressedSize),this.loadOptions.checkCRC32&&(this.decompressed=e.transformTo("string",this.decompressed.getContent()),g.crc32(this.decompressed)!==this.crc32))throw new Error("Corrupted zip : CRC32 mismatch")},readCentralPart:function(a){if(this.versionMadeBy=a.readString(2),this.versionNeeded=a.readInt(2),this.bitFlag=a.readInt(2),this.compressionMethod=a.readString(2),this.date=a.readDate(),this.crc32=a.readInt(4),this.compressedSize=a.readInt(4),this.uncompressedSize=a.readInt(4),this.fileNameLength=a.readInt(2),this.extraFieldsLength=a.readInt(2),this.fileCommentLength=a.readInt(2),this.diskNumberStart=a.readInt(2),this.internalFileAttributes=a.readInt(2),this.externalFileAttributes=a.readInt(4),this.localHeaderOffset=a.readInt(4),this.isEncrypted())throw new Error("Encrypted zip are not supported");this.fileName=a.readString(this.fileNameLength),this.readExtraFields(a),this.parseZIP64ExtraField(a),this.fileComment=a.readString(this.fileCommentLength),this.dir=16&this.externalFileAttributes?!0:!1},parseZIP64ExtraField:function(){if(this.extraFields[1]){var a=new d(this.extraFields[1].value);this.uncompressedSize===e.MAX_VALUE_32BITS&&(this.uncompressedSize=a.readInt(8)),this.compressedSize===e.MAX_VALUE_32BITS&&(this.compressedSize=a.readInt(8)),this.localHeaderOffset===e.MAX_VALUE_32BITS&&(this.localHeaderOffset=a.readInt(8)),this.diskNumberStart===e.MAX_VALUE_32BITS&&(this.diskNumberStart=a.readInt(4))}},readExtraFields:function(a){var b,c,d,e=a.index;for(this.extraFields=this.extraFields||{};a.index<e+this.extraFieldsLength;)b=a.readInt(2),c=a.readInt(2),d=a.readString(c),this.extraFields[b]={id:b,length:c,value:d}},handleUTF8:function(){if(this.useUTF8())this.fileName=g.utf8decode(this.fileName),this.fileComment=g.utf8decode(this.fileComment);else{var a=this.findExtraFieldUnicodePath();null!==a&&(this.fileName=a);var b=this.findExtraFieldUnicodeComment();null!==b&&(this.fileComment=b)}},findExtraFieldUnicodePath:function(){var a=this.extraFields[28789];if(a){var b=new d(a.value);return 1!==b.readInt(1)?null:g.crc32(this.fileName)!==b.readInt(4)?null:g.utf8decode(b.readString(a.length-5))}return null},findExtraFieldUnicodeComment:function(){var a=this.extraFields[25461];if(a){var b=new d(a.value);return 1!==b.readInt(1)?null:g.crc32(this.fileComment)!==b.readInt(4)?null:g.utf8decode(b.readString(a.length-5))}return null}},b.exports=c},{"./compressedObject":2,"./object":13,"./stringReader":15,"./utils":21}],24:[function(a,b){"use strict";var c=a("./lib/utils/common").assign,d=a("./lib/deflate"),e=a("./lib/inflate"),f=a("./lib/zlib/constants"),g={};c(g,d,e,f),b.exports=g},{"./lib/deflate":25,"./lib/inflate":26,"./lib/utils/common":27,"./lib/zlib/constants":30}],25:[function(a,b,c){"use strict";function d(a,b){var c=new s(b);if(c.push(a,!0),c.err)throw c.msg;return c.result}function e(a,b){return b=b||{},b.raw=!0,d(a,b)}function f(a,b){return b=b||{},b.gzip=!0,d(a,b)}var g=a("./zlib/deflate.js"),h=a("./utils/common"),i=a("./utils/strings"),j=a("./zlib/messages"),k=a("./zlib/zstream"),l=0,m=4,n=0,o=1,p=-1,q=0,r=8,s=function(a){this.options=h.assign({level:p,method:r,chunkSize:16384,windowBits:15,memLevel:8,strategy:q,to:""},a||{});var b=this.options;b.raw&&b.windowBits>0?b.windowBits=-b.windowBits:b.gzip&&b.windowBits>0&&b.windowBits<16&&(b.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new k,this.strm.avail_out=0;var c=g.deflateInit2(this.strm,b.level,b.method,b.windowBits,b.memLevel,b.strategy);if(c!==n)throw new Error(j[c]);b.header&&g.deflateSetHeader(this.strm,b.header)
+};s.prototype.push=function(a,b){var c,d,e=this.strm,f=this.options.chunkSize;if(this.ended)return!1;d=b===~~b?b:b===!0?m:l,e.input="string"==typeof a?i.string2buf(a):a,e.next_in=0,e.avail_in=e.input.length;do{if(0===e.avail_out&&(e.output=new h.Buf8(f),e.next_out=0,e.avail_out=f),c=g.deflate(e,d),c!==o&&c!==n)return this.onEnd(c),this.ended=!0,!1;(0===e.avail_out||0===e.avail_in&&d===m)&&this.onData("string"===this.options.to?i.buf2binstring(h.shrinkBuf(e.output,e.next_out)):h.shrinkBuf(e.output,e.next_out))}while((e.avail_in>0||0===e.avail_out)&&c!==o);return d===m?(c=g.deflateEnd(this.strm),this.onEnd(c),this.ended=!0,c===n):!0},s.prototype.onData=function(a){this.chunks.push(a)},s.prototype.onEnd=function(a){a===n&&(this.result="string"===this.options.to?this.chunks.join(""):h.flattenChunks(this.chunks)),this.chunks=[],this.err=a,this.msg=this.strm.msg},c.Deflate=s,c.deflate=d,c.deflateRaw=e,c.gzip=f},{"./utils/common":27,"./utils/strings":28,"./zlib/deflate.js":32,"./zlib/messages":37,"./zlib/zstream":39}],26:[function(a,b,c){"use strict";function d(a,b){var c=new m(b);if(c.push(a,!0),c.err)throw c.msg;return c.result}function e(a,b){return b=b||{},b.raw=!0,d(a,b)}var f=a("./zlib/inflate.js"),g=a("./utils/common"),h=a("./utils/strings"),i=a("./zlib/constants"),j=a("./zlib/messages"),k=a("./zlib/zstream"),l=a("./zlib/gzheader"),m=function(a){this.options=g.assign({chunkSize:16384,windowBits:0,to:""},a||{});var b=this.options;b.raw&&b.windowBits>=0&&b.windowBits<16&&(b.windowBits=-b.windowBits,0===b.windowBits&&(b.windowBits=-15)),!(b.windowBits>=0&&b.windowBits<16)||a&&a.windowBits||(b.windowBits+=32),b.windowBits>15&&b.windowBits<48&&0===(15&b.windowBits)&&(b.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new k,this.strm.avail_out=0;var c=f.inflateInit2(this.strm,b.windowBits);if(c!==i.Z_OK)throw new Error(j[c]);this.header=new l,f.inflateGetHeader(this.strm,this.header)};m.prototype.push=function(a,b){var c,d,e,j,k,l=this.strm,m=this.options.chunkSize;if(this.ended)return!1;d=b===~~b?b:b===!0?i.Z_FINISH:i.Z_NO_FLUSH,l.input="string"==typeof a?h.binstring2buf(a):a,l.next_in=0,l.avail_in=l.input.length;do{if(0===l.avail_out&&(l.output=new g.Buf8(m),l.next_out=0,l.avail_out=m),c=f.inflate(l,i.Z_NO_FLUSH),c!==i.Z_STREAM_END&&c!==i.Z_OK)return this.onEnd(c),this.ended=!0,!1;l.next_out&&(0===l.avail_out||c===i.Z_STREAM_END||0===l.avail_in&&d===i.Z_FINISH)&&("string"===this.options.to?(e=h.utf8border(l.output,l.next_out),j=l.next_out-e,k=h.buf2string(l.output,e),l.next_out=j,l.avail_out=m-j,j&&g.arraySet(l.output,l.output,e,j,0),this.onData(k)):this.onData(g.shrinkBuf(l.output,l.next_out)))}while(l.avail_in>0&&c!==i.Z_STREAM_END);return c===i.Z_STREAM_END&&(d=i.Z_FINISH),d===i.Z_FINISH?(c=f.inflateEnd(this.strm),this.onEnd(c),this.ended=!0,c===i.Z_OK):!0},m.prototype.onData=function(a){this.chunks.push(a)},m.prototype.onEnd=function(a){a===i.Z_OK&&(this.result="string"===this.options.to?this.chunks.join(""):g.flattenChunks(this.chunks)),this.chunks=[],this.err=a,this.msg=this.strm.msg},c.Inflate=m,c.inflate=d,c.inflateRaw=e,c.ungzip=d},{"./utils/common":27,"./utils/strings":28,"./zlib/constants":30,"./zlib/gzheader":33,"./zlib/inflate.js":35,"./zlib/messages":37,"./zlib/zstream":39}],27:[function(a,b,c){"use strict";var d="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;c.assign=function(a){for(var b=Array.prototype.slice.call(arguments,1);b.length;){var c=b.shift();if(c){if("object"!=typeof c)throw new TypeError(c+"must be non-object");for(var d in c)c.hasOwnProperty(d)&&(a[d]=c[d])}}return a},c.shrinkBuf=function(a,b){return a.length===b?a:a.subarray?a.subarray(0,b):(a.length=b,a)};var e={arraySet:function(a,b,c,d,e){if(b.subarray&&a.subarray)return void a.set(b.subarray(c,c+d),e);for(var f=0;d>f;f++)a[e+f]=b[c+f]},flattenChunks:function(a){var b,c,d,e,f,g;for(d=0,b=0,c=a.length;c>b;b++)d+=a[b].length;for(g=new Uint8Array(d),e=0,b=0,c=a.length;c>b;b++)f=a[b],g.set(f,e),e+=f.length;return g}},f={arraySet:function(a,b,c,d,e){for(var f=0;d>f;f++)a[e+f]=b[c+f]},flattenChunks:function(a){return[].concat.apply([],a)}};c.setTyped=function(a){a?(c.Buf8=Uint8Array,c.Buf16=Uint16Array,c.Buf32=Int32Array,c.assign(c,e)):(c.Buf8=Array,c.Buf16=Array,c.Buf32=Array,c.assign(c,f))},c.setTyped(d)},{}],28:[function(a,b,c){"use strict";function d(a,b){if(65537>b&&(a.subarray&&g||!a.subarray&&f))return String.fromCharCode.apply(null,e.shrinkBuf(a,b));for(var c="",d=0;b>d;d++)c+=String.fromCharCode(a[d]);return c}var e=a("./common"),f=!0,g=!0;try{String.fromCharCode.apply(null,[0])}catch(h){f=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(h){g=!1}for(var i=new e.Buf8(256),j=0;256>j;j++)i[j]=j>=252?6:j>=248?5:j>=240?4:j>=224?3:j>=192?2:1;i[254]=i[254]=1,c.string2buf=function(a){var b,c,d,f,g,h=a.length,i=0;for(f=0;h>f;f++)c=a.charCodeAt(f),55296===(64512&c)&&h>f+1&&(d=a.charCodeAt(f+1),56320===(64512&d)&&(c=65536+(c-55296<<10)+(d-56320),f++)),i+=128>c?1:2048>c?2:65536>c?3:4;for(b=new e.Buf8(i),g=0,f=0;i>g;f++)c=a.charCodeAt(f),55296===(64512&c)&&h>f+1&&(d=a.charCodeAt(f+1),56320===(64512&d)&&(c=65536+(c-55296<<10)+(d-56320),f++)),128>c?b[g++]=c:2048>c?(b[g++]=192|c>>>6,b[g++]=128|63&c):65536>c?(b[g++]=224|c>>>12,b[g++]=128|c>>>6&63,b[g++]=128|63&c):(b[g++]=240|c>>>18,b[g++]=128|c>>>12&63,b[g++]=128|c>>>6&63,b[g++]=128|63&c);return b},c.buf2binstring=function(a){return d(a,a.length)},c.binstring2buf=function(a){for(var b=new e.Buf8(a.length),c=0,d=b.length;d>c;c++)b[c]=a.charCodeAt(c);return b},c.buf2string=function(a,b){var c,e,f,g,h=b||a.length,j=new Array(2*h);for(e=0,c=0;h>c;)if(f=a[c++],128>f)j[e++]=f;else if(g=i[f],g>4)j[e++]=65533,c+=g-1;else{for(f&=2===g?31:3===g?15:7;g>1&&h>c;)f=f<<6|63&a[c++],g--;g>1?j[e++]=65533:65536>f?j[e++]=f:(f-=65536,j[e++]=55296|f>>10&1023,j[e++]=56320|1023&f)}return d(j,e)},c.utf8border=function(a,b){var c;for(b=b||a.length,b>a.length&&(b=a.length),c=b-1;c>=0&&128===(192&a[c]);)c--;return 0>c?b:0===c?b:c+i[a[c]]>b?c:b}},{"./common":27}],29:[function(a,b){"use strict";function c(a,b,c,d){for(var e=65535&a|0,f=a>>>16&65535|0,g=0;0!==c;){g=c>2e3?2e3:c,c-=g;do e=e+b[d++]|0,f=f+e|0;while(--g);e%=65521,f%=65521}return e|f<<16|0}b.exports=c},{}],30:[function(a,b){b.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],31:[function(a,b){"use strict";function c(){for(var a,b=[],c=0;256>c;c++){a=c;for(var d=0;8>d;d++)a=1&a?3988292384^a>>>1:a>>>1;b[c]=a}return b}function d(a,b,c,d){var f=e,g=d+c;a=-1^a;for(var h=d;g>h;h++)a=a>>>8^f[255&(a^b[h])];return-1^a}var e=c();b.exports=d},{}],32:[function(a,b,c){"use strict";function d(a,b){return a.msg=G[b],b}function e(a){return(a<<1)-(a>4?9:0)}function f(a){for(var b=a.length;--b>=0;)a[b]=0}function g(a){var b=a.state,c=b.pending;c>a.avail_out&&(c=a.avail_out),0!==c&&(C.arraySet(a.output,b.pending_buf,b.pending_out,c,a.next_out),a.next_out+=c,b.pending_out+=c,a.total_out+=c,a.avail_out-=c,b.pending-=c,0===b.pending&&(b.pending_out=0))}function h(a,b){D._tr_flush_block(a,a.block_start>=0?a.block_start:-1,a.strstart-a.block_start,b),a.block_start=a.strstart,g(a.strm)}function i(a,b){a.pending_buf[a.pending++]=b}function j(a,b){a.pending_buf[a.pending++]=b>>>8&255,a.pending_buf[a.pending++]=255&b}function k(a,b,c,d){var e=a.avail_in;return e>d&&(e=d),0===e?0:(a.avail_in-=e,C.arraySet(b,a.input,a.next_in,e,c),1===a.state.wrap?a.adler=E(a.adler,b,e,c):2===a.state.wrap&&(a.adler=F(a.adler,b,e,c)),a.next_in+=e,a.total_in+=e,e)}function l(a,b){var c,d,e=a.max_chain_length,f=a.strstart,g=a.prev_length,h=a.nice_match,i=a.strstart>a.w_size-jb?a.strstart-(a.w_size-jb):0,j=a.window,k=a.w_mask,l=a.prev,m=a.strstart+ib,n=j[f+g-1],o=j[f+g];a.prev_length>=a.good_match&&(e>>=2),h>a.lookahead&&(h=a.lookahead);do if(c=b,j[c+g]===o&&j[c+g-1]===n&&j[c]===j[f]&&j[++c]===j[f+1]){f+=2,c++;do;while(j[++f]===j[++c]&&j[++f]===j[++c]&&j[++f]===j[++c]&&j[++f]===j[++c]&&j[++f]===j[++c]&&j[++f]===j[++c]&&j[++f]===j[++c]&&j[++f]===j[++c]&&m>f);if(d=ib-(m-f),f=m-ib,d>g){if(a.match_start=b,g=d,d>=h)break;n=j[f+g-1],o=j[f+g]}}while((b=l[b&k])>i&&0!==--e);return g<=a.lookahead?g:a.lookahead}function m(a){var b,c,d,e,f,g=a.w_size;do{if(e=a.window_size-a.lookahead-a.strstart,a.strstart>=g+(g-jb)){C.arraySet(a.window,a.window,g,g,0),a.match_start-=g,a.strstart-=g,a.block_start-=g,c=a.hash_size,b=c;do d=a.head[--b],a.head[b]=d>=g?d-g:0;while(--c);c=g,b=c;do d=a.prev[--b],a.prev[b]=d>=g?d-g:0;while(--c);e+=g}if(0===a.strm.avail_in)break;if(c=k(a.strm,a.window,a.strstart+a.lookahead,e),a.lookahead+=c,a.lookahead+a.insert>=hb)for(f=a.strstart-a.insert,a.ins_h=a.window[f],a.ins_h=(a.ins_h<<a.hash_shift^a.window[f+1])&a.hash_mask;a.insert&&(a.ins_h=(a.ins_h<<a.hash_shift^a.window[f+hb-1])&a.hash_mask,a.prev[f&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=f,f++,a.insert--,!(a.lookahead+a.insert<hb)););}while(a.lookahead<jb&&0!==a.strm.avail_in)}function n(a,b){var c=65535;for(c>a.pending_buf_size-5&&(c=a.pending_buf_size-5);;){if(a.lookahead<=1){if(m(a),0===a.lookahead&&b===H)return sb;if(0===a.lookahead)break}a.strstart+=a.lookahead,a.lookahead=0;var d=a.block_start+c;if((0===a.strstart||a.strstart>=d)&&(a.lookahead=a.strstart-d,a.strstart=d,h(a,!1),0===a.strm.avail_out))return sb;if(a.strstart-a.block_start>=a.w_size-jb&&(h(a,!1),0===a.strm.avail_out))return sb}return a.insert=0,b===K?(h(a,!0),0===a.strm.avail_out?ub:vb):a.strstart>a.block_start&&(h(a,!1),0===a.strm.avail_out)?sb:sb}function o(a,b){for(var c,d;;){if(a.lookahead<jb){if(m(a),a.lookahead<jb&&b===H)return sb;if(0===a.lookahead)break}if(c=0,a.lookahead>=hb&&(a.ins_h=(a.ins_h<<a.hash_shift^a.window[a.strstart+hb-1])&a.hash_mask,c=a.prev[a.strstart&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=a.strstart),0!==c&&a.strstart-c<=a.w_size-jb&&(a.match_length=l(a,c)),a.match_length>=hb)if(d=D._tr_tally(a,a.strstart-a.match_start,a.match_length-hb),a.lookahead-=a.match_length,a.match_length<=a.max_lazy_match&&a.lookahead>=hb){a.match_length--;do a.strstart++,a.ins_h=(a.ins_h<<a.hash_shift^a.window[a.strstart+hb-1])&a.hash_mask,c=a.prev[a.strstart&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=a.strstart;while(0!==--a.match_length);a.strstart++}else a.strstart+=a.match_length,a.match_length=0,a.ins_h=a.window[a.strstart],a.ins_h=(a.ins_h<<a.hash_shift^a.window[a.strstart+1])&a.hash_mask;else d=D._tr_tally(a,0,a.window[a.strstart]),a.lookahead--,a.strstart++;if(d&&(h(a,!1),0===a.strm.avail_out))return sb}return a.insert=a.strstart<hb-1?a.strstart:hb-1,b===K?(h(a,!0),0===a.strm.avail_out?ub:vb):a.last_lit&&(h(a,!1),0===a.strm.avail_out)?sb:tb}function p(a,b){for(var c,d,e;;){if(a.lookahead<jb){if(m(a),a.lookahead<jb&&b===H)return sb;if(0===a.lookahead)break}if(c=0,a.lookahead>=hb&&(a.ins_h=(a.ins_h<<a.hash_shift^a.window[a.strstart+hb-1])&a.hash_mask,c=a.prev[a.strstart&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=a.strstart),a.prev_length=a.match_length,a.prev_match=a.match_start,a.match_length=hb-1,0!==c&&a.prev_length<a.max_lazy_match&&a.strstart-c<=a.w_size-jb&&(a.match_length=l(a,c),a.match_length<=5&&(a.strategy===S||a.match_length===hb&&a.strstart-a.match_start>4096)&&(a.match_length=hb-1)),a.prev_length>=hb&&a.match_length<=a.prev_length){e=a.strstart+a.lookahead-hb,d=D._tr_tally(a,a.strstart-1-a.prev_match,a.prev_length-hb),a.lookahead-=a.prev_length-1,a.prev_length-=2;do++a.strstart<=e&&(a.ins_h=(a.ins_h<<a.hash_shift^a.window[a.strstart+hb-1])&a.hash_mask,c=a.prev[a.strstart&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=a.strstart);while(0!==--a.prev_length);if(a.match_available=0,a.match_length=hb-1,a.strstart++,d&&(h(a,!1),0===a.strm.avail_out))return sb}else if(a.match_available){if(d=D._tr_tally(a,0,a.window[a.strstart-1]),d&&h(a,!1),a.strstart++,a.lookahead--,0===a.strm.avail_out)return sb}else a.match_available=1,a.strstart++,a.lookahead--}return a.match_available&&(d=D._tr_tally(a,0,a.window[a.strstart-1]),a.match_available=0),a.insert=a.strstart<hb-1?a.strstart:hb-1,b===K?(h(a,!0),0===a.strm.avail_out?ub:vb):a.last_lit&&(h(a,!1),0===a.strm.avail_out)?sb:tb}function q(a,b){for(var c,d,e,f,g=a.window;;){if(a.lookahead<=ib){if(m(a),a.lookahead<=ib&&b===H)return sb;if(0===a.lookahead)break}if(a.match_length=0,a.lookahead>=hb&&a.strstart>0&&(e=a.strstart-1,d=g[e],d===g[++e]&&d===g[++e]&&d===g[++e])){f=a.strstart+ib;do;while(d===g[++e]&&d===g[++e]&&d===g[++e]&&d===g[++e]&&d===g[++e]&&d===g[++e]&&d===g[++e]&&d===g[++e]&&f>e);a.match_length=ib-(f-e),a.match_length>a.lookahead&&(a.match_length=a.lookahead)}if(a.match_length>=hb?(c=D._tr_tally(a,1,a.match_length-hb),a.lookahead-=a.match_length,a.strstart+=a.match_length,a.match_length=0):(c=D._tr_tally(a,0,a.window[a.strstart]),a.lookahead--,a.strstart++),c&&(h(a,!1),0===a.strm.avail_out))return sb}return a.insert=0,b===K?(h(a,!0),0===a.strm.avail_out?ub:vb):a.last_lit&&(h(a,!1),0===a.strm.avail_out)?sb:tb}function r(a,b){for(var c;;){if(0===a.lookahead&&(m(a),0===a.lookahead)){if(b===H)return sb;break}if(a.match_length=0,c=D._tr_tally(a,0,a.window[a.strstart]),a.lookahead--,a.strstart++,c&&(h(a,!1),0===a.strm.avail_out))return sb}return a.insert=0,b===K?(h(a,!0),0===a.strm.avail_out?ub:vb):a.last_lit&&(h(a,!1),0===a.strm.avail_out)?sb:tb}function s(a){a.window_size=2*a.w_size,f(a.head),a.max_lazy_match=B[a.level].max_lazy,a.good_match=B[a.level].good_length,a.nice_match=B[a.level].nice_length,a.max_chain_length=B[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=hb-1,a.match_available=0,a.ins_h=0}function t(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=Y,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new C.Buf16(2*fb),this.dyn_dtree=new C.Buf16(2*(2*db+1)),this.bl_tree=new C.Buf16(2*(2*eb+1)),f(this.dyn_ltree),f(this.dyn_dtree),f(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new C.Buf16(gb+1),this.heap=new C.Buf16(2*cb+1),f(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new C.Buf16(2*cb+1),f(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function u(a){var b;return a&&a.state?(a.total_in=a.total_out=0,a.data_type=X,b=a.state,b.pending=0,b.pending_out=0,b.wrap<0&&(b.wrap=-b.wrap),b.status=b.wrap?lb:qb,a.adler=2===b.wrap?0:1,b.last_flush=H,D._tr_init(b),M):d(a,O)}function v(a){var b=u(a);return b===M&&s(a.state),b}function w(a,b){return a&&a.state?2!==a.state.wrap?O:(a.state.gzhead=b,M):O}function x(a,b,c,e,f,g){if(!a)return O;var h=1;if(b===R&&(b=6),0>e?(h=0,e=-e):e>15&&(h=2,e-=16),1>f||f>Z||c!==Y||8>e||e>15||0>b||b>9||0>g||g>V)return d(a,O);8===e&&(e=9);var i=new t;return a.state=i,i.strm=a,i.wrap=h,i.gzhead=null,i.w_bits=e,i.w_size=1<<i.w_bits,i.w_mask=i.w_size-1,i.hash_bits=f+7,i.hash_size=1<<i.hash_bits,i.hash_mask=i.hash_size-1,i.hash_shift=~~((i.hash_bits+hb-1)/hb),i.window=new C.Buf8(2*i.w_size),i.head=new C.Buf16(i.hash_size),i.prev=new C.Buf16(i.w_size),i.lit_bufsize=1<<f+6,i.pending_buf_size=4*i.lit_bufsize,i.pending_buf=new C.Buf8(i.pending_buf_size),i.d_buf=i.lit_bufsize>>1,i.l_buf=3*i.lit_bufsize,i.level=b,i.strategy=g,i.method=c,v(a)}function y(a,b){return x(a,b,Y,$,_,W)}function z(a,b){var c,h,k,l;if(!a||!a.state||b>L||0>b)return a?d(a,O):O;if(h=a.state,!a.output||!a.input&&0!==a.avail_in||h.status===rb&&b!==K)return d(a,0===a.avail_out?Q:O);if(h.strm=a,c=h.last_flush,h.last_flush=b,h.status===lb)if(2===h.wrap)a.adler=0,i(h,31),i(h,139),i(h,8),h.gzhead?(i(h,(h.gzhead.text?1:0)+(h.gzhead.hcrc?2:0)+(h.gzhead.extra?4:0)+(h.gzhead.name?8:0)+(h.gzhead.comment?16:0)),i(h,255&h.gzhead.time),i(h,h.gzhead.time>>8&255),i(h,h.gzhead.time>>16&255),i(h,h.gzhead.time>>24&255),i(h,9===h.level?2:h.strategy>=T||h.level<2?4:0),i(h,255&h.gzhead.os),h.gzhead.extra&&h.gzhead.extra.length&&(i(h,255&h.gzhead.extra.length),i(h,h.gzhead.extra.length>>8&255)),h.gzhead.hcrc&&(a.adler=F(a.adler,h.pending_buf,h.pending,0)),h.gzindex=0,h.status=mb):(i(h,0),i(h,0),i(h,0),i(h,0),i(h,0),i(h,9===h.level?2:h.strategy>=T||h.level<2?4:0),i(h,wb),h.status=qb);else{var m=Y+(h.w_bits-8<<4)<<8,n=-1;n=h.strategy>=T||h.level<2?0:h.level<6?1:6===h.level?2:3,m|=n<<6,0!==h.strstart&&(m|=kb),m+=31-m%31,h.status=qb,j(h,m),0!==h.strstart&&(j(h,a.adler>>>16),j(h,65535&a.adler)),a.adler=1}if(h.status===mb)if(h.gzhead.extra){for(k=h.pending;h.gzindex<(65535&h.gzhead.extra.length)&&(h.pending!==h.pending_buf_size||(h.gzhead.hcrc&&h.pending>k&&(a.adler=F(a.adler,h.pending_buf,h.pending-k,k)),g(a),k=h.pending,h.pending!==h.pending_buf_size));)i(h,255&h.gzhead.extra[h.gzindex]),h.gzindex++;h.gzhead.hcrc&&h.pending>k&&(a.adler=F(a.adler,h.pending_buf,h.pending-k,k)),h.gzindex===h.gzhead.extra.length&&(h.gzindex=0,h.status=nb)}else h.status=nb;if(h.status===nb)if(h.gzhead.name){k=h.pending;do{if(h.pending===h.pending_buf_size&&(h.gzhead.hcrc&&h.pending>k&&(a.adler=F(a.adler,h.pending_buf,h.pending-k,k)),g(a),k=h.pending,h.pending===h.pending_buf_size)){l=1;break}l=h.gzindex<h.gzhead.name.length?255&h.gzhead.name.charCodeAt(h.gzindex++):0,i(h,l)}while(0!==l);h.gzhead.hcrc&&h.pending>k&&(a.adler=F(a.adler,h.pending_buf,h.pending-k,k)),0===l&&(h.gzindex=0,h.status=ob)}else h.status=ob;if(h.status===ob)if(h.gzhead.comment){k=h.pending;do{if(h.pending===h.pending_buf_size&&(h.gzhead.hcrc&&h.pending>k&&(a.adler=F(a.adler,h.pending_buf,h.pending-k,k)),g(a),k=h.pending,h.pending===h.pending_buf_size)){l=1;break}l=h.gzindex<h.gzhead.comment.length?255&h.gzhead.comment.charCodeAt(h.gzindex++):0,i(h,l)}while(0!==l);h.gzhead.hcrc&&h.pending>k&&(a.adler=F(a.adler,h.pending_buf,h.pending-k,k)),0===l&&(h.status=pb)}else h.status=pb;if(h.status===pb&&(h.gzhead.hcrc?(h.pending+2>h.pending_buf_size&&g(a),h.pending+2<=h.pending_buf_size&&(i(h,255&a.adler),i(h,a.adler>>8&255),a.adler=0,h.status=qb)):h.status=qb),0!==h.pending){if(g(a),0===a.avail_out)return h.last_flush=-1,M}else if(0===a.avail_in&&e(b)<=e(c)&&b!==K)return d(a,Q);if(h.status===rb&&0!==a.avail_in)return d(a,Q);if(0!==a.avail_in||0!==h.lookahead||b!==H&&h.status!==rb){var o=h.strategy===T?r(h,b):h.strategy===U?q(h,b):B[h.level].func(h,b);if((o===ub||o===vb)&&(h.status=rb),o===sb||o===ub)return 0===a.avail_out&&(h.last_flush=-1),M;if(o===tb&&(b===I?D._tr_align(h):b!==L&&(D._tr_stored_block(h,0,0,!1),b===J&&(f(h.head),0===h.lookahead&&(h.strstart=0,h.block_start=0,h.insert=0))),g(a),0===a.avail_out))return h.last_flush=-1,M}return b!==K?M:h.wrap<=0?N:(2===h.wrap?(i(h,255&a.adler),i(h,a.adler>>8&255),i(h,a.adler>>16&255),i(h,a.adler>>24&255),i(h,255&a.total_in),i(h,a.total_in>>8&255),i(h,a.total_in>>16&255),i(h,a.total_in>>24&255)):(j(h,a.adler>>>16),j(h,65535&a.adler)),g(a),h.wrap>0&&(h.wrap=-h.wrap),0!==h.pending?M:N)}function A(a){var b;return a&&a.state?(b=a.state.status,b!==lb&&b!==mb&&b!==nb&&b!==ob&&b!==pb&&b!==qb&&b!==rb?d(a,O):(a.state=null,b===qb?d(a,P):M)):O}var B,C=a("../utils/common"),D=a("./trees"),E=a("./adler32"),F=a("./crc32"),G=a("./messages"),H=0,I=1,J=3,K=4,L=5,M=0,N=1,O=-2,P=-3,Q=-5,R=-1,S=1,T=2,U=3,V=4,W=0,X=2,Y=8,Z=9,$=15,_=8,ab=29,bb=256,cb=bb+1+ab,db=30,eb=19,fb=2*cb+1,gb=15,hb=3,ib=258,jb=ib+hb+1,kb=32,lb=42,mb=69,nb=73,ob=91,pb=103,qb=113,rb=666,sb=1,tb=2,ub=3,vb=4,wb=3,xb=function(a,b,c,d,e){this.good_length=a,this.max_lazy=b,this.nice_length=c,this.max_chain=d,this.func=e};B=[new xb(0,0,0,0,n),new xb(4,4,8,4,o),new xb(4,5,16,8,o),new xb(4,6,32,32,o),new xb(4,4,16,16,p),new xb(8,16,32,32,p),new xb(8,16,128,128,p),new xb(8,32,128,256,p),new xb(32,128,258,1024,p),new xb(32,258,258,4096,p)],c.deflateInit=y,c.deflateInit2=x,c.deflateReset=v,c.deflateResetKeep=u,c.deflateSetHeader=w,c.deflate=z,c.deflateEnd=A,c.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":27,"./adler32":29,"./crc32":31,"./messages":37,"./trees":38}],33:[function(a,b){"use strict";function c(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}b.exports=c},{}],34:[function(a,b){"use strict";var c=30,d=12;b.exports=function(a,b){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C;e=a.state,f=a.next_in,B=a.input,g=f+(a.avail_in-5),h=a.next_out,C=a.output,i=h-(b-a.avail_out),j=h+(a.avail_out-257),k=e.dmax,l=e.wsize,m=e.whave,n=e.wnext,o=e.window,p=e.hold,q=e.bits,r=e.lencode,s=e.distcode,t=(1<<e.lenbits)-1,u=(1<<e.distbits)-1;a:do{15>q&&(p+=B[f++]<<q,q+=8,p+=B[f++]<<q,q+=8),v=r[p&t];b:for(;;){if(w=v>>>24,p>>>=w,q-=w,w=v>>>16&255,0===w)C[h++]=65535&v;else{if(!(16&w)){if(0===(64&w)){v=r[(65535&v)+(p&(1<<w)-1)];continue b}if(32&w){e.mode=d;break a}a.msg="invalid literal/length code",e.mode=c;break a}x=65535&v,w&=15,w&&(w>q&&(p+=B[f++]<<q,q+=8),x+=p&(1<<w)-1,p>>>=w,q-=w),15>q&&(p+=B[f++]<<q,q+=8,p+=B[f++]<<q,q+=8),v=s[p&u];c:for(;;){if(w=v>>>24,p>>>=w,q-=w,w=v>>>16&255,!(16&w)){if(0===(64&w)){v=s[(65535&v)+(p&(1<<w)-1)];continue c}a.msg="invalid distance code",e.mode=c;break a}if(y=65535&v,w&=15,w>q&&(p+=B[f++]<<q,q+=8,w>q&&(p+=B[f++]<<q,q+=8)),y+=p&(1<<w)-1,y>k){a.msg="invalid distance too far back",e.mode=c;break a}if(p>>>=w,q-=w,w=h-i,y>w){if(w=y-w,w>m&&e.sane){a.msg="invalid distance too far back",e.mode=c;break a}if(z=0,A=o,0===n){if(z+=l-w,x>w){x-=w;do C[h++]=o[z++];while(--w);z=h-y,A=C}}else if(w>n){if(z+=l+n-w,w-=n,x>w){x-=w;do C[h++]=o[z++];while(--w);if(z=0,x>n){w=n,x-=w;do C[h++]=o[z++];while(--w);z=h-y,A=C}}}else if(z+=n-w,x>w){x-=w;do C[h++]=o[z++];while(--w);z=h-y,A=C}for(;x>2;)C[h++]=A[z++],C[h++]=A[z++],C[h++]=A[z++],x-=3;x&&(C[h++]=A[z++],x>1&&(C[h++]=A[z++]))}else{z=h-y;do C[h++]=C[z++],C[h++]=C[z++],C[h++]=C[z++],x-=3;while(x>2);x&&(C[h++]=C[z++],x>1&&(C[h++]=C[z++]))}break}}break}}while(g>f&&j>h);x=q>>3,f-=x,q-=x<<3,p&=(1<<q)-1,a.next_in=f,a.next_out=h,a.avail_in=g>f?5+(g-f):5-(f-g),a.avail_out=j>h?257+(j-h):257-(h-j),e.hold=p,e.bits=q}},{}],35:[function(a,b,c){"use strict";function d(a){return(a>>>24&255)+(a>>>8&65280)+((65280&a)<<8)+((255&a)<<24)}function e(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new r.Buf16(320),this.work=new r.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function f(a){var b;return a&&a.state?(b=a.state,a.total_in=a.total_out=b.total=0,a.msg="",b.wrap&&(a.adler=1&b.wrap),b.mode=K,b.last=0,b.havedict=0,b.dmax=32768,b.head=null,b.hold=0,b.bits=0,b.lencode=b.lendyn=new r.Buf32(ob),b.distcode=b.distdyn=new r.Buf32(pb),b.sane=1,b.back=-1,C):F}function g(a){var b;return a&&a.state?(b=a.state,b.wsize=0,b.whave=0,b.wnext=0,f(a)):F}function h(a,b){var c,d;return a&&a.state?(d=a.state,0>b?(c=0,b=-b):(c=(b>>4)+1,48>b&&(b&=15)),b&&(8>b||b>15)?F:(null!==d.window&&d.wbits!==b&&(d.window=null),d.wrap=c,d.wbits=b,g(a))):F}function i(a,b){var c,d;return a?(d=new e,a.state=d,d.window=null,c=h(a,b),c!==C&&(a.state=null),c):F}function j(a){return i(a,rb)}function k(a){if(sb){var b;for(p=new r.Buf32(512),q=new r.Buf32(32),b=0;144>b;)a.lens[b++]=8;for(;256>b;)a.lens[b++]=9;for(;280>b;)a.lens[b++]=7;for(;288>b;)a.lens[b++]=8;for(v(x,a.lens,0,288,p,0,a.work,{bits:9}),b=0;32>b;)a.lens[b++]=5;v(y,a.lens,0,32,q,0,a.work,{bits:5}),sb=!1}a.lencode=p,a.lenbits=9,a.distcode=q,a.distbits=5}function l(a,b,c,d){var e,f=a.state;return null===f.window&&(f.wsize=1<<f.wbits,f.wnext=0,f.whave=0,f.window=new r.Buf8(f.wsize)),d>=f.wsize?(r.arraySet(f.window,b,c-f.wsize,f.wsize,0),f.wnext=0,f.whave=f.wsize):(e=f.wsize-f.wnext,e>d&&(e=d),r.arraySet(f.window,b,c-d,e,f.wnext),d-=e,d?(r.arraySet(f.window,b,c-d,d,0),f.wnext=d,f.whave=f.wsize):(f.wnext+=e,f.wnext===f.wsize&&(f.wnext=0),f.whave<f.wsize&&(f.whave+=e))),0}function m(a,b){var c,e,f,g,h,i,j,m,n,o,p,q,ob,pb,qb,rb,sb,tb,ub,vb,wb,xb,yb,zb,Ab=0,Bb=new r.Buf8(4),Cb=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!a||!a.state||!a.output||!a.input&&0!==a.avail_in)return F;c=a.state,c.mode===V&&(c.mode=W),h=a.next_out,f=a.output,j=a.avail_out,g=a.next_in,e=a.input,i=a.avail_in,m=c.hold,n=c.bits,o=i,p=j,xb=C;a:for(;;)switch(c.mode){case K:if(0===c.wrap){c.mode=W;break}for(;16>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(2&c.wrap&&35615===m){c.check=0,Bb[0]=255&m,Bb[1]=m>>>8&255,c.check=t(c.check,Bb,2,0),m=0,n=0,c.mode=L;break}if(c.flags=0,c.head&&(c.head.done=!1),!(1&c.wrap)||(((255&m)<<8)+(m>>8))%31){a.msg="incorrect header check",c.mode=lb;break}if((15&m)!==J){a.msg="unknown compression method",c.mode=lb;break}if(m>>>=4,n-=4,wb=(15&m)+8,0===c.wbits)c.wbits=wb;else if(wb>c.wbits){a.msg="invalid window size",c.mode=lb;break}c.dmax=1<<wb,a.adler=c.check=1,c.mode=512&m?T:V,m=0,n=0;break;case L:for(;16>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(c.flags=m,(255&c.flags)!==J){a.msg="unknown compression method",c.mode=lb;break}if(57344&c.flags){a.msg="unknown header flags set",c.mode=lb;break}c.head&&(c.head.text=m>>8&1),512&c.flags&&(Bb[0]=255&m,Bb[1]=m>>>8&255,c.check=t(c.check,Bb,2,0)),m=0,n=0,c.mode=M;case M:for(;32>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}c.head&&(c.head.time=m),512&c.flags&&(Bb[0]=255&m,Bb[1]=m>>>8&255,Bb[2]=m>>>16&255,Bb[3]=m>>>24&255,c.check=t(c.check,Bb,4,0)),m=0,n=0,c.mode=N;case N:for(;16>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}c.head&&(c.head.xflags=255&m,c.head.os=m>>8),512&c.flags&&(Bb[0]=255&m,Bb[1]=m>>>8&255,c.check=t(c.check,Bb,2,0)),m=0,n=0,c.mode=O;case O:if(1024&c.flags){for(;16>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}c.length=m,c.head&&(c.head.extra_len=m),512&c.flags&&(Bb[0]=255&m,Bb[1]=m>>>8&255,c.check=t(c.check,Bb,2,0)),m=0,n=0}else c.head&&(c.head.extra=null);c.mode=P;case P:if(1024&c.flags&&(q=c.length,q>i&&(q=i),q&&(c.head&&(wb=c.head.extra_len-c.length,c.head.extra||(c.head.extra=new Array(c.head.extra_len)),r.arraySet(c.head.extra,e,g,q,wb)),512&c.flags&&(c.check=t(c.check,e,q,g)),i-=q,g+=q,c.length-=q),c.length))break a;c.length=0,c.mode=Q;case Q:if(2048&c.flags){if(0===i)break a;q=0;do wb=e[g+q++],c.head&&wb&&c.length<65536&&(c.head.name+=String.fromCharCode(wb));while(wb&&i>q);if(512&c.flags&&(c.check=t(c.check,e,q,g)),i-=q,g+=q,wb)break a}else c.head&&(c.head.name=null);c.length=0,c.mode=R;case R:if(4096&c.flags){if(0===i)break a;q=0;do wb=e[g+q++],c.head&&wb&&c.length<65536&&(c.head.comment+=String.fromCharCode(wb));while(wb&&i>q);if(512&c.flags&&(c.check=t(c.check,e,q,g)),i-=q,g+=q,wb)break a}else c.head&&(c.head.comment=null);c.mode=S;case S:if(512&c.flags){for(;16>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(m!==(65535&c.check)){a.msg="header crc mismatch",c.mode=lb;break}m=0,n=0}c.head&&(c.head.hcrc=c.flags>>9&1,c.head.done=!0),a.adler=c.check=0,c.mode=V;break;case T:for(;32>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}a.adler=c.check=d(m),m=0,n=0,c.mode=U;case U:if(0===c.havedict)return a.next_out=h,a.avail_out=j,a.next_in=g,a.avail_in=i,c.hold=m,c.bits=n,E;a.adler=c.check=1,c.mode=V;case V:if(b===A||b===B)break a;case W:if(c.last){m>>>=7&n,n-=7&n,c.mode=ib;break}for(;3>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}switch(c.last=1&m,m>>>=1,n-=1,3&m){case 0:c.mode=X;break;case 1:if(k(c),c.mode=bb,b===B){m>>>=2,n-=2;break a}break;case 2:c.mode=$;break;case 3:a.msg="invalid block type",c.mode=lb}m>>>=2,n-=2;break;case X:for(m>>>=7&n,n-=7&n;32>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if((65535&m)!==(m>>>16^65535)){a.msg="invalid stored block lengths",c.mode=lb;break}if(c.length=65535&m,m=0,n=0,c.mode=Y,b===B)break a;case Y:c.mode=Z;case Z:if(q=c.length){if(q>i&&(q=i),q>j&&(q=j),0===q)break a;r.arraySet(f,e,g,q,h),i-=q,g+=q,j-=q,h+=q,c.length-=q;break}c.mode=V;break;case $:for(;14>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(c.nlen=(31&m)+257,m>>>=5,n-=5,c.ndist=(31&m)+1,m>>>=5,n-=5,c.ncode=(15&m)+4,m>>>=4,n-=4,c.nlen>286||c.ndist>30){a.msg="too many length or distance symbols",c.mode=lb;break}c.have=0,c.mode=_;case _:for(;c.have<c.ncode;){for(;3>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}c.lens[Cb[c.have++]]=7&m,m>>>=3,n-=3}for(;c.have<19;)c.lens[Cb[c.have++]]=0;if(c.lencode=c.lendyn,c.lenbits=7,yb={bits:c.lenbits},xb=v(w,c.lens,0,19,c.lencode,0,c.work,yb),c.lenbits=yb.bits,xb){a.msg="invalid code lengths set",c.mode=lb;break}c.have=0,c.mode=ab;case ab:for(;c.have<c.nlen+c.ndist;){for(;Ab=c.lencode[m&(1<<c.lenbits)-1],qb=Ab>>>24,rb=Ab>>>16&255,sb=65535&Ab,!(n>=qb);){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(16>sb)m>>>=qb,n-=qb,c.lens[c.have++]=sb;else{if(16===sb){for(zb=qb+2;zb>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(m>>>=qb,n-=qb,0===c.have){a.msg="invalid bit length repeat",c.mode=lb;break}wb=c.lens[c.have-1],q=3+(3&m),m>>>=2,n-=2}else if(17===sb){for(zb=qb+3;zb>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}m>>>=qb,n-=qb,wb=0,q=3+(7&m),m>>>=3,n-=3}else{for(zb=qb+7;zb>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}m>>>=qb,n-=qb,wb=0,q=11+(127&m),m>>>=7,n-=7}if(c.have+q>c.nlen+c.ndist){a.msg="invalid bit length repeat",c.mode=lb;break}for(;q--;)c.lens[c.have++]=wb}}if(c.mode===lb)break;if(0===c.lens[256]){a.msg="invalid code -- missing end-of-block",c.mode=lb;break}if(c.lenbits=9,yb={bits:c.lenbits},xb=v(x,c.lens,0,c.nlen,c.lencode,0,c.work,yb),c.lenbits=yb.bits,xb){a.msg="invalid literal/lengths set",c.mode=lb;break}if(c.distbits=6,c.distcode=c.distdyn,yb={bits:c.distbits},xb=v(y,c.lens,c.nlen,c.ndist,c.distcode,0,c.work,yb),c.distbits=yb.bits,xb){a.msg="invalid distances set",c.mode=lb;break}if(c.mode=bb,b===B)break a;case bb:c.mode=cb;case cb:if(i>=6&&j>=258){a.next_out=h,a.avail_out=j,a.next_in=g,a.avail_in=i,c.hold=m,c.bits=n,u(a,p),h=a.next_out,f=a.output,j=a.avail_out,g=a.next_in,e=a.input,i=a.avail_in,m=c.hold,n=c.bits,c.mode===V&&(c.back=-1);break}for(c.back=0;Ab=c.lencode[m&(1<<c.lenbits)-1],qb=Ab>>>24,rb=Ab>>>16&255,sb=65535&Ab,!(n>=qb);){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(rb&&0===(240&rb)){for(tb=qb,ub=rb,vb=sb;Ab=c.lencode[vb+((m&(1<<tb+ub)-1)>>tb)],qb=Ab>>>24,rb=Ab>>>16&255,sb=65535&Ab,!(n>=tb+qb);){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}m>>>=tb,n-=tb,c.back+=tb}if(m>>>=qb,n-=qb,c.back+=qb,c.length=sb,0===rb){c.mode=hb;break}if(32&rb){c.back=-1,c.mode=V;break}if(64&rb){a.msg="invalid literal/length code",c.mode=lb;break}c.extra=15&rb,c.mode=db;case db:if(c.extra){for(zb=c.extra;zb>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}c.length+=m&(1<<c.extra)-1,m>>>=c.extra,n-=c.extra,c.back+=c.extra}c.was=c.length,c.mode=eb;case eb:for(;Ab=c.distcode[m&(1<<c.distbits)-1],qb=Ab>>>24,rb=Ab>>>16&255,sb=65535&Ab,!(n>=qb);){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(0===(240&rb)){for(tb=qb,ub=rb,vb=sb;Ab=c.distcode[vb+((m&(1<<tb+ub)-1)>>tb)],qb=Ab>>>24,rb=Ab>>>16&255,sb=65535&Ab,!(n>=tb+qb);){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}m>>>=tb,n-=tb,c.back+=tb}if(m>>>=qb,n-=qb,c.back+=qb,64&rb){a.msg="invalid distance code",c.mode=lb;break}c.offset=sb,c.extra=15&rb,c.mode=fb;case fb:if(c.extra){for(zb=c.extra;zb>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}c.offset+=m&(1<<c.extra)-1,m>>>=c.extra,n-=c.extra,c.back+=c.extra}if(c.offset>c.dmax){a.msg="invalid distance too far back",c.mode=lb;break}c.mode=gb;case gb:if(0===j)break a;
+if(q=p-j,c.offset>q){if(q=c.offset-q,q>c.whave&&c.sane){a.msg="invalid distance too far back",c.mode=lb;break}q>c.wnext?(q-=c.wnext,ob=c.wsize-q):ob=c.wnext-q,q>c.length&&(q=c.length),pb=c.window}else pb=f,ob=h-c.offset,q=c.length;q>j&&(q=j),j-=q,c.length-=q;do f[h++]=pb[ob++];while(--q);0===c.length&&(c.mode=cb);break;case hb:if(0===j)break a;f[h++]=c.length,j--,c.mode=cb;break;case ib:if(c.wrap){for(;32>n;){if(0===i)break a;i--,m|=e[g++]<<n,n+=8}if(p-=j,a.total_out+=p,c.total+=p,p&&(a.adler=c.check=c.flags?t(c.check,f,p,h-p):s(c.check,f,p,h-p)),p=j,(c.flags?m:d(m))!==c.check){a.msg="incorrect data check",c.mode=lb;break}m=0,n=0}c.mode=jb;case jb:if(c.wrap&&c.flags){for(;32>n;){if(0===i)break a;i--,m+=e[g++]<<n,n+=8}if(m!==(4294967295&c.total)){a.msg="incorrect length check",c.mode=lb;break}m=0,n=0}c.mode=kb;case kb:xb=D;break a;case lb:xb=G;break a;case mb:return H;case nb:default:return F}return a.next_out=h,a.avail_out=j,a.next_in=g,a.avail_in=i,c.hold=m,c.bits=n,(c.wsize||p!==a.avail_out&&c.mode<lb&&(c.mode<ib||b!==z))&&l(a,a.output,a.next_out,p-a.avail_out)?(c.mode=mb,H):(o-=a.avail_in,p-=a.avail_out,a.total_in+=o,a.total_out+=p,c.total+=p,c.wrap&&p&&(a.adler=c.check=c.flags?t(c.check,f,p,a.next_out-p):s(c.check,f,p,a.next_out-p)),a.data_type=c.bits+(c.last?64:0)+(c.mode===V?128:0)+(c.mode===bb||c.mode===Y?256:0),(0===o&&0===p||b===z)&&xb===C&&(xb=I),xb)}function n(a){if(!a||!a.state)return F;var b=a.state;return b.window&&(b.window=null),a.state=null,C}function o(a,b){var c;return a&&a.state?(c=a.state,0===(2&c.wrap)?F:(c.head=b,b.done=!1,C)):F}var p,q,r=a("../utils/common"),s=a("./adler32"),t=a("./crc32"),u=a("./inffast"),v=a("./inftrees"),w=0,x=1,y=2,z=4,A=5,B=6,C=0,D=1,E=2,F=-2,G=-3,H=-4,I=-5,J=8,K=1,L=2,M=3,N=4,O=5,P=6,Q=7,R=8,S=9,T=10,U=11,V=12,W=13,X=14,Y=15,Z=16,$=17,_=18,ab=19,bb=20,cb=21,db=22,eb=23,fb=24,gb=25,hb=26,ib=27,jb=28,kb=29,lb=30,mb=31,nb=32,ob=852,pb=592,qb=15,rb=qb,sb=!0;c.inflateReset=g,c.inflateReset2=h,c.inflateResetKeep=f,c.inflateInit=j,c.inflateInit2=i,c.inflate=m,c.inflateEnd=n,c.inflateGetHeader=o,c.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":27,"./adler32":29,"./crc32":31,"./inffast":34,"./inftrees":36}],36:[function(a,b){"use strict";var c=a("../utils/common"),d=15,e=852,f=592,g=0,h=1,i=2,j=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],k=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],l=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],m=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];b.exports=function(a,b,n,o,p,q,r,s){var t,u,v,w,x,y,z,A,B,C=s.bits,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=null,O=0,P=new c.Buf16(d+1),Q=new c.Buf16(d+1),R=null,S=0;for(D=0;d>=D;D++)P[D]=0;for(E=0;o>E;E++)P[b[n+E]]++;for(H=C,G=d;G>=1&&0===P[G];G--);if(H>G&&(H=G),0===G)return p[q++]=20971520,p[q++]=20971520,s.bits=1,0;for(F=1;G>F&&0===P[F];F++);for(F>H&&(H=F),K=1,D=1;d>=D;D++)if(K<<=1,K-=P[D],0>K)return-1;if(K>0&&(a===g||1!==G))return-1;for(Q[1]=0,D=1;d>D;D++)Q[D+1]=Q[D]+P[D];for(E=0;o>E;E++)0!==b[n+E]&&(r[Q[b[n+E]]++]=E);if(a===g?(N=R=r,y=19):a===h?(N=j,O-=257,R=k,S-=257,y=256):(N=l,R=m,y=-1),M=0,E=0,D=F,x=q,I=H,J=0,v=-1,L=1<<H,w=L-1,a===h&&L>e||a===i&&L>f)return 1;for(var T=0;;){T++,z=D-J,r[E]<y?(A=0,B=r[E]):r[E]>y?(A=R[S+r[E]],B=N[O+r[E]]):(A=96,B=0),t=1<<D-J,u=1<<I,F=u;do u-=t,p[x+(M>>J)+u]=z<<24|A<<16|B|0;while(0!==u);for(t=1<<D-1;M&t;)t>>=1;if(0!==t?(M&=t-1,M+=t):M=0,E++,0===--P[D]){if(D===G)break;D=b[n+r[E]]}if(D>H&&(M&w)!==v){for(0===J&&(J=H),x+=F,I=D-J,K=1<<I;G>I+J&&(K-=P[I+J],!(0>=K));)I++,K<<=1;if(L+=1<<I,a===h&&L>e||a===i&&L>f)return 1;v=M&w,p[v]=H<<24|I<<16|x-q|0}}return 0!==M&&(p[x+M]=D-J<<24|64<<16|0),s.bits=H,0}},{"../utils/common":27}],37:[function(a,b){"use strict";b.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],38:[function(a,b,c){"use strict";function d(a){for(var b=a.length;--b>=0;)a[b]=0}function e(a){return 256>a?gb[a]:gb[256+(a>>>7)]}function f(a,b){a.pending_buf[a.pending++]=255&b,a.pending_buf[a.pending++]=b>>>8&255}function g(a,b,c){a.bi_valid>V-c?(a.bi_buf|=b<<a.bi_valid&65535,f(a,a.bi_buf),a.bi_buf=b>>V-a.bi_valid,a.bi_valid+=c-V):(a.bi_buf|=b<<a.bi_valid&65535,a.bi_valid+=c)}function h(a,b,c){g(a,c[2*b],c[2*b+1])}function i(a,b){var c=0;do c|=1&a,a>>>=1,c<<=1;while(--b>0);return c>>>1}function j(a){16===a.bi_valid?(f(a,a.bi_buf),a.bi_buf=0,a.bi_valid=0):a.bi_valid>=8&&(a.pending_buf[a.pending++]=255&a.bi_buf,a.bi_buf>>=8,a.bi_valid-=8)}function k(a,b){var c,d,e,f,g,h,i=b.dyn_tree,j=b.max_code,k=b.stat_desc.static_tree,l=b.stat_desc.has_stree,m=b.stat_desc.extra_bits,n=b.stat_desc.extra_base,o=b.stat_desc.max_length,p=0;for(f=0;U>=f;f++)a.bl_count[f]=0;for(i[2*a.heap[a.heap_max]+1]=0,c=a.heap_max+1;T>c;c++)d=a.heap[c],f=i[2*i[2*d+1]+1]+1,f>o&&(f=o,p++),i[2*d+1]=f,d>j||(a.bl_count[f]++,g=0,d>=n&&(g=m[d-n]),h=i[2*d],a.opt_len+=h*(f+g),l&&(a.static_len+=h*(k[2*d+1]+g)));if(0!==p){do{for(f=o-1;0===a.bl_count[f];)f--;a.bl_count[f]--,a.bl_count[f+1]+=2,a.bl_count[o]--,p-=2}while(p>0);for(f=o;0!==f;f--)for(d=a.bl_count[f];0!==d;)e=a.heap[--c],e>j||(i[2*e+1]!==f&&(a.opt_len+=(f-i[2*e+1])*i[2*e],i[2*e+1]=f),d--)}}function l(a,b,c){var d,e,f=new Array(U+1),g=0;for(d=1;U>=d;d++)f[d]=g=g+c[d-1]<<1;for(e=0;b>=e;e++){var h=a[2*e+1];0!==h&&(a[2*e]=i(f[h]++,h))}}function m(){var a,b,c,d,e,f=new Array(U+1);for(c=0,d=0;O-1>d;d++)for(ib[d]=c,a=0;a<1<<_[d];a++)hb[c++]=d;for(hb[c-1]=d,e=0,d=0;16>d;d++)for(jb[d]=e,a=0;a<1<<ab[d];a++)gb[e++]=d;for(e>>=7;R>d;d++)for(jb[d]=e<<7,a=0;a<1<<ab[d]-7;a++)gb[256+e++]=d;for(b=0;U>=b;b++)f[b]=0;for(a=0;143>=a;)eb[2*a+1]=8,a++,f[8]++;for(;255>=a;)eb[2*a+1]=9,a++,f[9]++;for(;279>=a;)eb[2*a+1]=7,a++,f[7]++;for(;287>=a;)eb[2*a+1]=8,a++,f[8]++;for(l(eb,Q+1,f),a=0;R>a;a++)fb[2*a+1]=5,fb[2*a]=i(a,5);kb=new nb(eb,_,P+1,Q,U),lb=new nb(fb,ab,0,R,U),mb=new nb(new Array(0),bb,0,S,W)}function n(a){var b;for(b=0;Q>b;b++)a.dyn_ltree[2*b]=0;for(b=0;R>b;b++)a.dyn_dtree[2*b]=0;for(b=0;S>b;b++)a.bl_tree[2*b]=0;a.dyn_ltree[2*X]=1,a.opt_len=a.static_len=0,a.last_lit=a.matches=0}function o(a){a.bi_valid>8?f(a,a.bi_buf):a.bi_valid>0&&(a.pending_buf[a.pending++]=a.bi_buf),a.bi_buf=0,a.bi_valid=0}function p(a,b,c,d){o(a),d&&(f(a,c),f(a,~c)),E.arraySet(a.pending_buf,a.window,b,c,a.pending),a.pending+=c}function q(a,b,c,d){var e=2*b,f=2*c;return a[e]<a[f]||a[e]===a[f]&&d[b]<=d[c]}function r(a,b,c){for(var d=a.heap[c],e=c<<1;e<=a.heap_len&&(e<a.heap_len&&q(b,a.heap[e+1],a.heap[e],a.depth)&&e++,!q(b,d,a.heap[e],a.depth));)a.heap[c]=a.heap[e],c=e,e<<=1;a.heap[c]=d}function s(a,b,c){var d,f,i,j,k=0;if(0!==a.last_lit)do d=a.pending_buf[a.d_buf+2*k]<<8|a.pending_buf[a.d_buf+2*k+1],f=a.pending_buf[a.l_buf+k],k++,0===d?h(a,f,b):(i=hb[f],h(a,i+P+1,b),j=_[i],0!==j&&(f-=ib[i],g(a,f,j)),d--,i=e(d),h(a,i,c),j=ab[i],0!==j&&(d-=jb[i],g(a,d,j)));while(k<a.last_lit);h(a,X,b)}function t(a,b){var c,d,e,f=b.dyn_tree,g=b.stat_desc.static_tree,h=b.stat_desc.has_stree,i=b.stat_desc.elems,j=-1;for(a.heap_len=0,a.heap_max=T,c=0;i>c;c++)0!==f[2*c]?(a.heap[++a.heap_len]=j=c,a.depth[c]=0):f[2*c+1]=0;for(;a.heap_len<2;)e=a.heap[++a.heap_len]=2>j?++j:0,f[2*e]=1,a.depth[e]=0,a.opt_len--,h&&(a.static_len-=g[2*e+1]);for(b.max_code=j,c=a.heap_len>>1;c>=1;c--)r(a,f,c);e=i;do c=a.heap[1],a.heap[1]=a.heap[a.heap_len--],r(a,f,1),d=a.heap[1],a.heap[--a.heap_max]=c,a.heap[--a.heap_max]=d,f[2*e]=f[2*c]+f[2*d],a.depth[e]=(a.depth[c]>=a.depth[d]?a.depth[c]:a.depth[d])+1,f[2*c+1]=f[2*d+1]=e,a.heap[1]=e++,r(a,f,1);while(a.heap_len>=2);a.heap[--a.heap_max]=a.heap[1],k(a,b),l(f,j,a.bl_count)}function u(a,b,c){var d,e,f=-1,g=b[1],h=0,i=7,j=4;for(0===g&&(i=138,j=3),b[2*(c+1)+1]=65535,d=0;c>=d;d++)e=g,g=b[2*(d+1)+1],++h<i&&e===g||(j>h?a.bl_tree[2*e]+=h:0!==e?(e!==f&&a.bl_tree[2*e]++,a.bl_tree[2*Y]++):10>=h?a.bl_tree[2*Z]++:a.bl_tree[2*$]++,h=0,f=e,0===g?(i=138,j=3):e===g?(i=6,j=3):(i=7,j=4))}function v(a,b,c){var d,e,f=-1,i=b[1],j=0,k=7,l=4;for(0===i&&(k=138,l=3),d=0;c>=d;d++)if(e=i,i=b[2*(d+1)+1],!(++j<k&&e===i)){if(l>j){do h(a,e,a.bl_tree);while(0!==--j)}else 0!==e?(e!==f&&(h(a,e,a.bl_tree),j--),h(a,Y,a.bl_tree),g(a,j-3,2)):10>=j?(h(a,Z,a.bl_tree),g(a,j-3,3)):(h(a,$,a.bl_tree),g(a,j-11,7));j=0,f=e,0===i?(k=138,l=3):e===i?(k=6,l=3):(k=7,l=4)}}function w(a){var b;for(u(a,a.dyn_ltree,a.l_desc.max_code),u(a,a.dyn_dtree,a.d_desc.max_code),t(a,a.bl_desc),b=S-1;b>=3&&0===a.bl_tree[2*cb[b]+1];b--);return a.opt_len+=3*(b+1)+5+5+4,b}function x(a,b,c,d){var e;for(g(a,b-257,5),g(a,c-1,5),g(a,d-4,4),e=0;d>e;e++)g(a,a.bl_tree[2*cb[e]+1],3);v(a,a.dyn_ltree,b-1),v(a,a.dyn_dtree,c-1)}function y(a){var b,c=4093624447;for(b=0;31>=b;b++,c>>>=1)if(1&c&&0!==a.dyn_ltree[2*b])return G;if(0!==a.dyn_ltree[18]||0!==a.dyn_ltree[20]||0!==a.dyn_ltree[26])return H;for(b=32;P>b;b++)if(0!==a.dyn_ltree[2*b])return H;return G}function z(a){pb||(m(),pb=!0),a.l_desc=new ob(a.dyn_ltree,kb),a.d_desc=new ob(a.dyn_dtree,lb),a.bl_desc=new ob(a.bl_tree,mb),a.bi_buf=0,a.bi_valid=0,n(a)}function A(a,b,c,d){g(a,(J<<1)+(d?1:0),3),p(a,b,c,!0)}function B(a){g(a,K<<1,3),h(a,X,eb),j(a)}function C(a,b,c,d){var e,f,h=0;a.level>0?(a.strm.data_type===I&&(a.strm.data_type=y(a)),t(a,a.l_desc),t(a,a.d_desc),h=w(a),e=a.opt_len+3+7>>>3,f=a.static_len+3+7>>>3,e>=f&&(e=f)):e=f=c+5,e>=c+4&&-1!==b?A(a,b,c,d):a.strategy===F||f===e?(g(a,(K<<1)+(d?1:0),3),s(a,eb,fb)):(g(a,(L<<1)+(d?1:0),3),x(a,a.l_desc.max_code+1,a.d_desc.max_code+1,h+1),s(a,a.dyn_ltree,a.dyn_dtree)),n(a),d&&o(a)}function D(a,b,c){return a.pending_buf[a.d_buf+2*a.last_lit]=b>>>8&255,a.pending_buf[a.d_buf+2*a.last_lit+1]=255&b,a.pending_buf[a.l_buf+a.last_lit]=255&c,a.last_lit++,0===b?a.dyn_ltree[2*c]++:(a.matches++,b--,a.dyn_ltree[2*(hb[c]+P+1)]++,a.dyn_dtree[2*e(b)]++),a.last_lit===a.lit_bufsize-1}var E=a("../utils/common"),F=4,G=0,H=1,I=2,J=0,K=1,L=2,M=3,N=258,O=29,P=256,Q=P+1+O,R=30,S=19,T=2*Q+1,U=15,V=16,W=7,X=256,Y=16,Z=17,$=18,_=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],ab=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],bb=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],cb=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],db=512,eb=new Array(2*(Q+2));d(eb);var fb=new Array(2*R);d(fb);var gb=new Array(db);d(gb);var hb=new Array(N-M+1);d(hb);var ib=new Array(O);d(ib);var jb=new Array(R);d(jb);var kb,lb,mb,nb=function(a,b,c,d,e){this.static_tree=a,this.extra_bits=b,this.extra_base=c,this.elems=d,this.max_length=e,this.has_stree=a&&a.length},ob=function(a,b){this.dyn_tree=a,this.max_code=0,this.stat_desc=b},pb=!1;c._tr_init=z,c._tr_stored_block=A,c._tr_flush_block=C,c._tr_tally=D,c._tr_align=B},{"../utils/common":27}],39:[function(a,b){"use strict";function c(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}b.exports=c},{}]},{},[9])(9)}); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/mannwhitneyu/README.chromium b/chromium/third_party/catapult/tracing/third_party/mannwhitneyu/README.chromium
new file mode 100644
index 00000000000..78439f32f52
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/mannwhitneyu/README.chromium
@@ -0,0 +1,15 @@
+Name: Mann-Whitney U Test
+Short Name: mannwhitneyu
+URL: https://gist.github.com/gungorbudak/1c3989cc26b9567c6e50
+Version: 0
+Revision: f8f2918a366798793f5fb7e92de0df9142feb737
+Date: Jan 16 2016
+License: MIT
+License File: NOT_SHIPPED
+Security Critical: no
+
+Description:
+Mann Whitney U Test implemented in javascript
+
+Local Modifications:
+None.
diff --git a/chromium/third_party/catapult/tracing/third_party/mannwhitneyu/mannwhitneyu.js b/chromium/third_party/catapult/tracing/third_party/mannwhitneyu/mannwhitneyu.js
new file mode 100644
index 00000000000..2e2bda544b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/mannwhitneyu/mannwhitneyu.js
@@ -0,0 +1,197 @@
+'use strict';
+
+(function(exports) {
+
+ var rank = {
+ /*
+ * Standart ranking
+ *
+ * The MIT License, Copyright (c) 2014 Ben Magyar
+ */
+ standard: function(array, key) {
+ // sort the array
+ array = array.sort(function(a, b) {
+ var x = a[key];
+ var y = b[key];
+ return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+ });
+ // assign a naive ranking
+ for (var i = 1; i < array.length + 1; i++) {
+ array[i - 1]['rank'] = i;
+ }
+ return array;
+ },
+ /*
+ * Fractional ranking
+ *
+ * The MIT License, Copyright (c) 2014 Ben Magyar
+ */
+ fractional: function(array, key) {
+ array = this.standard(array, key);
+ // now apply fractional
+ var pos = 0;
+ while (pos < array.length) {
+ var sum = 0;
+ var i = 0;
+ for (i = 0; array[pos + i + 1] && (array[pos + i][key] === array[pos + i + 1][key]); i++) {
+ sum += array[pos + i]['rank'];
+ }
+ sum += array[pos + i]['rank'];
+ var endPos = pos + i + 1;
+ for (pos; pos < endPos; pos++) {
+ array[pos]['rank'] = sum / (i + 1);
+ }
+ pos = endPos;
+ }
+ return array;
+ },
+ rank: function(x, y) {
+ var nx = x.length,
+ ny = y.length,
+ combined = [],
+ ranked;
+ while (nx--) {
+ combined.push({
+ set: 'x',
+ val: x[nx]
+ });
+ }
+ while (ny--) {
+ combined.push({
+ set: 'y',
+ val: y[ny]
+ });
+ }
+ ranked = this.fractional(combined, 'val');
+ return ranked
+ }
+ };
+
+ /*
+ * Error function
+ *
+ * The MIT License, Copyright (c) 2013 jStat
+ */
+ var erf = function erf(x) {
+ var cof = [-1.3026537197817094, 6.4196979235649026e-1, 1.9476473204185836e-2, -9.561514786808631e-3, -9.46595344482036e-4, 3.66839497852761e-4,
+ 4.2523324806907e-5, -2.0278578112534e-5, -1.624290004647e-6,
+ 1.303655835580e-6, 1.5626441722e-8, -8.5238095915e-8,
+ 6.529054439e-9, 5.059343495e-9, -9.91364156e-10, -2.27365122e-10, 9.6467911e-11, 2.394038e-12, -6.886027e-12, 8.94487e-13, 3.13092e-13, -1.12708e-13, 3.81e-16, 7.106e-15, -1.523e-15, -9.4e-17, 1.21e-16, -2.8e-17
+ ];
+ var j = cof.length - 1;
+ var isneg = false;
+ var d = 0;
+ var dd = 0;
+ var t, ty, tmp, res;
+
+ if (x < 0) {
+ x = -x;
+ isneg = true;
+ }
+
+ t = 2 / (2 + x);
+ ty = 4 * t - 2;
+
+ for (; j > 0; j--) {
+ tmp = d;
+ d = ty * d - dd + cof[j];
+ dd = tmp;
+ }
+
+ res = t * Math.exp(-x * x + 0.5 * (cof[0] + ty * d) - dd);
+ return isneg ? res - 1 : 1 - res;
+ };
+
+ /*
+ * Normal distribution CDF
+ *
+ * The MIT License, Copyright (c) 2013 jStat
+ */
+ var dnorm = function(x, mean, std) {
+ return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * std * std)));
+ }
+
+ var statistic = function(x, y) {
+ var ranked = rank.rank(x, y),
+ nr = ranked.length,
+ nx = x.length,
+ ny = y.length,
+ ranksums = {
+ x: 0,
+ y: 0
+ },
+ i = 0, t = 0, nt = 1, tcf, ux, uy;
+
+ while (i < nr) {
+ if (i > 0) {
+ if (ranked[i].val == ranked[i-1].val) {
+ nt++;
+ } else {
+ if (nt > 1) {
+ t += Math.pow(nt, 3) - nt
+ nt = 1;
+ }
+ }
+ }
+ ranksums[ranked[i].set] += ranked[i].rank
+ i++;
+ }
+ tcf = 1 - (t / (Math.pow(nr, 3) - nr))
+ ux = nx*ny + (nx*(nx+1)/2) - ranksums.x;
+ uy = nx*ny - ux;
+
+ return {
+ tcf: tcf,
+ ux: ux,
+ uy: uy,
+ big: Math.max(ux, uy),
+ small: Math.min(ux, uy)
+ }
+ }
+
+ exports.test = function(x, y, alt, corr) {
+ // set default value for alternative
+ alt = typeof alt !== 'undefined' ? alt : 'two-sided';
+ // set default value for continuity
+ corr = typeof corr !== 'undefined' ? corr : true;
+ var nx = x.length, // x's size
+ ny = y.length, // y's size
+ f = 1,
+ u, mu, std, z, p;
+
+ // test statistic
+ u = statistic(x, y);
+
+ // mean compute and correct if given
+ if (corr) {
+ mu = (nx * ny / 2) + 0.5;
+ } else {
+ mu = nx * ny / 2;
+ }
+
+ // compute standard deviation using tie correction factor
+ std = Math.sqrt(u.tcf * nx * ny * (nx + ny + 1) / 12);
+
+ // compute z according to given alternative
+ if (alt == 'less') {
+ z = (u.ux - mu) / std;
+ } else if (alt == 'greater') {
+ z = (u.uy - mu) / std;
+ } else if (alt == 'two-sided') {
+ z = Math.abs((u.big - mu) / std);
+ } else {
+ console.log('Unknown alternative argument');
+ }
+
+ // factor to correct two sided p-value
+ if (alt == 'two-sided') {
+ f = 2;
+ }
+
+ // compute p-value using CDF of standard normal
+ p = dnorm(-z, 0, 1) * f;
+
+ return {U: u.small, p: p};
+ }
+
+})(typeof exports === 'undefined' ? this['mannwhitneyu'] = {} : exports); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/mocha/LICENSE b/chromium/third_party/catapult/tracing/third_party/mocha/LICENSE
new file mode 100644
index 00000000000..ca47f261b3f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/mocha/LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2011-2015 TJ Holowaychuk <tj@vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/chromium/third_party/catapult/tracing/third_party/mocha/README.chromium b/chromium/third_party/catapult/tracing/third_party/mocha/README.chromium
new file mode 100644
index 00000000000..e492702c6ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/mocha/README.chromium
@@ -0,0 +1,9 @@
+Name: Mocha, simple, flexible, fun javascript test framework for node.js & the browser
+Short Name: Mocha
+URL: https://github.com/mochajs/mocha
+Version: 2c2ed43ae7f047f333468e2043001bfb8c9d13d5
+License: MIT
+License File: NOT_SHIPPED
+Security Critical: no
+Description: Used for testing
+Local Modifications: None
diff --git a/chromium/third_party/catapult/tracing/third_party/mocha/mocha.css b/chromium/third_party/catapult/tracing/third_party/mocha/mocha.css
new file mode 100644
index 00000000000..42b9798fa4e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/mocha/mocha.css
@@ -0,0 +1,270 @@
+@charset "utf-8";
+
+body {
+ margin:0;
+}
+
+#mocha {
+ font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 60px 50px;
+}
+
+#mocha ul,
+#mocha li {
+ margin: 0;
+ padding: 0;
+}
+
+#mocha ul {
+ list-style: none;
+}
+
+#mocha h1,
+#mocha h2 {
+ margin: 0;
+}
+
+#mocha h1 {
+ margin-top: 15px;
+ font-size: 1em;
+ font-weight: 200;
+}
+
+#mocha h1 a {
+ text-decoration: none;
+ color: inherit;
+}
+
+#mocha h1 a:hover {
+ text-decoration: underline;
+}
+
+#mocha .suite .suite h1 {
+ margin-top: 0;
+ font-size: .8em;
+}
+
+#mocha .hidden {
+ display: none;
+}
+
+#mocha h2 {
+ font-size: 12px;
+ font-weight: normal;
+ cursor: pointer;
+}
+
+#mocha .suite {
+ margin-left: 15px;
+}
+
+#mocha .test {
+ margin-left: 15px;
+ overflow: hidden;
+}
+
+#mocha .test.pending:hover h2::after {
+ content: '(pending)';
+ font-family: arial, sans-serif;
+}
+
+#mocha .test.pass.medium .duration {
+ background: #c09853;
+}
+
+#mocha .test.pass.slow .duration {
+ background: #b94a48;
+}
+
+#mocha .test.pass::before {
+ content: '✓';
+ font-size: 12px;
+ display: block;
+ float: left;
+ margin-right: 5px;
+ color: #00d6b2;
+}
+
+#mocha .test.pass .duration {
+ font-size: 9px;
+ margin-left: 5px;
+ padding: 2px 5px;
+ color: #fff;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+ -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ -ms-border-radius: 5px;
+ -o-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#mocha .test.pass.fast .duration {
+ display: none;
+}
+
+#mocha .test.pending {
+ color: #0b97c4;
+}
+
+#mocha .test.pending::before {
+ content: '◦';
+ color: #0b97c4;
+}
+
+#mocha .test.fail {
+ color: #c00;
+}
+
+#mocha .test.fail pre {
+ color: black;
+}
+
+#mocha .test.fail::before {
+ content: '✖';
+ font-size: 12px;
+ display: block;
+ float: left;
+ margin-right: 5px;
+ color: #c00;
+}
+
+#mocha .test pre.error {
+ color: #c00;
+ max-height: 300px;
+ overflow: auto;
+}
+
+/**
+ * (1): approximate for browsers not supporting calc
+ * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
+ * ^^ seriously
+ */
+#mocha .test pre {
+ display: block;
+ float: left;
+ clear: left;
+ font: 12px/1.5 monaco, monospace;
+ margin: 5px;
+ padding: 15px;
+ border: 1px solid #eee;
+ max-width: 85%; /*(1)*/
+ max-width: calc(100% - 42px); /*(2)*/
+ word-wrap: break-word;
+ border-bottom-color: #ddd;
+ -webkit-border-radius: 3px;
+ -webkit-box-shadow: 0 1px 3px #eee;
+ -moz-border-radius: 3px;
+ -moz-box-shadow: 0 1px 3px #eee;
+ border-radius: 3px;
+}
+
+#mocha .test h2 {
+ position: relative;
+}
+
+#mocha .test a.replay {
+ position: absolute;
+ top: 3px;
+ right: 0;
+ text-decoration: none;
+ vertical-align: middle;
+ display: block;
+ width: 15px;
+ height: 15px;
+ line-height: 15px;
+ text-align: center;
+ background: #eee;
+ font-size: 15px;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+ -webkit-transition: opacity 200ms;
+ -moz-transition: opacity 200ms;
+ transition: opacity 200ms;
+ opacity: 0.3;
+ color: #888;
+}
+
+#mocha .test:hover a.replay {
+ opacity: 1;
+}
+
+#mocha-report.pass .test.fail {
+ display: none;
+}
+
+#mocha-report.fail .test.pass {
+ display: none;
+}
+
+#mocha-report.pending .test.pass,
+#mocha-report.pending .test.fail {
+ display: none;
+}
+#mocha-report.pending .test.pass.pending {
+ display: block;
+}
+
+#mocha-error {
+ color: #c00;
+ font-size: 1.5em;
+ font-weight: 100;
+ letter-spacing: 1px;
+}
+
+#mocha-stats {
+ position: fixed;
+ top: 15px;
+ right: 10px;
+ font-size: 12px;
+ margin: 0;
+ color: #888;
+ z-index: 1;
+}
+
+#mocha-stats .progress {
+ float: right;
+ padding-top: 0;
+}
+
+#mocha-stats em {
+ color: black;
+}
+
+#mocha-stats a {
+ text-decoration: none;
+ color: inherit;
+}
+
+#mocha-stats a:hover {
+ border-bottom: 1px solid #eee;
+}
+
+#mocha-stats li {
+ display: inline-block;
+ margin: 0 5px;
+ list-style: none;
+ padding-top: 11px;
+}
+
+#mocha-stats canvas {
+ width: 40px;
+ height: 40px;
+}
+
+#mocha code .comment { color: #ddd; }
+#mocha code .init { color: #2f6fad; }
+#mocha code .string { color: #5890ad; }
+#mocha code .keyword { color: #8a6343; }
+#mocha code .number { color: #2f6fad; }
+
+@media screen and (max-device-width: 480px) {
+ #mocha {
+ margin: 60px 0px;
+ }
+
+ #mocha #stats {
+ position: absolute;
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/mocha/mocha.js b/chromium/third_party/catapult/tracing/third_party/mocha/mocha.js
new file mode 100755
index 00000000000..cffddcbc130
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/mocha/mocha.js
@@ -0,0 +1,6557 @@
+;(function(){
+
+// CommonJS require()
+
+function require(p){
+ var path = require.resolve(p)
+ , mod = require.modules[path];
+ if (!mod) throw new Error('failed to require "' + p + '"');
+ if (!mod.exports) {
+ mod.exports = {};
+ mod.call(mod.exports, mod, mod.exports, require.relative(path));
+ }
+ return mod.exports;
+ }
+
+require.modules = {};
+
+require.resolve = function (path){
+ var orig = path
+ , reg = path + '.js'
+ , index = path + '/index.js';
+ return require.modules[reg] && reg
+ || require.modules[index] && index
+ || orig;
+ };
+
+require.register = function (path, fn){
+ require.modules[path] = fn;
+ };
+
+require.relative = function (parent) {
+ return function(p){
+ if ('.' != p.charAt(0)) return require(p);
+
+ var path = parent.split('/')
+ , segs = p.split('/');
+ path.pop();
+
+ for (var i = 0; i < segs.length; i++) {
+ var seg = segs[i];
+ if ('..' == seg) path.pop();
+ else if ('.' != seg) path.push(seg);
+ }
+
+ return require(path.join('/'));
+ };
+ };
+
+
+require.register("browser/debug.js", function(module, exports, require){
+module.exports = function(type){
+ return function(){
+ }
+};
+
+}); // module: browser/debug.js
+
+require.register("browser/diff.js", function(module, exports, require){
+/* See LICENSE file for terms of use */
+
+/*
+ * Text diff implementation.
+ *
+ * This library supports the following APIS:
+ * JsDiff.diffChars: Character by character diff
+ * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace
+ * JsDiff.diffLines: Line based diff
+ *
+ * JsDiff.diffCss: Diff targeted at CSS content
+ *
+ * These methods are based on the implementation proposed in
+ * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986).
+ * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927
+ */
+var JsDiff = (function() {
+ /*jshint maxparams: 5*/
+ function clonePath(path) {
+ return { newPos: path.newPos, components: path.components.slice(0) };
+ }
+ function removeEmpty(array) {
+ var ret = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array[i]) {
+ ret.push(array[i]);
+ }
+ }
+ return ret;
+ }
+ function escapeHTML(s) {
+ var n = s;
+ n = n.replace(/&/g, '&amp;');
+ n = n.replace(/</g, '&lt;');
+ n = n.replace(/>/g, '&gt;');
+ n = n.replace(/"/g, '&quot;');
+
+ return n;
+ }
+
+ var Diff = function(ignoreWhitespace) {
+ this.ignoreWhitespace = ignoreWhitespace;
+ };
+ Diff.prototype = {
+ diff: function(oldString, newString) {
+ // Handle the identity case (this is due to unrolling editLength == 0
+ if (newString === oldString) {
+ return [{ value: newString }];
+ }
+ if (!newString) {
+ return [{ value: oldString, removed: true }];
+ }
+ if (!oldString) {
+ return [{ value: newString, added: true }];
+ }
+
+ newString = this.tokenize(newString);
+ oldString = this.tokenize(oldString);
+
+ var newLen = newString.length, oldLen = oldString.length;
+ var maxEditLength = newLen + oldLen;
+ var bestPath = [{ newPos: -1, components: [] }];
+
+ // Seed editLength = 0
+ var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0);
+ if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) {
+ return bestPath[0].components;
+ }
+
+ for (var editLength = 1; editLength <= maxEditLength; editLength++) {
+ for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) {
+ var basePath;
+ var addPath = bestPath[diagonalPath-1],
+ removePath = bestPath[diagonalPath+1];
+ oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
+ if (addPath) {
+ // No one else is going to attempt to use this value, clear it
+ bestPath[diagonalPath-1] = undefined;
+ }
+
+ var canAdd = addPath && addPath.newPos+1 < newLen;
+ var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
+ if (!canAdd && !canRemove) {
+ bestPath[diagonalPath] = undefined;
+ continue;
+ }
+
+ // Select the diagonal that we want to branch from. We select the prior
+ // path whose position in the new string is the farthest from the origin
+ // and does not pass the bounds of the diff graph
+ if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) {
+ basePath = clonePath(removePath);
+ this.pushComponent(basePath.components, oldString[oldPos], undefined, true);
+ } else {
+ basePath = clonePath(addPath);
+ basePath.newPos++;
+ this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined);
+ }
+
+ var oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath);
+
+ if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) {
+ return basePath.components;
+ } else {
+ bestPath[diagonalPath] = basePath;
+ }
+ }
+ }
+ },
+
+ pushComponent: function(components, value, added, removed) {
+ var last = components[components.length-1];
+ if (last && last.added === added && last.removed === removed) {
+ // We need to clone here as the component clone operation is just
+ // as shallow array clone
+ components[components.length-1] =
+ {value: this.join(last.value, value), added: added, removed: removed };
+ } else {
+ components.push({value: value, added: added, removed: removed });
+ }
+ },
+ extractCommon: function(basePath, newString, oldString, diagonalPath) {
+ var newLen = newString.length,
+ oldLen = oldString.length,
+ newPos = basePath.newPos,
+ oldPos = newPos - diagonalPath;
+ while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) {
+ newPos++;
+ oldPos++;
+
+ this.pushComponent(basePath.components, newString[newPos], undefined, undefined);
+ }
+ basePath.newPos = newPos;
+ return oldPos;
+ },
+
+ equals: function(left, right) {
+ var reWhitespace = /\S/;
+ if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) {
+ return true;
+ } else {
+ return left === right;
+ }
+ },
+ join: function(left, right) {
+ return left + right;
+ },
+ tokenize: function(value) {
+ return value;
+ }
+ };
+
+ var CharDiff = new Diff();
+
+ var WordDiff = new Diff(true);
+ var WordWithSpaceDiff = new Diff();
+ WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) {
+ return removeEmpty(value.split(/(\s+|\b)/));
+ };
+
+ var CssDiff = new Diff(true);
+ CssDiff.tokenize = function(value) {
+ return removeEmpty(value.split(/([{}:;,]|\s+)/));
+ };
+
+ var LineDiff = new Diff();
+ LineDiff.tokenize = function(value) {
+ var retLines = [],
+ lines = value.split(/^/m);
+
+ for(var i = 0; i < lines.length; i++) {
+ var line = lines[i],
+ lastLine = lines[i - 1];
+
+ // Merge lines that may contain windows new lines
+ if (line == '\n' && lastLine && lastLine[lastLine.length - 1] === '\r') {
+ retLines[retLines.length - 1] += '\n';
+ } else if (line) {
+ retLines.push(line);
+ }
+ }
+
+ return retLines;
+ };
+
+ return {
+ Diff: Diff,
+
+ diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); },
+ diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); },
+ diffWordsWithSpace: function(oldStr, newStr) { return WordWithSpaceDiff.diff(oldStr, newStr); },
+ diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); },
+
+ diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); },
+
+ createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) {
+ var ret = [];
+
+ ret.push('Index: ' + fileName);
+ ret.push('===================================================================');
+ ret.push('--- ' + fileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader));
+ ret.push('+++ ' + fileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader));
+
+ var diff = LineDiff.diff(oldStr, newStr);
+ if (!diff[diff.length-1].value) {
+ diff.pop(); // Remove trailing newline add
+ }
+ diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier
+
+ function contextLines(lines) {
+ return lines.map(function(entry) { return ' ' + entry; });
+ }
+ function eofNL(curRange, i, current) {
+ var last = diff[diff.length-2],
+ isLast = i === diff.length-2,
+ isLastOfType = i === diff.length-3 && (current.added !== last.added || current.removed !== last.removed);
+
+ // Figure out if this is the last line for the given file and missing NL
+ if (!/\n$/.test(current.value) && (isLast || isLastOfType)) {
+ curRange.push('\\ No newline at end of file');
+ }
+ }
+
+ var oldRangeStart = 0, newRangeStart = 0, curRange = [],
+ oldLine = 1, newLine = 1;
+ for (var i = 0; i < diff.length; i++) {
+ var current = diff[i],
+ lines = current.lines || current.value.replace(/\n$/, '').split('\n');
+ current.lines = lines;
+
+ if (current.added || current.removed) {
+ if (!oldRangeStart) {
+ var prev = diff[i-1];
+ oldRangeStart = oldLine;
+ newRangeStart = newLine;
+
+ if (prev) {
+ curRange = contextLines(prev.lines.slice(-4));
+ oldRangeStart -= curRange.length;
+ newRangeStart -= curRange.length;
+ }
+ }
+ curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?'+':'-') + entry; }));
+ eofNL(curRange, i, current);
+
+ if (current.added) {
+ newLine += lines.length;
+ } else {
+ oldLine += lines.length;
+ }
+ } else {
+ if (oldRangeStart) {
+ // Close out any changes that have been output (or join overlapping)
+ if (lines.length <= 8 && i < diff.length-2) {
+ // Overlapping
+ curRange.push.apply(curRange, contextLines(lines));
+ } else {
+ // end the range and output
+ var contextSize = Math.min(lines.length, 4);
+ ret.push(
+ '@@ -' + oldRangeStart + ',' + (oldLine-oldRangeStart+contextSize)
+ + ' +' + newRangeStart + ',' + (newLine-newRangeStart+contextSize)
+ + ' @@');
+ ret.push.apply(ret, curRange);
+ ret.push.apply(ret, contextLines(lines.slice(0, contextSize)));
+ if (lines.length <= 4) {
+ eofNL(ret, i, current);
+ }
+
+ oldRangeStart = 0; newRangeStart = 0; curRange = [];
+ }
+ }
+ oldLine += lines.length;
+ newLine += lines.length;
+ }
+ }
+
+ return ret.join('\n') + '\n';
+ },
+
+ applyPatch: function(oldStr, uniDiff) {
+ var diffstr = uniDiff.split('\n');
+ var diff = [];
+ var remEOFNL = false,
+ addEOFNL = false;
+
+ for (var i = (diffstr[0][0]==='I'?4:0); i < diffstr.length; i++) {
+ if(diffstr[i][0] === '@') {
+ var meh = diffstr[i].split(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/);
+ diff.unshift({
+ start:meh[3],
+ oldlength:meh[2],
+ oldlines:[],
+ newlength:meh[4],
+ newlines:[]
+ });
+ } else if(diffstr[i][0] === '+') {
+ diff[0].newlines.push(diffstr[i].substr(1));
+ } else if(diffstr[i][0] === '-') {
+ diff[0].oldlines.push(diffstr[i].substr(1));
+ } else if(diffstr[i][0] === ' ') {
+ diff[0].newlines.push(diffstr[i].substr(1));
+ diff[0].oldlines.push(diffstr[i].substr(1));
+ } else if(diffstr[i][0] === '\\') {
+ if (diffstr[i-1][0] === '+') {
+ remEOFNL = true;
+ } else if(diffstr[i-1][0] === '-') {
+ addEOFNL = true;
+ }
+ }
+ }
+
+ var str = oldStr.split('\n');
+ for (var i = diff.length - 1; i >= 0; i--) {
+ var d = diff[i];
+ for (var j = 0; j < d.oldlength; j++) {
+ if(str[d.start-1+j] !== d.oldlines[j]) {
+ return false;
+ }
+ }
+ Array.prototype.splice.apply(str,[d.start-1,+d.oldlength].concat(d.newlines));
+ }
+
+ if (remEOFNL) {
+ while (!str[str.length-1]) {
+ str.pop();
+ }
+ } else if (addEOFNL) {
+ str.push('');
+ }
+ return str.join('\n');
+ },
+
+ convertChangesToXML: function(changes){
+ var ret = [];
+ for ( var i = 0; i < changes.length; i++) {
+ var change = changes[i];
+ if (change.added) {
+ ret.push('<ins>');
+ } else if (change.removed) {
+ ret.push('<del>');
+ }
+
+ ret.push(escapeHTML(change.value));
+
+ if (change.added) {
+ ret.push('</ins>');
+ } else if (change.removed) {
+ ret.push('</del>');
+ }
+ }
+ return ret.join('');
+ },
+
+ // See: http://code.google.com/p/google-diff-match-patch/wiki/API
+ convertChangesToDMP: function(changes){
+ var ret = [], change;
+ for ( var i = 0; i < changes.length; i++) {
+ change = changes[i];
+ ret.push([(change.added ? 1 : change.removed ? -1 : 0), change.value]);
+ }
+ return ret;
+ }
+ };
+})();
+
+if (typeof module !== 'undefined') {
+ module.exports = JsDiff;
+}
+
+}); // module: browser/diff.js
+
+require.register("browser/escape-string-regexp.js", function(module, exports, require){
+'use strict';
+
+var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
+
+module.exports = function (str) {
+ if (typeof str !== 'string') {
+ throw new TypeError('Expected a string');
+ }
+
+ return str.replace(matchOperatorsRe, '\\$&');
+};
+
+}); // module: browser/escape-string-regexp.js
+
+require.register("browser/events.js", function(module, exports, require){
+/**
+ * Module exports.
+ */
+
+exports.EventEmitter = EventEmitter;
+
+/**
+ * Check if `obj` is an array.
+ */
+
+function isArray(obj) {
+ return '[object Array]' == {}.toString.call(obj);
+}
+
+/**
+ * Event emitter constructor.
+ *
+ * @api public
+ */
+
+function EventEmitter(){};
+
+/**
+ * Adds a listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.on = function (name, fn) {
+ if (!this.$events) {
+ this.$events = {};
+ }
+
+ if (!this.$events[name]) {
+ this.$events[name] = fn;
+ } else if (isArray(this.$events[name])) {
+ this.$events[name].push(fn);
+ } else {
+ this.$events[name] = [this.$events[name], fn];
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.addListener = EventEmitter.prototype.on;
+
+/**
+ * Adds a volatile listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.once = function (name, fn) {
+ var self = this;
+
+ function on () {
+ self.removeListener(name, on);
+ fn.apply(this, arguments);
+ };
+
+ on.listener = fn;
+ this.on(name, on);
+
+ return this;
+};
+
+/**
+ * Removes a listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.removeListener = function (name, fn) {
+ if (this.$events && this.$events[name]) {
+ var list = this.$events[name];
+
+ if (isArray(list)) {
+ var pos = -1;
+
+ for (var i = 0, l = list.length; i < l; i++) {
+ if (list[i] === fn || (list[i].listener && list[i].listener === fn)) {
+ pos = i;
+ break;
+ }
+ }
+
+ if (pos < 0) {
+ return this;
+ }
+
+ list.splice(pos, 1);
+
+ if (!list.length) {
+ delete this.$events[name];
+ }
+ } else if (list === fn || (list.listener && list.listener === fn)) {
+ delete this.$events[name];
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Removes all listeners for an event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.removeAllListeners = function (name) {
+ if (name === undefined) {
+ this.$events = {};
+ return this;
+ }
+
+ if (this.$events && this.$events[name]) {
+ this.$events[name] = null;
+ }
+
+ return this;
+};
+
+/**
+ * Gets all listeners for a certain event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.listeners = function (name) {
+ if (!this.$events) {
+ this.$events = {};
+ }
+
+ if (!this.$events[name]) {
+ this.$events[name] = [];
+ }
+
+ if (!isArray(this.$events[name])) {
+ this.$events[name] = [this.$events[name]];
+ }
+
+ return this.$events[name];
+};
+
+/**
+ * Emits an event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.emit = function (name) {
+ if (!this.$events) {
+ return false;
+ }
+
+ var handler = this.$events[name];
+
+ if (!handler) {
+ return false;
+ }
+
+ var args = [].slice.call(arguments, 1);
+
+ if ('function' == typeof handler) {
+ handler.apply(this, args);
+ } else if (isArray(handler)) {
+ var listeners = handler.slice();
+
+ for (var i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].apply(this, args);
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+};
+
+}); // module: browser/events.js
+
+require.register("browser/fs.js", function(module, exports, require){
+
+}); // module: browser/fs.js
+
+require.register("browser/glob.js", function(module, exports, require){
+
+}); // module: browser/glob.js
+
+require.register("browser/path.js", function(module, exports, require){
+
+}); // module: browser/path.js
+
+require.register("browser/progress.js", function(module, exports, require){
+/**
+ * Expose `Progress`.
+ */
+
+module.exports = Progress;
+
+/**
+ * Initialize a new `Progress` indicator.
+ */
+
+function Progress() {
+ this.percent = 0;
+ this.size(0);
+ this.fontSize(11);
+ this.font('helvetica, arial, sans-serif');
+}
+
+/**
+ * Set progress size to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.size = function(n){
+ this._size = n;
+ return this;
+};
+
+/**
+ * Set text to `str`.
+ *
+ * @param {String} str
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.text = function(str){
+ this._text = str;
+ return this;
+};
+
+/**
+ * Set font size to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.fontSize = function(n){
+ this._fontSize = n;
+ return this;
+};
+
+/**
+ * Set font `family`.
+ *
+ * @param {String} family
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.font = function(family){
+ this._font = family;
+ return this;
+};
+
+/**
+ * Update percentage to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.update = function(n){
+ this.percent = n;
+ return this;
+};
+
+/**
+ * Draw on `ctx`.
+ *
+ * @param {CanvasRenderingContext2d} ctx
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.draw = function(ctx){
+ try {
+ var percent = Math.min(this.percent, 100)
+ , size = this._size
+ , half = size / 2
+ , x = half
+ , y = half
+ , rad = half - 1
+ , fontSize = this._fontSize;
+
+ ctx.font = fontSize + 'px ' + this._font;
+
+ var angle = Math.PI * 2 * (percent / 100);
+ ctx.clearRect(0, 0, size, size);
+
+ // outer circle
+ ctx.strokeStyle = '#9f9f9f';
+ ctx.beginPath();
+ ctx.arc(x, y, rad, 0, angle, false);
+ ctx.stroke();
+
+ // inner circle
+ ctx.strokeStyle = '#eee';
+ ctx.beginPath();
+ ctx.arc(x, y, rad - 1, 0, angle, true);
+ ctx.stroke();
+
+ // text
+ var text = this._text || (percent | 0) + '%'
+ , w = ctx.measureText(text).width;
+
+ ctx.fillText(
+ text
+ , x - w / 2 + 1
+ , y + fontSize / 2 - 1);
+ } catch (ex) {} //don't fail if we can't render progress
+ return this;
+};
+
+}); // module: browser/progress.js
+
+require.register("browser/tty.js", function(module, exports, require){
+exports.isatty = function(){
+ return true;
+};
+
+exports.getWindowSize = function(){
+ if ('innerHeight' in global) {
+ return [global.innerHeight, global.innerWidth];
+ } else {
+ // In a Web Worker, the DOM Window is not available.
+ return [640, 480];
+ }
+};
+
+}); // module: browser/tty.js
+
+require.register("context.js", function(module, exports, require){
+/**
+ * Expose `Context`.
+ */
+
+module.exports = Context;
+
+/**
+ * Initialize a new `Context`.
+ *
+ * @api private
+ */
+
+function Context(){}
+
+/**
+ * Set or get the context `Runnable` to `runnable`.
+ *
+ * @param {Runnable} runnable
+ * @return {Context}
+ * @api private
+ */
+
+Context.prototype.runnable = function(runnable){
+ if (0 == arguments.length) return this._runnable;
+ this.test = this._runnable = runnable;
+ return this;
+};
+
+/**
+ * Set test timeout `ms`.
+ *
+ * @param {Number} ms
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.timeout = function(ms){
+ if (arguments.length === 0) return this.runnable().timeout();
+ this.runnable().timeout(ms);
+ return this;
+};
+
+/**
+ * Set test timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.enableTimeouts = function (enabled) {
+ this.runnable().enableTimeouts(enabled);
+ return this;
+};
+
+
+/**
+ * Set test slowness threshold `ms`.
+ *
+ * @param {Number} ms
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.slow = function(ms){
+ this.runnable().slow(ms);
+ return this;
+};
+
+/**
+ * Mark a test as skipped.
+ *
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.skip = function(){
+ this.runnable().skip();
+ return this;
+};
+
+/**
+ * Inspect the context void of `._runnable`.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Context.prototype.inspect = function(){
+ return JSON.stringify(this, function(key, val){
+ if ('_runnable' == key) return;
+ if ('test' == key) return;
+ return val;
+ }, 2);
+};
+
+}); // module: context.js
+
+require.register("hook.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Runnable = require('./runnable');
+
+/**
+ * Expose `Hook`.
+ */
+
+module.exports = Hook;
+
+/**
+ * Initialize a new `Hook` with the given `title` and callback `fn`.
+ *
+ * @param {String} title
+ * @param {Function} fn
+ * @api private
+ */
+
+function Hook(title, fn) {
+ Runnable.call(this, title, fn);
+ this.type = 'hook';
+}
+
+/**
+ * Inherit from `Runnable.prototype`.
+ */
+
+function F(){};
+F.prototype = Runnable.prototype;
+Hook.prototype = new F;
+Hook.prototype.constructor = Hook;
+
+
+/**
+ * Get or set the test `err`.
+ *
+ * @param {Error} err
+ * @return {Error}
+ * @api public
+ */
+
+Hook.prototype.error = function(err){
+ if (0 == arguments.length) {
+ var err = this._error;
+ this._error = null;
+ return err;
+ }
+
+ this._error = err;
+};
+
+}); // module: hook.js
+
+require.register("interfaces/bdd.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test')
+ , utils = require('../utils')
+ , escapeRe = require('browser/escape-string-regexp');
+
+/**
+ * BDD-style interface:
+ *
+ * describe('Array', function(){
+ * describe('#indexOf()', function(){
+ * it('should return -1 when not present', function(){
+ *
+ * });
+ *
+ * it('should return the index when present', function(){
+ *
+ * });
+ * });
+ * });
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('pre-require', function(context, file, mocha){
+
+ var common = require('./common')(suites, context);
+
+ context.before = common.before;
+ context.after = common.after;
+ context.beforeEach = common.beforeEach;
+ context.afterEach = common.afterEach;
+ context.run = mocha.options.delay && common.runWithSuite(suite);
+ /**
+ * Describe a "suite" with the given `title`
+ * and callback `fn` containing nested suites
+ * and/or tests.
+ */
+
+ context.describe = context.context = function(title, fn){
+ var suite = Suite.create(suites[0], title);
+ suite.file = file;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ return suite;
+ };
+
+ /**
+ * Pending describe.
+ */
+
+ context.xdescribe =
+ context.xcontext =
+ context.describe.skip = function(title, fn){
+ var suite = Suite.create(suites[0], title);
+ suite.pending = true;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ };
+
+ /**
+ * Exclusive suite.
+ */
+
+ context.describe.only = function(title, fn){
+ var suite = context.describe(title, fn);
+ mocha.grep(suite.fullTitle());
+ return suite;
+ };
+
+ /**
+ * Describe a specification or test-case
+ * with the given `title` and callback `fn`
+ * acting as a thunk.
+ */
+
+ context.it = context.specify = function(title, fn){
+ var suite = suites[0];
+ if (suite.pending) fn = null;
+ var test = new Test(title, fn);
+ test.file = file;
+ suite.addTest(test);
+ return test;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.it.only = function(title, fn){
+ var test = context.it(title, fn);
+ var reString = '^' + escapeRe(test.fullTitle()) + '$';
+ mocha.grep(new RegExp(reString));
+ return test;
+ };
+
+ /**
+ * Pending test case.
+ */
+
+ context.xit =
+ context.xspecify =
+ context.it.skip = function(title){
+ context.it(title);
+ };
+
+ });
+};
+
+}); // module: interfaces/bdd.js
+
+require.register("interfaces/common.js", function(module, exports, require){
+/**
+ * Functions common to more than one interface
+ * @module lib/interfaces/common
+ */
+
+'use strict';
+
+module.exports = function (suites, context) {
+
+ return {
+ /**
+ * This is only present if flag --delay is passed into Mocha. It triggers
+ * root suite execution. Returns a function which runs the root suite.
+ */
+ runWithSuite: function runWithSuite(suite) {
+ return function run() {
+ suite.run();
+ };
+ },
+
+ /**
+ * Execute before running tests.
+ */
+ before: function (name, fn) {
+ suites[0].beforeAll(name, fn);
+ },
+
+ /**
+ * Execute after running tests.
+ */
+ after: function (name, fn) {
+ suites[0].afterAll(name, fn);
+ },
+
+ /**
+ * Execute before each test case.
+ */
+ beforeEach: function (name, fn) {
+ suites[0].beforeEach(name, fn);
+ },
+
+ /**
+ * Execute after each test case.
+ */
+ afterEach: function (name, fn) {
+ suites[0].afterEach(name, fn);
+ },
+
+ test: {
+ /**
+ * Pending test case.
+ */
+ skip: function (title) {
+ context.test(title);
+ }
+ }
+ }
+};
+
+}); // module: interfaces/common.js
+
+require.register("interfaces/exports.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test');
+
+/**
+ * TDD-style interface:
+ *
+ * exports.Array = {
+ * '#indexOf()': {
+ * 'should return -1 when the value is not present': function(){
+ *
+ * },
+ *
+ * 'should return the correct index when the value is present': function(){
+ *
+ * }
+ * }
+ * };
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('require', visit);
+
+ function visit(obj, file) {
+ var suite;
+ for (var key in obj) {
+ if ('function' == typeof obj[key]) {
+ var fn = obj[key];
+ switch (key) {
+ case 'before':
+ suites[0].beforeAll(fn);
+ break;
+ case 'after':
+ suites[0].afterAll(fn);
+ break;
+ case 'beforeEach':
+ suites[0].beforeEach(fn);
+ break;
+ case 'afterEach':
+ suites[0].afterEach(fn);
+ break;
+ default:
+ var test = new Test(key, fn);
+ test.file = file;
+ suites[0].addTest(test);
+ }
+ } else {
+ suite = Suite.create(suites[0], key);
+ suites.unshift(suite);
+ visit(obj[key]);
+ suites.shift();
+ }
+ }
+ }
+};
+
+}); // module: interfaces/exports.js
+
+require.register("interfaces/index.js", function(module, exports, require){
+exports.bdd = require('./bdd');
+exports.tdd = require('./tdd');
+exports.qunit = require('./qunit');
+exports.exports = require('./exports');
+
+}); // module: interfaces/index.js
+
+require.register("interfaces/qunit.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test')
+ , escapeRe = require('browser/escape-string-regexp')
+ , utils = require('../utils');
+
+/**
+ * QUnit-style interface:
+ *
+ * suite('Array');
+ *
+ * test('#length', function(){
+ * var arr = [1,2,3];
+ * ok(arr.length == 3);
+ * });
+ *
+ * test('#indexOf()', function(){
+ * var arr = [1,2,3];
+ * ok(arr.indexOf(1) == 0);
+ * ok(arr.indexOf(2) == 1);
+ * ok(arr.indexOf(3) == 2);
+ * });
+ *
+ * suite('String');
+ *
+ * test('#length', function(){
+ * ok('foo'.length == 3);
+ * });
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('pre-require', function(context, file, mocha){
+
+ var common = require('./common')(suites, context);
+
+ context.before = common.before;
+ context.after = common.after;
+ context.beforeEach = common.beforeEach;
+ context.afterEach = common.afterEach;
+ context.run = mocha.options.delay && common.runWithSuite(suite);
+ /**
+ * Describe a "suite" with the given `title`.
+ */
+
+ context.suite = function(title){
+ if (suites.length > 1) suites.shift();
+ var suite = Suite.create(suites[0], title);
+ suite.file = file;
+ suites.unshift(suite);
+ return suite;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.suite.only = function(title, fn){
+ var suite = context.suite(title, fn);
+ mocha.grep(suite.fullTitle());
+ };
+
+ /**
+ * Describe a specification or test-case
+ * with the given `title` and callback `fn`
+ * acting as a thunk.
+ */
+
+ context.test = function(title, fn){
+ var test = new Test(title, fn);
+ test.file = file;
+ suites[0].addTest(test);
+ return test;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.test.only = function(title, fn){
+ var test = context.test(title, fn);
+ var reString = '^' + escapeRe(test.fullTitle()) + '$';
+ mocha.grep(new RegExp(reString));
+ };
+
+ context.test.skip = common.test.skip;
+
+ });
+};
+
+}); // module: interfaces/qunit.js
+
+require.register("interfaces/tdd.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test')
+ , escapeRe = require('browser/escape-string-regexp')
+ , utils = require('../utils');
+
+/**
+ * TDD-style interface:
+ *
+ * suite('Array', function(){
+ * suite('#indexOf()', function(){
+ * suiteSetup(function(){
+ *
+ * });
+ *
+ * test('should return -1 when not present', function(){
+ *
+ * });
+ *
+ * test('should return the index when present', function(){
+ *
+ * });
+ *
+ * suiteTeardown(function(){
+ *
+ * });
+ * });
+ * });
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('pre-require', function(context, file, mocha){
+
+ var common = require('./common')(suites, context);
+
+ context.setup = common.beforeEach;
+ context.teardown = common.afterEach;
+ context.suiteSetup = common.before;
+ context.suiteTeardown = common.after;
+ context.run = mocha.options.delay && common.runWithSuite(suite);
+ /**
+ * Describe a "suite" with the given `title`
+ * and callback `fn` containing nested suites
+ * and/or tests.
+ */
+
+ context.suite = function(title, fn){
+ var suite = Suite.create(suites[0], title);
+ suite.file = file;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ return suite;
+ };
+
+ /**
+ * Pending suite.
+ */
+ context.suite.skip = function(title, fn) {
+ var suite = Suite.create(suites[0], title);
+ suite.pending = true;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.suite.only = function(title, fn){
+ var suite = context.suite(title, fn);
+ mocha.grep(suite.fullTitle());
+ };
+
+ /**
+ * Describe a specification or test-case
+ * with the given `title` and callback `fn`
+ * acting as a thunk.
+ */
+
+ context.test = function(title, fn){
+ var suite = suites[0];
+ if (suite.pending) fn = null;
+ var test = new Test(title, fn);
+ test.file = file;
+ suite.addTest(test);
+ return test;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.test.only = function(title, fn){
+ var test = context.test(title, fn);
+ var reString = '^' + escapeRe(test.fullTitle()) + '$';
+ mocha.grep(new RegExp(reString));
+ };
+
+ context.test.skip = common.test.skip;
+ });
+};
+
+}); // module: interfaces/tdd.js
+
+require.register("mocha.js", function(module, exports, require){
+/*!
+ * mocha
+ * Copyright(c) 2011 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var path = require('browser/path')
+ , escapeRe = require('browser/escape-string-regexp')
+ , utils = require('./utils');
+
+/**
+ * Expose `Mocha`.
+ */
+
+exports = module.exports = Mocha;
+
+/**
+ * To require local UIs and reporters when running in node.
+ */
+
+if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
+ var join = path.join
+ , cwd = process.cwd();
+ module.paths.push(cwd, join(cwd, 'node_modules'));
+}
+
+/**
+ * Expose internals.
+ */
+
+exports.utils = utils;
+exports.interfaces = require('./interfaces');
+exports.reporters = require('./reporters');
+exports.Runnable = require('./runnable');
+exports.Context = require('./context');
+exports.Runner = require('./runner');
+exports.Suite = require('./suite');
+exports.Hook = require('./hook');
+exports.Test = require('./test');
+
+/**
+ * Return image `name` path.
+ *
+ * @param {String} name
+ * @return {String}
+ * @api private
+ */
+
+function image(name) {
+ return __dirname + '/../images/' + name + '.png';
+}
+
+/**
+ * Setup mocha with `options`.
+ *
+ * Options:
+ *
+ * - `ui` name "bdd", "tdd", "exports" etc
+ * - `reporter` reporter instance, defaults to `mocha.reporters.spec`
+ * - `globals` array of accepted globals
+ * - `timeout` timeout in milliseconds
+ * - `bail` bail on the first test failure
+ * - `slow` milliseconds to wait before considering a test slow
+ * - `ignoreLeaks` ignore global leaks
+ * - `fullTrace` display the full stack-trace on failing
+ * - `grep` string or regexp to filter tests with
+ *
+ * @param {Object} options
+ * @api public
+ */
+
+function Mocha(options) {
+ options = options || {};
+ this.files = [];
+ this.options = options;
+ if (options.grep) this.grep(new RegExp(options.grep));
+ if (options.fgrep) this.grep(options.fgrep);
+ this.suite = new exports.Suite('', new exports.Context);
+ this.ui(options.ui);
+ this.bail(options.bail);
+ this.reporter(options.reporter, options.reporterOptions);
+ if (null != options.timeout) this.timeout(options.timeout);
+ this.useColors(options.useColors);
+ if (options.enableTimeouts !== null) this.enableTimeouts(options.enableTimeouts);
+ if (options.slow) this.slow(options.slow);
+
+ this.suite.on('pre-require', function (context) {
+ exports.afterEach = context.afterEach || context.teardown;
+ exports.after = context.after || context.suiteTeardown;
+ exports.beforeEach = context.beforeEach || context.setup;
+ exports.before = context.before || context.suiteSetup;
+ exports.describe = context.describe || context.suite;
+ exports.it = context.it || context.test;
+ exports.setup = context.setup || context.beforeEach;
+ exports.suiteSetup = context.suiteSetup || context.before;
+ exports.suiteTeardown = context.suiteTeardown || context.after;
+ exports.suite = context.suite || context.describe;
+ exports.teardown = context.teardown || context.afterEach;
+ exports.test = context.test || context.it;
+ exports.run = context.run;
+ });
+}
+
+/**
+ * Enable or disable bailing on the first failure.
+ *
+ * @param {Boolean} [bail]
+ * @api public
+ */
+
+Mocha.prototype.bail = function(bail){
+ if (0 == arguments.length) bail = true;
+ this.suite.bail(bail);
+ return this;
+};
+
+/**
+ * Add test `file`.
+ *
+ * @param {String} file
+ * @api public
+ */
+
+Mocha.prototype.addFile = function(file){
+ this.files.push(file);
+ return this;
+};
+
+/**
+ * Set reporter to `reporter`, defaults to "spec".
+ *
+ * @param {String|Function} reporter name or constructor
+ * @param {Object} reporterOptions optional options
+ * @api public
+ */
+Mocha.prototype.reporter = function(reporter, reporterOptions){
+ if ('function' == typeof reporter) {
+ this._reporter = reporter;
+ } else {
+ reporter = reporter || 'spec';
+ var _reporter;
+ try { _reporter = require('./reporters/' + reporter); } catch (err) {}
+ if (!_reporter) try { _reporter = require(reporter); } catch (err) {}
+ if (!_reporter && reporter === 'teamcity')
+ console.warn('The Teamcity reporter was moved to a package named ' +
+ 'mocha-teamcity-reporter ' +
+ '(https://npmjs.org/package/mocha-teamcity-reporter).');
+ if (!_reporter) throw new Error('invalid reporter "' + reporter + '"');
+ this._reporter = _reporter;
+ }
+ this.options.reporterOptions = reporterOptions;
+ return this;
+};
+
+/**
+ * Set test UI `name`, defaults to "bdd".
+ *
+ * @param {String} bdd
+ * @api public
+ */
+
+Mocha.prototype.ui = function(name){
+ name = name || 'bdd';
+ this._ui = exports.interfaces[name];
+ if (!this._ui) try { this._ui = require(name); } catch (err) {}
+ if (!this._ui) throw new Error('invalid interface "' + name + '"');
+ this._ui = this._ui(this.suite);
+ return this;
+};
+
+/**
+ * Load registered files.
+ *
+ * @api private
+ */
+
+Mocha.prototype.loadFiles = function(fn){
+ var self = this;
+ var suite = this.suite;
+ var pending = this.files.length;
+ this.files.forEach(function(file){
+ file = path.resolve(file);
+ suite.emit('pre-require', global, file, self);
+ suite.emit('require', require(file), file, self);
+ suite.emit('post-require', global, file, self);
+ --pending || (fn && fn());
+ });
+};
+
+/**
+ * Enable growl support.
+ *
+ * @api private
+ */
+
+Mocha.prototype._growl = function(runner, reporter) {
+ var notify = require('growl');
+
+ runner.on('end', function(){
+ var stats = reporter.stats;
+ if (stats.failures) {
+ var msg = stats.failures + ' of ' + runner.total + ' tests failed';
+ notify(msg, { name: 'mocha', title: 'Failed', image: image('error') });
+ } else {
+ notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', {
+ name: 'mocha'
+ , title: 'Passed'
+ , image: image('ok')
+ });
+ }
+ });
+};
+
+/**
+ * Add regexp to grep, if `re` is a string it is escaped.
+ *
+ * @param {RegExp|String} re
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.grep = function(re){
+ this.options.grep = 'string' == typeof re
+ ? new RegExp(escapeRe(re))
+ : re;
+ return this;
+};
+
+/**
+ * Invert `.grep()` matches.
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.invert = function(){
+ this.options.invert = true;
+ return this;
+};
+
+/**
+ * Ignore global leaks.
+ *
+ * @param {Boolean} ignore
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.ignoreLeaks = function(ignore){
+ this.options.ignoreLeaks = !!ignore;
+ return this;
+};
+
+/**
+ * Enable global leak checking.
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.checkLeaks = function(){
+ this.options.ignoreLeaks = false;
+ return this;
+};
+
+/**
+ * Display long stack-trace on failing
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.fullTrace = function() {
+ this.options.fullStackTrace = true;
+ return this;
+};
+
+/**
+ * Enable growl support.
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.growl = function(){
+ this.options.growl = true;
+ return this;
+};
+
+/**
+ * Ignore `globals` array or string.
+ *
+ * @param {Array|String} globals
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.globals = function(globals){
+ this.options.globals = (this.options.globals || []).concat(globals);
+ return this;
+};
+
+/**
+ * Emit color output.
+ *
+ * @param {Boolean} colors
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.useColors = function(colors){
+ if (colors !== undefined) {
+ this.options.useColors = colors;
+ }
+ return this;
+};
+
+/**
+ * Use inline diffs rather than +/-.
+ *
+ * @param {Boolean} inlineDiffs
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.useInlineDiffs = function(inlineDiffs) {
+ this.options.useInlineDiffs = arguments.length && inlineDiffs != undefined
+ ? inlineDiffs
+ : false;
+ return this;
+};
+
+/**
+ * Set the timeout in milliseconds.
+ *
+ * @param {Number} timeout
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.timeout = function(timeout){
+ this.suite.timeout(timeout);
+ return this;
+};
+
+/**
+ * Set slowness threshold in milliseconds.
+ *
+ * @param {Number} slow
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.slow = function(slow){
+ this.suite.slow(slow);
+ return this;
+};
+
+/**
+ * Enable timeouts.
+ *
+ * @param {Boolean} enabled
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.enableTimeouts = function(enabled) {
+ this.suite.enableTimeouts(arguments.length && enabled !== undefined
+ ? enabled
+ : true);
+ return this
+};
+
+/**
+ * Makes all tests async (accepting a callback)
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.asyncOnly = function(){
+ this.options.asyncOnly = true;
+ return this;
+};
+
+/**
+ * Disable syntax highlighting (in browser).
+ * @returns {Mocha}
+ * @api public
+ */
+Mocha.prototype.noHighlighting = function() {
+ this.options.noHighlighting = true;
+ return this;
+};
+
+/**
+ * Delay root suite execution.
+ * @returns {Mocha}
+ * @api public
+ */
+Mocha.prototype.delay = function delay() {
+ this.options.delay = true;
+ return this;
+};
+
+/**
+ * Run tests and invoke `fn()` when complete.
+ *
+ * @param {Function} fn
+ * @return {Runner}
+ * @api public
+ */
+Mocha.prototype.run = function(fn){
+ if (this.files.length) this.loadFiles();
+ var suite = this.suite;
+ var options = this.options;
+ options.files = this.files;
+ var runner = new exports.Runner(suite, options.delay);
+ var reporter = new this._reporter(runner, options);
+ runner.ignoreLeaks = false !== options.ignoreLeaks;
+ runner.fullStackTrace = options.fullStackTrace;
+ runner.asyncOnly = options.asyncOnly;
+ if (options.grep) runner.grep(options.grep, options.invert);
+ if (options.globals) runner.globals(options.globals);
+ if (options.growl) this._growl(runner, reporter);
+ if (options.useColors !== undefined) {
+ exports.reporters.Base.useColors = options.useColors;
+ }
+ exports.reporters.Base.inlineDiffs = options.useInlineDiffs;
+
+ function done(failures) {
+ if (reporter.done) {
+ reporter.done(failures, fn);
+ } else fn && fn(failures);
+ }
+
+ return runner.run(done);
+};
+
+}); // module: mocha.js
+
+require.register("ms.js", function(module, exports, require){
+/**
+ * Helpers.
+ */
+
+var s = 1000;
+var m = s * 60;
+var h = m * 60;
+var d = h * 24;
+var y = d * 365.25;
+
+/**
+ * Parse or format the given `val`.
+ *
+ * Options:
+ *
+ * - `long` verbose formatting [false]
+ *
+ * @param {String|Number} val
+ * @param {Object} options
+ * @return {String|Number}
+ * @api public
+ */
+
+module.exports = function(val, options){
+ options = options || {};
+ if ('string' == typeof val) return parse(val);
+ return options['long'] ? longFormat(val) : shortFormat(val);
+};
+
+/**
+ * Parse the given `str` and return milliseconds.
+ *
+ * @param {String} str
+ * @return {Number}
+ * @api private
+ */
+
+function parse(str) {
+ var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str);
+ if (!match) return;
+ var n = parseFloat(match[1]);
+ var type = (match[2] || 'ms').toLowerCase();
+ switch (type) {
+ case 'years':
+ case 'year':
+ case 'y':
+ return n * y;
+ case 'days':
+ case 'day':
+ case 'd':
+ return n * d;
+ case 'hours':
+ case 'hour':
+ case 'h':
+ return n * h;
+ case 'minutes':
+ case 'minute':
+ case 'm':
+ return n * m;
+ case 'seconds':
+ case 'second':
+ case 's':
+ return n * s;
+ case 'ms':
+ return n;
+ }
+}
+
+/**
+ * Short format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function shortFormat(ms) {
+ if (ms >= d) return Math.round(ms / d) + 'd';
+ if (ms >= h) return Math.round(ms / h) + 'h';
+ if (ms >= m) return Math.round(ms / m) + 'm';
+ if (ms >= s) return Math.round(ms / s) + 's';
+ return ms + 'ms';
+}
+
+/**
+ * Long format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function longFormat(ms) {
+ return plural(ms, d, 'day')
+ || plural(ms, h, 'hour')
+ || plural(ms, m, 'minute')
+ || plural(ms, s, 'second')
+ || ms + ' ms';
+}
+
+/**
+ * Pluralization helper.
+ */
+
+function plural(ms, n, name) {
+ if (ms < n) return;
+ if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
+ return Math.ceil(ms / n) + ' ' + name + 's';
+}
+
+}); // module: ms.js
+
+require.register("pending.js", function(module, exports, require){
+
+/**
+ * Expose `Pending`.
+ */
+
+module.exports = Pending;
+
+/**
+ * Initialize a new `Pending` error with the given message.
+ *
+ * @param {String} message
+ */
+
+function Pending(message) {
+ this.message = message;
+}
+
+}); // module: pending.js
+
+require.register("reporters/base.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var tty = require('browser/tty')
+ , diff = require('browser/diff')
+ , ms = require('../ms')
+ , utils = require('../utils')
+ , supportsColor = process.env ? require('supports-color') : null;
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Check if both stdio streams are associated with a tty.
+ */
+
+var isatty = tty.isatty(1) && tty.isatty(2);
+
+/**
+ * Expose `Base`.
+ */
+
+exports = module.exports = Base;
+
+/**
+ * Enable coloring by default, except in the browser interface.
+ */
+
+exports.useColors = process.env
+ ? (supportsColor || (process.env.MOCHA_COLORS !== undefined))
+ : false;
+
+/**
+ * Inline diffs instead of +/-
+ */
+
+exports.inlineDiffs = false;
+
+/**
+ * Default color map.
+ */
+
+exports.colors = {
+ 'pass': 90
+ , 'fail': 31
+ , 'bright pass': 92
+ , 'bright fail': 91
+ , 'bright yellow': 93
+ , 'pending': 36
+ , 'suite': 0
+ , 'error title': 0
+ , 'error message': 31
+ , 'error stack': 90
+ , 'checkmark': 32
+ , 'fast': 90
+ , 'medium': 33
+ , 'slow': 31
+ , 'green': 32
+ , 'light': 90
+ , 'diff gutter': 90
+ , 'diff added': 42
+ , 'diff removed': 41
+};
+
+/**
+ * Default symbol map.
+ */
+
+exports.symbols = {
+ ok: '✓',
+ err: '✖',
+ dot: '․'
+};
+
+// With node.js on Windows: use symbols available in terminal default fonts
+if ('win32' == process.platform) {
+ exports.symbols.ok = '\u221A';
+ exports.symbols.err = '\u00D7';
+ exports.symbols.dot = '.';
+}
+
+/**
+ * Color `str` with the given `type`,
+ * allowing colors to be disabled,
+ * as well as user-defined color
+ * schemes.
+ *
+ * @param {String} type
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+var color = exports.color = function(type, str) {
+ if (!exports.useColors) return String(str);
+ return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m';
+};
+
+/**
+ * Expose term window size, with some
+ * defaults for when stderr is not a tty.
+ */
+
+exports.window = {
+ width: isatty
+ ? process.stdout.getWindowSize
+ ? process.stdout.getWindowSize(1)[0]
+ : tty.getWindowSize()[1]
+ : 75
+};
+
+/**
+ * Expose some basic cursor interactions
+ * that are common among reporters.
+ */
+
+exports.cursor = {
+ hide: function(){
+ isatty && process.stdout.write('\u001b[?25l');
+ },
+
+ show: function(){
+ isatty && process.stdout.write('\u001b[?25h');
+ },
+
+ deleteLine: function(){
+ isatty && process.stdout.write('\u001b[2K');
+ },
+
+ beginningOfLine: function(){
+ isatty && process.stdout.write('\u001b[0G');
+ },
+
+ CR: function(){
+ if (isatty) {
+ exports.cursor.deleteLine();
+ exports.cursor.beginningOfLine();
+ } else {
+ process.stdout.write('\r');
+ }
+ }
+};
+
+/**
+ * Outut the given `failures` as a list.
+ *
+ * @param {Array} failures
+ * @api public
+ */
+
+exports.list = function(failures){
+ console.log();
+ failures.forEach(function(test, i){
+ // format
+ var fmt = color('error title', ' %s) %s:\n')
+ + color('error message', ' %s')
+ + color('error stack', '\n%s\n');
+
+ // msg
+ var err = test.err
+ , message = err.message || ''
+ , stack = err.stack || message
+ , index = stack.indexOf(message) + message.length
+ , msg = stack.slice(0, index)
+ , actual = err.actual
+ , expected = err.expected
+ , escape = true;
+
+ // uncaught
+ if (err.uncaught) {
+ msg = 'Uncaught ' + msg;
+ }
+ // explicitly show diff
+ if (err.showDiff && sameType(actual, expected)) {
+
+ if ('string' !== typeof actual) {
+ escape = false;
+ err.actual = actual = utils.stringify(actual);
+ err.expected = expected = utils.stringify(expected);
+ }
+
+ fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n');
+ var match = message.match(/^([^:]+): expected/);
+ msg = '\n ' + color('error message', match ? match[1] : msg);
+
+ if (exports.inlineDiffs) {
+ msg += inlineDiff(err, escape);
+ } else {
+ msg += unifiedDiff(err, escape);
+ }
+ }
+
+ // indent stack trace without msg
+ stack = stack.slice(index ? index + 1 : index)
+ .replace(/^/gm, ' ');
+
+ console.log(fmt, (i + 1), test.fullTitle(), msg, stack);
+ });
+};
+
+/**
+ * Initialize a new `Base` reporter.
+ *
+ * All other reporters generally
+ * inherit from this reporter, providing
+ * stats such as test duration, number
+ * of tests passed / failed etc.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Base(runner) {
+ var self = this
+ , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 }
+ , failures = this.failures = [];
+
+ if (!runner) return;
+ this.runner = runner;
+
+ runner.stats = stats;
+
+ runner.on('start', function(){
+ stats.start = new Date;
+ });
+
+ runner.on('suite', function(suite){
+ stats.suites = stats.suites || 0;
+ suite.root || stats.suites++;
+ });
+
+ runner.on('test end', function(test){
+ stats.tests = stats.tests || 0;
+ stats.tests++;
+ });
+
+ runner.on('pass', function(test){
+ stats.passes = stats.passes || 0;
+
+ var medium = test.slow() / 2;
+ test.speed = test.duration > test.slow()
+ ? 'slow'
+ : test.duration > medium
+ ? 'medium'
+ : 'fast';
+
+ stats.passes++;
+ });
+
+ runner.on('fail', function(test, err){
+ stats.failures = stats.failures || 0;
+ stats.failures++;
+ test.err = err;
+ failures.push(test);
+ });
+
+ runner.on('end', function(){
+ stats.end = new Date;
+ stats.duration = new Date - stats.start;
+ });
+
+ runner.on('pending', function(){
+ stats.pending++;
+ });
+}
+
+/**
+ * Output common epilogue used by many of
+ * the bundled reporters.
+ *
+ * @api public
+ */
+
+Base.prototype.epilogue = function(){
+ var stats = this.stats;
+ var tests;
+ var fmt;
+
+ console.log();
+
+ // passes
+ fmt = color('bright pass', ' ')
+ + color('green', ' %d passing')
+ + color('light', ' (%s)');
+
+ console.log(fmt,
+ stats.passes || 0,
+ ms(stats.duration));
+
+ // pending
+ if (stats.pending) {
+ fmt = color('pending', ' ')
+ + color('pending', ' %d pending');
+
+ console.log(fmt, stats.pending);
+ }
+
+ // failures
+ if (stats.failures) {
+ fmt = color('fail', ' %d failing');
+
+ console.log(fmt, stats.failures);
+
+ Base.list(this.failures);
+ console.log();
+ }
+
+ console.log();
+};
+
+/**
+ * Pad the given `str` to `len`.
+ *
+ * @param {String} str
+ * @param {String} len
+ * @return {String}
+ * @api private
+ */
+
+function pad(str, len) {
+ str = String(str);
+ return Array(len - str.length + 1).join(' ') + str;
+}
+
+
+/**
+ * Returns an inline diff between 2 strings with coloured ANSI output
+ *
+ * @param {Error} Error with actual/expected
+ * @return {String} Diff
+ * @api private
+ */
+
+function inlineDiff(err, escape) {
+ var msg = errorDiff(err, 'WordsWithSpace', escape);
+
+ // linenos
+ var lines = msg.split('\n');
+ if (lines.length > 4) {
+ var width = String(lines.length).length;
+ msg = lines.map(function(str, i){
+ return pad(++i, width) + ' |' + ' ' + str;
+ }).join('\n');
+ }
+
+ // legend
+ msg = '\n'
+ + color('diff removed', 'actual')
+ + ' '
+ + color('diff added', 'expected')
+ + '\n\n'
+ + msg
+ + '\n';
+
+ // indent
+ msg = msg.replace(/^/gm, ' ');
+ return msg;
+}
+
+/**
+ * Returns a unified diff between 2 strings
+ *
+ * @param {Error} Error with actual/expected
+ * @return {String} Diff
+ * @api private
+ */
+
+function unifiedDiff(err, escape) {
+ var indent = ' ';
+ function cleanUp(line) {
+ if (escape) {
+ line = escapeInvisibles(line);
+ }
+ if (line[0] === '+') return indent + colorLines('diff added', line);
+ if (line[0] === '-') return indent + colorLines('diff removed', line);
+ if (line.match(/\@\@/)) return null;
+ if (line.match(/\\ No newline/)) return null;
+ else return indent + line;
+ }
+ function notBlank(line) {
+ return line != null;
+ }
+ var msg = diff.createPatch('string', err.actual, err.expected);
+ var lines = msg.split('\n').splice(4);
+ return '\n '
+ + colorLines('diff added', '+ expected') + ' '
+ + colorLines('diff removed', '- actual')
+ + '\n\n'
+ + lines.map(cleanUp).filter(notBlank).join('\n');
+}
+
+/**
+ * Return a character diff for `err`.
+ *
+ * @param {Error} err
+ * @return {String}
+ * @api private
+ */
+
+function errorDiff(err, type, escape) {
+ var actual = escape ? escapeInvisibles(err.actual) : err.actual;
+ var expected = escape ? escapeInvisibles(err.expected) : err.expected;
+ return diff['diff' + type](actual, expected).map(function(str){
+ if (str.added) return colorLines('diff added', str.value);
+ if (str.removed) return colorLines('diff removed', str.value);
+ return str.value;
+ }).join('');
+}
+
+/**
+ * Returns a string with all invisible characters in plain text
+ *
+ * @param {String} line
+ * @return {String}
+ * @api private
+ */
+function escapeInvisibles(line) {
+ return line.replace(/\t/g, '<tab>')
+ .replace(/\r/g, '<CR>')
+ .replace(/\n/g, '<LF>\n');
+}
+
+/**
+ * Color lines for `str`, using the color `name`.
+ *
+ * @param {String} name
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+function colorLines(name, str) {
+ return str.split('\n').map(function(str){
+ return color(name, str);
+ }).join('\n');
+}
+
+/**
+ * Check that a / b have the same type.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Boolean}
+ * @api private
+ */
+
+function sameType(a, b) {
+ a = Object.prototype.toString.call(a);
+ b = Object.prototype.toString.call(b);
+ return a == b;
+}
+
+}); // module: reporters/base.js
+
+require.register("reporters/doc.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils');
+
+/**
+ * Expose `Doc`.
+ */
+
+exports = module.exports = Doc;
+
+/**
+ * Initialize a new `Doc` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Doc(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , total = runner.total
+ , indents = 2;
+
+ function indent() {
+ return Array(indents).join(' ');
+ }
+
+ runner.on('suite', function(suite){
+ if (suite.root) return;
+ ++indents;
+ console.log('%s<section class="suite">', indent());
+ ++indents;
+ console.log('%s<h1>%s</h1>', indent(), utils.escape(suite.title));
+ console.log('%s<dl>', indent());
+ });
+
+ runner.on('suite end', function(suite){
+ if (suite.root) return;
+ console.log('%s</dl>', indent());
+ --indents;
+ console.log('%s</section>', indent());
+ --indents;
+ });
+
+ runner.on('pass', function(test){
+ console.log('%s <dt>%s</dt>', indent(), utils.escape(test.title));
+ var code = utils.escape(utils.clean(test.fn.toString()));
+ console.log('%s <dd><pre><code>%s</code></pre></dd>', indent(), code);
+ });
+
+ runner.on('fail', function(test, err){
+ console.log('%s <dt class="error">%s</dt>', indent(), utils.escape(test.title));
+ var code = utils.escape(utils.clean(test.fn.toString()));
+ console.log('%s <dd class="error"><pre><code>%s</code></pre></dd>', indent(), code);
+ console.log('%s <dd class="error">%s</dd>', indent(), utils.escape(err));
+ });
+}
+
+}); // module: reporters/doc.js
+
+require.register("reporters/dot.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , color = Base.color;
+
+/**
+ * Expose `Dot`.
+ */
+
+exports = module.exports = Dot;
+
+/**
+ * Initialize a new `Dot` matrix test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Dot(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .75 | 0
+ , n = -1;
+
+ runner.on('start', function(){
+ process.stdout.write('\n ');
+ });
+
+ runner.on('pending', function(test){
+ if (++n % width == 0) process.stdout.write('\n ');
+ process.stdout.write(color('pending', Base.symbols.dot));
+ });
+
+ runner.on('pass', function(test){
+ if (++n % width == 0) process.stdout.write('\n ');
+ if ('slow' == test.speed) {
+ process.stdout.write(color('bright yellow', Base.symbols.dot));
+ } else {
+ process.stdout.write(color(test.speed, Base.symbols.dot));
+ }
+ });
+
+ runner.on('fail', function(test, err){
+ if (++n % width == 0) process.stdout.write('\n ');
+ process.stdout.write(color('fail', Base.symbols.dot));
+ });
+
+ runner.on('end', function(){
+ console.log();
+ self.epilogue();
+ });
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+Dot.prototype = new F;
+Dot.prototype.constructor = Dot;
+
+
+}); // module: reporters/dot.js
+
+require.register("reporters/html-cov.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var JSONCov = require('./json-cov')
+ , fs = require('browser/fs');
+
+/**
+ * Expose `HTMLCov`.
+ */
+
+exports = module.exports = HTMLCov;
+
+/**
+ * Initialize a new `JsCoverage` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function HTMLCov(runner) {
+ var jade = require('jade')
+ , file = __dirname + '/templates/coverage.jade'
+ , str = fs.readFileSync(file, 'utf8')
+ , fn = jade.compile(str, { filename: file })
+ , self = this;
+
+ JSONCov.call(this, runner, false);
+
+ runner.on('end', function(){
+ process.stdout.write(fn({
+ cov: self.cov
+ , coverageClass: coverageClass
+ }));
+ });
+}
+
+/**
+ * Return coverage class for `n`.
+ *
+ * @return {String}
+ * @api private
+ */
+
+function coverageClass(n) {
+ if (n >= 75) return 'high';
+ if (n >= 50) return 'medium';
+ if (n >= 25) return 'low';
+ return 'terrible';
+}
+
+}); // module: reporters/html-cov.js
+
+require.register("reporters/html.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils')
+ , Progress = require('../browser/progress')
+ , escape = utils.escape;
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Expose `HTML`.
+ */
+
+exports = module.exports = HTML;
+
+/**
+ * Stats template.
+ */
+
+var statsTemplate = '<ul id="mocha-stats">'
+ + '<li class="progress"><canvas width="40" height="40"></canvas></li>'
+ + '<li class="passes"><a href="#">passes:</a> <em>0</em></li>'
+ + '<li class="failures"><a href="#">failures:</a> <em>0</em></li>'
+ + '<li class="duration">duration: <em>0</em>s</li>'
+ + '</ul>';
+
+/**
+ * Initialize a new `HTML` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function HTML(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , total = runner.total
+ , stat = fragment(statsTemplate)
+ , items = stat.getElementsByTagName('li')
+ , passes = items[1].getElementsByTagName('em')[0]
+ , passesLink = items[1].getElementsByTagName('a')[0]
+ , failures = items[2].getElementsByTagName('em')[0]
+ , failuresLink = items[2].getElementsByTagName('a')[0]
+ , duration = items[3].getElementsByTagName('em')[0]
+ , canvas = stat.getElementsByTagName('canvas')[0]
+ , report = fragment('<ul id="mocha-report"></ul>')
+ , stack = [report]
+ , progress
+ , ctx
+ , root = document.getElementById('mocha');
+
+ if (canvas.getContext) {
+ var ratio = window.devicePixelRatio || 1;
+ canvas.style.width = canvas.width;
+ canvas.style.height = canvas.height;
+ canvas.width *= ratio;
+ canvas.height *= ratio;
+ ctx = canvas.getContext('2d');
+ ctx.scale(ratio, ratio);
+ progress = new Progress;
+ }
+
+ if (!root) return error('#mocha div missing, add it to your document');
+
+ // pass toggle
+ on(passesLink, 'click', function(){
+ unhide();
+ var name = /pass/.test(report.className) ? '' : ' pass';
+ report.className = report.className.replace(/fail|pass/g, '') + name;
+ if (report.className.trim()) hideSuitesWithout('test pass');
+ });
+
+ // failure toggle
+ on(failuresLink, 'click', function(){
+ unhide();
+ var name = /fail/.test(report.className) ? '' : ' fail';
+ report.className = report.className.replace(/fail|pass/g, '') + name;
+ if (report.className.trim()) hideSuitesWithout('test fail');
+ });
+
+ root.appendChild(stat);
+ root.appendChild(report);
+
+ if (progress) progress.size(40);
+
+ runner.on('suite', function(suite){
+ if (suite.root) return;
+
+ // suite
+ var url = self.suiteURL(suite);
+ var el = fragment('<li class="suite"><h1><a href="%s">%s</a></h1></li>', url, escape(suite.title));
+
+ // container
+ stack[0].appendChild(el);
+ stack.unshift(document.createElement('ul'));
+ el.appendChild(stack[0]);
+ });
+
+ runner.on('suite end', function(suite){
+ if (suite.root) return;
+ stack.shift();
+ });
+
+ runner.on('fail', function(test, err){
+ if ('hook' == test.type) runner.emit('test end', test);
+ });
+
+ runner.on('test end', function(test){
+ // TODO: add to stats
+ var percent = stats.tests / this.total * 100 | 0;
+ if (progress) progress.update(percent).draw(ctx);
+
+ // update stats
+ var ms = new Date - stats.start;
+ text(passes, stats.passes);
+ text(failures, stats.failures);
+ text(duration, (ms / 1000).toFixed(2));
+
+ // test
+ if ('passed' == test.state) {
+ var url = self.testURL(test);
+ var el = fragment('<li class="test pass %e"><h2>%e<span class="duration">%ems</span> <a href="%s" class="replay">‣</a></h2></li>', test.speed, test.title, test.duration, url);
+ } else if (test.pending) {
+ var el = fragment('<li class="test pass pending"><h2>%e</h2></li>', test.title);
+ } else {
+ var el = fragment('<li class="test fail"><h2>%e <a href="%e" class="replay">‣</a></h2></li>', test.title, self.testURL(test));
+ var str = test.err.stack || test.err.toString();
+
+ // FF / Opera do not add the message
+ if (!~str.indexOf(test.err.message)) {
+ str = test.err.message + '\n' + str;
+ }
+
+ // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
+ // check for the result of the stringifying.
+ if ('[object Error]' == str) str = test.err.message;
+
+ // Safari doesn't give you a stack. Let's at least provide a source line.
+ if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) {
+ str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")";
+ }
+
+ el.appendChild(fragment('<pre class="error">%e</pre>', str));
+ }
+
+ // toggle code
+ // TODO: defer
+ if (!test.pending) {
+ var h2 = el.getElementsByTagName('h2')[0];
+
+ on(h2, 'click', function(){
+ pre.style.display = 'none' == pre.style.display
+ ? 'block'
+ : 'none';
+ });
+
+ var pre = fragment('<pre><code>%e</code></pre>', utils.clean(test.fn.toString()));
+ el.appendChild(pre);
+ pre.style.display = 'none';
+ }
+
+ // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack.
+ if (stack[0]) stack[0].appendChild(el);
+ });
+}
+
+/**
+ * Makes a URL, preserving querystring ("search") parameters.
+ * @param {string} s
+ * @returns {string} your new URL
+ */
+var makeUrl = function makeUrl(s) {
+ var search = window.location.search;
+
+ // Remove previous grep query parameter if present
+ if (search) {
+ search = search.replace(/[?&]grep=[^&\s]*/g, '').replace(/^&/, '?');
+ }
+
+ return window.location.pathname + (search ? search + '&' : '?' ) + 'grep=' + encodeURIComponent(s);
+};
+
+/**
+ * Provide suite URL
+ *
+ * @param {Object} [suite]
+ */
+HTML.prototype.suiteURL = function(suite){
+ return makeUrl(suite.fullTitle());
+};
+
+/**
+ * Provide test URL
+ *
+ * @param {Object} [test]
+ */
+
+HTML.prototype.testURL = function(test){
+ return makeUrl(test.fullTitle());
+};
+
+/**
+ * Display error `msg`.
+ */
+
+function error(msg) {
+ document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
+}
+
+/**
+ * Return a DOM fragment from `html`.
+ */
+
+function fragment(html) {
+ var args = arguments
+ , div = document.createElement('div')
+ , i = 1;
+
+ div.innerHTML = html.replace(/%([se])/g, function(_, type){
+ switch (type) {
+ case 's': return String(args[i++]);
+ case 'e': return escape(args[i++]);
+ }
+ });
+
+ return div.firstChild;
+}
+
+/**
+ * Check for suites that do not have elements
+ * with `classname`, and hide them.
+ */
+
+function hideSuitesWithout(classname) {
+ var suites = document.getElementsByClassName('suite');
+ for (var i = 0; i < suites.length; i++) {
+ var els = suites[i].getElementsByClassName(classname);
+ if (0 == els.length) suites[i].className += ' hidden';
+ }
+}
+
+/**
+ * Unhide .hidden suites.
+ */
+
+function unhide() {
+ var els = document.getElementsByClassName('suite hidden');
+ for (var i = 0; i < els.length; ++i) {
+ els[i].className = els[i].className.replace('suite hidden', 'suite');
+ }
+}
+
+/**
+ * Set `el` text to `str`.
+ */
+
+function text(el, str) {
+ if (el.textContent) {
+ el.textContent = str;
+ } else {
+ el.innerText = str;
+ }
+}
+
+/**
+ * Listen on `event` with callback `fn`.
+ */
+
+function on(el, event, fn) {
+ if (el.addEventListener) {
+ el.addEventListener(event, fn, false);
+ } else {
+ el.attachEvent('on' + event, fn);
+ }
+}
+
+}); // module: reporters/html.js
+
+require.register("reporters/index.js", function(module, exports, require){
+exports.Base = require('./base');
+exports.Dot = require('./dot');
+exports.Doc = require('./doc');
+exports.TAP = require('./tap');
+exports.JSON = require('./json');
+exports.HTML = require('./html');
+exports.List = require('./list');
+exports.Min = require('./min');
+exports.Spec = require('./spec');
+exports.Nyan = require('./nyan');
+exports.XUnit = require('./xunit');
+exports.Markdown = require('./markdown');
+exports.Progress = require('./progress');
+exports.Landing = require('./landing');
+exports.JSONCov = require('./json-cov');
+exports.HTMLCov = require('./html-cov');
+exports.JSONStream = require('./json-stream');
+
+}); // module: reporters/index.js
+
+require.register("reporters/json-cov.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base');
+
+/**
+ * Expose `JSONCov`.
+ */
+
+exports = module.exports = JSONCov;
+
+/**
+ * Initialize a new `JsCoverage` reporter.
+ *
+ * @param {Runner} runner
+ * @param {Boolean} output
+ * @api public
+ */
+
+function JSONCov(runner, output) {
+ var self = this
+ , output = 1 == arguments.length ? true : output;
+
+ Base.call(this, runner);
+
+ var tests = []
+ , failures = []
+ , passes = [];
+
+ runner.on('test end', function(test){
+ tests.push(test);
+ });
+
+ runner.on('pass', function(test){
+ passes.push(test);
+ });
+
+ runner.on('fail', function(test){
+ failures.push(test);
+ });
+
+ runner.on('end', function(){
+ var cov = global._$jscoverage || {};
+ var result = self.cov = map(cov);
+ result.stats = self.stats;
+ result.tests = tests.map(clean);
+ result.failures = failures.map(clean);
+ result.passes = passes.map(clean);
+ if (!output) return;
+ process.stdout.write(JSON.stringify(result, null, 2 ));
+ });
+}
+
+/**
+ * Map jscoverage data to a JSON structure
+ * suitable for reporting.
+ *
+ * @param {Object} cov
+ * @return {Object}
+ * @api private
+ */
+
+function map(cov) {
+ var ret = {
+ instrumentation: 'node-jscoverage'
+ , sloc: 0
+ , hits: 0
+ , misses: 0
+ , coverage: 0
+ , files: []
+ };
+
+ for (var filename in cov) {
+ var data = coverage(filename, cov[filename]);
+ ret.files.push(data);
+ ret.hits += data.hits;
+ ret.misses += data.misses;
+ ret.sloc += data.sloc;
+ }
+
+ ret.files.sort(function(a, b) {
+ return a.filename.localeCompare(b.filename);
+ });
+
+ if (ret.sloc > 0) {
+ ret.coverage = (ret.hits / ret.sloc) * 100;
+ }
+
+ return ret;
+}
+
+/**
+ * Map jscoverage data for a single source file
+ * to a JSON structure suitable for reporting.
+ *
+ * @param {String} filename name of the source file
+ * @param {Object} data jscoverage coverage data
+ * @return {Object}
+ * @api private
+ */
+
+function coverage(filename, data) {
+ var ret = {
+ filename: filename,
+ coverage: 0,
+ hits: 0,
+ misses: 0,
+ sloc: 0,
+ source: {}
+ };
+
+ data.source.forEach(function(line, num){
+ num++;
+
+ if (data[num] === 0) {
+ ret.misses++;
+ ret.sloc++;
+ } else if (data[num] !== undefined) {
+ ret.hits++;
+ ret.sloc++;
+ }
+
+ ret.source[num] = {
+ source: line
+ , coverage: data[num] === undefined
+ ? ''
+ : data[num]
+ };
+ });
+
+ ret.coverage = ret.hits / ret.sloc * 100;
+
+ return ret;
+}
+
+/**
+ * Return a plain-object representation of `test`
+ * free of cyclic properties etc.
+ *
+ * @param {Object} test
+ * @return {Object}
+ * @api private
+ */
+
+function clean(test) {
+ return {
+ title: test.title
+ , fullTitle: test.fullTitle()
+ , duration: test.duration
+ }
+}
+
+}); // module: reporters/json-cov.js
+
+require.register("reporters/json-stream.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , color = Base.color;
+
+/**
+ * Expose `List`.
+ */
+
+exports = module.exports = List;
+
+/**
+ * Initialize a new `List` test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function List(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , total = runner.total;
+
+ runner.on('start', function(){
+ console.log(JSON.stringify(['start', { total: total }]));
+ });
+
+ runner.on('pass', function(test){
+ console.log(JSON.stringify(['pass', clean(test)]));
+ });
+
+ runner.on('fail', function(test, err){
+ test = clean(test);
+ test.err = err.message;
+ console.log(JSON.stringify(['fail', test]));
+ });
+
+ runner.on('end', function(){
+ process.stdout.write(JSON.stringify(['end', self.stats]));
+ });
+}
+
+/**
+ * Return a plain-object representation of `test`
+ * free of cyclic properties etc.
+ *
+ * @param {Object} test
+ * @return {Object}
+ * @api private
+ */
+
+function clean(test) {
+ return {
+ title: test.title
+ , fullTitle: test.fullTitle()
+ , duration: test.duration
+ }
+}
+
+}); // module: reporters/json-stream.js
+
+require.register("reporters/json.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `JSON`.
+ */
+
+exports = module.exports = JSONReporter;
+
+/**
+ * Initialize a new `JSON` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function JSONReporter(runner) {
+ var self = this;
+ Base.call(this, runner);
+
+ var tests = []
+ , pending = []
+ , failures = []
+ , passes = [];
+
+ runner.on('test end', function(test){
+ tests.push(test);
+ });
+
+ runner.on('pass', function(test){
+ passes.push(test);
+ });
+
+ runner.on('fail', function(test){
+ failures.push(test);
+ });
+
+ runner.on('pending', function(test){
+ pending.push(test);
+ });
+
+ runner.on('end', function(){
+ var obj = {
+ stats: self.stats,
+ tests: tests.map(clean),
+ pending: pending.map(clean),
+ failures: failures.map(clean),
+ passes: passes.map(clean)
+ };
+
+ runner.testResults = obj;
+
+ process.stdout.write(JSON.stringify(obj, null, 2));
+ });
+}
+
+/**
+ * Return a plain-object representation of `test`
+ * free of cyclic properties etc.
+ *
+ * @param {Object} test
+ * @return {Object}
+ * @api private
+ */
+
+function clean(test) {
+ return {
+ title: test.title,
+ fullTitle: test.fullTitle(),
+ duration: test.duration,
+ err: errorJSON(test.err || {})
+ }
+}
+
+/**
+ * Transform `error` into a JSON object.
+ * @param {Error} err
+ * @return {Object}
+ */
+
+function errorJSON(err) {
+ var res = {};
+ Object.getOwnPropertyNames(err).forEach(function(key) {
+ res[key] = err[key];
+ }, err);
+ return res;
+}
+
+}); // module: reporters/json.js
+
+require.register("reporters/landing.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `Landing`.
+ */
+
+exports = module.exports = Landing;
+
+/**
+ * Airplane color.
+ */
+
+Base.colors.plane = 0;
+
+/**
+ * Airplane crash color.
+ */
+
+Base.colors['plane crash'] = 31;
+
+/**
+ * Runway color.
+ */
+
+Base.colors.runway = 90;
+
+/**
+ * Initialize a new `Landing` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Landing(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .75 | 0
+ , total = runner.total
+ , stream = process.stdout
+ , plane = color('plane', '✈')
+ , crashed = -1
+ , n = 0;
+
+ function runway() {
+ var buf = Array(width).join('-');
+ return ' ' + color('runway', buf);
+ }
+
+ runner.on('start', function(){
+ stream.write('\n\n\n ');
+ cursor.hide();
+ });
+
+ runner.on('test end', function(test){
+ // check if the plane crashed
+ var col = -1 == crashed
+ ? width * ++n / total | 0
+ : crashed;
+
+ // show the crash
+ if ('failed' == test.state) {
+ plane = color('plane crash', '✈');
+ crashed = col;
+ }
+
+ // render landing strip
+ stream.write('\u001b['+(width+1)+'D\u001b[2A');
+ stream.write(runway());
+ stream.write('\n ');
+ stream.write(color('runway', Array(col).join('⋅')));
+ stream.write(plane)
+ stream.write(color('runway', Array(width - col).join('⋅') + '\n'));
+ stream.write(runway());
+ stream.write('\u001b[0m');
+ });
+
+ runner.on('end', function(){
+ cursor.show();
+ console.log();
+ self.epilogue();
+ });
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+Landing.prototype = new F;
+Landing.prototype.constructor = Landing;
+
+
+}); // module: reporters/landing.js
+
+require.register("reporters/list.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `List`.
+ */
+
+exports = module.exports = List;
+
+/**
+ * Initialize a new `List` test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function List(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , n = 0;
+
+ runner.on('start', function(){
+ console.log();
+ });
+
+ runner.on('test', function(test){
+ process.stdout.write(color('pass', ' ' + test.fullTitle() + ': '));
+ });
+
+ runner.on('pending', function(test){
+ var fmt = color('checkmark', ' -')
+ + color('pending', ' %s');
+ console.log(fmt, test.fullTitle());
+ });
+
+ runner.on('pass', function(test){
+ var fmt = color('checkmark', ' '+Base.symbols.dot)
+ + color('pass', ' %s: ')
+ + color(test.speed, '%dms');
+ cursor.CR();
+ console.log(fmt, test.fullTitle(), test.duration);
+ });
+
+ runner.on('fail', function(test, err){
+ cursor.CR();
+ console.log(color('fail', ' %d) %s'), ++n, test.fullTitle());
+ });
+
+ runner.on('end', self.epilogue.bind(self));
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+List.prototype = new F;
+List.prototype.constructor = List;
+
+
+}); // module: reporters/list.js
+
+require.register("reporters/markdown.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils');
+
+/**
+ * Constants
+ */
+
+var SUITE_PREFIX = '$';
+
+/**
+ * Expose `Markdown`.
+ */
+
+exports = module.exports = Markdown;
+
+/**
+ * Initialize a new `Markdown` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Markdown(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , level = 0
+ , buf = '';
+
+ function title(str) {
+ return Array(level).join('#') + ' ' + str;
+ }
+
+ function indent() {
+ return Array(level).join(' ');
+ }
+
+ function mapTOC(suite, obj) {
+ var ret = obj,
+ key = SUITE_PREFIX + suite.title;
+ obj = obj[key] = obj[key] || { suite: suite };
+ suite.suites.forEach(function(suite){
+ mapTOC(suite, obj);
+ });
+ return ret;
+ }
+
+ function stringifyTOC(obj, level) {
+ ++level;
+ var buf = '';
+ var link;
+ for (var key in obj) {
+ if ('suite' == key) continue;
+ if (key !== SUITE_PREFIX) {
+ link = ' - [' + key.substring(1) + ']';
+ link += '(#' + utils.slug(obj[key].suite.fullTitle()) + ')\n';
+ buf += Array(level).join(' ') + link;
+ }
+ buf += stringifyTOC(obj[key], level);
+ }
+ return buf;
+ }
+
+ function generateTOC(suite) {
+ var obj = mapTOC(suite, {});
+ return stringifyTOC(obj, 0);
+ }
+
+ generateTOC(runner.suite);
+
+ runner.on('suite', function(suite){
+ ++level;
+ var slug = utils.slug(suite.fullTitle());
+ buf += '<a name="' + slug + '"></a>' + '\n';
+ buf += title(suite.title) + '\n';
+ });
+
+ runner.on('suite end', function(suite){
+ --level;
+ });
+
+ runner.on('pass', function(test){
+ var code = utils.clean(test.fn.toString());
+ buf += test.title + '.\n';
+ buf += '\n```js\n';
+ buf += code + '\n';
+ buf += '```\n\n';
+ });
+
+ runner.on('end', function(){
+ process.stdout.write('# TOC\n');
+ process.stdout.write(generateTOC(runner.suite));
+ process.stdout.write(buf);
+ });
+}
+
+}); // module: reporters/markdown.js
+
+require.register("reporters/min.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base');
+
+/**
+ * Expose `Min`.
+ */
+
+exports = module.exports = Min;
+
+/**
+ * Initialize a new `Min` minimal test reporter (best used with --watch).
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Min(runner) {
+ Base.call(this, runner);
+
+ runner.on('start', function(){
+ // clear screen
+ process.stdout.write('\u001b[2J');
+ // set cursor position
+ process.stdout.write('\u001b[1;3H');
+ });
+
+ runner.on('end', this.epilogue.bind(this));
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+Min.prototype = new F;
+Min.prototype.constructor = Min;
+
+
+}); // module: reporters/min.js
+
+require.register("reporters/nyan.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base');
+
+/**
+ * Expose `Dot`.
+ */
+
+exports = module.exports = NyanCat;
+
+/**
+ * Initialize a new `Dot` matrix test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function NyanCat(runner) {
+ Base.call(this, runner);
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .75 | 0
+ , rainbowColors = this.rainbowColors = self.generateColors()
+ , colorIndex = this.colorIndex = 0
+ , numerOfLines = this.numberOfLines = 4
+ , trajectories = this.trajectories = [[], [], [], []]
+ , nyanCatWidth = this.nyanCatWidth = 11
+ , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth)
+ , scoreboardWidth = this.scoreboardWidth = 5
+ , tick = this.tick = 0
+ , n = 0;
+
+ runner.on('start', function(){
+ Base.cursor.hide();
+ self.draw();
+ });
+
+ runner.on('pending', function(test){
+ self.draw();
+ });
+
+ runner.on('pass', function(test){
+ self.draw();
+ });
+
+ runner.on('fail', function(test, err){
+ self.draw();
+ });
+
+ runner.on('end', function(){
+ Base.cursor.show();
+ for (var i = 0; i < self.numberOfLines; i++) write('\n');
+ self.epilogue();
+ });
+}
+
+/**
+ * Draw the nyan cat
+ *
+ * @api private
+ */
+
+NyanCat.prototype.draw = function(){
+ this.appendRainbow();
+ this.drawScoreboard();
+ this.drawRainbow();
+ this.drawNyanCat();
+ this.tick = !this.tick;
+};
+
+/**
+ * Draw the "scoreboard" showing the number
+ * of passes, failures and pending tests.
+ *
+ * @api private
+ */
+
+NyanCat.prototype.drawScoreboard = function(){
+ var stats = this.stats;
+
+ function draw(type, n) {
+ write(' ');
+ write(Base.color(type, n));
+ write('\n');
+ }
+
+ draw('green', stats.passes);
+ draw('fail', stats.failures);
+ draw('pending', stats.pending);
+ write('\n');
+
+ this.cursorUp(this.numberOfLines);
+};
+
+/**
+ * Append the rainbow.
+ *
+ * @api private
+ */
+
+NyanCat.prototype.appendRainbow = function(){
+ var segment = this.tick ? '_' : '-';
+ var rainbowified = this.rainbowify(segment);
+
+ for (var index = 0; index < this.numberOfLines; index++) {
+ var trajectory = this.trajectories[index];
+ if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift();
+ trajectory.push(rainbowified);
+ }
+};
+
+/**
+ * Draw the rainbow.
+ *
+ * @api private
+ */
+
+NyanCat.prototype.drawRainbow = function(){
+ var self = this;
+
+ this.trajectories.forEach(function(line, index) {
+ write('\u001b[' + self.scoreboardWidth + 'C');
+ write(line.join(''));
+ write('\n');
+ });
+
+ this.cursorUp(this.numberOfLines);
+};
+
+/**
+ * Draw the nyan cat
+ *
+ * @api private
+ */
+
+NyanCat.prototype.drawNyanCat = function() {
+ var self = this;
+ var startWidth = this.scoreboardWidth + this.trajectories[0].length;
+ var dist = '\u001b[' + startWidth + 'C';
+ var padding = '';
+
+ write(dist);
+ write('_,------,');
+ write('\n');
+
+ write(dist);
+ padding = self.tick ? ' ' : ' ';
+ write('_|' + padding + '/\\_/\\ ');
+ write('\n');
+
+ write(dist);
+ padding = self.tick ? '_' : '__';
+ var tail = self.tick ? '~' : '^';
+ var face;
+ write(tail + '|' + padding + this.face() + ' ');
+ write('\n');
+
+ write(dist);
+ padding = self.tick ? ' ' : ' ';
+ write(padding + '"" "" ');
+ write('\n');
+
+ this.cursorUp(this.numberOfLines);
+};
+
+/**
+ * Draw nyan cat face.
+ *
+ * @return {String}
+ * @api private
+ */
+
+NyanCat.prototype.face = function() {
+ var stats = this.stats;
+ if (stats.failures) {
+ return '( x .x)';
+ } else if (stats.pending) {
+ return '( o .o)';
+ } else if(stats.passes) {
+ return '( ^ .^)';
+ } else {
+ return '( - .-)';
+ }
+};
+
+/**
+ * Move cursor up `n`.
+ *
+ * @param {Number} n
+ * @api private
+ */
+
+NyanCat.prototype.cursorUp = function(n) {
+ write('\u001b[' + n + 'A');
+};
+
+/**
+ * Move cursor down `n`.
+ *
+ * @param {Number} n
+ * @api private
+ */
+
+NyanCat.prototype.cursorDown = function(n) {
+ write('\u001b[' + n + 'B');
+};
+
+/**
+ * Generate rainbow colors.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+NyanCat.prototype.generateColors = function(){
+ var colors = [];
+
+ for (var i = 0; i < (6 * 7); i++) {
+ var pi3 = Math.floor(Math.PI / 3);
+ var n = (i * (1.0 / 6));
+ var r = Math.floor(3 * Math.sin(n) + 3);
+ var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3);
+ var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3);
+ colors.push(36 * r + 6 * g + b + 16);
+ }
+
+ return colors;
+};
+
+/**
+ * Apply rainbow to the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+NyanCat.prototype.rainbowify = function(str){
+ if (!Base.useColors)
+ return str;
+ var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length];
+ this.colorIndex += 1;
+ return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m';
+};
+
+/**
+ * Stdout helper.
+ */
+
+function write(string) {
+ process.stdout.write(string);
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+NyanCat.prototype = new F;
+NyanCat.prototype.constructor = NyanCat;
+
+
+}); // module: reporters/nyan.js
+
+require.register("reporters/progress.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `Progress`.
+ */
+
+exports = module.exports = Progress;
+
+/**
+ * General progress bar color.
+ */
+
+Base.colors.progress = 90;
+
+/**
+ * Initialize a new `Progress` bar test reporter.
+ *
+ * @param {Runner} runner
+ * @param {Object} options
+ * @api public
+ */
+
+function Progress(runner, options) {
+ Base.call(this, runner);
+
+ var self = this
+ , options = options || {}
+ , stats = this.stats
+ , width = Base.window.width * .50 | 0
+ , total = runner.total
+ , complete = 0
+ , max = Math.max
+ , lastN = -1;
+
+ // default chars
+ options.open = options.open || '[';
+ options.complete = options.complete || '▬';
+ options.incomplete = options.incomplete || Base.symbols.dot;
+ options.close = options.close || ']';
+ options.verbose = false;
+
+ // tests started
+ runner.on('start', function(){
+ console.log();
+ cursor.hide();
+ });
+
+ // tests complete
+ runner.on('test end', function(){
+ complete++;
+ var incomplete = total - complete
+ , percent = complete / total
+ , n = width * percent | 0
+ , i = width - n;
+
+ if (lastN === n && !options.verbose) {
+ // Don't re-render the line if it hasn't changed
+ return;
+ }
+ lastN = n;
+
+ cursor.CR();
+ process.stdout.write('\u001b[J');
+ process.stdout.write(color('progress', ' ' + options.open));
+ process.stdout.write(Array(n).join(options.complete));
+ process.stdout.write(Array(i).join(options.incomplete));
+ process.stdout.write(color('progress', options.close));
+ if (options.verbose) {
+ process.stdout.write(color('progress', ' ' + complete + ' of ' + total));
+ }
+ });
+
+ // tests are complete, output some stats
+ // and the failures if any
+ runner.on('end', function(){
+ cursor.show();
+ console.log();
+ self.epilogue();
+ });
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+Progress.prototype = new F;
+Progress.prototype.constructor = Progress;
+
+
+}); // module: reporters/progress.js
+
+require.register("reporters/spec.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `Spec`.
+ */
+
+exports = module.exports = Spec;
+
+/**
+ * Initialize a new `Spec` test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Spec(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , indents = 0
+ , n = 0;
+
+ function indent() {
+ return Array(indents).join(' ')
+ }
+
+ runner.on('start', function(){
+ console.log();
+ });
+
+ runner.on('suite', function(suite){
+ ++indents;
+ console.log(color('suite', '%s%s'), indent(), suite.title);
+ });
+
+ runner.on('suite end', function(suite){
+ --indents;
+ if (1 == indents) console.log();
+ });
+
+ runner.on('pending', function(test){
+ var fmt = indent() + color('pending', ' - %s');
+ console.log(fmt, test.title);
+ });
+
+ runner.on('pass', function(test){
+ if ('fast' == test.speed) {
+ var fmt = indent()
+ + color('checkmark', ' ' + Base.symbols.ok)
+ + color('pass', ' %s ');
+ cursor.CR();
+ console.log(fmt, test.title);
+ } else {
+ var fmt = indent()
+ + color('checkmark', ' ' + Base.symbols.ok)
+ + color('pass', ' %s ')
+ + color(test.speed, '(%dms)');
+ cursor.CR();
+ console.log(fmt, test.title, test.duration);
+ }
+ });
+
+ runner.on('fail', function(test, err){
+ cursor.CR();
+ console.log(indent() + color('fail', ' %d) %s'), ++n, test.title);
+ });
+
+ runner.on('end', self.epilogue.bind(self));
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+Spec.prototype = new F;
+Spec.prototype.constructor = Spec;
+
+
+}); // module: reporters/spec.js
+
+require.register("reporters/tap.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `TAP`.
+ */
+
+exports = module.exports = TAP;
+
+/**
+ * Initialize a new `TAP` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function TAP(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , n = 1
+ , passes = 0
+ , failures = 0;
+
+ runner.on('start', function(){
+ var total = runner.grepTotal(runner.suite);
+ console.log('%d..%d', 1, total);
+ });
+
+ runner.on('test end', function(){
+ ++n;
+ });
+
+ runner.on('pending', function(test){
+ console.log('ok %d %s # SKIP -', n, title(test));
+ });
+
+ runner.on('pass', function(test){
+ passes++;
+ console.log('ok %d %s', n, title(test));
+ });
+
+ runner.on('fail', function(test, err){
+ failures++;
+ console.log('not ok %d %s', n, title(test));
+ if (err.stack) console.log(err.stack.replace(/^/gm, ' '));
+ });
+
+ runner.on('end', function(){
+ console.log('# tests ' + (passes + failures));
+ console.log('# pass ' + passes);
+ console.log('# fail ' + failures);
+ });
+}
+
+/**
+ * Return a TAP-safe title of `test`
+ *
+ * @param {Object} test
+ * @return {String}
+ * @api private
+ */
+
+function title(test) {
+ return test.fullTitle().replace(/#/g, '');
+}
+
+}); // module: reporters/tap.js
+
+require.register("reporters/xunit.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils')
+ , fs = require('browser/fs')
+ , escape = utils.escape;
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Expose `XUnit`.
+ */
+
+exports = module.exports = XUnit;
+
+/**
+ * Initialize a new `XUnit` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function XUnit(runner, options) {
+ Base.call(this, runner);
+ var stats = this.stats
+ , tests = []
+ , self = this;
+
+ if (options.reporterOptions && options.reporterOptions.output) {
+ if (! fs.createWriteStream) {
+ throw new Error('file output not supported in browser');
+ }
+ self.fileStream = fs.createWriteStream(options.reporterOptions.output);
+ }
+
+ runner.on('pending', function(test){
+ tests.push(test);
+ });
+
+ runner.on('pass', function(test){
+ tests.push(test);
+ });
+
+ runner.on('fail', function(test){
+ tests.push(test);
+ });
+
+ runner.on('end', function(){
+ self.write(tag('testsuite', {
+ name: 'Mocha Tests'
+ , tests: stats.tests
+ , failures: stats.failures
+ , errors: stats.failures
+ , skipped: stats.tests - stats.failures - stats.passes
+ , timestamp: (new Date).toUTCString()
+ , time: (stats.duration / 1000) || 0
+ }, false));
+
+ tests.forEach(function(t) { self.test(t); });
+ self.write('</testsuite>');
+ });
+}
+
+/**
+ * Override done to close the stream (if it's a file).
+ */
+XUnit.prototype.done = function(failures, fn) {
+ if (this.fileStream) {
+ this.fileStream.end(function() {
+ fn(failures);
+ });
+ } else {
+ fn(failures);
+ }
+};
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){};
+F.prototype = Base.prototype;
+XUnit.prototype = new F;
+XUnit.prototype.constructor = XUnit;
+
+
+/**
+ * Write out the given line
+ */
+XUnit.prototype.write = function(line) {
+ if (this.fileStream) {
+ this.fileStream.write(line + '\n');
+ } else {
+ console.log(line);
+ }
+};
+
+/**
+ * Output tag for the given `test.`
+ */
+
+XUnit.prototype.test = function(test, ostream) {
+ var attrs = {
+ classname: test.parent.fullTitle()
+ , name: test.title
+ , time: (test.duration / 1000) || 0
+ };
+
+ if ('failed' == test.state) {
+ var err = test.err;
+ this.write(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack))));
+ } else if (test.pending) {
+ this.write(tag('testcase', attrs, false, tag('skipped', {}, true)));
+ } else {
+ this.write(tag('testcase', attrs, true) );
+ }
+};
+
+/**
+ * HTML tag helper.
+ */
+
+function tag(name, attrs, close, content) {
+ var end = close ? '/>' : '>'
+ , pairs = []
+ , tag;
+
+ for (var key in attrs) {
+ pairs.push(key + '="' + escape(attrs[key]) + '"');
+ }
+
+ tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end;
+ if (content) tag += content + '</' + name + end;
+ return tag;
+}
+
+/**
+ * Return cdata escaped CDATA `str`.
+ */
+
+function cdata(str) {
+ return '<![CDATA[' + escape(str) + ']]>';
+}
+
+}); // module: reporters/xunit.js
+
+require.register("runnable.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('browser/events').EventEmitter
+ , debug = require('browser/debug')('mocha:runnable')
+ , Pending = require('./pending')
+ , milliseconds = require('./ms')
+ , utils = require('./utils');
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Object#toString().
+ */
+
+var toString = Object.prototype.toString;
+
+/**
+ * Expose `Runnable`.
+ */
+
+module.exports = Runnable;
+
+/**
+ * Initialize a new `Runnable` with the given `title` and callback `fn`.
+ *
+ * @param {String} title
+ * @param {Function} fn
+ * @api private
+ */
+
+function Runnable(title, fn) {
+ this.title = title;
+ this.fn = fn;
+ this.async = fn && fn.length;
+ this.sync = ! this.async;
+ this._timeout = 2000;
+ this._slow = 75;
+ this._enableTimeouts = true;
+ this.timedOut = false;
+ this._trace = new Error('done() called multiple times')
+}
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+function F(){};
+F.prototype = EventEmitter.prototype;
+Runnable.prototype = new F;
+Runnable.prototype.constructor = Runnable;
+
+
+/**
+ * Set & get timeout `ms`.
+ *
+ * @param {Number|String} ms
+ * @return {Runnable|Number} ms or self
+ * @api private
+ */
+
+Runnable.prototype.timeout = function(ms){
+ if (0 == arguments.length) return this._timeout;
+ if (ms === 0) this._enableTimeouts = false;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('timeout %d', ms);
+ this._timeout = ms;
+ if (this.timer) this.resetTimeout();
+ return this;
+};
+
+/**
+ * Set & get slow `ms`.
+ *
+ * @param {Number|String} ms
+ * @return {Runnable|Number} ms or self
+ * @api private
+ */
+
+Runnable.prototype.slow = function(ms){
+ if (0 === arguments.length) return this._slow;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('timeout %d', ms);
+ this._slow = ms;
+ return this;
+};
+
+/**
+ * Set and & get timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Runnable|Boolean} enabled or self
+ * @api private
+ */
+
+Runnable.prototype.enableTimeouts = function(enabled){
+ if (arguments.length === 0) return this._enableTimeouts;
+ debug('enableTimeouts %s', enabled);
+ this._enableTimeouts = enabled;
+ return this;
+};
+
+/**
+ * Halt and mark as pending.
+ *
+ * @api private
+ */
+
+Runnable.prototype.skip = function(){
+ throw new Pending();
+};
+
+/**
+ * Return the full title generated by recursively
+ * concatenating the parent's full title.
+ *
+ * @return {String}
+ * @api public
+ */
+
+Runnable.prototype.fullTitle = function(){
+ return this.parent.fullTitle() + ' ' + this.title;
+};
+
+/**
+ * Clear the timeout.
+ *
+ * @api private
+ */
+
+Runnable.prototype.clearTimeout = function(){
+ clearTimeout(this.timer);
+};
+
+/**
+ * Inspect the runnable void of private properties.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Runnable.prototype.inspect = function(){
+ return JSON.stringify(this, function(key, val){
+ if ('_' == key[0]) return;
+ if ('parent' == key) return '#<Suite>';
+ if ('ctx' == key) return '#<Context>';
+ return val;
+ }, 2);
+};
+
+/**
+ * Reset the timeout.
+ *
+ * @api private
+ */
+
+Runnable.prototype.resetTimeout = function(){
+ var self = this;
+ var ms = this.timeout() || 1e9;
+
+ if (!this._enableTimeouts) return;
+ this.clearTimeout();
+ this.timer = setTimeout(function(){
+ if (!self._enableTimeouts) return;
+ self.callback(new Error('timeout of ' + ms + 'ms exceeded. Ensure the done() callback is being called in this test.'));
+ self.timedOut = true;
+ }, ms);
+};
+
+/**
+ * Whitelist these globals for this test run
+ *
+ * @api private
+ */
+Runnable.prototype.globals = function(arr){
+ var self = this;
+ this._allowedGlobals = arr;
+};
+
+/**
+ * Run the test and invoke `fn(err)`.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+Runnable.prototype.run = function(fn){
+ var self = this
+ , start = new Date
+ , ctx = this.ctx
+ , finished
+ , emitted;
+
+ // Some times the ctx exists but it is not runnable
+ if (ctx && ctx.runnable) ctx.runnable(this);
+
+ // called multiple times
+ function multiple(err) {
+ if (emitted) return;
+ emitted = true;
+ self.emit('error', err || new Error('done() called multiple times; stacktrace may be inaccurate'));
+ }
+
+ // finished
+ function done(err) {
+ var ms = self.timeout();
+ if (self.timedOut) return;
+ if (finished) return multiple(err || self._trace);
+
+ // Discard the resolution if this test has already failed asynchronously
+ if (self.state) return;
+
+ self.clearTimeout();
+ self.duration = new Date - start;
+ finished = true;
+ if (!err && self.duration > ms && self._enableTimeouts) err = new Error('timeout of ' + ms + 'ms exceeded. Ensure the done() callback is being called in this test.');
+ fn(err);
+ }
+
+ // for .resetTimeout()
+ this.callback = done;
+
+ // explicit async with `done` argument
+ if (this.async) {
+ this.resetTimeout();
+
+ try {
+ this.fn.call(ctx, function(err){
+ if (err instanceof Error || toString.call(err) === "[object Error]") return done(err);
+ if (null != err) {
+ if (Object.prototype.toString.call(err) === '[object Object]') {
+ return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err)));
+ } else {
+ return done(new Error('done() invoked with non-Error: ' + err));
+ }
+ }
+ done();
+ });
+ } catch (err) {
+ done(utils.getError(err));
+ }
+ return;
+ }
+
+ if (this.asyncOnly) {
+ return done(new Error('--async-only option in use without declaring `done()`'));
+ }
+
+ // sync or promise-returning
+ try {
+ if (this.pending) {
+ done();
+ } else {
+ callFn(this.fn);
+ }
+ } catch (err) {
+ done(utils.getError(err));
+ }
+
+ function callFn(fn) {
+ var result = fn.call(ctx);
+ if (result && typeof result.then === 'function') {
+ self.resetTimeout();
+ result
+ .then(function() {
+ done()
+ },
+ function(reason) {
+ done(reason || new Error('Promise rejected with no or falsy reason'))
+ });
+ } else {
+ done();
+ }
+ }
+};
+
+}); // module: runnable.js
+
+require.register("runner.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('browser/events').EventEmitter
+ , debug = require('browser/debug')('mocha:runner')
+ , Pending = require('./pending')
+ , Test = require('./test')
+ , utils = require('./utils')
+ , filter = utils.filter
+ , keys = utils.keys
+ , type = utils.type
+ , stringify = utils.stringify
+ , stackFilter = utils.stackTraceFilter();
+
+/**
+ * Non-enumerable globals.
+ */
+
+var globals = [
+ 'setTimeout',
+ 'clearTimeout',
+ 'setInterval',
+ 'clearInterval',
+ 'XMLHttpRequest',
+ 'Date',
+ 'setImmediate',
+ 'clearImmediate'
+];
+
+/**
+ * Expose `Runner`.
+ */
+
+module.exports = Runner;
+
+/**
+ * Initialize a `Runner` for the given `suite`.
+ *
+ * Events:
+ *
+ * - `start` execution started
+ * - `end` execution complete
+ * - `suite` (suite) test suite execution started
+ * - `suite end` (suite) all tests (and sub-suites) have finished
+ * - `test` (test) test execution started
+ * - `test end` (test) test completed
+ * - `hook` (hook) hook execution started
+ * - `hook end` (hook) hook complete
+ * - `pass` (test) test passed
+ * - `fail` (test, err) test failed
+ * - `pending` (test) test pending
+ *
+ * @param {Suite} suite Root suite
+ * @param {boolean} [delay] Whether or not to delay execution of root suite
+ * until ready.
+ * @api public
+ */
+
+function Runner(suite, delay) {
+ var self = this;
+ this._globals = [];
+ this._abort = false;
+ this._delay = delay;
+ this.suite = suite;
+ this.total = suite.total();
+ this.failures = 0;
+ this.on('test end', function(test){ self.checkGlobals(test); });
+ this.on('hook end', function(hook){ self.checkGlobals(hook); });
+ this.grep(/.*/);
+ this.globals(this.globalProps().concat(extraGlobals()));
+}
+
+/**
+ * Wrapper for setImmediate, process.nextTick, or browser polyfill.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.immediately = global.setImmediate || process.nextTick;
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+function F(){};
+F.prototype = EventEmitter.prototype;
+Runner.prototype = new F;
+Runner.prototype.constructor = Runner;
+
+
+/**
+ * Run tests with full titles matching `re`. Updates runner.total
+ * with number of tests matched.
+ *
+ * @param {RegExp} re
+ * @param {Boolean} invert
+ * @return {Runner} for chaining
+ * @api public
+ */
+
+Runner.prototype.grep = function(re, invert){
+ debug('grep %s', re);
+ this._grep = re;
+ this._invert = invert;
+ this.total = this.grepTotal(this.suite);
+ return this;
+};
+
+/**
+ * Returns the number of tests matching the grep search for the
+ * given suite.
+ *
+ * @param {Suite} suite
+ * @return {Number}
+ * @api public
+ */
+
+Runner.prototype.grepTotal = function(suite) {
+ var self = this;
+ var total = 0;
+
+ suite.eachTest(function(test){
+ var match = self._grep.test(test.fullTitle());
+ if (self._invert) match = !match;
+ if (match) total++;
+ });
+
+ return total;
+};
+
+/**
+ * Return a list of global properties.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+Runner.prototype.globalProps = function() {
+ var props = utils.keys(global);
+
+ // non-enumerables
+ for (var i = 0; i < globals.length; ++i) {
+ if (~utils.indexOf(props, globals[i])) continue;
+ props.push(globals[i]);
+ }
+
+ return props;
+};
+
+/**
+ * Allow the given `arr` of globals.
+ *
+ * @param {Array} arr
+ * @return {Runner} for chaining
+ * @api public
+ */
+
+Runner.prototype.globals = function(arr){
+ if (0 == arguments.length) return this._globals;
+ debug('globals %j', arr);
+ this._globals = this._globals.concat(arr);
+ return this;
+};
+
+/**
+ * Check for global variable leaks.
+ *
+ * @api private
+ */
+
+Runner.prototype.checkGlobals = function(test){
+ if (this.ignoreLeaks) return;
+ var ok = this._globals;
+
+ var globals = this.globalProps();
+ var leaks;
+
+ if (test) {
+ ok = ok.concat(test._allowedGlobals || []);
+ }
+
+ if(this.prevGlobalsLength == globals.length) return;
+ this.prevGlobalsLength = globals.length;
+
+ leaks = filterLeaks(ok, globals);
+ this._globals = this._globals.concat(leaks);
+
+ if (leaks.length > 1) {
+ this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + ''));
+ } else if (leaks.length) {
+ this.fail(test, new Error('global leak detected: ' + leaks[0]));
+ }
+};
+
+/**
+ * Fail the given `test`.
+ *
+ * @param {Test} test
+ * @param {Error} err
+ * @api private
+ */
+
+Runner.prototype.fail = function(test, err) {
+ ++this.failures;
+ test.state = 'failed';
+
+ if (!(err instanceof Error)) {
+ err = new Error('the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)');
+ }
+
+ err.stack = this.fullStackTrace
+ ? err.stack
+ : stackFilter(err.stack);
+
+ this.emit('fail', test, err);
+};
+
+/**
+ * Fail the given `hook` with `err`.
+ *
+ * Hook failures work in the following pattern:
+ * - If bail, then exit
+ * - Failed `before` hook skips all tests in a suite and subsuites,
+ * but jumps to corresponding `after` hook
+ * - Failed `before each` hook skips remaining tests in a
+ * suite and jumps to corresponding `after each` hook,
+ * which is run only once
+ * - Failed `after` hook does not alter
+ * execution order
+ * - Failed `after each` hook skips remaining tests in a
+ * suite and subsuites, but executes other `after each`
+ * hooks
+ *
+ * @param {Hook} hook
+ * @param {Error} err
+ * @api private
+ */
+
+Runner.prototype.failHook = function(hook, err){
+ this.fail(hook, err);
+ if (this.suite.bail()) {
+ this.emit('end');
+ }
+};
+
+/**
+ * Run hook `name` callbacks and then invoke `fn()`.
+ *
+ * @param {String} name
+ * @param {Function} function
+ * @api private
+ */
+
+Runner.prototype.hook = function(name, fn){
+ var suite = this.suite
+ , hooks = suite['_' + name]
+ , self = this
+ , timer;
+
+ function next(i) {
+ var hook = hooks[i];
+ if (!hook) return fn();
+ self.currentRunnable = hook;
+
+ hook.ctx.currentTest = self.test;
+
+ self.emit('hook', hook);
+
+ hook.on('error', function(err){
+ self.failHook(hook, err);
+ });
+
+ hook.run(function(err){
+ hook.removeAllListeners('error');
+ var testError = hook.error();
+ if (testError) self.fail(self.test, testError);
+ if (err) {
+ if (err instanceof Pending) {
+ suite.pending = true;
+ } else {
+ self.failHook(hook, err);
+
+ // stop executing hooks, notify callee of hook err
+ return fn(err);
+ }
+ }
+ self.emit('hook end', hook);
+ delete hook.ctx.currentTest;
+ next(++i);
+ });
+ }
+
+ Runner.immediately(function(){
+ next(0);
+ });
+};
+
+/**
+ * Run hook `name` for the given array of `suites`
+ * in order, and callback `fn(err, errSuite)`.
+ *
+ * @param {String} name
+ * @param {Array} suites
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.hooks = function(name, suites, fn){
+ var self = this
+ , orig = this.suite;
+
+ function next(suite) {
+ self.suite = suite;
+
+ if (!suite) {
+ self.suite = orig;
+ return fn();
+ }
+
+ self.hook(name, function(err){
+ if (err) {
+ var errSuite = self.suite;
+ self.suite = orig;
+ return fn(err, errSuite);
+ }
+
+ next(suites.pop());
+ });
+ }
+
+ next(suites.pop());
+};
+
+/**
+ * Run hooks from the top level down.
+ *
+ * @param {String} name
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.hookUp = function(name, fn){
+ var suites = [this.suite].concat(this.parents()).reverse();
+ this.hooks(name, suites, fn);
+};
+
+/**
+ * Run hooks from the bottom up.
+ *
+ * @param {String} name
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.hookDown = function(name, fn){
+ var suites = [this.suite].concat(this.parents());
+ this.hooks(name, suites, fn);
+};
+
+/**
+ * Return an array of parent Suites from
+ * closest to furthest.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+Runner.prototype.parents = function(){
+ var suite = this.suite
+ , suites = [];
+ while (suite = suite.parent) suites.push(suite);
+ return suites;
+};
+
+/**
+ * Run the current test and callback `fn(err)`.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.runTest = function(fn){
+ var test = this.test
+ , self = this;
+
+ if (this.asyncOnly) test.asyncOnly = true;
+
+ try {
+ test.on('error', function(err){
+ self.fail(test, err);
+ });
+ test.run(fn);
+ } catch (err) {
+ fn(err);
+ }
+};
+
+/**
+ * Run tests in the given `suite` and invoke
+ * the callback `fn()` when complete.
+ *
+ * @param {Suite} suite
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.runTests = function(suite, fn){
+ var self = this
+ , tests = suite.tests.slice()
+ , test;
+
+
+ function hookErr(err, errSuite, after) {
+ // before/after Each hook for errSuite failed:
+ var orig = self.suite;
+
+ // for failed 'after each' hook start from errSuite parent,
+ // otherwise start from errSuite itself
+ self.suite = after ? errSuite.parent : errSuite;
+
+ if (self.suite) {
+ // call hookUp afterEach
+ self.hookUp('afterEach', function(err2, errSuite2) {
+ self.suite = orig;
+ // some hooks may fail even now
+ if (err2) return hookErr(err2, errSuite2, true);
+ // report error suite
+ fn(errSuite);
+ });
+ } else {
+ // there is no need calling other 'after each' hooks
+ self.suite = orig;
+ fn(errSuite);
+ }
+ }
+
+ function next(err, errSuite) {
+ // if we bail after first err
+ if (self.failures && suite._bail) return fn();
+
+ if (self._abort) return fn();
+
+ if (err) return hookErr(err, errSuite, true);
+
+ // next test
+ test = tests.shift();
+
+ // all done
+ if (!test) return fn();
+
+ // grep
+ var match = self._grep.test(test.fullTitle());
+ if (self._invert) match = !match;
+ if (!match) return next();
+
+ // pending
+ if (test.pending) {
+ self.emit('pending', test);
+ self.emit('test end', test);
+ return next();
+ }
+
+ // execute test and hook(s)
+ self.emit('test', self.test = test);
+ self.hookDown('beforeEach', function(err, errSuite){
+
+ if (suite.pending) {
+ self.emit('pending', test);
+ self.emit('test end', test);
+ return next();
+ }
+ if (err) return hookErr(err, errSuite, false);
+
+ self.currentRunnable = self.test;
+ self.runTest(function(err){
+ test = self.test;
+
+ if (err) {
+ if (err instanceof Pending) {
+ self.emit('pending', test);
+ } else {
+ self.fail(test, err);
+ }
+ self.emit('test end', test);
+
+ if (err instanceof Pending) {
+ return next();
+ }
+
+ return self.hookUp('afterEach', next);
+ }
+
+ test.state = 'passed';
+ self.emit('pass', test);
+ self.emit('test end', test);
+ self.hookUp('afterEach', next);
+ });
+ });
+ }
+
+ this.next = next;
+ next();
+};
+
+/**
+ * Run the given `suite` and invoke the
+ * callback `fn()` when complete.
+ *
+ * @param {Suite} suite
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.runSuite = function(suite, fn){
+ var total = this.grepTotal(suite)
+ , self = this
+ , i = 0;
+
+ debug('run suite %s', suite.fullTitle());
+
+ if (!total) return fn();
+
+ this.emit('suite', this.suite = suite);
+
+ function next(errSuite) {
+ if (errSuite) {
+ // current suite failed on a hook from errSuite
+ if (errSuite == suite) {
+ // if errSuite is current suite
+ // continue to the next sibling suite
+ return done();
+ } else {
+ // errSuite is among the parents of current suite
+ // stop execution of errSuite and all sub-suites
+ return done(errSuite);
+ }
+ }
+
+ if (self._abort) return done();
+
+ var curr = suite.suites[i++];
+ if (!curr) return done();
+ self.runSuite(curr, next);
+ }
+
+ function done(errSuite) {
+ self.suite = suite;
+ self.hook('afterAll', function(){
+ self.emit('suite end', suite);
+ fn(errSuite);
+ });
+ }
+
+ this.hook('beforeAll', function(err){
+ if (err) return done();
+ self.runTests(suite, next);
+ });
+};
+
+/**
+ * Handle uncaught exceptions.
+ *
+ * @param {Error} err
+ * @api private
+ */
+
+Runner.prototype.uncaught = function(err){
+ if (err) {
+ debug('uncaught exception %s', err !== function () {
+ return this;
+ }.call(err) ? err : ( err.message || err ));
+ } else {
+ debug('uncaught undefined exception');
+ err = utils.undefinedError();
+ }
+ err.uncaught = true;
+
+ var runnable = this.currentRunnable;
+ if (!runnable) return;
+
+ runnable.clearTimeout();
+
+ // Ignore errors if complete
+ if (runnable.state) return;
+ this.fail(runnable, err);
+
+ // recover from test
+ if ('test' == runnable.type) {
+ this.emit('test end', runnable);
+ this.hookUp('afterEach', this.next);
+ return;
+ }
+
+ // bail on hooks
+ this.emit('end');
+};
+
+/**
+ * Run the root suite and invoke `fn(failures)`
+ * on completion.
+ *
+ * @param {Function} fn
+ * @return {Runner} for chaining
+ * @api public
+ */
+
+Runner.prototype.run = function(fn){
+ var self = this,
+ rootSuite = this.suite;
+
+ fn = fn || function(){};
+
+ function uncaught(err){
+ self.uncaught(err);
+ }
+
+ function start() {
+ self.emit('start');
+ self.runSuite(rootSuite, function(){
+ debug('finished running');
+ self.emit('end');
+ });
+ }
+
+ debug('start');
+
+ // callback
+ this.on('end', function(){
+ debug('end');
+ process.removeListener('uncaughtException', uncaught);
+ fn(self.failures);
+ });
+
+ // uncaught exception
+ process.on('uncaughtException', uncaught);
+
+ if (this._delay) {
+ // for reporters, I guess.
+ // might be nice to debounce some dots while we wait.
+ this.emit('waiting', rootSuite);
+ rootSuite.once('run', start);
+ }
+ else {
+ start();
+ }
+
+ return this;
+};
+
+/**
+ * Cleanly abort execution
+ *
+ * @return {Runner} for chaining
+ * @api public
+ */
+Runner.prototype.abort = function(){
+ debug('aborting');
+ this._abort = true;
+};
+
+/**
+ * Filter leaks with the given globals flagged as `ok`.
+ *
+ * @param {Array} ok
+ * @param {Array} globals
+ * @return {Array}
+ * @api private
+ */
+
+function filterLeaks(ok, globals) {
+ return filter(globals, function(key){
+ // Firefox and Chrome exposes iframes as index inside the window object
+ if (/^d+/.test(key)) return false;
+
+ // in firefox
+ // if runner runs in an iframe, this iframe's window.getInterface method not init at first
+ // it is assigned in some seconds
+ if (global.navigator && /^getInterface/.test(key)) return false;
+
+ // an iframe could be approached by window[iframeIndex]
+ // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak
+ if (global.navigator && /^\d+/.test(key)) return false;
+
+ // Opera and IE expose global variables for HTML element IDs (issue #243)
+ if (/^mocha-/.test(key)) return false;
+
+ var matched = filter(ok, function(ok){
+ if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]);
+ return key == ok;
+ });
+ return matched.length == 0 && (!global.navigator || 'onerror' !== key);
+ });
+}
+
+/**
+ * Array of globals dependent on the environment.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+function extraGlobals() {
+ if (typeof(process) === 'object' &&
+ typeof(process.version) === 'string') {
+
+ var nodeVersion = process.version.split('.').reduce(function(a, v) {
+ return a << 8 | v;
+ });
+
+ // 'errno' was renamed to process._errno in v0.9.11.
+
+ if (nodeVersion < 0x00090B) {
+ return ['errno'];
+ }
+ }
+
+ return [];
+}
+
+}); // module: runner.js
+
+require.register("suite.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('browser/events').EventEmitter
+ , debug = require('browser/debug')('mocha:suite')
+ , milliseconds = require('./ms')
+ , utils = require('./utils')
+ , Hook = require('./hook');
+
+/**
+ * Expose `Suite`.
+ */
+
+exports = module.exports = Suite;
+
+/**
+ * Create a new `Suite` with the given `title`
+ * and parent `Suite`. When a suite with the
+ * same title is already present, that suite
+ * is returned to provide nicer reporter
+ * and more flexible meta-testing.
+ *
+ * @param {Suite} parent
+ * @param {String} title
+ * @return {Suite}
+ * @api public
+ */
+
+exports.create = function(parent, title){
+ var suite = new Suite(title, parent.ctx);
+ suite.parent = parent;
+ if (parent.pending) suite.pending = true;
+ title = suite.fullTitle();
+ parent.addSuite(suite);
+ return suite;
+};
+
+/**
+ * Initialize a new `Suite` with the given
+ * `title` and `ctx`.
+ *
+ * @param {String} title
+ * @param {Context} ctx
+ * @api private
+ */
+
+function Suite(title, parentContext) {
+ this.title = title;
+ var context = function() {};
+ context.prototype = parentContext;
+ this.ctx = new context();
+ this.suites = [];
+ this.tests = [];
+ this.pending = false;
+ this._beforeEach = [];
+ this._beforeAll = [];
+ this._afterEach = [];
+ this._afterAll = [];
+ this.root = !title;
+ this._timeout = 2000;
+ this._enableTimeouts = true;
+ this._slow = 75;
+ this._bail = false;
+ this.delayed = false;
+}
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+function F(){};
+F.prototype = EventEmitter.prototype;
+Suite.prototype = new F;
+Suite.prototype.constructor = Suite;
+
+
+/**
+ * Return a clone of this `Suite`.
+ *
+ * @return {Suite}
+ * @api private
+ */
+
+Suite.prototype.clone = function(){
+ var suite = new Suite(this.title);
+ debug('clone');
+ suite.ctx = this.ctx;
+ suite.timeout(this.timeout());
+ suite.enableTimeouts(this.enableTimeouts());
+ suite.slow(this.slow());
+ suite.bail(this.bail());
+ return suite;
+};
+
+/**
+ * Set timeout `ms` or short-hand such as "2s".
+ *
+ * @param {Number|String} ms
+ * @return {Suite|Number} for chaining
+ * @api private
+ */
+
+Suite.prototype.timeout = function(ms){
+ if (0 == arguments.length) return this._timeout;
+ if (ms.toString() === '0') this._enableTimeouts = false;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('timeout %d', ms);
+ this._timeout = parseInt(ms, 10);
+ return this;
+};
+
+/**
+ * Set timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Suite|Boolean} self or enabled
+ * @api private
+ */
+
+Suite.prototype.enableTimeouts = function(enabled){
+ if (arguments.length === 0) return this._enableTimeouts;
+ debug('enableTimeouts %s', enabled);
+ this._enableTimeouts = enabled;
+ return this;
+};
+
+/**
+ * Set slow `ms` or short-hand such as "2s".
+ *
+ * @param {Number|String} ms
+ * @return {Suite|Number} for chaining
+ * @api private
+ */
+
+Suite.prototype.slow = function(ms){
+ if (0 === arguments.length) return this._slow;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('slow %d', ms);
+ this._slow = ms;
+ return this;
+};
+
+/**
+ * Sets whether to bail after first error.
+ *
+ * @param {Boolean} bail
+ * @return {Suite|Number} for chaining
+ * @api private
+ */
+
+Suite.prototype.bail = function(bail){
+ if (0 == arguments.length) return this._bail;
+ debug('bail %s', bail);
+ this._bail = bail;
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` before running tests.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.beforeAll = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"before all" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._beforeAll.push(hook);
+ this.emit('beforeAll', hook);
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` after running tests.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.afterAll = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"after all" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._afterAll.push(hook);
+ this.emit('afterAll', hook);
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` before each test case.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.beforeEach = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"before each" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._beforeEach.push(hook);
+ this.emit('beforeEach', hook);
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` after each test case.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.afterEach = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"after each" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._afterEach.push(hook);
+ this.emit('afterEach', hook);
+ return this;
+};
+
+/**
+ * Add a test `suite`.
+ *
+ * @param {Suite} suite
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.addSuite = function(suite){
+ suite.parent = this;
+ suite.timeout(this.timeout());
+ suite.enableTimeouts(this.enableTimeouts());
+ suite.slow(this.slow());
+ suite.bail(this.bail());
+ this.suites.push(suite);
+ this.emit('suite', suite);
+ return this;
+};
+
+/**
+ * Add a `test` to this suite.
+ *
+ * @param {Test} test
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.addTest = function(test){
+ test.parent = this;
+ test.timeout(this.timeout());
+ test.enableTimeouts(this.enableTimeouts());
+ test.slow(this.slow());
+ test.ctx = this.ctx;
+ this.tests.push(test);
+ this.emit('test', test);
+ return this;
+};
+
+/**
+ * Return the full title generated by recursively
+ * concatenating the parent's full title.
+ *
+ * @return {String}
+ * @api public
+ */
+
+Suite.prototype.fullTitle = function(){
+ if (this.parent) {
+ var full = this.parent.fullTitle();
+ if (full) return full + ' ' + this.title;
+ }
+ return this.title;
+};
+
+/**
+ * Return the total number of tests.
+ *
+ * @return {Number}
+ * @api public
+ */
+
+Suite.prototype.total = function(){
+ return utils.reduce(this.suites, function(sum, suite){
+ return sum + suite.total();
+ }, 0) + this.tests.length;
+};
+
+/**
+ * Iterates through each suite recursively to find
+ * all tests. Applies a function in the format
+ * `fn(test)`.
+ *
+ * @param {Function} fn
+ * @return {Suite}
+ * @api private
+ */
+
+Suite.prototype.eachTest = function(fn){
+ utils.forEach(this.tests, fn);
+ utils.forEach(this.suites, function(suite){
+ suite.eachTest(fn);
+ });
+ return this;
+};
+
+/**
+ * This will run the root suite if we happen to be running in delayed mode.
+ */
+Suite.prototype.run = function run() {
+ if (this.root) {
+ this.emit('run');
+ }
+};
+
+}); // module: suite.js
+
+require.register("test.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Runnable = require('./runnable');
+
+/**
+ * Expose `Test`.
+ */
+
+module.exports = Test;
+
+/**
+ * Initialize a new `Test` with the given `title` and callback `fn`.
+ *
+ * @param {String} title
+ * @param {Function} fn
+ * @api private
+ */
+
+function Test(title, fn) {
+ Runnable.call(this, title, fn);
+ this.pending = !fn;
+ this.type = 'test';
+}
+
+/**
+ * Inherit from `Runnable.prototype`.
+ */
+
+function F(){};
+F.prototype = Runnable.prototype;
+Test.prototype = new F;
+Test.prototype.constructor = Test;
+
+
+}); // module: test.js
+
+require.register("utils.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var fs = require('browser/fs')
+ , path = require('browser/path')
+ , basename = path.basename
+ , exists = fs.existsSync || path.existsSync
+ , glob = require('browser/glob')
+ , join = path.join
+ , debug = require('browser/debug')('mocha:watch');
+
+/**
+ * Ignored directories.
+ */
+
+var ignore = ['node_modules', '.git'];
+
+/**
+ * Escape special characters in the given string of html.
+ *
+ * @param {String} html
+ * @return {String}
+ * @api private
+ */
+
+exports.escape = function(html){
+ return String(html)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+};
+
+/**
+ * Array#forEach (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} scope
+ * @api private
+ */
+
+exports.forEach = function(arr, fn, scope){
+ for (var i = 0, l = arr.length; i < l; i++)
+ fn.call(scope, arr[i], i);
+};
+
+/**
+ * Array#map (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} scope
+ * @api private
+ */
+
+exports.map = function(arr, fn, scope){
+ var result = [];
+ for (var i = 0, l = arr.length; i < l; i++)
+ result.push(fn.call(scope, arr[i], i, arr));
+ return result;
+};
+
+/**
+ * Array#indexOf (<=IE8)
+ *
+ * @parma {Array} arr
+ * @param {Object} obj to find index of
+ * @param {Number} start
+ * @api private
+ */
+
+exports.indexOf = function(arr, obj, start){
+ for (var i = start || 0, l = arr.length; i < l; i++) {
+ if (arr[i] === obj)
+ return i;
+ }
+ return -1;
+};
+
+/**
+ * Array#reduce (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} initial value
+ * @api private
+ */
+
+exports.reduce = function(arr, fn, val){
+ var rval = val;
+
+ for (var i = 0, l = arr.length; i < l; i++) {
+ rval = fn(rval, arr[i], i, arr);
+ }
+
+ return rval;
+};
+
+/**
+ * Array#filter (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.filter = function(arr, fn){
+ var ret = [];
+
+ for (var i = 0, l = arr.length; i < l; i++) {
+ var val = arr[i];
+ if (fn(val, i, arr)) ret.push(val);
+ }
+
+ return ret;
+};
+
+/**
+ * Object.keys (<=IE8)
+ *
+ * @param {Object} obj
+ * @return {Array} keys
+ * @api private
+ */
+
+exports.keys = Object.keys || function(obj) {
+ var keys = []
+ , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8
+
+ for (var key in obj) {
+ if (has.call(obj, key)) {
+ keys.push(key);
+ }
+ }
+
+ return keys;
+};
+
+/**
+ * Watch the given `files` for changes
+ * and invoke `fn(file)` on modification.
+ *
+ * @param {Array} files
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.watch = function(files, fn){
+ var options = { interval: 100 };
+ files.forEach(function(file){
+ debug('file %s', file);
+ fs.watchFile(file, options, function(curr, prev){
+ if (prev.mtime < curr.mtime) fn(file);
+ });
+ });
+};
+
+/**
+ * Array.isArray (<=IE8)
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+var isArray = Array.isArray || function (obj) {
+ return '[object Array]' == {}.toString.call(obj);
+};
+
+/**
+ * @description
+ * Buffer.prototype.toJSON polyfill
+ * @type {Function}
+ */
+if(typeof Buffer !== 'undefined' && Buffer.prototype) {
+ Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () {
+ return Array.prototype.slice.call(this, 0);
+ };
+}
+
+/**
+ * Ignored files.
+ */
+
+function ignored(path){
+ return !~ignore.indexOf(path);
+}
+
+/**
+ * Lookup files in the given `dir`.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+exports.files = function(dir, ext, ret){
+ ret = ret || [];
+ ext = ext || ['js'];
+
+ var re = new RegExp('\\.(' + ext.join('|') + ')$');
+
+ fs.readdirSync(dir)
+ .filter(ignored)
+ .forEach(function(path){
+ path = join(dir, path);
+ if (fs.statSync(path).isDirectory()) {
+ exports.files(path, ext, ret);
+ } else if (path.match(re)) {
+ ret.push(path);
+ }
+ });
+
+ return ret;
+};
+
+/**
+ * Compute a slug from the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+exports.slug = function(str){
+ return str
+ .toLowerCase()
+ .replace(/ +/g, '-')
+ .replace(/[^-\w]/g, '');
+};
+
+/**
+ * Strip the function definition from `str`,
+ * and re-indent for pre whitespace.
+ */
+
+exports.clean = function(str) {
+ str = str
+ .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '')
+ .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '')
+ .replace(/\s+\}$/, '');
+
+ var spaces = str.match(/^\n?( *)/)[1].length
+ , tabs = str.match(/^\n?(\t*)/)[1].length
+ , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm');
+
+ str = str.replace(re, '');
+
+ return exports.trim(str);
+};
+
+/**
+ * Trim the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+exports.trim = function(str){
+ return str.replace(/^\s+|\s+$/g, '');
+};
+
+/**
+ * Parse the given `qs`.
+ *
+ * @param {String} qs
+ * @return {Object}
+ * @api private
+ */
+
+exports.parseQuery = function(qs){
+ return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){
+ var i = pair.indexOf('=')
+ , key = pair.slice(0, i)
+ , val = pair.slice(++i);
+
+ obj[key] = decodeURIComponent(val);
+ return obj;
+ }, {});
+};
+
+/**
+ * Highlight the given string of `js`.
+ *
+ * @param {String} js
+ * @return {String}
+ * @api private
+ */
+
+function highlight(js) {
+ return js
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
+ .replace(/('.*?')/gm, '<span class="string">$1</span>')
+ .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
+ .replace(/(\d+)/gm, '<span class="number">$1</span>')
+ .replace(/\bnew[ \t]+(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
+ .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '<span class="keyword">$1</span>')
+}
+
+/**
+ * Highlight the contents of tag `name`.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+exports.highlightTags = function(name) {
+ var code = document.getElementById('mocha').getElementsByTagName(name);
+ for (var i = 0, len = code.length; i < len; ++i) {
+ code[i].innerHTML = highlight(code[i].innerHTML);
+ }
+};
+
+/**
+ * If a value could have properties, and has none, this function is called, which returns
+ * a string representation of the empty value.
+ *
+ * Functions w/ no properties return `'[Function]'`
+ * Arrays w/ length === 0 return `'[]'`
+ * Objects w/ no properties return `'{}'`
+ * All else: return result of `value.toString()`
+ *
+ * @param {*} value Value to inspect
+ * @param {string} [type] The type of the value, if known.
+ * @returns {string}
+ */
+var emptyRepresentation = function emptyRepresentation(value, type) {
+ type = type || exports.type(value);
+
+ switch(type) {
+ case 'function':
+ return '[Function]';
+ case 'object':
+ return '{}';
+ case 'array':
+ return '[]';
+ default:
+ return value.toString();
+ }
+};
+
+/**
+ * Takes some variable and asks `{}.toString()` what it thinks it is.
+ * @param {*} value Anything
+ * @example
+ * type({}) // 'object'
+ * type([]) // 'array'
+ * type(1) // 'number'
+ * type(false) // 'boolean'
+ * type(Infinity) // 'number'
+ * type(null) // 'null'
+ * type(new Date()) // 'date'
+ * type(/foo/) // 'regexp'
+ * type('type') // 'string'
+ * type(global) // 'global'
+ * @api private
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
+ * @returns {string}
+ */
+exports.type = function type(value) {
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
+ return 'buffer';
+ }
+ return Object.prototype.toString.call(value)
+ .replace(/^\[.+\s(.+?)\]$/, '$1')
+ .toLowerCase();
+};
+
+/**
+ * @summary Stringify `value`.
+ * @description Different behavior depending on type of value.
+ * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively.
+ * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes.
+ * - If `value` is an *empty* object, function, or array, return result of function
+ * {@link emptyRepresentation}.
+ * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of
+ * JSON.stringify().
+ *
+ * @see exports.type
+ * @param {*} value
+ * @return {string}
+ * @api private
+ */
+
+exports.stringify = function(value) {
+ var type = exports.type(value);
+
+ if (!~exports.indexOf(['object', 'array', 'function'], type)) {
+ if(type != 'buffer') {
+ return jsonStringify(value);
+ }
+ var json = value.toJSON();
+ // Based on the toJSON result
+ return jsonStringify(json.data && json.type ? json.data : json, 2)
+ .replace(/,(\n|$)/g, '$1');
+ }
+
+ for (var prop in value) {
+ if (Object.prototype.hasOwnProperty.call(value, prop)) {
+ return jsonStringify(exports.canonicalize(value), 2).replace(/,(\n|$)/g, '$1');
+ }
+ }
+
+ return emptyRepresentation(value, type);
+};
+
+/**
+ * @description
+ * like JSON.stringify but more sense.
+ * @param {Object} object
+ * @param {Number=} spaces
+ * @param {number=} depth
+ * @returns {*}
+ * @private
+ */
+function jsonStringify(object, spaces, depth) {
+ if(typeof spaces == 'undefined') return _stringify(object); // primitive types
+
+ depth = depth || 1;
+ var space = spaces * depth
+ , str = isArray(object) ? '[' : '{'
+ , end = isArray(object) ? ']' : '}'
+ , length = object.length || exports.keys(object).length
+ , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill
+
+ function _stringify(val) {
+ switch (exports.type(val)) {
+ case 'null':
+ case 'undefined':
+ val = '[' + val + ']';
+ break;
+ case 'array':
+ case 'object':
+ val = jsonStringify(val, spaces, depth + 1);
+ break;
+ case 'boolean':
+ case 'regexp':
+ case 'number':
+ val = val === 0 && (1/val) === -Infinity // `-0`
+ ? '-0'
+ : val.toString();
+ break;
+ case 'date':
+ val = '[Date: ' + val.toISOString() + ']';
+ break;
+ case 'buffer':
+ var json = val.toJSON();
+ // Based on the toJSON result
+ json = json.data && json.type ? json.data : json;
+ val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']';
+ break;
+ default:
+ val = (val == '[Function]' || val == '[Circular]')
+ ? val
+ : '"' + val + '"'; //string
+ }
+ return val;
+ }
+
+ for(var i in object) {
+ if(!object.hasOwnProperty(i)) continue; // not my business
+ --length;
+ str += '\n ' + repeat(' ', space)
+ + (isArray(object) ? '' : '"' + i + '": ') // key
+ + _stringify(object[i]) // value
+ + (length ? ',' : ''); // comma
+ }
+
+ return str + (str.length != 1 // [], {}
+ ? '\n' + repeat(' ', --space) + end
+ : end);
+}
+
+/**
+ * Return if obj is a Buffer
+ * @param {Object} arg
+ * @return {Boolean}
+ * @api private
+ */
+exports.isBuffer = function (arg) {
+ return typeof Buffer !== 'undefined' && Buffer.isBuffer(arg);
+};
+
+/**
+ * @summary Return a new Thing that has the keys in sorted order. Recursive.
+ * @description If the Thing...
+ * - has already been seen, return string `'[Circular]'`
+ * - is `undefined`, return string `'[undefined]'`
+ * - is `null`, return value `null`
+ * - is some other primitive, return the value
+ * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method
+ * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again.
+ * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()`
+ *
+ * @param {*} value Thing to inspect. May or may not have properties.
+ * @param {Array} [stack=[]] Stack of seen values
+ * @return {(Object|Array|Function|string|undefined)}
+ * @see {@link exports.stringify}
+ * @api private
+ */
+
+exports.canonicalize = function(value, stack) {
+ var canonicalizedObj,
+ type = exports.type(value),
+ prop,
+ withStack = function withStack(value, fn) {
+ stack.push(value);
+ fn();
+ stack.pop();
+ };
+
+ stack = stack || [];
+
+ if (exports.indexOf(stack, value) !== -1) {
+ return '[Circular]';
+ }
+
+ switch(type) {
+ case 'undefined':
+ case 'buffer':
+ case 'null':
+ canonicalizedObj = value;
+ break;
+ case 'array':
+ withStack(value, function () {
+ canonicalizedObj = exports.map(value, function (item) {
+ return exports.canonicalize(item, stack);
+ });
+ });
+ break;
+ case 'function':
+ for (prop in value) {
+ canonicalizedObj = {};
+ break;
+ }
+ if (!canonicalizedObj) {
+ canonicalizedObj = emptyRepresentation(value, type);
+ break;
+ }
+ /* falls through */
+ case 'object':
+ canonicalizedObj = canonicalizedObj || {};
+ withStack(value, function () {
+ exports.forEach(exports.keys(value).sort(), function (key) {
+ canonicalizedObj[key] = exports.canonicalize(value[key], stack);
+ });
+ });
+ break;
+ case 'date':
+ case 'number':
+ case 'regexp':
+ case 'boolean':
+ canonicalizedObj = value;
+ break;
+ default:
+ canonicalizedObj = value.toString();
+ }
+
+ return canonicalizedObj;
+};
+
+/**
+ * Lookup file names at the given `path`.
+ */
+exports.lookupFiles = function lookupFiles(path, extensions, recursive) {
+ var files = [];
+ var re = new RegExp('\\.(' + extensions.join('|') + ')$');
+
+ if (!exists(path)) {
+ if (exists(path + '.js')) {
+ path += '.js';
+ } else {
+ files = glob.sync(path);
+ if (!files.length) throw new Error("cannot resolve path (or pattern) '" + path + "'");
+ return files;
+ }
+ }
+
+ try {
+ var stat = fs.statSync(path);
+ if (stat.isFile()) return path;
+ }
+ catch (ignored) {
+ return;
+ }
+
+ fs.readdirSync(path).forEach(function(file) {
+ file = join(path, file);
+ try {
+ var stat = fs.statSync(file);
+ if (stat.isDirectory()) {
+ if (recursive) {
+ files = files.concat(lookupFiles(file, extensions, recursive));
+ }
+ return;
+ }
+ }
+ catch (ignored) {
+ return;
+ }
+ if (!stat.isFile() || !re.test(file) || basename(file)[0] === '.') return;
+ files.push(file);
+ });
+
+ return files;
+};
+
+/**
+ * Generate an undefined error with a message warning the user.
+ *
+ * @return {Error}
+ */
+
+exports.undefinedError = function() {
+ return new Error('Caught undefined error, did you throw without specifying what?');
+};
+
+/**
+ * Generate an undefined error if `err` is not defined.
+ *
+ * @param {Error} err
+ * @return {Error}
+ */
+
+exports.getError = function(err) {
+ return err || exports.undefinedError();
+};
+
+
+/**
+ * @summary
+ * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`)
+ * @description
+ * When invoking this function you get a filter function that get the Error.stack as an input,
+ * and return a prettify output.
+ * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace).
+ * @returns {Function}
+ */
+
+exports.stackTraceFilter = function() {
+ var slash = '/'
+ , is = typeof document === 'undefined'
+ ? { node: true }
+ : { browser: true }
+ , cwd = is.node
+ ? process.cwd() + slash
+ : location.href.replace(/\/[^\/]*$/, '/');
+
+ function isNodeModule (line) {
+ return (~line.indexOf('node_modules'));
+ }
+
+ function isMochaInternal (line) {
+ return (~line.indexOf('node_modules' + slash + 'mocha')) ||
+ (~line.indexOf('components' + slash + 'mochajs')) ||
+ (~line.indexOf('components' + slash + 'mocha'));
+ }
+
+ // node_modules, bower, componentJS
+ function isBrowserModule(line) {
+ return (~line.indexOf('node_modules')) ||
+ (~line.indexOf('components'));
+ }
+
+ function isNodeInternal (line) {
+ return (~line.indexOf('(timers.js:')) ||
+ (~line.indexOf('(events.js:')) ||
+ (~line.indexOf('(node.js:')) ||
+ (~line.indexOf('(module.js:')) ||
+ (~line.indexOf('GeneratorFunctionPrototype.next (native)')) ||
+ false
+ }
+
+ return function(stack) {
+ stack = stack.split('\n');
+
+ stack = stack.reduce(function (list, line) {
+ if (is.node && (isNodeModule(line) ||
+ isMochaInternal(line) ||
+ isNodeInternal(line)))
+ return list;
+
+ if (is.browser && (isBrowserModule(line)))
+ return list;
+
+ // Clean up cwd(absolute)
+ list.push(line.replace(cwd, ''));
+ return list;
+ }, []);
+
+ return stack.join('\n');
+ }
+};
+}); // module: utils.js
+// The global object is "self" in Web Workers.
+var global = (function() { return this; })();
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date;
+var setTimeout = global.setTimeout;
+var setInterval = global.setInterval;
+var clearTimeout = global.clearTimeout;
+var clearInterval = global.clearInterval;
+
+/**
+ * Node shims.
+ *
+ * These are meant only to allow
+ * mocha.js to run untouched, not
+ * to allow running node code in
+ * the browser.
+ */
+
+var process = {};
+process.exit = function(status){};
+process.stdout = {};
+
+var uncaughtExceptionHandlers = [];
+
+var originalOnerrorHandler = global.onerror;
+
+/**
+ * Remove uncaughtException listener.
+ * Revert to original onerror handler if previously defined.
+ */
+
+process.removeListener = function(e, fn){
+ if ('uncaughtException' == e) {
+ if (originalOnerrorHandler) {
+ global.onerror = originalOnerrorHandler;
+ } else {
+ global.onerror = function() {};
+ }
+ var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn);
+ if (i != -1) { uncaughtExceptionHandlers.splice(i, 1); }
+ }
+};
+
+/**
+ * Implements uncaughtException listener.
+ */
+
+process.on = function(e, fn){
+ if ('uncaughtException' == e) {
+ global.onerror = function(err, url, line){
+ fn(new Error(err + ' (' + url + ':' + line + ')'));
+ return true;
+ };
+ uncaughtExceptionHandlers.push(fn);
+ }
+};
+
+/**
+ * Expose mocha.
+ */
+
+var Mocha = global.Mocha = require('mocha'),
+ mocha = global.mocha = new Mocha({ reporter: 'html' });
+
+// The BDD UI is registered by default, but no UI will be functional in the
+// browser without an explicit call to the overridden `mocha.ui` (see below).
+// Ensure that this default UI does not expose its methods to the global scope.
+mocha.suite.removeAllListeners('pre-require');
+
+var immediateQueue = []
+ , immediateTimeout;
+
+function timeslice() {
+ var immediateStart = new Date().getTime();
+ while (immediateQueue.length && (new Date().getTime() - immediateStart) < 100) {
+ immediateQueue.shift()();
+ }
+ if (immediateQueue.length) {
+ immediateTimeout = setTimeout(timeslice, 0);
+ } else {
+ immediateTimeout = null;
+ }
+}
+
+/**
+ * High-performance override of Runner.immediately.
+ */
+
+Mocha.Runner.immediately = function(callback) {
+ immediateQueue.push(callback);
+ if (!immediateTimeout) {
+ immediateTimeout = setTimeout(timeslice, 0);
+ }
+};
+
+/**
+ * Function to allow assertion libraries to throw errors directly into mocha.
+ * This is useful when running tests in a browser because window.onerror will
+ * only receive the 'message' attribute of the Error.
+ */
+mocha.throwError = function(err) {
+ Mocha.utils.forEach(uncaughtExceptionHandlers, function (fn) {
+ fn(err);
+ });
+ throw err;
+};
+
+/**
+ * Override ui to ensure that the ui functions are initialized.
+ * Normally this would happen in Mocha.prototype.loadFiles.
+ */
+
+mocha.ui = function(ui){
+ Mocha.prototype.ui.call(this, ui);
+ this.suite.emit('pre-require', global, null, this);
+ return this;
+};
+
+/**
+ * Setup mocha with the given setting options.
+ */
+
+mocha.setup = function(opts){
+ if ('string' == typeof opts) opts = { ui: opts };
+ for (var opt in opts) {
+ if (opts.hasOwnProperty(opt)) {
+ this[opt](opts[opt]);
+ }
+ }
+ return this;
+};
+
+/**
+ * Run mocha, returning the Runner.
+ */
+
+mocha.run = function(fn){
+ var options = mocha.options;
+ mocha.globals('location');
+
+ var query = Mocha.utils.parseQuery(global.location.search || '');
+ if (query.grep) mocha.grep(new RegExp(query.grep));
+ if (query.fgrep) mocha.grep(query.fgrep);
+ if (query.invert) mocha.invert();
+
+ return Mocha.prototype.run.call(mocha, function(err){
+ // The DOM Document is not available in Web Workers.
+ var document = global.document;
+ if (document && document.getElementById('mocha') && options.noHighlighting !== true) {
+ Mocha.utils.highlightTags('code');
+ }
+ if (fn) fn(err);
+ });
+};
+
+/**
+ * Expose the process shim.
+ */
+
+Mocha.process = process;
+})();
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/.npmignore b/chromium/third_party/catapult/tracing/third_party/oboe/.npmignore
new file mode 100644
index 00000000000..7751ce36714
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/.npmignore
@@ -0,0 +1,12 @@
+libpeerconnection.log
+*.idea
+*.iml
+*.tidy
+*.backup
+.DS_Store
+writing
+dissertation
+src
+examples
+build
+benchmarking \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/.travis.yml b/chromium/third_party/catapult/tracing/third_party/oboe/.travis.yml
new file mode 100644
index 00000000000..6e5919de39a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - "0.10"
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/CONTRIBUTING.md b/chromium/third_party/catapult/tracing/third_party/oboe/CONTRIBUTING.md
new file mode 100644
index 00000000000..2dae8c9c4db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/CONTRIBUTING.md
@@ -0,0 +1,71 @@
+# Contributing to Oboe.js
+
+:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
+
+The following is a set of guidelines for contributing to Oboe.js. These are
+just guidelines, not rules, use your best judgment and feel free to propose
+changes to this document in a pull request.
+
+## Read the docs :)
+
+Oboe.js has some [awesome documentation](http://oboejs.com/)
+explaining how and why to use the library.
+
+## Questions/Help
+
+Sometimes your question can be addressed by reading the
+[API](http://oboejs.com/api) closely. It's short and nicely organized!
+
+Please post questions to [StackOverflow](http://stackoverflow.com/)
+using the `oboe.js` and`javascript` tags.
+
+If you file an issue with an implementation question, it will be closed.
+We're not trying to be mean, it just helps keep the issues tab cleaner so we can
+ keep improving the library.
+
+## Reporting Bugs / Requesting Features
+
+If you've found an issue, please submit it in
+[the issues](https://github.com/jimhigson/oboe.js/issues).
+
+To increase our ability to help, please:
+- If it's a server-side bug, fork our
+[bug-template](https://github.com/JuanCaicedo/oboe-bug-template), recreate your
+bug, and then provide a link to that repo.
+- If it's a client-side template, provide a link to a
+[jsbin](https://jsbin.com/)/[codepen](http://codepen.io/)/[plunkr](https://plnkr.co/)
+that demonstrates the issue (if it's on the client), or a github repo
+(if it's on the server), greatly increases our ability to help.
+
+## Pull Requests
+
+If you would like to add functionality, please submit
+[an issue](https://github.com/jimhigson/oboe.js/issues) first to make sure it's
+a direction we want to take.
+
+Please do the following:
+* Follow the existing styles
+* Create an example for that demonstrates your changes so people can see how
+your changes work
+
+In your PR description include any information that will help a maintainer
+understand and test your changes. The easier it is to read and run your PR,
+the faster it can get merged!
+
+### What does Oboe need help with?
+
+#### Helping others!
+
+There is a [Google Group](https://groups.google.com/forum/#!forum/oboejs) for
+general project discussion. If you have a moment to help other people using the
+library, please stop in.
+
+#### Contributing to community
+
+- Write examples! The website has a [section](http://oboejs.com/examples)
+showing common use-cases, and it could always use some more. Feel free to submit
+a PR to [the website](https://github.com/jimhigson/oboe.js-website).
+- We would also like to showcase applications using Oboe, so if you've published
+one and want to share it, file it
+[in the website issues](https://github.com/jimhigson/oboe.js-website/issues)
+and we'll showcase it!
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/Gruntfile.js b/chromium/third_party/catapult/tracing/third_party/oboe/Gruntfile.js
new file mode 100644
index 00000000000..8503f59954f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/Gruntfile.js
@@ -0,0 +1,398 @@
+module.exports = function (grunt) {
+
+ function runNpmScript(command, cb) {
+ var opts = {
+ cmd: 'npm',
+ args: ['run', command],
+ opts: {
+ stdio: 'inherit'
+ }
+ };
+
+ grunt.util.spawn(opts, function(error, result, code) {
+ if(error) {
+ grunt.fail.warn(command + " failed.");
+ }
+ cb();
+ });
+ }
+
+ var autoStartBrowsers = ['Chrome', 'Firefox', 'Safari'];
+
+ var STREAM_SOURCE_PORT_HTTP = 4567;
+
+ // NB: source files are order sensitive
+ var OBOE_BROWSER_SOURCE_FILES = [
+ 'build/version.js'
+ , 'src/LICENCE.js'
+ , 'src/functional.js'
+ , 'src/util.js'
+ , 'src/lists.js'
+ , 'src/libs/clarinet.js'
+ , 'src/ascentManager.js'
+ , 'src/parseResponseHeaders.browser.js'
+ , 'src/detectCrossOrigin.browser.js'
+ , 'src/streamingHttp.browser.js'
+ , 'src/jsonPathSyntax.js'
+ , 'src/ascent.js'
+ , 'src/incrementalContentBuilder.js'
+ , 'src/jsonPath.js'
+ , 'src/singleEventPubSub.js'
+ , 'src/pubSub.js'
+ , 'src/events.js'
+ , 'src/patternAdapter.js'
+ , 'src/instanceApi.js'
+ , 'src/wire.js'
+ , 'src/defaults.js'
+ , 'src/publicApi.js'
+ ];
+
+ var OBOE_NODE_SOURCE_FILES = [
+ 'build/version.js'
+ , 'src/LICENCE.js'
+ , 'src/functional.js'
+ , 'src/util.js'
+ , 'src/lists.js'
+ , 'src/libs/clarinet.js'
+ , 'src/ascentManager.js'
+ , 'src/streamingHttp.node.js'
+ , 'src/jsonPathSyntax.js'
+ , 'src/ascent.js'
+ , 'src/incrementalContentBuilder.js'
+ , 'src/jsonPath.js'
+ , 'src/singleEventPubSub.js'
+ , 'src/pubSub.js'
+ , 'src/events.js'
+ , 'src/patternAdapter.js'
+ , 'src/instanceApi.js'
+ , 'src/wire.js'
+ , 'src/defaults.js'
+ , 'src/publicApi.js'
+ ];
+
+ var FILES_TRIGGERING_KARMA = [
+ 'src/**/*.js',
+ 'test/specs/*.spec.js',
+ 'test/libs/*.js'
+ ];
+
+ // load the wrapper file for packaging source targeted at either
+ // browser or node
+ function wrapper(target){
+ return require('fs')
+ .readFileSync('src/wrapper.' + target + '.js', 'utf8')
+ .split('// ---contents--- //');
+ }
+
+ grunt.initConfig({
+
+ pkg:grunt.file.readJSON("package.json")
+
+ , clean: ['dist/*.js', 'build/*.js']
+
+ , concat: {
+ browser:{
+ src: OBOE_BROWSER_SOURCE_FILES,
+ dest: 'build/oboe-browser.concat.js'
+ },
+ node:{
+ src: OBOE_NODE_SOURCE_FILES,
+ dest: 'build/oboe-node.concat.js'
+ }
+ }
+
+ , wrap: {
+ browserPackage: {
+ src: 'build/oboe-browser.concat.js',
+ dest: '.',
+ wrapper: wrapper('browser')
+ },
+
+ nodePackage: {
+ src: 'build/oboe-node.concat.js',
+ dest: '.',
+ wrapper: wrapper('node')
+ }
+ }
+
+
+ , uglify: {
+ build:{
+ files:{
+ 'build/oboe-browser.min.js': 'build/oboe-browser.concat.js'
+ }
+ }
+ }
+
+ , karma: {
+ options:{
+ singleRun: true,
+ proxies: {
+ '/testServer' : 'http://localhost:' + STREAM_SOURCE_PORT_HTTP
+ },
+ // test results reporter to use
+ // possible values: 'dots', 'progress', 'junit'
+ reporters : ['progress'],
+
+ // enable / disable colors in the output (reporters and logs)
+ colors : true
+ }
+ ,
+ 'coverage':{
+ reporters : ['coverage'],
+ preprocessors: {
+ // source files to generate coverage for
+ // (these files will be instrumented by Istanbul)
+ 'src/**/*.js': ['coverage']
+ },
+ 'browsers': ['PhantomJS'],
+ configFile: 'test/unit.conf.js'
+ }
+
+ ,
+ 'precaptured-dev': {
+ // for doing a single test run with already captured browsers during development.
+ // this is good for running tests in browsers karma can't easily start such as
+ // IE running inside a Windows VM on a unix dev environment
+ browsers: [],
+ configFile: 'test/unit.conf.js',
+ singleRun: 'true'
+ }
+ ,
+ 'single-dev': {
+ browsers: autoStartBrowsers,
+ configFile: 'test/unit.conf.js'
+ }
+ ,
+ 'single-concat': {
+ browsers: autoStartBrowsers,
+ configFile: 'test/concat.conf.js'
+ }
+ ,
+ 'single-minified': {
+ browsers: autoStartBrowsers,
+ configFile: 'test/min.conf.js'
+ }
+
+ ,
+ 'single-amd': {
+ browsers: autoStartBrowsers,
+ configFile: 'test/amd.conf.js'
+ }
+
+ ,
+ 'single-browser-http': {
+ browsers: autoStartBrowsers,
+ configFile: 'test/http.conf.js'
+ }
+
+ ,
+ 'persist': {
+ // for setting up a persistent karma server.
+ // To start the server, the task is:
+ // karma:persist
+ // To run these, the task is:
+ // karma:persist:run
+ configFile: 'test/unit.conf.js',
+ browsers: [],
+ singleRun:false,
+ background:true
+ }
+ }
+
+ , copy: {
+ browserDist: {
+ files: [
+ {src: ['build/oboe-browser.min.js'], dest: 'dist/oboe-browser.min.js'}
+ , {src: ['build/oboe-browser.concat.js'], dest: 'dist/oboe-browser.js' }
+ ]
+ },
+ nodeDist: {
+ files: [
+ {src: ['build/oboe-node.concat.js'], dest: 'dist/oboe-node.js'}
+ ]
+ }
+ }
+
+ , exec:{
+ // these might not go too well on Windows :-) - get Cygwin.
+ reportMinifiedSize:{
+ command: "echo minified size is `wc -c < dist/oboe-browser.min.js` bytes"
+ },
+ reportMinifiedAndGzippedSize:{
+ command: "echo Size after gzip is `gzip --best --stdout dist/oboe-browser.min.js | wc -c` bytes - max 5120"
+ },
+ createGitVersionJs:{
+ command: "echo \"// `git describe`\" > build/version.js"
+ }
+ }
+
+ , watch:{
+ karma:{
+ files:FILES_TRIGGERING_KARMA,
+ tasks:['karma:persist:run']
+ },
+
+ // like above but reports the file size. This is good for
+ // watching while developing to make sure it doesn't get
+ // too big. Doesn't run tests against minified.
+ karmaAndSize:{
+ files: FILES_TRIGGERING_KARMA,
+ tasks:[
+ 'karma:persist:run',
+ 'browser-build',
+ 'dist-sizes']
+ },
+
+ // like above but reports the file size. This is good for
+ // watching while developing to make sure it doesn't get
+ // too big. Doesn't run tests against minified.
+ testNode:{
+ files: FILES_TRIGGERING_KARMA,
+ tasks:[
+ 'node-build']
+ },
+
+ restartStreamSourceAndRunTests:{
+ // this fails at the moment because start-stream-source
+ // fails if run more than once - the port is taken.
+ files: ['test/streamsource.js'],
+ tasks: ['start-stream-source', 'karma:persist:run']
+ }
+ }
+
+ , concurrent:{
+ watchDev: {
+ tasks:[ 'watch:karmaAndSize', 'watch:restartStreamSourceAndRunTests' ],
+ options:{
+ logConcurrentOutput: true
+ }
+ }
+ }
+
+ });
+
+ require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
+
+ var streamSource;
+
+ grunt.registerTask('start-stream-source', function () {
+ grunt.log.ok('do we have a streaming source already?', !!streamSource);
+
+ // if we previously loaded the streamsource, stop it to let the new one in:
+ if( streamSource ) {
+ grunt.log.ok('there seems to be a streaming server already, let\'s stop it');
+ streamSource.stop();
+ }
+
+ streamSource = require('./test/streamsource.js');
+ streamSource.start(STREAM_SOURCE_PORT_HTTP, grunt);
+ });
+
+ grunt.registerTask("jasmine_node_oboe", "Runs jasmine-node.", function() {
+ runNpmScript('test-node', this.async());
+ });
+
+ // change the auto-starting browsers so that future tests will use
+ // phantomjs instead of actual browsers. Can do:
+ // grunt headless-mode default
+ // to run without any actual browsers
+ grunt.registerTask('headless-mode', function(){
+ autoStartBrowsers.length = 0;
+ autoStartBrowsers.push('PhantomJS');
+ });
+
+ grunt.registerTask('test-start-server', [
+ 'karma:persist'
+ ]);
+
+ grunt.registerTask('test-run', [
+ 'karma:persist:run'
+ ]);
+
+ grunt.registerTask('dist-sizes', [
+ 'exec:reportMinifiedAndGzippedSize'
+ ]);
+
+ grunt.registerTask('node-build', [
+ 'exec:createGitVersionJs',
+ 'concat:node',
+ 'wrap:nodePackage',
+ 'copy:nodeDist'
+ ]);
+
+ grunt.registerTask('node-build-test', [
+ 'node-build',
+ 'jasmine_node_oboe'
+ ]);
+
+ grunt.registerTask('node', [
+ 'start-stream-source',
+ 'node-build-test'
+ ]);
+
+ grunt.registerTask('browser-build', [
+ 'exec:createGitVersionJs',
+ 'concat:browser',
+ 'concat:node',
+ 'wrap:browserPackage',
+ 'uglify',
+ 'copy:browserDist'
+ ]);
+
+ grunt.registerTask('browser-build-test', [
+ 'karma:single-dev',
+ 'karma:single-browser-http',
+ 'browser-build',
+ 'karma:single-concat',
+ 'karma:single-minified',
+ 'karma:single-amd'
+ ]);
+
+ grunt.registerTask('build', [
+ 'browser-build',
+ 'node-build'
+ ]);
+
+ // build and run just the integration tests.
+ grunt.registerTask('build-integration-test', [
+ 'build',
+ 'start-stream-source',
+ 'karma:single-concat',
+ 'jasmine_node_oboe',
+ 'dist-sizes'
+ ]);
+
+ grunt.registerTask('default', [
+
+ 'clear',
+ 'clean',
+ 'start-stream-source',
+
+ 'browser-build-test',
+
+ 'node-build-test',
+
+ 'dist-sizes'
+ ]);
+
+
+
+ // browser-test-auto-run or node-test-auto-run
+ //
+ // The most useful for developing. Start this task, capture some browsers
+ // (unless node) then edit the code. Tests will be run as the code is
+ // saved.
+ grunt.registerTask('browser-test-auto-run', [
+ 'start-stream-source',
+ 'karma:persist',
+ 'concurrent:watchDev'
+ ]);
+ grunt.registerTask('node-test-auto-run', [
+ 'start-stream-source',
+ 'watch:testNode'
+ ]);
+ grunt.registerTask('coverage', [
+ 'karma:coverage'
+ ]);
+};
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/LICENCE b/chromium/third_party/catapult/tracing/third_party/oboe/LICENCE
new file mode 100755
index 00000000000..261adc505fc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/LICENCE
@@ -0,0 +1,26 @@
+Copyright (c) 2013, Jim Higson
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. 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.
+
+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.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of the FreeBSD Project. \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/README.chromium b/chromium/third_party/catapult/tracing/third_party/oboe/README.chromium
new file mode 100644
index 00000000000..0f31e09f70e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/README.chromium
@@ -0,0 +1,19 @@
+Name: Oboe.js
+Short Name: oboe
+URL: https://github.com/jimhigson/oboe.js
+Version: 2.1.3
+Revision: 4b43d2f5e50733a6c6eac3725142ac1c1a69b0e3
+Date: Tue Jan 31 15:08:00 EST 2017
+License: BSD-2-Clause
+License File: LICENCSE
+Security Critical: no
+
+Description:
+Oboe.js is an open source Javascript library for loading JSON using streaming, combining the convenience of DOM with the speed and fluidity of SAX.
+
+Local Modifications:
+Although the whole repository is pulled, we only use dist/oboe-browser.js and dist/oboe-node.js. There are some local modifications, explained below:
+
+* dist/oboe-node.js:1092: the internal 'http' module is used to remove dependency on external the 'http-https' module.
+* dist/oboe-node.js:2439-2442: two methods (write, finish) are exposed in the public API for manually sending data to the parser.
+* dist/oboe-browser.js:2545-2548: two methods (write, finish) are exposed in the public API for manually sending data to the parser.
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/README.md b/chromium/third_party/catapult/tracing/third_party/oboe/README.md
new file mode 100644
index 00000000000..094b88f1fba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/README.md
@@ -0,0 +1,24 @@
+Oboe.js is an [open source](LICENCE) Javascript library
+for loading JSON using streaming, combining the convenience of DOM with
+the speed and fluidity of SAX.
+
+It can parse any JSON as a stream, is small enough to be a [micro-library](http://microjs.com/#),
+doesn't have dependencies, and doesn't care which other libraries you need it to speak to.
+
+We can load trees [larger than the available memory](http://oboejs.com/examples#loading-json-trees-larger-than-the-available-ram).
+Or we can [instantiate classical OOP models from JSON](http://oboejs.com/examples#demarshalling-json-to-an-oop-model),
+or [completely transform your JSON](http://oboejs.com/examples#transforming-json-while-it-is-streaming) while it is being read.
+
+Oboe makes it really easy to start using json from a response before the ajax request completes.
+Or even if it never completes.
+
+Where next?
+-----------
+
+- [The website](http://oboejs.com)
+- Visualise [faster web applications through streaming](http://oboejs.com/why)
+- Visit the [project homepage](http://oboejs.com)
+- Browse [code examples](http://oboejs.com/examples)
+- Learn the Oboe.js [API](http://oboejs.com/api)
+- [Download](http://oboejs.com/download) the library
+- [Discuss](http://oboejs.com/discuss) Oboe.js
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/.gitIgnore b/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/.gitIgnore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/.gitIgnore
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkClient.js b/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkClient.js
new file mode 100644
index 00000000000..31546d08554
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkClient.js
@@ -0,0 +1,127 @@
+
+/* call this script from the command line with first argument either
+ oboe, jsonParse, or clarinet.
+
+ This script won't time the events, I'm using `time` on the command line
+ to keep things simple.
+ */
+
+require('color');
+
+var DB_URL = 'http://localhost:4444/db';
+
+
+function aggregateWithOboe() {
+
+ var oboe = require('../dist/oboe-node.js');
+
+ oboe(DB_URL).node('{id url}.url', function(url){
+
+ oboe(url).node('name', function(name){
+
+ console.log(name);
+ this.abort();
+ console.log( process.memoryUsage().heapUsed );
+ });
+ });
+}
+
+function aggregateWithJsonParse() {
+
+ var getJson = require('get-json');
+
+ getJson(DB_URL, function(err, records) {
+
+ records.data.forEach( function( record ){
+
+ var url = record.url;
+
+ getJson(url, function(err, record) {
+ console.log(record.name);
+ console.log( process.memoryUsage().heapUsed );
+ });
+ });
+
+ });
+
+}
+
+
+function aggregateWithClarinet() {
+
+ var clarinet = require('clarinet');
+ var http = require('http');
+ var outerClarinetStream = clarinet.createStream();
+ var outerKey;
+
+ var outerRequest = http.request(DB_URL, function(res) {
+
+ res.pipe(outerClarinetStream);
+ });
+
+ outerClarinetStream = clarinet.createStream();
+
+ outerRequest.end();
+
+ outerClarinetStream.on('openobject', function( keyName ){
+ if( keyName ) {
+ outerKey = keyName;
+ }
+ });
+
+ outerClarinetStream.on('key', function(keyName){
+ outerKey = keyName;
+ });
+
+ outerClarinetStream.on('value', function(value){
+ if( outerKey == 'url' ) {
+ innerRequest(value)
+ }
+ });
+
+
+ function innerRequest(url) {
+
+ var innerRequest = http.request(url, function(res) {
+
+ res.pipe(innerClarinetStream);
+ });
+
+ var innerClarinetStream = clarinet.createStream();
+
+ innerRequest.end();
+
+ var innerKey;
+
+ innerClarinetStream.on('openobject', function( keyName ){
+ if( keyName ) {
+ innerKey = keyName;
+ }
+ });
+
+ innerClarinetStream.on('key', function(keyName){
+ innerKey = keyName;
+ });
+
+ innerClarinetStream.on('value', function(value){
+ if( innerKey == 'name' ) {
+ console.log( value )
+ console.log( process.memoryUsage().heapUsed );
+ }
+ });
+ }
+}
+
+var strategies = {
+ oboe: aggregateWithOboe,
+ jsonParse: aggregateWithJsonParse,
+ clarinet: aggregateWithClarinet
+}
+
+var strategyName = process.argv[2];
+
+// use any of the above three strategies depending on a command line argument:
+console.log('benchmarking strategy', strategyName);
+
+strategies[strategyName]();
+
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkServer.js b/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkServer.js
new file mode 100644
index 00000000000..c9f4fc84e55
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/benchmarking/benchmarkServer.js
@@ -0,0 +1,94 @@
+/**
+ */
+
+"use strict";
+
+var PORT = 4444;
+
+var TIME_BETWEEN_RECORDS = 15;
+// 80 records but only every other one has a URL:
+var NUMBER_OF_RECORDS = 80;
+
+function sendJsonHeaders(res) {
+ var JSON_MIME_TYPE = "application/octet-stream";
+ res.setHeader("Content-Type", JSON_MIME_TYPE);
+ res.writeHead(200);
+}
+
+function serveItemList(_req, res) {
+
+ console.log('slow fake db server: send simulated database data');
+
+ res.write('{"data": [');
+
+ var i = 0;
+
+ var inervalId = setInterval(function () {
+
+ if( i % 2 == 0 ) {
+
+ res.write(JSON.stringify({
+ "id": i,
+ "url": "http://localhost:4444/item/" + i
+ }));
+ } else {
+ res.write(JSON.stringify({
+ "id": i
+ }));
+ }
+
+ if (i == NUMBER_OF_RECORDS) {
+
+ res.end(']}');
+
+ clearInterval(inervalId);
+
+ console.log('db server: finished writing to stream');
+ } else {
+ res.write(',');
+ }
+
+ i++;
+
+ }, TIME_BETWEEN_RECORDS);
+}
+
+function serveItem(req, res){
+
+ var id = req.params.id;
+
+ console.log('will output fake record with id', id);
+
+ setTimeout(function(){
+ // the items served are all the same except for the id field.
+ // this is realistic looking but randomly generated object fro
+ // <project>/test/json/oneHundredrecords.json
+ res.end(JSON.stringify({
+ "id" : id,
+ "url": "http://localhost:4444/item/" + id,
+ "guid": "046447ee-da78-478c-b518-b612111942a5",
+ "picture": "http://placehold.it/32x32",
+ "age": 37,
+ "name": "Humanoid robot number " + id,
+ "company": "Robotomic",
+ "phone": "806-587-2379",
+ "email": "payton@robotomic.com"
+ }));
+
+ }, TIME_BETWEEN_RECORDS);
+
+}
+
+function routing() {
+ var Router = require('node-simple-router'),
+ router = Router();
+
+ router.get( '/db', serveItemList);
+ router.get( '/item/:id', serveItem);
+
+ return router;
+}
+
+var server = require('http').createServer(routing()).listen(PORT);
+
+console.log('Benchmark server started on port', String(PORT));
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/build/README.md b/chromium/third_party/catapult/tracing/third_party/oboe/build/README.md
new file mode 100644
index 00000000000..85484c6378f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/build/README.md
@@ -0,0 +1 @@
+Directory used for the building. Nothing to see here. \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/component.json b/chromium/third_party/catapult/tracing/third_party/oboe/component.json
new file mode 100644
index 00000000000..c124a8b55eb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/component.json
@@ -0,0 +1,18 @@
+{
+ "name": "oboe",
+ "version": "2.1.2",
+ "main": "dist/oboe-browser.js",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "components",
+ "writing",
+ "src",
+ "examples",
+ "build",
+ "benchmarking",
+ "package.json",
+ "Gruntfile.js",
+ "test"
+ ]
+} \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.js b/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.js
new file mode 100644
index 00000000000..619e1d45ac4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.js
@@ -0,0 +1,2707 @@
+// This file is the concatenation of many js files.
+// See http://github.com/jimhigson/oboe.js for the raw source
+
+// having a local undefined, window, Object etc allows slightly better minification:
+(function (window, Object, Array, Error, JSON, undefined ) {
+
+ // v2.1.3-2-gc85b5c4
+
+/*
+
+Copyright (c) 2013, Jim Higson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+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
+HOLDER 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.
+
+*/
+
+/**
+ * Partially complete a function.
+ *
+ * var add3 = partialComplete( function add(a,b){return a+b}, 3 );
+ *
+ * add3(4) // gives 7
+ *
+ * function wrap(left, right, cen){return left + " " + cen + " " + right;}
+ *
+ * var pirateGreeting = partialComplete( wrap , "I'm", ", a mighty pirate!" );
+ *
+ * pirateGreeting("Guybrush Threepwood");
+ * // gives "I'm Guybrush Threepwood, a mighty pirate!"
+ */
+var partialComplete = varArgs(function( fn, args ) {
+
+ // this isn't the shortest way to write this but it does
+ // avoid creating a new array each time to pass to fn.apply,
+ // otherwise could just call boundArgs.concat(callArgs)
+
+ var numBoundArgs = args.length;
+
+ return varArgs(function( callArgs ) {
+
+ for (var i = 0; i < callArgs.length; i++) {
+ args[numBoundArgs + i] = callArgs[i];
+ }
+
+ args.length = numBoundArgs + callArgs.length;
+
+ return fn.apply(this, args);
+ });
+ }),
+
+/**
+ * Compose zero or more functions:
+ *
+ * compose(f1, f2, f3)(x) = f1(f2(f3(x))))
+ *
+ * The last (inner-most) function may take more than one parameter:
+ *
+ * compose(f1, f2, f3)(x,y) = f1(f2(f3(x,y))))
+ */
+ compose = varArgs(function(fns) {
+
+ var fnsList = arrayAsList(fns);
+
+ function next(params, curFn) {
+ return [apply(params, curFn)];
+ }
+
+ return varArgs(function(startParams){
+
+ return foldR(next, startParams, fnsList)[0];
+ });
+ });
+
+/**
+ * A more optimised version of compose that takes exactly two functions
+ * @param f1
+ * @param f2
+ */
+function compose2(f1, f2){
+ return function(){
+ return f1.call(this,f2.apply(this,arguments));
+ }
+}
+
+/**
+ * Generic form for a function to get a property from an object
+ *
+ * var o = {
+ * foo:'bar'
+ * }
+ *
+ * var getFoo = attr('foo')
+ *
+ * fetFoo(o) // returns 'bar'
+ *
+ * @param {String} key the property name
+ */
+function attr(key) {
+ return function(o) { return o[key]; };
+}
+
+/**
+ * Call a list of functions with the same args until one returns a
+ * truthy result. Similar to the || operator.
+ *
+ * So:
+ * lazyUnion([f1,f2,f3 ... fn])( p1, p2 ... pn )
+ *
+ * Is equivalent to:
+ * apply([p1, p2 ... pn], f1) ||
+ * apply([p1, p2 ... pn], f2) ||
+ * apply([p1, p2 ... pn], f3) ... apply(fn, [p1, p2 ... pn])
+ *
+ * @returns the first return value that is given that is truthy.
+ */
+ var lazyUnion = varArgs(function(fns) {
+
+ return varArgs(function(params){
+
+ var maybeValue;
+
+ for (var i = 0; i < len(fns); i++) {
+
+ maybeValue = apply(params, fns[i]);
+
+ if( maybeValue ) {
+ return maybeValue;
+ }
+ }
+ });
+ });
+
+/**
+ * This file declares various pieces of functional programming.
+ *
+ * This isn't a general purpose functional library, to keep things small it
+ * has just the parts useful for Oboe.js.
+ */
+
+
+/**
+ * Call a single function with the given arguments array.
+ * Basically, a functional-style version of the OO-style Function#apply for
+ * when we don't care about the context ('this') of the call.
+ *
+ * The order of arguments allows partial completion of the arguments array
+ */
+function apply(args, fn) {
+ return fn.apply(undefined, args);
+}
+
+/**
+ * Define variable argument functions but cut out all that tedious messing about
+ * with the arguments object. Delivers the variable-length part of the arguments
+ * list as an array.
+ *
+ * Eg:
+ *
+ * var myFunction = varArgs(
+ * function( fixedArgument, otherFixedArgument, variableNumberOfArguments ){
+ * console.log( variableNumberOfArguments );
+ * }
+ * )
+ *
+ * myFunction('a', 'b', 1, 2, 3); // logs [1,2,3]
+ *
+ * var myOtherFunction = varArgs(function( variableNumberOfArguments ){
+ * console.log( variableNumberOfArguments );
+ * })
+ *
+ * myFunction(1, 2, 3); // logs [1,2,3]
+ *
+ */
+function varArgs(fn){
+
+ var numberOfFixedArguments = fn.length -1,
+ slice = Array.prototype.slice;
+
+
+ if( numberOfFixedArguments == 0 ) {
+ // an optimised case for when there are no fixed args:
+
+ return function(){
+ return fn.call(this, slice.call(arguments));
+ }
+
+ } else if( numberOfFixedArguments == 1 ) {
+ // an optimised case for when there are is one fixed args:
+
+ return function(){
+ return fn.call(this, arguments[0], slice.call(arguments, 1));
+ }
+ }
+
+ // general case
+
+ // we know how many arguments fn will always take. Create a
+ // fixed-size array to hold that many, to be re-used on
+ // every call to the returned function
+ var argsHolder = Array(fn.length);
+
+ return function(){
+
+ for (var i = 0; i < numberOfFixedArguments; i++) {
+ argsHolder[i] = arguments[i];
+ }
+
+ argsHolder[numberOfFixedArguments] =
+ slice.call(arguments, numberOfFixedArguments);
+
+ return fn.apply( this, argsHolder);
+ }
+}
+
+
+/**
+ * Swap the order of parameters to a binary function
+ *
+ * A bit like this flip: http://zvon.org/other/haskell/Outputprelude/flip_f.html
+ */
+function flip(fn){
+ return function(a, b){
+ return fn(b,a);
+ }
+}
+
+
+/**
+ * Create a function which is the intersection of two other functions.
+ *
+ * Like the && operator, if the first is truthy, the second is never called,
+ * otherwise the return value from the second is returned.
+ */
+function lazyIntersection(fn1, fn2) {
+
+ return function (param) {
+
+ return fn1(param) && fn2(param);
+ };
+}
+
+/**
+ * A function which does nothing
+ */
+function noop(){}
+
+/**
+ * A function which is always happy
+ */
+function always(){return true}
+
+/**
+ * Create a function which always returns the same
+ * value
+ *
+ * var return3 = functor(3);
+ *
+ * return3() // gives 3
+ * return3() // still gives 3
+ * return3() // will always give 3
+ */
+function functor(val){
+ return function(){
+ return val;
+ }
+}
+
+/**
+ * This file defines some loosely associated syntactic sugar for
+ * Javascript programming
+ */
+
+
+/**
+ * Returns true if the given candidate is of type T
+ */
+function isOfType(T, maybeSomething){
+ return maybeSomething && maybeSomething.constructor === T;
+}
+
+var len = attr('length'),
+ isString = partialComplete(isOfType, String);
+
+/**
+ * I don't like saying this:
+ *
+ * foo !=== undefined
+ *
+ * because of the double-negative. I find this:
+ *
+ * defined(foo)
+ *
+ * easier to read.
+ */
+function defined( value ) {
+ return value !== undefined;
+}
+
+/**
+ * Returns true if object o has a key named like every property in
+ * the properties array. Will give false if any are missing, or if o
+ * is not an object.
+ */
+function hasAllProperties(fieldList, o) {
+
+ return (o instanceof Object)
+ &&
+ all(function (field) {
+ return (field in o);
+ }, fieldList);
+}
+/**
+ * Like cons in Lisp
+ */
+function cons(x, xs) {
+
+ /* Internally lists are linked 2-element Javascript arrays.
+
+ Ideally the return here would be Object.freeze([x,xs])
+ so that bugs related to mutation are found fast.
+ However, cons is right on the critical path for
+ performance and this slows oboe-mark down by
+ ~25%. Under theoretical future JS engines that freeze more
+ efficiently (possibly even use immutability to
+ run faster) this should be considered for
+ restoration.
+ */
+
+ return [x,xs];
+}
+
+/**
+ * The empty list
+ */
+var emptyList = null,
+
+/**
+ * Get the head of a list.
+ *
+ * Ie, head(cons(a,b)) = a
+ */
+ head = attr(0),
+
+/**
+ * Get the tail of a list.
+ *
+ * Ie, tail(cons(a,b)) = b
+ */
+ tail = attr(1);
+
+
+/**
+ * Converts an array to a list
+ *
+ * asList([a,b,c])
+ *
+ * is equivalent to:
+ *
+ * cons(a, cons(b, cons(c, emptyList)))
+ **/
+function arrayAsList(inputArray){
+
+ return reverseList(
+ inputArray.reduce(
+ flip(cons),
+ emptyList
+ )
+ );
+}
+
+/**
+ * A varargs version of arrayAsList. Works a bit like list
+ * in LISP.
+ *
+ * list(a,b,c)
+ *
+ * is equivalent to:
+ *
+ * cons(a, cons(b, cons(c, emptyList)))
+ */
+var list = varArgs(arrayAsList);
+
+/**
+ * Convert a list back to a js native array
+ */
+function listAsArray(list){
+
+ return foldR( function(arraySoFar, listItem){
+
+ arraySoFar.unshift(listItem);
+ return arraySoFar;
+
+ }, [], list );
+
+}
+
+/**
+ * Map a function over a list
+ */
+function map(fn, list) {
+
+ return list
+ ? cons(fn(head(list)), map(fn,tail(list)))
+ : emptyList
+ ;
+}
+
+/**
+ * foldR implementation. Reduce a list down to a single value.
+ *
+ * @pram {Function} fn (rightEval, curVal) -> result
+ */
+function foldR(fn, startValue, list) {
+
+ return list
+ ? fn(foldR(fn, startValue, tail(list)), head(list))
+ : startValue
+ ;
+}
+
+/**
+ * foldR implementation. Reduce a list down to a single value.
+ *
+ * @pram {Function} fn (rightEval, curVal) -> result
+ */
+function foldR1(fn, list) {
+
+ return tail(list)
+ ? fn(foldR1(fn, tail(list)), head(list))
+ : head(list)
+ ;
+}
+
+
+/**
+ * Return a list like the one given but with the first instance equal
+ * to item removed
+ */
+function without(list, test, removedFn) {
+
+ return withoutInner(list, removedFn || noop);
+
+ function withoutInner(subList, removedFn) {
+ return subList
+ ? ( test(head(subList))
+ ? (removedFn(head(subList)), tail(subList))
+ : cons(head(subList), withoutInner(tail(subList), removedFn))
+ )
+ : emptyList
+ ;
+ }
+}
+
+/**
+ * Returns true if the given function holds for every item in
+ * the list, false otherwise
+ */
+function all(fn, list) {
+
+ return !list ||
+ ( fn(head(list)) && all(fn, tail(list)) );
+}
+
+/**
+ * Call every function in a list of functions with the same arguments
+ *
+ * This doesn't make any sense if we're doing pure functional because
+ * it doesn't return anything. Hence, this is only really useful if the
+ * functions being called have side-effects.
+ */
+function applyEach(fnList, args) {
+
+ if( fnList ) {
+ head(fnList).apply(null, args);
+
+ applyEach(tail(fnList), args);
+ }
+}
+
+/**
+ * Reverse the order of a list
+ */
+function reverseList(list){
+
+ // js re-implementation of 3rd solution from:
+ // http://www.haskell.org/haskellwiki/99_questions/Solutions/5
+ function reverseInner( list, reversedAlready ) {
+ if( !list ) {
+ return reversedAlready;
+ }
+
+ return reverseInner(tail(list), cons(head(list), reversedAlready))
+ }
+
+ return reverseInner(list, emptyList);
+}
+
+function first(test, list) {
+ return list &&
+ (test(head(list))
+ ? head(list)
+ : first(test,tail(list)));
+}
+
+/*
+ This is a slightly hacked-up browser only version of clarinet
+
+ * some features removed to help keep browser Oboe under
+ the 5k micro-library limit
+ * plug directly into event bus
+
+ For the original go here:
+ https://github.com/dscape/clarinet
+
+ We receive the events:
+ STREAM_DATA
+ STREAM_END
+
+ We emit the events:
+ SAX_KEY
+ SAX_VALUE_OPEN
+ SAX_VALUE_CLOSE
+ FAIL_EVENT
+ */
+
+function clarinet(eventBus) {
+ "use strict";
+
+ var
+ // shortcut some events on the bus
+ emitSaxKey = eventBus(SAX_KEY).emit,
+ emitValueOpen = eventBus(SAX_VALUE_OPEN).emit,
+ emitValueClose = eventBus(SAX_VALUE_CLOSE).emit,
+ emitFail = eventBus(FAIL_EVENT).emit,
+
+ MAX_BUFFER_LENGTH = 64 * 1024
+ , stringTokenPattern = /[\\"\n]/g
+ , _n = 0
+
+ // states
+ , BEGIN = _n++
+ , VALUE = _n++ // general stuff
+ , OPEN_OBJECT = _n++ // {
+ , CLOSE_OBJECT = _n++ // }
+ , OPEN_ARRAY = _n++ // [
+ , CLOSE_ARRAY = _n++ // ]
+ , STRING = _n++ // ""
+ , OPEN_KEY = _n++ // , "a"
+ , CLOSE_KEY = _n++ // :
+ , TRUE = _n++ // r
+ , TRUE2 = _n++ // u
+ , TRUE3 = _n++ // e
+ , FALSE = _n++ // a
+ , FALSE2 = _n++ // l
+ , FALSE3 = _n++ // s
+ , FALSE4 = _n++ // e
+ , NULL = _n++ // u
+ , NULL2 = _n++ // l
+ , NULL3 = _n++ // l
+ , NUMBER_DECIMAL_POINT = _n++ // .
+ , NUMBER_DIGIT = _n // [0-9]
+
+ // setup initial parser values
+ , bufferCheckPosition = MAX_BUFFER_LENGTH
+ , latestError
+ , c
+ , p
+ , textNode = undefined
+ , numberNode = ""
+ , slashed = false
+ , closed = false
+ , state = BEGIN
+ , stack = []
+ , unicodeS = null
+ , unicodeI = 0
+ , depth = 0
+ , position = 0
+ , column = 0 //mostly for error reporting
+ , line = 1
+ ;
+
+ function checkBufferLength () {
+
+ var maxActual = 0;
+
+ if (textNode !== undefined && textNode.length > MAX_BUFFER_LENGTH) {
+ emitError("Max buffer length exceeded: textNode");
+ maxActual = Math.max(maxActual, textNode.length);
+ }
+ if (numberNode.length > MAX_BUFFER_LENGTH) {
+ emitError("Max buffer length exceeded: numberNode");
+ maxActual = Math.max(maxActual, numberNode.length);
+ }
+
+ bufferCheckPosition = (MAX_BUFFER_LENGTH - maxActual)
+ + position;
+ }
+
+ eventBus(STREAM_DATA).on(handleData);
+
+ /* At the end of the http content close the clarinet
+ This will provide an error if the total content provided was not
+ valid json, ie if not all arrays, objects and Strings closed properly */
+ eventBus(STREAM_END).on(handleStreamEnd);
+
+ function emitError (errorString) {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+
+ latestError = Error(errorString + "\nLn: "+line+
+ "\nCol: "+column+
+ "\nChr: "+c);
+
+ emitFail(errorReport(undefined, undefined, latestError));
+ }
+
+ function handleStreamEnd() {
+ if( state == BEGIN ) {
+ // Handle the case where the stream closes without ever receiving
+ // any input. This isn't an error - response bodies can be blank,
+ // particularly for 204 http responses
+
+ // Because of how Oboe is currently implemented, we parse a
+ // completely empty stream as containing an empty object.
+ // This is because Oboe's done event is only fired when the
+ // root object of the JSON stream closes.
+
+ // This should be decoupled and attached instead to the input stream
+ // from the http (or whatever) resource ending.
+ // If this decoupling could happen the SAX parser could simply emit
+ // zero events on a completely empty input.
+ emitValueOpen({});
+ emitValueClose();
+
+ closed = true;
+ return;
+ }
+
+ if (state !== VALUE || depth !== 0)
+ emitError("Unexpected end");
+
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+
+ closed = true;
+ }
+
+ function whitespace(c){
+ return c == '\r' || c == '\n' || c == ' ' || c == '\t';
+ }
+
+ function handleData (chunk) {
+
+ // this used to throw the error but inside Oboe we will have already
+ // gotten the error when it was emitted. The important thing is to
+ // not continue with the parse.
+ if (latestError)
+ return;
+
+ if (closed) {
+ return emitError("Cannot write after close");
+ }
+
+ var i = 0;
+ c = chunk[0];
+
+ while (c) {
+ p = c;
+ c = chunk[i++];
+ if(!c) break;
+
+ position ++;
+ if (c == "\n") {
+ line ++;
+ column = 0;
+ } else column ++;
+ switch (state) {
+
+ case BEGIN:
+ if (c === "{") state = OPEN_OBJECT;
+ else if (c === "[") state = OPEN_ARRAY;
+ else if (!whitespace(c))
+ return emitError("Non-whitespace before {[.");
+ continue;
+
+ case OPEN_KEY:
+ case OPEN_OBJECT:
+ if (whitespace(c)) continue;
+ if(state === OPEN_KEY) stack.push(CLOSE_KEY);
+ else {
+ if(c === '}') {
+ emitValueOpen({});
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ continue;
+ } else stack.push(CLOSE_OBJECT);
+ }
+ if(c === '"')
+ state = STRING;
+ else
+ return emitError("Malformed object key should start with \" ");
+ continue;
+
+ case CLOSE_KEY:
+ case CLOSE_OBJECT:
+ if (whitespace(c)) continue;
+
+ if(c===':') {
+ if(state === CLOSE_OBJECT) {
+ stack.push(CLOSE_OBJECT);
+
+ if (textNode !== undefined) {
+ // was previously (in upstream Clarinet) one event
+ // - object open came with the text of the first
+ emitValueOpen({});
+ emitSaxKey(textNode);
+ textNode = undefined;
+ }
+ depth++;
+ } else {
+ if (textNode !== undefined) {
+ emitSaxKey(textNode);
+ textNode = undefined;
+ }
+ }
+ state = VALUE;
+ } else if (c==='}') {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ } else if(c===',') {
+ if(state === CLOSE_OBJECT)
+ stack.push(CLOSE_OBJECT);
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ state = OPEN_KEY;
+ } else
+ return emitError('Bad object');
+ continue;
+
+ case OPEN_ARRAY: // after an array there always a value
+ case VALUE:
+ if (whitespace(c)) continue;
+ if(state===OPEN_ARRAY) {
+ emitValueOpen([]);
+ depth++;
+ state = VALUE;
+ if(c === ']') {
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ continue;
+ } else {
+ stack.push(CLOSE_ARRAY);
+ }
+ }
+ if(c === '"') state = STRING;
+ else if(c === '{') state = OPEN_OBJECT;
+ else if(c === '[') state = OPEN_ARRAY;
+ else if(c === 't') state = TRUE;
+ else if(c === 'f') state = FALSE;
+ else if(c === 'n') state = NULL;
+ else if(c === '-') { // keep and continue
+ numberNode += c;
+ } else if(c==='0') {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else if('123456789'.indexOf(c) !== -1) {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else
+ return emitError("Bad value");
+ continue;
+
+ case CLOSE_ARRAY:
+ if(c===',') {
+ stack.push(CLOSE_ARRAY);
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ state = VALUE;
+ } else if (c===']') {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ } else if (whitespace(c))
+ continue;
+ else
+ return emitError('Bad array');
+ continue;
+
+ case STRING:
+ if (textNode === undefined) {
+ textNode = "";
+ }
+
+ // thanks thejh, this is an about 50% performance improvement.
+ var starti = i-1;
+
+ STRING_BIGLOOP: while (true) {
+
+ // zero means "no unicode active". 1-4 mean "parse some more". end after 4.
+ while (unicodeI > 0) {
+ unicodeS += c;
+ c = chunk.charAt(i++);
+ if (unicodeI === 4) {
+ // TODO this might be slow? well, probably not used too often anyway
+ textNode += String.fromCharCode(parseInt(unicodeS, 16));
+ unicodeI = 0;
+ starti = i-1;
+ } else {
+ unicodeI++;
+ }
+ // we can just break here: no stuff we skipped that still has to be sliced out or so
+ if (!c) break STRING_BIGLOOP;
+ }
+ if (c === '"' && !slashed) {
+ state = stack.pop() || VALUE;
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ if (c === '\\' && !slashed) {
+ slashed = true;
+ textNode += chunk.substring(starti, i-1);
+ c = chunk.charAt(i++);
+ if (!c) break;
+ }
+ if (slashed) {
+ slashed = false;
+ if (c === 'n') { textNode += '\n'; }
+ else if (c === 'r') { textNode += '\r'; }
+ else if (c === 't') { textNode += '\t'; }
+ else if (c === 'f') { textNode += '\f'; }
+ else if (c === 'b') { textNode += '\b'; }
+ else if (c === 'u') {
+ // \uxxxx. meh!
+ unicodeI = 1;
+ unicodeS = '';
+ } else {
+ textNode += c;
+ }
+ c = chunk.charAt(i++);
+ starti = i-1;
+ if (!c) break;
+ else continue;
+ }
+
+ stringTokenPattern.lastIndex = i;
+ var reResult = stringTokenPattern.exec(chunk);
+ if (!reResult) {
+ i = chunk.length+1;
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ i = reResult.index+1;
+ c = chunk.charAt(reResult.index);
+ if (!c) {
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ }
+ continue;
+
+ case TRUE:
+ if (!c) continue; // strange buffers
+ if (c==='r') state = TRUE2;
+ else
+ return emitError( 'Invalid true started with t'+ c);
+ continue;
+
+ case TRUE2:
+ if (!c) continue;
+ if (c==='u') state = TRUE3;
+ else
+ return emitError('Invalid true started with tr'+ c);
+ continue;
+
+ case TRUE3:
+ if (!c) continue;
+ if(c==='e') {
+ emitValueOpen(true);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid true started with tru'+ c);
+ continue;
+
+ case FALSE:
+ if (!c) continue;
+ if (c==='a') state = FALSE2;
+ else
+ return emitError('Invalid false started with f'+ c);
+ continue;
+
+ case FALSE2:
+ if (!c) continue;
+ if (c==='l') state = FALSE3;
+ else
+ return emitError('Invalid false started with fa'+ c);
+ continue;
+
+ case FALSE3:
+ if (!c) continue;
+ if (c==='s') state = FALSE4;
+ else
+ return emitError('Invalid false started with fal'+ c);
+ continue;
+
+ case FALSE4:
+ if (!c) continue;
+ if (c==='e') {
+ emitValueOpen(false);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid false started with fals'+ c);
+ continue;
+
+ case NULL:
+ if (!c) continue;
+ if (c==='u') state = NULL2;
+ else
+ return emitError('Invalid null started with n'+ c);
+ continue;
+
+ case NULL2:
+ if (!c) continue;
+ if (c==='l') state = NULL3;
+ else
+ return emitError('Invalid null started with nu'+ c);
+ continue;
+
+ case NULL3:
+ if (!c) continue;
+ if(c==='l') {
+ emitValueOpen(null);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid null started with nul'+ c);
+ continue;
+
+ case NUMBER_DECIMAL_POINT:
+ if(c==='.') {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else
+ return emitError('Leading zero not followed by .');
+ continue;
+
+ case NUMBER_DIGIT:
+ if('0123456789'.indexOf(c) !== -1) numberNode += c;
+ else if (c==='.') {
+ if(numberNode.indexOf('.')!==-1)
+ return emitError('Invalid number has two dots');
+ numberNode += c;
+ } else if (c==='e' || c==='E') {
+ if(numberNode.indexOf('e')!==-1 ||
+ numberNode.indexOf('E')!==-1 )
+ return emitError('Invalid number has two exponential');
+ numberNode += c;
+ } else if (c==="+" || c==="-") {
+ if(!(p==='e' || p==='E'))
+ return emitError('Invalid symbol in number');
+ numberNode += c;
+ } else {
+ if (numberNode) {
+ emitValueOpen(parseFloat(numberNode));
+ emitValueClose();
+ numberNode = "";
+ }
+ i--; // go back one
+ state = stack.pop() || VALUE;
+ }
+ continue;
+
+ default:
+ return emitError("Unknown state: " + state);
+ }
+ }
+ if (position >= bufferCheckPosition)
+ checkBufferLength();
+ }
+}
+
+
+/**
+ * A bridge used to assign stateless functions to listen to clarinet.
+ *
+ * As well as the parameter from clarinet, each callback will also be passed
+ * the result of the last callback.
+ *
+ * This may also be used to clear all listeners by assigning zero handlers:
+ *
+ * ascentManager( clarinet, {} )
+ */
+function ascentManager(oboeBus, handlers){
+ "use strict";
+
+ var listenerId = {},
+ ascent;
+
+ function stateAfter(handler) {
+ return function(param){
+ ascent = handler( ascent, param);
+ }
+ }
+
+ for( var eventName in handlers ) {
+
+ oboeBus(eventName).on(stateAfter(handlers[eventName]), listenerId);
+ }
+
+ oboeBus(NODE_SWAP).on(function(newNode) {
+
+ var oldHead = head(ascent),
+ key = keyOf(oldHead),
+ ancestors = tail(ascent),
+ parentNode;
+
+ if( ancestors ) {
+ parentNode = nodeOf(head(ancestors));
+ parentNode[key] = newNode;
+ }
+ });
+
+ oboeBus(NODE_DROP).on(function() {
+
+ var oldHead = head(ascent),
+ key = keyOf(oldHead),
+ ancestors = tail(ascent),
+ parentNode;
+
+ if( ancestors ) {
+ parentNode = nodeOf(head(ancestors));
+
+ delete parentNode[key];
+ }
+ });
+
+ oboeBus(ABORTING).on(function(){
+
+ for( var eventName in handlers ) {
+ oboeBus(eventName).un(listenerId);
+ }
+ });
+}
+
+// based on gist https://gist.github.com/monsur/706839
+
+/**
+ * XmlHttpRequest's getAllResponseHeaders() method returns a string of response
+ * headers according to the format described here:
+ * http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method
+ * This method parses that string into a user-friendly key/value pair object.
+ */
+function parseResponseHeaders(headerStr) {
+ var headers = {};
+
+ headerStr && headerStr.split('\u000d\u000a')
+ .forEach(function(headerPair){
+
+ // Can't use split() here because it does the wrong thing
+ // if the header value has the string ": " in it.
+ var index = headerPair.indexOf('\u003a\u0020');
+
+ headers[headerPair.substring(0, index)]
+ = headerPair.substring(index + 2);
+ });
+
+ return headers;
+}
+
+/**
+ * Detect if a given URL is cross-origin in the scope of the
+ * current page.
+ *
+ * Browser only (since cross-origin has no meaning in Node.js)
+ *
+ * @param {Object} pageLocation - as in window.location
+ * @param {Object} ajaxHost - an object like window.location describing the
+ * origin of the url that we want to ajax in
+ */
+function isCrossOrigin(pageLocation, ajaxHost) {
+
+ /*
+ * NB: defaultPort only knows http and https.
+ * Returns undefined otherwise.
+ */
+ function defaultPort(protocol) {
+ return {'http:':80, 'https:':443}[protocol];
+ }
+
+ function portOf(location) {
+ // pageLocation should always have a protocol. ajaxHost if no port or
+ // protocol is specified, should use the port of the containing page
+
+ return location.port || defaultPort(location.protocol||pageLocation.protocol);
+ }
+
+ // if ajaxHost doesn't give a domain, port is the same as pageLocation
+ // it can't give a protocol but not a domain
+ // it can't give a port but not a domain
+
+ return !!( (ajaxHost.protocol && (ajaxHost.protocol != pageLocation.protocol)) ||
+ (ajaxHost.host && (ajaxHost.host != pageLocation.host)) ||
+ (ajaxHost.host && (portOf(ajaxHost) != portOf(pageLocation)))
+ );
+}
+
+/* turn any url into an object like window.location */
+function parseUrlOrigin(url) {
+ // url could be domain-relative
+ // url could give a domain
+
+ // cross origin means:
+ // same domain
+ // same port
+ // some protocol
+ // so, same everything up to the first (single) slash
+ // if such is given
+ //
+ // can ignore everything after that
+
+ var URL_HOST_PATTERN = /(\w+:)?(?:\/\/)([\w.-]+)?(?::(\d+))?\/?/,
+
+ // if no match, use an empty array so that
+ // subexpressions 1,2,3 are all undefined
+ // and will ultimately return all empty
+ // strings as the parse result:
+ urlHostMatch = URL_HOST_PATTERN.exec(url) || [];
+
+ return {
+ protocol: urlHostMatch[1] || '',
+ host: urlHostMatch[2] || '',
+ port: urlHostMatch[3] || ''
+ };
+}
+
+function httpTransport(){
+ return new XMLHttpRequest();
+}
+
+/**
+ * A wrapper around the browser XmlHttpRequest object that raises an
+ * event whenever a new part of the response is available.
+ *
+ * In older browsers progressive reading is impossible so all the
+ * content is given in a single call. For newer ones several events
+ * should be raised, allowing progressive interpretation of the response.
+ *
+ * @param {Function} oboeBus an event bus local to this Oboe instance
+ * @param {XMLHttpRequest} xhr the xhr to use as the transport. Under normal
+ * operation, will have been created using httpTransport() above
+ * but for tests a stub can be provided instead.
+ * @param {String} method one of 'GET' 'POST' 'PUT' 'PATCH' 'DELETE'
+ * @param {String} url the url to make a request to
+ * @param {String|Null} data some content to be sent with the request.
+ * Only valid if method is POST or PUT.
+ * @param {Object} [headers] the http request headers to send
+ * @param {boolean} withCredentials the XHR withCredentials property will be
+ * set to this value
+ */
+function streamingHttp(oboeBus, xhr, method, url, data, headers, withCredentials) {
+
+ "use strict";
+
+ var emitStreamData = oboeBus(STREAM_DATA).emit,
+ emitFail = oboeBus(FAIL_EVENT).emit,
+ numberOfCharsAlreadyGivenToCallback = 0,
+ stillToSendStartEvent = true;
+
+ // When an ABORTING message is put on the event bus abort
+ // the ajax request
+ oboeBus( ABORTING ).on( function(){
+
+ // if we keep the onreadystatechange while aborting the XHR gives
+ // a callback like a successful call so first remove this listener
+ // by assigning null:
+ xhr.onreadystatechange = null;
+
+ xhr.abort();
+ });
+
+ /**
+ * Handle input from the underlying xhr: either a state change,
+ * the progress event or the request being complete.
+ */
+ function handleProgress() {
+
+ var textSoFar = xhr.responseText,
+ newText = textSoFar.substr(numberOfCharsAlreadyGivenToCallback);
+
+
+ /* Raise the event for new text.
+
+ On older browsers, the new text is the whole response.
+ On newer/better ones, the fragment part that we got since
+ last progress. */
+
+ if( newText ) {
+ emitStreamData( newText );
+ }
+
+ numberOfCharsAlreadyGivenToCallback = len(textSoFar);
+ }
+
+
+ if('onprogress' in xhr){ // detect browser support for progressive delivery
+ xhr.onprogress = handleProgress;
+ }
+
+ xhr.onreadystatechange = function() {
+
+ function sendStartIfNotAlready() {
+ // Internet Explorer is very unreliable as to when xhr.status etc can
+ // be read so has to be protected with try/catch and tried again on
+ // the next readyState if it fails
+ try{
+ stillToSendStartEvent && oboeBus( HTTP_START ).emit(
+ xhr.status,
+ parseResponseHeaders(xhr.getAllResponseHeaders()) );
+ stillToSendStartEvent = false;
+ } catch(e){/* do nothing, will try again on next readyState*/}
+ }
+
+ switch( xhr.readyState ) {
+
+ case 2: // HEADERS_RECEIVED
+ case 3: // LOADING
+ return sendStartIfNotAlready();
+
+ case 4: // DONE
+ sendStartIfNotAlready(); // if xhr.status hasn't been available yet, it must be NOW, huh IE?
+
+ // is this a 2xx http code?
+ var successful = String(xhr.status)[0] == 2;
+
+ if( successful ) {
+ // In Chrome 29 (not 28) no onprogress is emitted when a response
+ // is complete before the onload. We need to always do handleInput
+ // in case we get the load but have not had a final progress event.
+ // This looks like a bug and may change in future but let's take
+ // the safest approach and assume we might not have received a
+ // progress event for each part of the response
+ handleProgress();
+
+ oboeBus(STREAM_END).emit();
+ } else {
+
+ emitFail( errorReport(
+ xhr.status,
+ xhr.responseText
+ ));
+ }
+ }
+ };
+
+ try{
+
+ xhr.open(method, url, true);
+
+ for( var headerName in headers ){
+ xhr.setRequestHeader(headerName, headers[headerName]);
+ }
+
+ if( !isCrossOrigin(window.location, parseUrlOrigin(url)) ) {
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ }
+
+ xhr.withCredentials = withCredentials;
+
+ xhr.send(data);
+
+ } catch( e ) {
+
+ // To keep a consistent interface with Node, we can't emit an event here.
+ // Node's streaming http adaptor receives the error as an asynchronous
+ // event rather than as an exception. If we emitted now, the Oboe user
+ // has had no chance to add a .fail listener so there is no way
+ // the event could be useful. For both these reasons defer the
+ // firing to the next JS frame.
+ window.setTimeout(
+ partialComplete(emitFail, errorReport(undefined, undefined, e))
+ , 0
+ );
+ }
+}
+
+var jsonPathSyntax = (function() {
+
+ var
+
+ /**
+ * Export a regular expression as a simple function by exposing just
+ * the Regex#exec. This allows regex tests to be used under the same
+ * interface as differently implemented tests, or for a user of the
+ * tests to not concern themselves with their implementation as regular
+ * expressions.
+ *
+ * This could also be expressed point-free as:
+ * Function.prototype.bind.bind(RegExp.prototype.exec),
+ *
+ * But that's far too confusing! (and not even smaller once minified
+ * and gzipped)
+ */
+ regexDescriptor = function regexDescriptor(regex) {
+ return regex.exec.bind(regex);
+ }
+
+ /**
+ * Join several regular expressions and express as a function.
+ * This allows the token patterns to reuse component regular expressions
+ * instead of being expressed in full using huge and confusing regular
+ * expressions.
+ */
+ , jsonPathClause = varArgs(function( componentRegexes ) {
+
+ // The regular expressions all start with ^ because we
+ // only want to find matches at the start of the
+ // JSONPath fragment we are inspecting
+ componentRegexes.unshift(/^/);
+
+ return regexDescriptor(
+ RegExp(
+ componentRegexes.map(attr('source')).join('')
+ )
+ );
+ })
+
+ , possiblyCapturing = /(\$?)/
+ , namedNode = /([\w-_]+|\*)/
+ , namePlaceholder = /()/
+ , nodeInArrayNotation = /\["([^"]+)"\]/
+ , numberedNodeInArrayNotation = /\[(\d+|\*)\]/
+ , fieldList = /{([\w ]*?)}/
+ , optionalFieldList = /(?:{([\w ]*?)})?/
+
+
+ // foo or *
+ , jsonPathNamedNodeInObjectNotation = jsonPathClause(
+ possiblyCapturing,
+ namedNode,
+ optionalFieldList
+ )
+
+ // ["foo"]
+ , jsonPathNamedNodeInArrayNotation = jsonPathClause(
+ possiblyCapturing,
+ nodeInArrayNotation,
+ optionalFieldList
+ )
+
+ // [2] or [*]
+ , jsonPathNumberedNodeInArrayNotation = jsonPathClause(
+ possiblyCapturing,
+ numberedNodeInArrayNotation,
+ optionalFieldList
+ )
+
+ // {a b c}
+ , jsonPathPureDuckTyping = jsonPathClause(
+ possiblyCapturing,
+ namePlaceholder,
+ fieldList
+ )
+
+ // ..
+ , jsonPathDoubleDot = jsonPathClause(/\.\./)
+
+ // .
+ , jsonPathDot = jsonPathClause(/\./)
+
+ // !
+ , jsonPathBang = jsonPathClause(
+ possiblyCapturing,
+ /!/
+ )
+
+ // nada!
+ , emptyString = jsonPathClause(/$/)
+
+ ;
+
+
+ /* We export only a single function. When called, this function injects
+ into another function the descriptors from above.
+ */
+ return function (fn){
+ return fn(
+ lazyUnion(
+ jsonPathNamedNodeInObjectNotation
+ , jsonPathNamedNodeInArrayNotation
+ , jsonPathNumberedNodeInArrayNotation
+ , jsonPathPureDuckTyping
+ )
+ , jsonPathDoubleDot
+ , jsonPathDot
+ , jsonPathBang
+ , emptyString
+ );
+ };
+
+}());
+/**
+ * Get a new key->node mapping
+ *
+ * @param {String|Number} key
+ * @param {Object|Array|String|Number|null} node a value found in the json
+ */
+function namedNode(key, node) {
+ return {key:key, node:node};
+}
+
+/** get the key of a namedNode */
+var keyOf = attr('key');
+
+/** get the node from a namedNode */
+var nodeOf = attr('node');
+/**
+ * This file provides various listeners which can be used to build up
+ * a changing ascent based on the callbacks provided by Clarinet. It listens
+ * to the low-level events from Clarinet and emits higher-level ones.
+ *
+ * The building up is stateless so to track a JSON file
+ * ascentManager.js is required to store the ascent state
+ * between calls.
+ */
+
+
+
+/**
+ * A special value to use in the path list to represent the path 'to' a root
+ * object (which doesn't really have any path). This prevents the need for
+ * special-casing detection of the root object and allows it to be treated
+ * like any other object. We might think of this as being similar to the
+ * 'unnamed root' domain ".", eg if I go to
+ * http://en.wikipedia.org./wiki/En/Main_page the dot after 'org' deliminates
+ * the unnamed root of the DNS.
+ *
+ * This is kept as an object to take advantage that in Javascript's OO objects
+ * are guaranteed to be distinct, therefore no other object can possibly clash
+ * with this one. Strings, numbers etc provide no such guarantee.
+ **/
+var ROOT_PATH = {};
+
+
+/**
+ * Create a new set of handlers for clarinet's events, bound to the emit
+ * function given.
+ */
+function incrementalContentBuilder( oboeBus ) {
+
+ var emitNodeOpened = oboeBus(NODE_OPENED).emit,
+ emitNodeClosed = oboeBus(NODE_CLOSED).emit,
+ emitRootOpened = oboeBus(ROOT_PATH_FOUND).emit,
+ emitRootClosed = oboeBus(ROOT_NODE_FOUND).emit;
+
+ function arrayIndicesAreKeys( possiblyInconsistentAscent, newDeepestNode) {
+
+ /* for values in arrays we aren't pre-warned of the coming paths
+ (Clarinet gives no call to onkey like it does for values in objects)
+ so if we are in an array we need to create this path ourselves. The
+ key will be len(parentNode) because array keys are always sequential
+ numbers. */
+
+ var parentNode = nodeOf( head( possiblyInconsistentAscent));
+
+ return isOfType( Array, parentNode)
+ ?
+ keyFound( possiblyInconsistentAscent,
+ len(parentNode),
+ newDeepestNode
+ )
+ :
+ // nothing needed, return unchanged
+ possiblyInconsistentAscent
+ ;
+ }
+
+ function nodeOpened( ascent, newDeepestNode ) {
+
+ if( !ascent ) {
+ // we discovered the root node,
+ emitRootOpened( newDeepestNode);
+
+ return keyFound( ascent, ROOT_PATH, newDeepestNode);
+ }
+
+ // we discovered a non-root node
+
+ var arrayConsistentAscent = arrayIndicesAreKeys( ascent, newDeepestNode),
+ ancestorBranches = tail( arrayConsistentAscent),
+ previouslyUnmappedName = keyOf( head( arrayConsistentAscent));
+
+ appendBuiltContent(
+ ancestorBranches,
+ previouslyUnmappedName,
+ newDeepestNode
+ );
+
+ return cons(
+ namedNode( previouslyUnmappedName, newDeepestNode ),
+ ancestorBranches
+ );
+ }
+
+
+ /**
+ * Add a new value to the object we are building up to represent the
+ * parsed JSON
+ */
+ function appendBuiltContent( ancestorBranches, key, node ){
+
+ nodeOf( head( ancestorBranches))[key] = node;
+ }
+
+
+ /**
+ * For when we find a new key in the json.
+ *
+ * @param {String|Number|Object} newDeepestName the key. If we are in an
+ * array will be a number, otherwise a string. May take the special
+ * value ROOT_PATH if the root node has just been found
+ *
+ * @param {String|Number|Object|Array|Null|undefined} [maybeNewDeepestNode]
+ * usually this won't be known so can be undefined. Can't use null
+ * to represent unknown because null is a valid value in JSON
+ **/
+ function keyFound(ascent, newDeepestName, maybeNewDeepestNode) {
+
+ if( ascent ) { // if not root
+
+ // If we have the key but (unless adding to an array) no known value
+ // yet. Put that key in the output but against no defined value:
+ appendBuiltContent( ascent, newDeepestName, maybeNewDeepestNode );
+ }
+
+ var ascentWithNewPath = cons(
+ namedNode( newDeepestName,
+ maybeNewDeepestNode),
+ ascent
+ );
+
+ emitNodeOpened( ascentWithNewPath);
+
+ return ascentWithNewPath;
+ }
+
+
+ /**
+ * For when the current node ends.
+ */
+ function nodeClosed( ascent ) {
+
+ emitNodeClosed( ascent);
+
+ return tail( ascent) ||
+ // If there are no nodes left in the ascent the root node
+ // just closed. Emit a special event for this:
+ emitRootClosed(nodeOf(head(ascent)));
+ }
+
+ var contentBuilderHandlers = {};
+ contentBuilderHandlers[SAX_VALUE_OPEN] = nodeOpened;
+ contentBuilderHandlers[SAX_VALUE_CLOSE] = nodeClosed;
+ contentBuilderHandlers[SAX_KEY] = keyFound;
+ return contentBuilderHandlers;
+}
+
+/**
+ * The jsonPath evaluator compiler used for Oboe.js.
+ *
+ * One function is exposed. This function takes a String JSONPath spec and
+ * returns a function to test candidate ascents for matches.
+ *
+ * String jsonPath -> (List ascent) -> Boolean|Object
+ *
+ * This file is coded in a pure functional style. That is, no function has
+ * side effects, every function evaluates to the same value for the same
+ * arguments and no variables are reassigned.
+ */
+// the call to jsonPathSyntax injects the token syntaxes that are needed
+// inside the compiler
+var jsonPathCompiler = jsonPathSyntax(function (pathNodeSyntax,
+ doubleDotSyntax,
+ dotSyntax,
+ bangSyntax,
+ emptySyntax ) {
+
+ var CAPTURING_INDEX = 1;
+ var NAME_INDEX = 2;
+ var FIELD_LIST_INDEX = 3;
+
+ var headKey = compose2(keyOf, head),
+ headNode = compose2(nodeOf, head);
+
+ /**
+ * Create an evaluator function for a named path node, expressed in the
+ * JSONPath like:
+ * foo
+ * ["bar"]
+ * [2]
+ */
+ function nameClause(previousExpr, detection ) {
+
+ var name = detection[NAME_INDEX],
+
+ matchesName = ( !name || name == '*' )
+ ? always
+ : function(ascent){return headKey(ascent) == name};
+
+
+ return lazyIntersection(matchesName, previousExpr);
+ }
+
+ /**
+ * Create an evaluator function for a a duck-typed node, expressed like:
+ *
+ * {spin, taste, colour}
+ * .particle{spin, taste, colour}
+ * *{spin, taste, colour}
+ */
+ function duckTypeClause(previousExpr, detection) {
+
+ var fieldListStr = detection[FIELD_LIST_INDEX];
+
+ if (!fieldListStr)
+ return previousExpr; // don't wrap at all, return given expr as-is
+
+ var hasAllrequiredFields = partialComplete(
+ hasAllProperties,
+ arrayAsList(fieldListStr.split(/\W+/))
+ ),
+
+ isMatch = compose2(
+ hasAllrequiredFields,
+ headNode
+ );
+
+ return lazyIntersection(isMatch, previousExpr);
+ }
+
+ /**
+ * Expression for $, returns the evaluator function
+ */
+ function capture( previousExpr, detection ) {
+
+ // extract meaning from the detection
+ var capturing = !!detection[CAPTURING_INDEX];
+
+ if (!capturing)
+ return previousExpr; // don't wrap at all, return given expr as-is
+
+ return lazyIntersection(previousExpr, head);
+
+ }
+
+ /**
+ * Create an evaluator function that moves onto the next item on the
+ * lists. This function is the place where the logic to move up a
+ * level in the ascent exists.
+ *
+ * Eg, for JSONPath ".foo" we need skip1(nameClause(always, [,'foo']))
+ */
+ function skip1(previousExpr) {
+
+
+ if( previousExpr == always ) {
+ /* If there is no previous expression this consume command
+ is at the start of the jsonPath.
+ Since JSONPath specifies what we'd like to find but not
+ necessarily everything leading down to it, when running
+ out of JSONPath to check against we default to true */
+ return always;
+ }
+
+ /** return true if the ascent we have contains only the JSON root,
+ * false otherwise
+ */
+ function notAtRoot(ascent){
+ return headKey(ascent) != ROOT_PATH;
+ }
+
+ return lazyIntersection(
+ /* If we're already at the root but there are more
+ expressions to satisfy, can't consume any more. No match.
+
+ This check is why none of the other exprs have to be able
+ to handle empty lists; skip1 is the only evaluator that
+ moves onto the next token and it refuses to do so once it
+ reaches the last item in the list. */
+ notAtRoot,
+
+ /* We are not at the root of the ascent yet.
+ Move to the next level of the ascent by handing only
+ the tail to the previous expression */
+ compose2(previousExpr, tail)
+ );
+
+ }
+
+ /**
+ * Create an evaluator function for the .. (double dot) token. Consumes
+ * zero or more levels of the ascent, the fewest that are required to find
+ * a match when given to previousExpr.
+ */
+ function skipMany(previousExpr) {
+
+ if( previousExpr == always ) {
+ /* If there is no previous expression this consume command
+ is at the start of the jsonPath.
+ Since JSONPath specifies what we'd like to find but not
+ necessarily everything leading down to it, when running
+ out of JSONPath to check against we default to true */
+ return always;
+ }
+
+ var
+ // In JSONPath .. is equivalent to !.. so if .. reaches the root
+ // the match has succeeded. Ie, we might write ..foo or !..foo
+ // and both should match identically.
+ terminalCaseWhenArrivingAtRoot = rootExpr(),
+ terminalCaseWhenPreviousExpressionIsSatisfied = previousExpr,
+ recursiveCase = skip1(function(ascent) {
+ return cases(ascent);
+ }),
+
+ cases = lazyUnion(
+ terminalCaseWhenArrivingAtRoot
+ , terminalCaseWhenPreviousExpressionIsSatisfied
+ , recursiveCase
+ );
+
+ return cases;
+ }
+
+ /**
+ * Generate an evaluator for ! - matches only the root element of the json
+ * and ignores any previous expressions since nothing may precede !.
+ */
+ function rootExpr() {
+
+ return function(ascent){
+ return headKey(ascent) == ROOT_PATH;
+ };
+ }
+
+ /**
+ * Generate a statement wrapper to sit around the outermost
+ * clause evaluator.
+ *
+ * Handles the case where the capturing is implicit because the JSONPath
+ * did not contain a '$' by returning the last node.
+ */
+ function statementExpr(lastClause) {
+
+ return function(ascent) {
+
+ // kick off the evaluation by passing through to the last clause
+ var exprMatch = lastClause(ascent);
+
+ return exprMatch === true ? head(ascent) : exprMatch;
+ };
+ }
+
+ /**
+ * For when a token has been found in the JSONPath input.
+ * Compiles the parser for that token and returns in combination with the
+ * parser already generated.
+ *
+ * @param {Function} exprs a list of the clause evaluator generators for
+ * the token that was found
+ * @param {Function} parserGeneratedSoFar the parser already found
+ * @param {Array} detection the match given by the regex engine when
+ * the feature was found
+ */
+ function expressionsReader( exprs, parserGeneratedSoFar, detection ) {
+
+ // if exprs is zero-length foldR will pass back the
+ // parserGeneratedSoFar as-is so we don't need to treat
+ // this as a special case
+
+ return foldR(
+ function( parserGeneratedSoFar, expr ){
+
+ return expr(parserGeneratedSoFar, detection);
+ },
+ parserGeneratedSoFar,
+ exprs
+ );
+
+ }
+
+ /**
+ * If jsonPath matches the given detector function, creates a function which
+ * evaluates against every clause in the clauseEvaluatorGenerators. The
+ * created function is propagated to the onSuccess function, along with
+ * the remaining unparsed JSONPath substring.
+ *
+ * The intended use is to create a clauseMatcher by filling in
+ * the first two arguments, thus providing a function that knows
+ * some syntax to match and what kind of generator to create if it
+ * finds it. The parameter list once completed is:
+ *
+ * (jsonPath, parserGeneratedSoFar, onSuccess)
+ *
+ * onSuccess may be compileJsonPathToFunction, to recursively continue
+ * parsing after finding a match or returnFoundParser to stop here.
+ */
+ function generateClauseReaderIfTokenFound (
+
+ tokenDetector, clauseEvaluatorGenerators,
+
+ jsonPath, parserGeneratedSoFar, onSuccess) {
+
+ var detected = tokenDetector(jsonPath);
+
+ if(detected) {
+ var compiledParser = expressionsReader(
+ clauseEvaluatorGenerators,
+ parserGeneratedSoFar,
+ detected
+ ),
+
+ remainingUnparsedJsonPath = jsonPath.substr(len(detected[0]));
+
+ return onSuccess(remainingUnparsedJsonPath, compiledParser);
+ }
+ }
+
+ /**
+ * Partially completes generateClauseReaderIfTokenFound above.
+ */
+ function clauseMatcher(tokenDetector, exprs) {
+
+ return partialComplete(
+ generateClauseReaderIfTokenFound,
+ tokenDetector,
+ exprs
+ );
+ }
+
+ /**
+ * clauseForJsonPath is a function which attempts to match against
+ * several clause matchers in order until one matches. If non match the
+ * jsonPath expression is invalid and an error is thrown.
+ *
+ * The parameter list is the same as a single clauseMatcher:
+ *
+ * (jsonPath, parserGeneratedSoFar, onSuccess)
+ */
+ var clauseForJsonPath = lazyUnion(
+
+ clauseMatcher(pathNodeSyntax , list( capture,
+ duckTypeClause,
+ nameClause,
+ skip1 ))
+
+ , clauseMatcher(doubleDotSyntax , list( skipMany))
+
+ // dot is a separator only (like whitespace in other languages) but
+ // rather than make it a special case, use an empty list of
+ // expressions when this token is found
+ , clauseMatcher(dotSyntax , list() )
+
+ , clauseMatcher(bangSyntax , list( capture,
+ rootExpr))
+
+ , clauseMatcher(emptySyntax , list( statementExpr))
+
+ , function (jsonPath) {
+ throw Error('"' + jsonPath + '" could not be tokenised')
+ }
+ );
+
+
+ /**
+ * One of two possible values for the onSuccess argument of
+ * generateClauseReaderIfTokenFound.
+ *
+ * When this function is used, generateClauseReaderIfTokenFound simply
+ * returns the compiledParser that it made, regardless of if there is
+ * any remaining jsonPath to be compiled.
+ */
+ function returnFoundParser(_remainingJsonPath, compiledParser){
+ return compiledParser
+ }
+
+ /**
+ * Recursively compile a JSONPath expression.
+ *
+ * This function serves as one of two possible values for the onSuccess
+ * argument of generateClauseReaderIfTokenFound, meaning continue to
+ * recursively compile. Otherwise, returnFoundParser is given and
+ * compilation terminates.
+ */
+ function compileJsonPathToFunction( uncompiledJsonPath,
+ parserGeneratedSoFar ) {
+
+ /**
+ * On finding a match, if there is remaining text to be compiled
+ * we want to either continue parsing using a recursive call to
+ * compileJsonPathToFunction. Otherwise, we want to stop and return
+ * the parser that we have found so far.
+ */
+ var onFind = uncompiledJsonPath
+ ? compileJsonPathToFunction
+ : returnFoundParser;
+
+ return clauseForJsonPath(
+ uncompiledJsonPath,
+ parserGeneratedSoFar,
+ onFind
+ );
+ }
+
+ /**
+ * This is the function that we expose to the rest of the library.
+ */
+ return function(jsonPath){
+
+ try {
+ // Kick off the recursive parsing of the jsonPath
+ return compileJsonPathToFunction(jsonPath, always);
+
+ } catch( e ) {
+ throw Error( 'Could not compile "' + jsonPath +
+ '" because ' + e.message
+ );
+ }
+ }
+
+});
+
+/**
+ * A pub/sub which is responsible for a single event type. A
+ * multi-event type event bus is created by pubSub by collecting
+ * several of these.
+ *
+ * @param {String} eventType
+ * the name of the events managed by this singleEventPubSub
+ * @param {singleEventPubSub} [newListener]
+ * place to notify of new listeners
+ * @param {singleEventPubSub} [removeListener]
+ * place to notify of when listeners are removed
+ */
+function singleEventPubSub(eventType, newListener, removeListener){
+
+ /** we are optimised for emitting events over firing them.
+ * As well as the tuple list which stores event ids and
+ * listeners there is a list with just the listeners which
+ * can be iterated more quickly when we are emitting
+ */
+ var listenerTupleList,
+ listenerList;
+
+ function hasId(id){
+ return function(tuple) {
+ return tuple.id == id;
+ };
+ }
+
+ return {
+
+ /**
+ * @param {Function} listener
+ * @param {*} listenerId
+ * an id that this listener can later by removed by.
+ * Can be of any type, to be compared to other ids using ==
+ */
+ on:function( listener, listenerId ) {
+
+ var tuple = {
+ listener: listener
+ , id: listenerId || listener // when no id is given use the
+ // listener function as the id
+ };
+
+ if( newListener ) {
+ newListener.emit(eventType, listener, tuple.id);
+ }
+
+ listenerTupleList = cons( tuple, listenerTupleList );
+ listenerList = cons( listener, listenerList );
+
+ return this; // chaining
+ },
+
+ emit:function () {
+ applyEach( listenerList, arguments );
+ },
+
+ un: function( listenerId ) {
+
+ var removed;
+
+ listenerTupleList = without(
+ listenerTupleList,
+ hasId(listenerId),
+ function(tuple){
+ removed = tuple;
+ }
+ );
+
+ if( removed ) {
+ listenerList = without( listenerList, function(listener){
+ return listener == removed.listener;
+ });
+
+ if( removeListener ) {
+ removeListener.emit(eventType, removed.listener, removed.id);
+ }
+ }
+ },
+
+ listeners: function(){
+ // differs from Node EventEmitter: returns list, not array
+ return listenerList;
+ },
+
+ hasListener: function(listenerId){
+ var test = listenerId? hasId(listenerId) : always;
+
+ return defined(first( test, listenerTupleList));
+ }
+ };
+}
+
+/**
+ * pubSub is a curried interface for listening to and emitting
+ * events.
+ *
+ * If we get a bus:
+ *
+ * var bus = pubSub();
+ *
+ * We can listen to event 'foo' like:
+ *
+ * bus('foo').on(myCallback)
+ *
+ * And emit event foo like:
+ *
+ * bus('foo').emit()
+ *
+ * or, with a parameter:
+ *
+ * bus('foo').emit('bar')
+ *
+ * All functions can be cached and don't need to be
+ * bound. Ie:
+ *
+ * var fooEmitter = bus('foo').emit
+ * fooEmitter('bar'); // emit an event
+ * fooEmitter('baz'); // emit another
+ *
+ * There's also an uncurried[1] shortcut for .emit and .on:
+ *
+ * bus.on('foo', callback)
+ * bus.emit('foo', 'bar')
+ *
+ * [1]: http://zvon.org/other/haskell/Outputprelude/uncurry_f.html
+ */
+function pubSub(){
+
+ var singles = {},
+ newListener = newSingle('newListener'),
+ removeListener = newSingle('removeListener');
+
+ function newSingle(eventName) {
+ return singles[eventName] = singleEventPubSub(
+ eventName,
+ newListener,
+ removeListener
+ );
+ }
+
+ /** pubSub instances are functions */
+ function pubSubInstance( eventName ){
+
+ return singles[eventName] || newSingle( eventName );
+ }
+
+ // add convenience EventEmitter-style uncurried form of 'emit' and 'on'
+ ['emit', 'on', 'un'].forEach(function(methodName){
+
+ pubSubInstance[methodName] = varArgs(function(eventName, parameters){
+ apply( parameters, pubSubInstance( eventName )[methodName]);
+ });
+ });
+
+ return pubSubInstance;
+}
+
+/**
+ * This file declares some constants to use as names for event types.
+ */
+
+var // the events which are never exported are kept as
+ // the smallest possible representation, in numbers:
+ _S = 1,
+
+ // fired whenever a new node starts in the JSON stream:
+ NODE_OPENED = _S++,
+
+ // fired whenever a node closes in the JSON stream:
+ NODE_CLOSED = _S++,
+
+ // called if a .node callback returns a value -
+ NODE_SWAP = _S++,
+ NODE_DROP = _S++,
+
+ FAIL_EVENT = 'fail',
+
+ ROOT_NODE_FOUND = _S++,
+ ROOT_PATH_FOUND = _S++,
+
+ HTTP_START = 'start',
+ STREAM_DATA = 'data',
+ STREAM_END = 'end',
+ ABORTING = _S++,
+
+ // SAX events butchered from Clarinet
+ SAX_KEY = _S++,
+ SAX_VALUE_OPEN = _S++,
+ SAX_VALUE_CLOSE = _S++;
+
+function errorReport(statusCode, body, error) {
+ try{
+ var jsonBody = JSON.parse(body);
+ }catch(e){}
+
+ return {
+ statusCode:statusCode,
+ body:body,
+ jsonBody:jsonBody,
+ thrown:error
+ };
+}
+
+/**
+ * The pattern adaptor listens for newListener and removeListener
+ * events. When patterns are added or removed it compiles the JSONPath
+ * and wires them up.
+ *
+ * When nodes and paths are found it emits the fully-qualified match
+ * events with parameters ready to ship to the outside world
+ */
+
+function patternAdapter(oboeBus, jsonPathCompiler) {
+
+ var predicateEventMap = {
+ node:oboeBus(NODE_CLOSED)
+ , path:oboeBus(NODE_OPENED)
+ };
+
+ function emitMatchingNode(emitMatch, node, ascent) {
+
+ /*
+ We're now calling to the outside world where Lisp-style
+ lists will not be familiar. Convert to standard arrays.
+
+ Also, reverse the order because it is more common to
+ list paths "root to leaf" than "leaf to root" */
+ var descent = reverseList(ascent);
+
+ emitMatch(
+ node,
+
+ // To make a path, strip off the last item which is the special
+ // ROOT_PATH token for the 'path' to the root node
+ listAsArray(tail(map(keyOf,descent))), // path
+ listAsArray(map(nodeOf, descent)) // ancestors
+ );
+ }
+
+ /*
+ * Set up the catching of events such as NODE_CLOSED and NODE_OPENED and, if
+ * matching the specified pattern, propagate to pattern-match events such as
+ * oboeBus('node:!')
+ *
+ *
+ *
+ * @param {Function} predicateEvent
+ * either oboeBus(NODE_CLOSED) or oboeBus(NODE_OPENED).
+ * @param {Function} compiledJsonPath
+ */
+ function addUnderlyingListener( fullEventName, predicateEvent, compiledJsonPath ){
+
+ var emitMatch = oboeBus(fullEventName).emit;
+
+ predicateEvent.on( function (ascent) {
+
+ var maybeMatchingMapping = compiledJsonPath(ascent);
+
+ /* Possible values for maybeMatchingMapping are now:
+
+ false:
+ we did not match
+
+ an object/array/string/number/null:
+ we matched and have the node that matched.
+ Because nulls are valid json values this can be null.
+
+ undefined:
+ we matched but don't have the matching node yet.
+ ie, we know there is an upcoming node that matches but we
+ can't say anything else about it.
+ */
+ if (maybeMatchingMapping !== false) {
+
+ emitMatchingNode(
+ emitMatch,
+ nodeOf(maybeMatchingMapping),
+ ascent
+ );
+ }
+ }, fullEventName);
+
+ oboeBus('removeListener').on( function(removedEventName){
+
+ // if the fully qualified match event listener is later removed, clean up
+ // by removing the underlying listener if it was the last using that pattern:
+
+ if( removedEventName == fullEventName ) {
+
+ if( !oboeBus(removedEventName).listeners( )) {
+ predicateEvent.un( fullEventName );
+ }
+ }
+ });
+ }
+
+ oboeBus('newListener').on( function(fullEventName){
+
+ var match = /(node|path):(.*)/.exec(fullEventName);
+
+ if( match ) {
+ var predicateEvent = predicateEventMap[match[1]];
+
+ if( !predicateEvent.hasListener( fullEventName) ) {
+
+ addUnderlyingListener(
+ fullEventName,
+ predicateEvent,
+ jsonPathCompiler( match[2] )
+ );
+ }
+ }
+ })
+
+}
+
+/**
+ * The instance API is the thing that is returned when oboe() is called.
+ * it allows:
+ *
+ * - listeners for various events to be added and removed
+ * - the http response header/headers to be read
+ */
+function instanceApi(oboeBus, contentSource){
+
+ var oboeApi,
+ fullyQualifiedNamePattern = /^(node|path):./,
+ rootNodeFinishedEvent = oboeBus(ROOT_NODE_FOUND),
+ emitNodeDrop = oboeBus(NODE_DROP).emit,
+ emitNodeSwap = oboeBus(NODE_SWAP).emit,
+
+ /**
+ * Add any kind of listener that the instance api exposes
+ */
+ addListener = varArgs(function( eventId, parameters ){
+
+ if( oboeApi[eventId] ) {
+
+ // for events added as .on(event, callback), if there is a
+ // .event() equivalent with special behaviour , pass through
+ // to that:
+ apply(parameters, oboeApi[eventId]);
+ } else {
+
+ // we have a standard Node.js EventEmitter 2-argument call.
+ // The first parameter is the listener.
+ var event = oboeBus(eventId),
+ listener = parameters[0];
+
+ if( fullyQualifiedNamePattern.test(eventId) ) {
+
+ // allow fully-qualified node/path listeners
+ // to be added
+ addForgettableCallback(event, listener);
+ } else {
+
+ // the event has no special handling, pass through
+ // directly onto the event bus:
+ event.on( listener);
+ }
+ }
+
+ return oboeApi; // chaining
+ }),
+
+ /**
+ * Remove any kind of listener that the instance api exposes
+ */
+ removeListener = function( eventId, p2, p3 ){
+
+ if( eventId == 'done' ) {
+
+ rootNodeFinishedEvent.un(p2);
+
+ } else if( eventId == 'node' || eventId == 'path' ) {
+
+ // allow removal of node and path
+ oboeBus.un(eventId + ':' + p2, p3);
+ } else {
+
+ // we have a standard Node.js EventEmitter 2-argument call.
+ // The second parameter is the listener. This may be a call
+ // to remove a fully-qualified node/path listener but requires
+ // no special handling
+ var listener = p2;
+
+ oboeBus(eventId).un(listener);
+ }
+
+ return oboeApi; // chaining
+ };
+
+ /**
+ * Add a callback, wrapped in a try/catch so as to not break the
+ * execution of Oboe if an exception is thrown (fail events are
+ * fired instead)
+ *
+ * The callback is used as the listener id so that it can later be
+ * removed using .un(callback)
+ */
+ function addProtectedCallback(eventName, callback) {
+ oboeBus(eventName).on(protectedCallback(callback), callback);
+ return oboeApi; // chaining
+ }
+
+ /**
+ * Add a callback where, if .forget() is called during the callback's
+ * execution, the callback will be de-registered
+ */
+ function addForgettableCallback(event, callback, listenerId) {
+
+ // listenerId is optional and if not given, the original
+ // callback will be used
+ listenerId = listenerId || callback;
+
+ var safeCallback = protectedCallback(callback);
+
+ event.on( function() {
+
+ var discard = false;
+
+ oboeApi.forget = function(){
+ discard = true;
+ };
+
+ apply( arguments, safeCallback );
+
+ delete oboeApi.forget;
+
+ if( discard ) {
+ event.un(listenerId);
+ }
+ }, listenerId);
+
+ return oboeApi; // chaining
+ }
+
+ /**
+ * wrap a callback so that if it throws, Oboe.js doesn't crash but instead
+ * throw the error in another event loop
+ */
+ function protectedCallback( callback ) {
+ return function() {
+ try{
+ return callback.apply(oboeApi, arguments);
+ }catch(e) {
+ setTimeout(function() {
+ throw new Error(e.message);
+ });
+ }
+ }
+ }
+
+ /**
+ * Return the fully qualified event for when a pattern matches
+ * either a node or a path
+ *
+ * @param type {String} either 'node' or 'path'
+ */
+ function fullyQualifiedPatternMatchEvent(type, pattern) {
+ return oboeBus(type + ':' + pattern);
+ }
+
+ function wrapCallbackToSwapNodeIfSomethingReturned( callback ) {
+ return function() {
+ var returnValueFromCallback = callback.apply(this, arguments);
+
+ if( defined(returnValueFromCallback) ) {
+
+ if( returnValueFromCallback == oboe.drop ) {
+ emitNodeDrop();
+ } else {
+ emitNodeSwap(returnValueFromCallback);
+ }
+ }
+ }
+ }
+
+ function addSingleNodeOrPathListener(eventId, pattern, callback) {
+
+ var effectiveCallback;
+
+ if( eventId == 'node' ) {
+ effectiveCallback = wrapCallbackToSwapNodeIfSomethingReturned(callback);
+ } else {
+ effectiveCallback = callback;
+ }
+
+ addForgettableCallback(
+ fullyQualifiedPatternMatchEvent(eventId, pattern),
+ effectiveCallback,
+ callback
+ );
+ }
+
+ /**
+ * Add several listeners at a time, from a map
+ */
+ function addMultipleNodeOrPathListeners(eventId, listenerMap) {
+
+ for( var pattern in listenerMap ) {
+ addSingleNodeOrPathListener(eventId, pattern, listenerMap[pattern]);
+ }
+ }
+
+ /**
+ * implementation behind .onPath() and .onNode()
+ */
+ function addNodeOrPathListenerApi( eventId, jsonPathOrListenerMap, callback ){
+
+ if( isString(jsonPathOrListenerMap) ) {
+ addSingleNodeOrPathListener(eventId, jsonPathOrListenerMap, callback);
+
+ } else {
+ addMultipleNodeOrPathListeners(eventId, jsonPathOrListenerMap);
+ }
+
+ return oboeApi; // chaining
+ }
+
+
+ // some interface methods are only filled in after we receive
+ // values and are noops before that:
+ oboeBus(ROOT_PATH_FOUND).on( function(rootNode) {
+ oboeApi.root = functor(rootNode);
+ });
+
+ /**
+ * When content starts make the headers readable through the
+ * instance API
+ */
+ oboeBus(HTTP_START).on( function(_statusCode, headers) {
+
+ oboeApi.header = function(name) {
+ return name ? headers[name]
+ : headers
+ ;
+ }
+ });
+
+ /**
+ * Construct and return the public API of the Oboe instance to be
+ * returned to the calling application
+ */
+ return oboeApi = {
+ on : addListener,
+ addListener : addListener,
+ removeListener : removeListener,
+ emit : oboeBus.emit,
+
+ node : partialComplete(addNodeOrPathListenerApi, 'node'),
+ path : partialComplete(addNodeOrPathListenerApi, 'path'),
+
+ done : partialComplete(addForgettableCallback, rootNodeFinishedEvent),
+ start : partialComplete(addProtectedCallback, HTTP_START ),
+
+ // fail doesn't use protectedCallback because
+ // could lead to non-terminating loops
+ fail : oboeBus(FAIL_EVENT).on,
+
+ // public api calling abort fires the ABORTING event
+ abort : oboeBus(ABORTING).emit,
+
+ // for manually feeding data
+ write : oboeBus(STREAM_DATA).emit,
+ finish : oboeBus(STREAM_END).emit,
+
+ // initially return nothing for header and root
+ header : noop,
+ root : noop,
+
+ source : contentSource
+ };
+}
+
+/**
+ * This file sits just behind the API which is used to attain a new
+ * Oboe instance. It creates the new components that are required
+ * and introduces them to each other.
+ */
+
+function wire (httpMethodName, contentSource, body, headers, withCredentials){
+
+ var oboeBus = pubSub();
+
+ // Wire the input stream in if we are given a content source.
+ // This will usually be the case. If not, the instance created
+ // will have to be passed content from an external source.
+
+ if( contentSource ) {
+
+ streamingHttp( oboeBus,
+ httpTransport(),
+ httpMethodName,
+ contentSource,
+ body,
+ headers,
+ withCredentials
+ );
+ }
+
+ clarinet(oboeBus);
+
+ ascentManager(oboeBus, incrementalContentBuilder(oboeBus));
+
+ patternAdapter(oboeBus, jsonPathCompiler);
+
+ return instanceApi(oboeBus, contentSource);
+}
+
+function applyDefaults( passthrough, url, httpMethodName, body, headers, withCredentials, cached ){
+
+ headers = headers ?
+ // Shallow-clone the headers array. This allows it to be
+ // modified without side effects to the caller. We don't
+ // want to change objects that the user passes in.
+ JSON.parse(JSON.stringify(headers))
+ : {};
+
+ if( body ) {
+ if( !isString(body) ) {
+
+ // If the body is not a string, stringify it. This allows objects to
+ // be given which will be sent as JSON.
+ body = JSON.stringify(body);
+
+ // Default Content-Type to JSON unless given otherwise.
+ headers['Content-Type'] = headers['Content-Type'] || 'application/json';
+ }
+ } else {
+ body = null;
+ }
+
+ // support cache busting like jQuery.ajax({cache:false})
+ function modifiedUrl(baseUrl, cached) {
+
+ if( cached === false ) {
+
+ if( baseUrl.indexOf('?') == -1 ) {
+ baseUrl += '?';
+ } else {
+ baseUrl += '&';
+ }
+
+ baseUrl += '_=' + new Date().getTime();
+ }
+ return baseUrl;
+ }
+
+ return passthrough( httpMethodName || 'GET', modifiedUrl(url, cached), body, headers, withCredentials || false );
+}
+
+// export public API
+function oboe(arg1) {
+
+ // We use duck-typing to detect if the parameter given is a stream, with the
+ // below list of parameters.
+ // Unpipe and unshift would normally be present on a stream but this breaks
+ // compatibility with Request streams.
+ // See https://github.com/jimhigson/oboe.js/issues/65
+
+ var nodeStreamMethodNames = list('resume', 'pause', 'pipe'),
+ isStream = partialComplete(
+ hasAllProperties
+ , nodeStreamMethodNames
+ );
+
+ if( arg1 ) {
+ if (isStream(arg1) || isString(arg1)) {
+
+ // simple version for GETs. Signature is:
+ // oboe( url )
+ // or, under node:
+ // oboe( readableStream )
+ return applyDefaults(
+ wire,
+ arg1 // url
+ );
+
+ } else {
+
+ // method signature is:
+ // oboe({method:m, url:u, body:b, headers:{...}})
+
+ return applyDefaults(
+ wire,
+ arg1.url,
+ arg1.method,
+ arg1.body,
+ arg1.headers,
+ arg1.withCredentials,
+ arg1.cached
+ );
+
+ }
+ } else {
+ // wire up a no-AJAX, no-stream Oboe. Will have to have content
+ // fed in externally and using .emit.
+ return wire();
+ }
+}
+
+/* oboe.drop is a special value. If a node callback returns this value the
+ parsed node is deleted from the JSON
+ */
+oboe.drop = function() {
+ return oboe.drop;
+};
+
+
+ if ( typeof define === "function" && define.amd ) {
+ define( "oboe", [], function () { return oboe; } );
+ } else if (typeof exports === 'object') {
+ module.exports = oboe;
+ } else {
+ window.oboe = oboe;
+ }
+})((function(){
+ // Access to the window object throws an exception in HTML5 web workers so
+ // point it to "self" if it runs in a web worker
+ try {
+ return window;
+ } catch (e) {
+ return self;
+ }
+ }()), Object, Array, Error, JSON);
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.min.js b/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.min.js
new file mode 100644
index 00000000000..89d5b88a9e0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-browser.min.js
@@ -0,0 +1 @@
+!function(a,b,c,d,e,f){function g(a,b){return function(){return a.call(this,b.apply(this,arguments))}}function h(a){return function(b){return b[a]}}function i(a,b){return b.apply(f,a)}function j(a){var b=a.length-1,d=c.prototype.slice;if(0==b)return function(){return a.call(this,d.call(arguments))};if(1==b)return function(){return a.call(this,arguments[0],d.call(arguments,1))};var e=c(a.length);return function(){for(var c=0;b>c;c++)e[c]=arguments[c];return e[b]=d.call(arguments,b),a.apply(this,e)}}function k(a){return function(b,c){return a(c,b)}}function l(a,b){return function(c){return a(c)&&b(c)}}function m(){}function n(){return!0}function o(a){return function(){return a}}function p(a,b){return b&&b.constructor===a}function q(a){return a!==f}function r(a,c){return c instanceof b&&y(function(a){return a in c},a)}function s(a,b){return[a,b]}function t(a){return A(a.reduce(k(s),X))}function u(a){return w(function(a,b){return a.unshift(b),a},[],a)}function v(a,b){return b?s(a(Y(b)),v(a,Z(b))):X}function w(a,b,c){return c?a(w(a,b,Z(c)),Y(c)):b}function x(a,b,c){function d(a,c){return a?b(Y(a))?(c(Y(a)),Z(a)):s(Y(a),d(Z(a),c)):X}return d(a,c||m)}function y(a,b){return!b||a(Y(b))&&y(a,Z(b))}function z(a,b){a&&(Y(a).apply(null,b),z(Z(a),b))}function A(a){function b(a,c){return a?b(Z(a),s(Y(a),c)):c}return b(a,X)}function B(a,b){return b&&(a(Y(b))?Y(b):B(a,Z(b)))}function C(a){"use strict";function b(){var a=0;P!==f&&P.length>p&&(c("Max buffer length exceeded: textNode"),a=Math.max(a,P.length)),Q.length>p&&(c("Max buffer length exceeded: numberNode"),a=Math.max(a,Q.length)),O=p-a+Y}function c(a){P!==f&&(m(P),n(),P=f),i=d(a+"\nLn: "+$+"\nCol: "+Z+"\nChr: "+j),o(N(f,f,i))}function e(){return T==s?(m({}),n(),void(S=!0)):((T!==t||0!==X)&&c("Unexpected end"),P!==f&&(m(P),n(),P=f),void(S=!0))}function g(a){return"\r"==a||"\n"==a||" "==a||" "==a}function h(a){if(!i){if(S)return c("Cannot write after close");var d=0;for(j=a[0];j&&(k=j,j=a[d++]);)switch(Y++,"\n"==j?($++,Z=0):Z++,T){case s:if("{"===j)T=u;else if("["===j)T=w;else if(!g(j))return c("Non-whitespace before {[.");continue;case z:case u:if(g(j))continue;if(T===z)U.push(A);else{if("}"===j){m({}),n(),T=U.pop()||t;continue}U.push(v)}if('"'!==j)return c('Malformed object key should start with " ');T=y;continue;case A:case v:if(g(j))continue;if(":"===j)T===v?(U.push(v),P!==f&&(m({}),l(P),P=f),X++):P!==f&&(l(P),P=f),T=t;else if("}"===j)P!==f&&(m(P),n(),P=f),n(),X--,T=U.pop()||t;else{if(","!==j)return c("Bad object");T===v&&U.push(v),P!==f&&(m(P),n(),P=f),T=z}continue;case w:case t:if(g(j))continue;if(T===w){if(m([]),X++,T=t,"]"===j){n(),X--,T=U.pop()||t;continue}U.push(x)}if('"'===j)T=y;else if("{"===j)T=u;else if("["===j)T=w;else if("t"===j)T=B;else if("f"===j)T=E;else if("n"===j)T=I;else if("-"===j)Q+=j;else if("0"===j)Q+=j,T=M;else{if(-1==="123456789".indexOf(j))return c("Bad value");Q+=j,T=M}continue;case x:if(","===j)U.push(x),P!==f&&(m(P),n(),P=f),T=t;else{if("]"!==j){if(g(j))continue;return c("Bad array")}P!==f&&(m(P),n(),P=f),n(),X--,T=U.pop()||t}continue;case y:P===f&&(P="");var e=d-1;a:for(;;){for(;W>0;)if(V+=j,j=a.charAt(d++),4===W?(P+=String.fromCharCode(parseInt(V,16)),W=0,e=d-1):W++,!j)break a;if('"'===j&&!R){T=U.pop()||t,P+=a.substring(e,d-1);break}if("\\"===j&&!R&&(R=!0,P+=a.substring(e,d-1),j=a.charAt(d++),!j))break;if(R){if(R=!1,"n"===j?P+="\n":"r"===j?P+="\r":"t"===j?P+=" ":"f"===j?P+="\f":"b"===j?P+="\b":"u"===j?(W=1,V=""):P+=j,j=a.charAt(d++),e=d-1,j)continue;break}q.lastIndex=d;var h=q.exec(a);if(!h){d=a.length+1,P+=a.substring(e,d-1);break}if(d=h.index+1,j=a.charAt(h.index),!j){P+=a.substring(e,d-1);break}}continue;case B:if(!j)continue;if("r"!==j)return c("Invalid true started with t"+j);T=C;continue;case C:if(!j)continue;if("u"!==j)return c("Invalid true started with tr"+j);T=D;continue;case D:if(!j)continue;if("e"!==j)return c("Invalid true started with tru"+j);m(!0),n(),T=U.pop()||t;continue;case E:if(!j)continue;if("a"!==j)return c("Invalid false started with f"+j);T=F;continue;case F:if(!j)continue;if("l"!==j)return c("Invalid false started with fa"+j);T=G;continue;case G:if(!j)continue;if("s"!==j)return c("Invalid false started with fal"+j);T=H;continue;case H:if(!j)continue;if("e"!==j)return c("Invalid false started with fals"+j);m(!1),n(),T=U.pop()||t;continue;case I:if(!j)continue;if("u"!==j)return c("Invalid null started with n"+j);T=J;continue;case J:if(!j)continue;if("l"!==j)return c("Invalid null started with nu"+j);T=K;continue;case K:if(!j)continue;if("l"!==j)return c("Invalid null started with nul"+j);m(null),n(),T=U.pop()||t;continue;case L:if("."!==j)return c("Leading zero not followed by .");Q+=j,T=M;continue;case M:if(-1!=="0123456789".indexOf(j))Q+=j;else if("."===j){if(-1!==Q.indexOf("."))return c("Invalid number has two dots");Q+=j}else if("e"===j||"E"===j){if(-1!==Q.indexOf("e")||-1!==Q.indexOf("E"))return c("Invalid number has two exponential");Q+=j}else if("+"===j||"-"===j){if("e"!==k&&"E"!==k)return c("Invalid symbol in number");Q+=j}else Q&&(m(parseFloat(Q)),n(),Q=""),d--,T=U.pop()||t;continue;default:return c("Unknown state: "+T)}Y>=O&&b()}}var i,j,k,l=a(qa).emit,m=a(ra).emit,n=a(sa).emit,o=a(ja).emit,p=65536,q=/[\\"\n]/g,r=0,s=r++,t=r++,u=r++,v=r++,w=r++,x=r++,y=r++,z=r++,A=r++,B=r++,C=r++,D=r++,E=r++,F=r++,G=r++,H=r++,I=r++,J=r++,K=r++,L=r++,M=r,O=p,P=f,Q="",R=!1,S=!1,T=s,U=[],V=null,W=0,X=0,Y=0,Z=0,$=1;a(na).on(h),a(oa).on(e)}function D(a,b){"use strict";function c(a){return function(b){d=a(d,b)}}var d,e={};for(var f in b)a(f).on(c(b[f]),e);a(ha).on(function(a){var b,c=Y(d),e=aa(c),f=Z(d);f&&(b=ba(Y(f)),b[e]=a)}),a(ia).on(function(){var a,b=Y(d),c=aa(b),e=Z(d);e&&(a=ba(Y(e)),delete a[c])}),a(pa).on(function(){for(var c in b)a(c).un(e)})}function E(a){var b={};return a&&a.split("\r\n").forEach(function(a){var c=a.indexOf(": ");b[a.substring(0,c)]=a.substring(c+2)}),b}function F(a,b){function c(a){return{"http:":80,"https:":443}[a]}function d(b){return b.port||c(b.protocol||a.protocol)}return!!(b.protocol&&b.protocol!=a.protocol||b.host&&b.host!=a.host||b.host&&d(b)!=d(a))}function G(a){var b=/(\w+:)?(?:\/\/)([\w.-]+)?(?::(\d+))?\/?/,c=b.exec(a)||[];return{protocol:c[1]||"",host:c[2]||"",port:c[3]||""}}function H(){return new XMLHttpRequest}function I(b,c,d,e,g,h,i){"use strict";function j(){var a=c.responseText,b=a.substr(m);b&&k(b),m=V(a)}var k=b(na).emit,l=b(ja).emit,m=0,n=!0;b(pa).on(function(){c.onreadystatechange=null,c.abort()}),"onprogress"in c&&(c.onprogress=j),c.onreadystatechange=function(){function a(){try{n&&b(ma).emit(c.status,E(c.getAllResponseHeaders())),n=!1}catch(a){}}switch(c.readyState){case 2:case 3:return a();case 4:a();var d=2==String(c.status)[0];d?(j(),b(oa).emit()):l(N(c.status,c.responseText))}};try{c.open(d,e,!0);for(var o in h)c.setRequestHeader(o,h[o]);F(a.location,G(e))||c.setRequestHeader("X-Requested-With","XMLHttpRequest"),c.withCredentials=i,c.send(g)}catch(p){a.setTimeout(T(l,N(f,f,p)),0)}}function J(a,b){return{key:a,node:b}}function K(a){function b(a,b){var d=ba(Y(a));return p(c,d)?f(a,V(d),b):a}function d(a,c){if(!a)return j(c),f(a,ca,c);var d=b(a,c),g=Z(d),h=aa(Y(d));return e(g,h,c),s(J(h,c),g)}function e(a,b,c){ba(Y(a))[b]=c}function f(a,b,c){a&&e(a,b,c);var d=s(J(b,c),a);return h(d),d}function g(a){return i(a),Z(a)||k(ba(Y(a)))}var h=a(fa).emit,i=a(ga).emit,j=a(la).emit,k=a(ka).emit,l={};return l[ra]=d,l[sa]=g,l[qa]=f,l}function L(a,b,c){function d(a){return function(b){return b.id==a}}var e,f;return{on:function(c,d){var g={listener:c,id:d||c};return b&&b.emit(a,c,g.id),e=s(g,e),f=s(c,f),this},emit:function(){z(f,arguments)},un:function(b){var g;e=x(e,d(b),function(a){g=a}),g&&(f=x(f,function(a){return a==g.listener}),c&&c.emit(a,g.listener,g.id))},listeners:function(){return f},hasListener:function(a){var b=a?d(a):n;return q(B(b,e))}}}function M(){function a(a){return c[a]=L(a,d,e)}function b(b){return c[b]||a(b)}var c={},d=a("newListener"),e=a("removeListener");return["emit","on","un"].forEach(function(a){b[a]=j(function(c,d){i(d,b(c)[a])})}),b}function N(a,b,c){try{var d=e.parse(b)}catch(f){}return{statusCode:a,body:b,jsonBody:d,thrown:c}}function O(a,b){function c(a,b,c){var d=A(c);a(b,u(Z(v(aa,d))),u(v(ba,d)))}function d(b,d,e){var f=a(b).emit;d.on(function(a){var b=e(a);b!==!1&&c(f,ba(b),a)},b),a("removeListener").on(function(c){c==b&&(a(c).listeners()||d.un(b))})}var e={node:a(ga),path:a(fa)};a("newListener").on(function(a){var c=/(node|path):(.*)/.exec(a);if(c){var f=e[c[1]];f.hasListener(a)||d(a,f,b(c[2]))}})}function P(a,b){function c(b,c){return a(b).on(f(c),c),p}function e(a,b,c){c=c||b;var d=f(b);return a.on(function(){var b=!1;p.forget=function(){b=!0},i(arguments,d),delete p.forget,b&&a.un(c)},c),p}function f(a){return function(){try{return a.apply(p,arguments)}catch(b){setTimeout(function(){throw new d(b.message)})}}}function g(b,c){return a(b+":"+c)}function h(a){return function(){var b=a.apply(this,arguments);q(b)&&(b==S.drop?t():u(b))}}function k(a,b,c){var d;d="node"==a?h(c):c,e(g(a,b),d,c)}function l(a,b){for(var c in b)k(a,c,b[c])}function n(a,b,c){return W(b)?k(a,b,c):l(a,b),p}var p,r=/^(node|path):./,s=a(ka),t=a(ia).emit,u=a(ha).emit,v=j(function(b,c){if(p[b])i(c,p[b]);else{var d=a(b),f=c[0];r.test(b)?e(d,f):d.on(f)}return p}),w=function(b,c,d){if("done"==b)s.un(c);else if("node"==b||"path"==b)a.un(b+":"+c,d);else{var e=c;a(b).un(e)}return p};return a(la).on(function(a){p.root=o(a)}),a(ma).on(function(a,b){p.header=function(a){return a?b[a]:b}}),p={on:v,addListener:v,removeListener:w,emit:a.emit,node:T(n,"node"),path:T(n,"path"),done:T(e,s),start:T(c,ma),fail:a(ja).on,abort:a(pa).emit,header:m,root:m,source:b}}function Q(a,b,c,d,e){var f=M();return b&&I(f,H(),a,b,c,d,e),C(f),D(f,K(f)),O(f,da),P(f,b)}function R(a,b,c,d,f,g,h){function i(a,b){return b===!1&&(a+=-1==a.indexOf("?")?"?":"&",a+="_="+(new Date).getTime()),a}return f=f?e.parse(e.stringify(f)):{},d?W(d)||(d=e.stringify(d),f["Content-Type"]=f["Content-Type"]||"application/json"):d=null,a(c||"GET",i(b,h),d,f,g||!1)}function S(a){var b=$("resume","pause","pipe"),c=T(r,b);return a?c(a)||W(a)?R(Q,a):R(Q,a.url,a.method,a.body,a.headers,a.withCredentials,a.cached):Q()}var T=j(function(a,b){var c=b.length;return j(function(d){for(var e=0;e<d.length;e++)b[c+e]=d[e];return b.length=c+d.length,a.apply(this,b)})}),U=(j(function(a){function b(a,b){return[i(a,b)]}var c=t(a);return j(function(a){return w(b,a,c)[0]})}),j(function(a){return j(function(b){for(var c,d=0;d<V(a);d++)if(c=i(b,a[d]))return c})})),V=h("length"),W=T(p,String),X=null,Y=h(0),Z=h(1),$=j(t),_=function(){var a=function(a){return a.exec.bind(a)},b=j(function(b){return b.unshift(/^/),a(RegExp(b.map(h("source")).join("")))}),c=/(\$?)/,d=/([\w-_]+|\*)/,e=/()/,f=/\["([^"]+)"\]/,g=/\[(\d+|\*)\]/,i=/{([\w ]*?)}/,k=/(?:{([\w ]*?)})?/,l=b(c,d,k),m=b(c,f,k),n=b(c,g,k),o=b(c,e,i),p=b(/\.\./),q=b(/\./),r=b(c,/!/),s=b(/$/);return function(a){return a(U(l,m,n,o),p,q,r,s)}}(),aa=h("key"),ba=h("node"),ca={},da=_(function(a,b,c,e,f){function h(a,b){var c=b[z],d=c&&"*"!=c?function(a){return B(a)==c}:n;return l(d,a)}function i(a,b){var c=b[A];if(!c)return a;var d=T(r,t(c.split(/\W+/))),e=g(d,C);return l(e,a)}function j(a,b){var c=!!b[y];return c?l(a,Y):a}function k(a){function b(a){return B(a)!=ca}return a==n?n:l(b,g(a,Z))}function m(a){if(a==n)return n;var b=o(),c=a,d=k(function(a){return e(a)}),e=U(b,c,d);return e}function o(){return function(a){return B(a)==ca}}function p(a){return function(b){var c=a(b);return c===!0?Y(b):c}}function q(a,b,c){return w(function(a,b){return b(a,c)},b,a)}function s(a,b,c,d,e){var f=a(c);if(f){var g=q(b,d,f),h=c.substr(V(f[0]));return e(h,g)}}function u(a,b){return T(s,a,b)}function v(a,b){return b}function x(a,b){var c=a?x:v;return D(a,b,c)}var y=1,z=2,A=3,B=g(aa,Y),C=g(ba,Y),D=U(u(a,$(j,i,h,k)),u(b,$(m)),u(c,$()),u(e,$(j,o)),u(f,$(p)),function(a){throw d('"'+a+'" could not be tokenised')});return function(a){try{return x(a,n)}catch(b){throw d('Could not compile "'+a+'" because '+b.message)}}}),ea=1,fa=ea++,ga=ea++,ha=ea++,ia=ea++,ja="fail",ka=ea++,la=ea++,ma="start",na="data",oa="end",pa=ea++,qa=ea++,ra=ea++,sa=ea++;S.drop=function(){return S.drop},"function"==typeof define&&define.amd?define("oboe",[],function(){return S}):"object"==typeof exports?module.exports=S:a.oboe=S}(function(){try{return window}catch(a){return self}}(),Object,Array,Error,JSON); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-node.js b/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-node.js
new file mode 100644
index 00000000000..f8fbb15895b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/dist/oboe-node.js
@@ -0,0 +1,2587 @@
+// this file is the concatenation of several js files. See http://github.com/jimhigson/oboe.js
+// for the unconcatenated source
+
+module.exports = (function () {
+
+ // v2.1.3-2-gc85b5c4
+
+/*
+
+Copyright (c) 2013, Jim Higson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+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
+HOLDER 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.
+
+*/
+
+/**
+ * Partially complete a function.
+ *
+ * var add3 = partialComplete( function add(a,b){return a+b}, 3 );
+ *
+ * add3(4) // gives 7
+ *
+ * function wrap(left, right, cen){return left + " " + cen + " " + right;}
+ *
+ * var pirateGreeting = partialComplete( wrap , "I'm", ", a mighty pirate!" );
+ *
+ * pirateGreeting("Guybrush Threepwood");
+ * // gives "I'm Guybrush Threepwood, a mighty pirate!"
+ */
+var partialComplete = varArgs(function( fn, args ) {
+
+ // this isn't the shortest way to write this but it does
+ // avoid creating a new array each time to pass to fn.apply,
+ // otherwise could just call boundArgs.concat(callArgs)
+
+ var numBoundArgs = args.length;
+
+ return varArgs(function( callArgs ) {
+
+ for (var i = 0; i < callArgs.length; i++) {
+ args[numBoundArgs + i] = callArgs[i];
+ }
+
+ args.length = numBoundArgs + callArgs.length;
+
+ return fn.apply(this, args);
+ });
+ }),
+
+/**
+ * Compose zero or more functions:
+ *
+ * compose(f1, f2, f3)(x) = f1(f2(f3(x))))
+ *
+ * The last (inner-most) function may take more than one parameter:
+ *
+ * compose(f1, f2, f3)(x,y) = f1(f2(f3(x,y))))
+ */
+ compose = varArgs(function(fns) {
+
+ var fnsList = arrayAsList(fns);
+
+ function next(params, curFn) {
+ return [apply(params, curFn)];
+ }
+
+ return varArgs(function(startParams){
+
+ return foldR(next, startParams, fnsList)[0];
+ });
+ });
+
+/**
+ * A more optimised version of compose that takes exactly two functions
+ * @param f1
+ * @param f2
+ */
+function compose2(f1, f2){
+ return function(){
+ return f1.call(this,f2.apply(this,arguments));
+ }
+}
+
+/**
+ * Generic form for a function to get a property from an object
+ *
+ * var o = {
+ * foo:'bar'
+ * }
+ *
+ * var getFoo = attr('foo')
+ *
+ * fetFoo(o) // returns 'bar'
+ *
+ * @param {String} key the property name
+ */
+function attr(key) {
+ return function(o) { return o[key]; };
+}
+
+/**
+ * Call a list of functions with the same args until one returns a
+ * truthy result. Similar to the || operator.
+ *
+ * So:
+ * lazyUnion([f1,f2,f3 ... fn])( p1, p2 ... pn )
+ *
+ * Is equivalent to:
+ * apply([p1, p2 ... pn], f1) ||
+ * apply([p1, p2 ... pn], f2) ||
+ * apply([p1, p2 ... pn], f3) ... apply(fn, [p1, p2 ... pn])
+ *
+ * @returns the first return value that is given that is truthy.
+ */
+ var lazyUnion = varArgs(function(fns) {
+
+ return varArgs(function(params){
+
+ var maybeValue;
+
+ for (var i = 0; i < len(fns); i++) {
+
+ maybeValue = apply(params, fns[i]);
+
+ if( maybeValue ) {
+ return maybeValue;
+ }
+ }
+ });
+ });
+
+/**
+ * This file declares various pieces of functional programming.
+ *
+ * This isn't a general purpose functional library, to keep things small it
+ * has just the parts useful for Oboe.js.
+ */
+
+
+/**
+ * Call a single function with the given arguments array.
+ * Basically, a functional-style version of the OO-style Function#apply for
+ * when we don't care about the context ('this') of the call.
+ *
+ * The order of arguments allows partial completion of the arguments array
+ */
+function apply(args, fn) {
+ return fn.apply(undefined, args);
+}
+
+/**
+ * Define variable argument functions but cut out all that tedious messing about
+ * with the arguments object. Delivers the variable-length part of the arguments
+ * list as an array.
+ *
+ * Eg:
+ *
+ * var myFunction = varArgs(
+ * function( fixedArgument, otherFixedArgument, variableNumberOfArguments ){
+ * console.log( variableNumberOfArguments );
+ * }
+ * )
+ *
+ * myFunction('a', 'b', 1, 2, 3); // logs [1,2,3]
+ *
+ * var myOtherFunction = varArgs(function( variableNumberOfArguments ){
+ * console.log( variableNumberOfArguments );
+ * })
+ *
+ * myFunction(1, 2, 3); // logs [1,2,3]
+ *
+ */
+function varArgs(fn){
+
+ var numberOfFixedArguments = fn.length -1,
+ slice = Array.prototype.slice;
+
+
+ if( numberOfFixedArguments == 0 ) {
+ // an optimised case for when there are no fixed args:
+
+ return function(){
+ return fn.call(this, slice.call(arguments));
+ }
+
+ } else if( numberOfFixedArguments == 1 ) {
+ // an optimised case for when there are is one fixed args:
+
+ return function(){
+ return fn.call(this, arguments[0], slice.call(arguments, 1));
+ }
+ }
+
+ // general case
+
+ // we know how many arguments fn will always take. Create a
+ // fixed-size array to hold that many, to be re-used on
+ // every call to the returned function
+ var argsHolder = Array(fn.length);
+
+ return function(){
+
+ for (var i = 0; i < numberOfFixedArguments; i++) {
+ argsHolder[i] = arguments[i];
+ }
+
+ argsHolder[numberOfFixedArguments] =
+ slice.call(arguments, numberOfFixedArguments);
+
+ return fn.apply( this, argsHolder);
+ }
+}
+
+
+/**
+ * Swap the order of parameters to a binary function
+ *
+ * A bit like this flip: http://zvon.org/other/haskell/Outputprelude/flip_f.html
+ */
+function flip(fn){
+ return function(a, b){
+ return fn(b,a);
+ }
+}
+
+
+/**
+ * Create a function which is the intersection of two other functions.
+ *
+ * Like the && operator, if the first is truthy, the second is never called,
+ * otherwise the return value from the second is returned.
+ */
+function lazyIntersection(fn1, fn2) {
+
+ return function (param) {
+
+ return fn1(param) && fn2(param);
+ };
+}
+
+/**
+ * A function which does nothing
+ */
+function noop(){}
+
+/**
+ * A function which is always happy
+ */
+function always(){return true}
+
+/**
+ * Create a function which always returns the same
+ * value
+ *
+ * var return3 = functor(3);
+ *
+ * return3() // gives 3
+ * return3() // still gives 3
+ * return3() // will always give 3
+ */
+function functor(val){
+ return function(){
+ return val;
+ }
+}
+
+/**
+ * This file defines some loosely associated syntactic sugar for
+ * Javascript programming
+ */
+
+
+/**
+ * Returns true if the given candidate is of type T
+ */
+function isOfType(T, maybeSomething){
+ return maybeSomething && maybeSomething.constructor === T;
+}
+
+var len = attr('length'),
+ isString = partialComplete(isOfType, String);
+
+/**
+ * I don't like saying this:
+ *
+ * foo !=== undefined
+ *
+ * because of the double-negative. I find this:
+ *
+ * defined(foo)
+ *
+ * easier to read.
+ */
+function defined( value ) {
+ return value !== undefined;
+}
+
+/**
+ * Returns true if object o has a key named like every property in
+ * the properties array. Will give false if any are missing, or if o
+ * is not an object.
+ */
+function hasAllProperties(fieldList, o) {
+
+ return (o instanceof Object)
+ &&
+ all(function (field) {
+ return (field in o);
+ }, fieldList);
+}
+/**
+ * Like cons in Lisp
+ */
+function cons(x, xs) {
+
+ /* Internally lists are linked 2-element Javascript arrays.
+
+ Ideally the return here would be Object.freeze([x,xs])
+ so that bugs related to mutation are found fast.
+ However, cons is right on the critical path for
+ performance and this slows oboe-mark down by
+ ~25%. Under theoretical future JS engines that freeze more
+ efficiently (possibly even use immutability to
+ run faster) this should be considered for
+ restoration.
+ */
+
+ return [x,xs];
+}
+
+/**
+ * The empty list
+ */
+var emptyList = null,
+
+/**
+ * Get the head of a list.
+ *
+ * Ie, head(cons(a,b)) = a
+ */
+ head = attr(0),
+
+/**
+ * Get the tail of a list.
+ *
+ * Ie, tail(cons(a,b)) = b
+ */
+ tail = attr(1);
+
+
+/**
+ * Converts an array to a list
+ *
+ * asList([a,b,c])
+ *
+ * is equivalent to:
+ *
+ * cons(a, cons(b, cons(c, emptyList)))
+ **/
+function arrayAsList(inputArray){
+
+ return reverseList(
+ inputArray.reduce(
+ flip(cons),
+ emptyList
+ )
+ );
+}
+
+/**
+ * A varargs version of arrayAsList. Works a bit like list
+ * in LISP.
+ *
+ * list(a,b,c)
+ *
+ * is equivalent to:
+ *
+ * cons(a, cons(b, cons(c, emptyList)))
+ */
+var list = varArgs(arrayAsList);
+
+/**
+ * Convert a list back to a js native array
+ */
+function listAsArray(list){
+
+ return foldR( function(arraySoFar, listItem){
+
+ arraySoFar.unshift(listItem);
+ return arraySoFar;
+
+ }, [], list );
+
+}
+
+/**
+ * Map a function over a list
+ */
+function map(fn, list) {
+
+ return list
+ ? cons(fn(head(list)), map(fn,tail(list)))
+ : emptyList
+ ;
+}
+
+/**
+ * foldR implementation. Reduce a list down to a single value.
+ *
+ * @pram {Function} fn (rightEval, curVal) -> result
+ */
+function foldR(fn, startValue, list) {
+
+ return list
+ ? fn(foldR(fn, startValue, tail(list)), head(list))
+ : startValue
+ ;
+}
+
+/**
+ * foldR implementation. Reduce a list down to a single value.
+ *
+ * @pram {Function} fn (rightEval, curVal) -> result
+ */
+function foldR1(fn, list) {
+
+ return tail(list)
+ ? fn(foldR1(fn, tail(list)), head(list))
+ : head(list)
+ ;
+}
+
+
+/**
+ * Return a list like the one given but with the first instance equal
+ * to item removed
+ */
+function without(list, test, removedFn) {
+
+ return withoutInner(list, removedFn || noop);
+
+ function withoutInner(subList, removedFn) {
+ return subList
+ ? ( test(head(subList))
+ ? (removedFn(head(subList)), tail(subList))
+ : cons(head(subList), withoutInner(tail(subList), removedFn))
+ )
+ : emptyList
+ ;
+ }
+}
+
+/**
+ * Returns true if the given function holds for every item in
+ * the list, false otherwise
+ */
+function all(fn, list) {
+
+ return !list ||
+ ( fn(head(list)) && all(fn, tail(list)) );
+}
+
+/**
+ * Call every function in a list of functions with the same arguments
+ *
+ * This doesn't make any sense if we're doing pure functional because
+ * it doesn't return anything. Hence, this is only really useful if the
+ * functions being called have side-effects.
+ */
+function applyEach(fnList, args) {
+
+ if( fnList ) {
+ head(fnList).apply(null, args);
+
+ applyEach(tail(fnList), args);
+ }
+}
+
+/**
+ * Reverse the order of a list
+ */
+function reverseList(list){
+
+ // js re-implementation of 3rd solution from:
+ // http://www.haskell.org/haskellwiki/99_questions/Solutions/5
+ function reverseInner( list, reversedAlready ) {
+ if( !list ) {
+ return reversedAlready;
+ }
+
+ return reverseInner(tail(list), cons(head(list), reversedAlready))
+ }
+
+ return reverseInner(list, emptyList);
+}
+
+function first(test, list) {
+ return list &&
+ (test(head(list))
+ ? head(list)
+ : first(test,tail(list)));
+}
+
+/*
+ This is a slightly hacked-up browser only version of clarinet
+
+ * some features removed to help keep browser Oboe under
+ the 5k micro-library limit
+ * plug directly into event bus
+
+ For the original go here:
+ https://github.com/dscape/clarinet
+
+ We receive the events:
+ STREAM_DATA
+ STREAM_END
+
+ We emit the events:
+ SAX_KEY
+ SAX_VALUE_OPEN
+ SAX_VALUE_CLOSE
+ FAIL_EVENT
+ */
+
+function clarinet(eventBus) {
+ "use strict";
+
+ var
+ // shortcut some events on the bus
+ emitSaxKey = eventBus(SAX_KEY).emit,
+ emitValueOpen = eventBus(SAX_VALUE_OPEN).emit,
+ emitValueClose = eventBus(SAX_VALUE_CLOSE).emit,
+ emitFail = eventBus(FAIL_EVENT).emit,
+
+ MAX_BUFFER_LENGTH = 64 * 1024
+ , stringTokenPattern = /[\\"\n]/g
+ , _n = 0
+
+ // states
+ , BEGIN = _n++
+ , VALUE = _n++ // general stuff
+ , OPEN_OBJECT = _n++ // {
+ , CLOSE_OBJECT = _n++ // }
+ , OPEN_ARRAY = _n++ // [
+ , CLOSE_ARRAY = _n++ // ]
+ , STRING = _n++ // ""
+ , OPEN_KEY = _n++ // , "a"
+ , CLOSE_KEY = _n++ // :
+ , TRUE = _n++ // r
+ , TRUE2 = _n++ // u
+ , TRUE3 = _n++ // e
+ , FALSE = _n++ // a
+ , FALSE2 = _n++ // l
+ , FALSE3 = _n++ // s
+ , FALSE4 = _n++ // e
+ , NULL = _n++ // u
+ , NULL2 = _n++ // l
+ , NULL3 = _n++ // l
+ , NUMBER_DECIMAL_POINT = _n++ // .
+ , NUMBER_DIGIT = _n // [0-9]
+
+ // setup initial parser values
+ , bufferCheckPosition = MAX_BUFFER_LENGTH
+ , latestError
+ , c
+ , p
+ , textNode = undefined
+ , numberNode = ""
+ , slashed = false
+ , closed = false
+ , state = BEGIN
+ , stack = []
+ , unicodeS = null
+ , unicodeI = 0
+ , depth = 0
+ , position = 0
+ , column = 0 //mostly for error reporting
+ , line = 1
+ ;
+
+ function checkBufferLength () {
+
+ var maxActual = 0;
+
+ if (textNode !== undefined && textNode.length > MAX_BUFFER_LENGTH) {
+ emitError("Max buffer length exceeded: textNode");
+ maxActual = Math.max(maxActual, textNode.length);
+ }
+ if (numberNode.length > MAX_BUFFER_LENGTH) {
+ emitError("Max buffer length exceeded: numberNode");
+ maxActual = Math.max(maxActual, numberNode.length);
+ }
+
+ bufferCheckPosition = (MAX_BUFFER_LENGTH - maxActual)
+ + position;
+ }
+
+ eventBus(STREAM_DATA).on(handleData);
+
+ /* At the end of the http content close the clarinet
+ This will provide an error if the total content provided was not
+ valid json, ie if not all arrays, objects and Strings closed properly */
+ eventBus(STREAM_END).on(handleStreamEnd);
+
+ function emitError (errorString) {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+
+ latestError = Error(errorString + "\nLn: "+line+
+ "\nCol: "+column+
+ "\nChr: "+c);
+
+ emitFail(errorReport(undefined, undefined, latestError));
+ }
+
+ function handleStreamEnd() {
+ if( state == BEGIN ) {
+ // Handle the case where the stream closes without ever receiving
+ // any input. This isn't an error - response bodies can be blank,
+ // particularly for 204 http responses
+
+ // Because of how Oboe is currently implemented, we parse a
+ // completely empty stream as containing an empty object.
+ // This is because Oboe's done event is only fired when the
+ // root object of the JSON stream closes.
+
+ // This should be decoupled and attached instead to the input stream
+ // from the http (or whatever) resource ending.
+ // If this decoupling could happen the SAX parser could simply emit
+ // zero events on a completely empty input.
+ emitValueOpen({});
+ emitValueClose();
+
+ closed = true;
+ return;
+ }
+
+ if (state !== VALUE || depth !== 0)
+ emitError("Unexpected end");
+
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+
+ closed = true;
+ }
+
+ function whitespace(c){
+ return c == '\r' || c == '\n' || c == ' ' || c == '\t';
+ }
+
+ function handleData (chunk) {
+
+ // this used to throw the error but inside Oboe we will have already
+ // gotten the error when it was emitted. The important thing is to
+ // not continue with the parse.
+ if (latestError)
+ return;
+
+ if (closed) {
+ return emitError("Cannot write after close");
+ }
+
+ var i = 0;
+ c = chunk[0];
+
+ while (c) {
+ p = c;
+ c = chunk[i++];
+ if(!c) break;
+
+ position ++;
+ if (c == "\n") {
+ line ++;
+ column = 0;
+ } else column ++;
+ switch (state) {
+
+ case BEGIN:
+ if (c === "{") state = OPEN_OBJECT;
+ else if (c === "[") state = OPEN_ARRAY;
+ else if (!whitespace(c))
+ return emitError("Non-whitespace before {[.");
+ continue;
+
+ case OPEN_KEY:
+ case OPEN_OBJECT:
+ if (whitespace(c)) continue;
+ if(state === OPEN_KEY) stack.push(CLOSE_KEY);
+ else {
+ if(c === '}') {
+ emitValueOpen({});
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ continue;
+ } else stack.push(CLOSE_OBJECT);
+ }
+ if(c === '"')
+ state = STRING;
+ else
+ return emitError("Malformed object key should start with \" ");
+ continue;
+
+ case CLOSE_KEY:
+ case CLOSE_OBJECT:
+ if (whitespace(c)) continue;
+
+ if(c===':') {
+ if(state === CLOSE_OBJECT) {
+ stack.push(CLOSE_OBJECT);
+
+ if (textNode !== undefined) {
+ // was previously (in upstream Clarinet) one event
+ // - object open came with the text of the first
+ emitValueOpen({});
+ emitSaxKey(textNode);
+ textNode = undefined;
+ }
+ depth++;
+ } else {
+ if (textNode !== undefined) {
+ emitSaxKey(textNode);
+ textNode = undefined;
+ }
+ }
+ state = VALUE;
+ } else if (c==='}') {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ } else if(c===',') {
+ if(state === CLOSE_OBJECT)
+ stack.push(CLOSE_OBJECT);
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ state = OPEN_KEY;
+ } else
+ return emitError('Bad object');
+ continue;
+
+ case OPEN_ARRAY: // after an array there always a value
+ case VALUE:
+ if (whitespace(c)) continue;
+ if(state===OPEN_ARRAY) {
+ emitValueOpen([]);
+ depth++;
+ state = VALUE;
+ if(c === ']') {
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ continue;
+ } else {
+ stack.push(CLOSE_ARRAY);
+ }
+ }
+ if(c === '"') state = STRING;
+ else if(c === '{') state = OPEN_OBJECT;
+ else if(c === '[') state = OPEN_ARRAY;
+ else if(c === 't') state = TRUE;
+ else if(c === 'f') state = FALSE;
+ else if(c === 'n') state = NULL;
+ else if(c === '-') { // keep and continue
+ numberNode += c;
+ } else if(c==='0') {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else if('123456789'.indexOf(c) !== -1) {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else
+ return emitError("Bad value");
+ continue;
+
+ case CLOSE_ARRAY:
+ if(c===',') {
+ stack.push(CLOSE_ARRAY);
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ state = VALUE;
+ } else if (c===']') {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ } else if (whitespace(c))
+ continue;
+ else
+ return emitError('Bad array');
+ continue;
+
+ case STRING:
+ if (textNode === undefined) {
+ textNode = "";
+ }
+
+ // thanks thejh, this is an about 50% performance improvement.
+ var starti = i-1;
+
+ STRING_BIGLOOP: while (true) {
+
+ // zero means "no unicode active". 1-4 mean "parse some more". end after 4.
+ while (unicodeI > 0) {
+ unicodeS += c;
+ c = chunk.charAt(i++);
+ if (unicodeI === 4) {
+ // TODO this might be slow? well, probably not used too often anyway
+ textNode += String.fromCharCode(parseInt(unicodeS, 16));
+ unicodeI = 0;
+ starti = i-1;
+ } else {
+ unicodeI++;
+ }
+ // we can just break here: no stuff we skipped that still has to be sliced out or so
+ if (!c) break STRING_BIGLOOP;
+ }
+ if (c === '"' && !slashed) {
+ state = stack.pop() || VALUE;
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ if (c === '\\' && !slashed) {
+ slashed = true;
+ textNode += chunk.substring(starti, i-1);
+ c = chunk.charAt(i++);
+ if (!c) break;
+ }
+ if (slashed) {
+ slashed = false;
+ if (c === 'n') { textNode += '\n'; }
+ else if (c === 'r') { textNode += '\r'; }
+ else if (c === 't') { textNode += '\t'; }
+ else if (c === 'f') { textNode += '\f'; }
+ else if (c === 'b') { textNode += '\b'; }
+ else if (c === 'u') {
+ // \uxxxx. meh!
+ unicodeI = 1;
+ unicodeS = '';
+ } else {
+ textNode += c;
+ }
+ c = chunk.charAt(i++);
+ starti = i-1;
+ if (!c) break;
+ else continue;
+ }
+
+ stringTokenPattern.lastIndex = i;
+ var reResult = stringTokenPattern.exec(chunk);
+ if (!reResult) {
+ i = chunk.length+1;
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ i = reResult.index+1;
+ c = chunk.charAt(reResult.index);
+ if (!c) {
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ }
+ continue;
+
+ case TRUE:
+ if (!c) continue; // strange buffers
+ if (c==='r') state = TRUE2;
+ else
+ return emitError( 'Invalid true started with t'+ c);
+ continue;
+
+ case TRUE2:
+ if (!c) continue;
+ if (c==='u') state = TRUE3;
+ else
+ return emitError('Invalid true started with tr'+ c);
+ continue;
+
+ case TRUE3:
+ if (!c) continue;
+ if(c==='e') {
+ emitValueOpen(true);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid true started with tru'+ c);
+ continue;
+
+ case FALSE:
+ if (!c) continue;
+ if (c==='a') state = FALSE2;
+ else
+ return emitError('Invalid false started with f'+ c);
+ continue;
+
+ case FALSE2:
+ if (!c) continue;
+ if (c==='l') state = FALSE3;
+ else
+ return emitError('Invalid false started with fa'+ c);
+ continue;
+
+ case FALSE3:
+ if (!c) continue;
+ if (c==='s') state = FALSE4;
+ else
+ return emitError('Invalid false started with fal'+ c);
+ continue;
+
+ case FALSE4:
+ if (!c) continue;
+ if (c==='e') {
+ emitValueOpen(false);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid false started with fals'+ c);
+ continue;
+
+ case NULL:
+ if (!c) continue;
+ if (c==='u') state = NULL2;
+ else
+ return emitError('Invalid null started with n'+ c);
+ continue;
+
+ case NULL2:
+ if (!c) continue;
+ if (c==='l') state = NULL3;
+ else
+ return emitError('Invalid null started with nu'+ c);
+ continue;
+
+ case NULL3:
+ if (!c) continue;
+ if(c==='l') {
+ emitValueOpen(null);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid null started with nul'+ c);
+ continue;
+
+ case NUMBER_DECIMAL_POINT:
+ if(c==='.') {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else
+ return emitError('Leading zero not followed by .');
+ continue;
+
+ case NUMBER_DIGIT:
+ if('0123456789'.indexOf(c) !== -1) numberNode += c;
+ else if (c==='.') {
+ if(numberNode.indexOf('.')!==-1)
+ return emitError('Invalid number has two dots');
+ numberNode += c;
+ } else if (c==='e' || c==='E') {
+ if(numberNode.indexOf('e')!==-1 ||
+ numberNode.indexOf('E')!==-1 )
+ return emitError('Invalid number has two exponential');
+ numberNode += c;
+ } else if (c==="+" || c==="-") {
+ if(!(p==='e' || p==='E'))
+ return emitError('Invalid symbol in number');
+ numberNode += c;
+ } else {
+ if (numberNode) {
+ emitValueOpen(parseFloat(numberNode));
+ emitValueClose();
+ numberNode = "";
+ }
+ i--; // go back one
+ state = stack.pop() || VALUE;
+ }
+ continue;
+
+ default:
+ return emitError("Unknown state: " + state);
+ }
+ }
+ if (position >= bufferCheckPosition)
+ checkBufferLength();
+ }
+}
+
+
+/**
+ * A bridge used to assign stateless functions to listen to clarinet.
+ *
+ * As well as the parameter from clarinet, each callback will also be passed
+ * the result of the last callback.
+ *
+ * This may also be used to clear all listeners by assigning zero handlers:
+ *
+ * ascentManager( clarinet, {} )
+ */
+function ascentManager(oboeBus, handlers){
+ "use strict";
+
+ var listenerId = {},
+ ascent;
+
+ function stateAfter(handler) {
+ return function(param){
+ ascent = handler( ascent, param);
+ }
+ }
+
+ for( var eventName in handlers ) {
+
+ oboeBus(eventName).on(stateAfter(handlers[eventName]), listenerId);
+ }
+
+ oboeBus(NODE_SWAP).on(function(newNode) {
+
+ var oldHead = head(ascent),
+ key = keyOf(oldHead),
+ ancestors = tail(ascent),
+ parentNode;
+
+ if( ancestors ) {
+ parentNode = nodeOf(head(ancestors));
+ parentNode[key] = newNode;
+ }
+ });
+
+ oboeBus(NODE_DROP).on(function() {
+
+ var oldHead = head(ascent),
+ key = keyOf(oldHead),
+ ancestors = tail(ascent),
+ parentNode;
+
+ if( ancestors ) {
+ parentNode = nodeOf(head(ancestors));
+
+ delete parentNode[key];
+ }
+ });
+
+ oboeBus(ABORTING).on(function(){
+
+ for( var eventName in handlers ) {
+ oboeBus(eventName).un(listenerId);
+ }
+ });
+}
+
+var httpTransport = functor(require('http'));
+
+/**
+ * A wrapper around the browser XmlHttpRequest object that raises an
+ * event whenever a new part of the response is available.
+ *
+ * In older browsers progressive reading is impossible so all the
+ * content is given in a single call. For newer ones several events
+ * should be raised, allowing progressive interpretation of the response.
+ *
+ * @param {Function} oboeBus an event bus local to this Oboe instance
+ * @param {XMLHttpRequest} transport the http implementation to use as the transport. Under normal
+ * operation, will have been created using httpTransport() above
+ * and therefore be Node's http
+ * but for tests a stub may be provided instead.
+ * @param {String} method one of 'GET' 'POST' 'PUT' 'PATCH' 'DELETE'
+ * @param {String} contentSource the url to make a request to, or a stream to read from
+ * @param {String|Null} data some content to be sent with the request.
+ * Only valid if method is POST or PUT.
+ * @param {Object} [headers] the http request headers to send
+ */
+function streamingHttp(oboeBus, transport, method, contentSource, data, headers) {
+ "use strict";
+
+ /* receiving data after calling .abort on Node's http has been observed in the
+ wild. Keep aborted as state so that if the request has been aborted we
+ can ignore new data from that point on */
+ var aborted = false;
+
+ function readStreamToEventBus(readableStream) {
+
+ // use stream in flowing mode
+ readableStream.on('data', function (chunk) {
+
+ // avoid reading the stream after aborting the request
+ if( !aborted ) {
+ oboeBus(STREAM_DATA).emit(chunk.toString());
+ }
+ });
+
+ readableStream.on('end', function() {
+
+ // avoid reading the stream after aborting the request
+ if( !aborted ) {
+ oboeBus(STREAM_END).emit();
+ }
+ });
+ }
+
+ function readStreamToEnd(readableStream, callback){
+ var content = '';
+
+ readableStream.on('data', function (chunk) {
+
+ content += chunk.toString();
+ });
+
+ readableStream.on('end', function() {
+
+ callback( content );
+ });
+ }
+
+ function openUrlAsStream( url ) {
+
+ var parsedUrl = require('url').parse(url);
+
+ return transport.request({
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port,
+ path: parsedUrl.path,
+ method: method,
+ headers: headers,
+ protocol: parsedUrl.protocol
+ });
+ }
+
+ function fetchUrl() {
+ if( !contentSource.match(/https?:\/\//) ) {
+ throw new Error(
+ 'Supported protocols when passing a URL into Oboe are http and https. ' +
+ 'If you wish to use another protocol, please pass a ReadableStream ' +
+ '(http://nodejs.org/api/stream.html#stream_class_stream_readable) like ' +
+ 'oboe(fs.createReadStream("my_file")). I was given the URL: ' +
+ contentSource
+ );
+ }
+
+ var req = openUrlAsStream(contentSource);
+
+ req.on('response', function(res){
+ var statusCode = res.statusCode,
+ successful = String(statusCode)[0] == 2;
+
+ oboeBus(HTTP_START).emit( res.statusCode, res.headers);
+
+ if( successful ) {
+
+ readStreamToEventBus(res)
+
+ } else {
+ readStreamToEnd(res, function(errorBody){
+ oboeBus(FAIL_EVENT).emit(
+ errorReport( statusCode, errorBody )
+ );
+ });
+ }
+ });
+
+ req.on('error', function(e) {
+ oboeBus(FAIL_EVENT).emit(
+ errorReport(undefined, undefined, e )
+ );
+ });
+
+ oboeBus(ABORTING).on( function(){
+ aborted = true;
+ req.abort();
+ });
+
+ if( data ) {
+ req.write(data);
+ }
+
+ req.end();
+ }
+
+ if( isString(contentSource) ) {
+ fetchUrl(contentSource);
+ } else {
+ // contentsource is a stream
+ readStreamToEventBus(contentSource);
+ }
+
+}
+
+var jsonPathSyntax = (function() {
+
+ var
+
+ /**
+ * Export a regular expression as a simple function by exposing just
+ * the Regex#exec. This allows regex tests to be used under the same
+ * interface as differently implemented tests, or for a user of the
+ * tests to not concern themselves with their implementation as regular
+ * expressions.
+ *
+ * This could also be expressed point-free as:
+ * Function.prototype.bind.bind(RegExp.prototype.exec),
+ *
+ * But that's far too confusing! (and not even smaller once minified
+ * and gzipped)
+ */
+ regexDescriptor = function regexDescriptor(regex) {
+ return regex.exec.bind(regex);
+ }
+
+ /**
+ * Join several regular expressions and express as a function.
+ * This allows the token patterns to reuse component regular expressions
+ * instead of being expressed in full using huge and confusing regular
+ * expressions.
+ */
+ , jsonPathClause = varArgs(function( componentRegexes ) {
+
+ // The regular expressions all start with ^ because we
+ // only want to find matches at the start of the
+ // JSONPath fragment we are inspecting
+ componentRegexes.unshift(/^/);
+
+ return regexDescriptor(
+ RegExp(
+ componentRegexes.map(attr('source')).join('')
+ )
+ );
+ })
+
+ , possiblyCapturing = /(\$?)/
+ , namedNode = /([\w-_]+|\*)/
+ , namePlaceholder = /()/
+ , nodeInArrayNotation = /\["([^"]+)"\]/
+ , numberedNodeInArrayNotation = /\[(\d+|\*)\]/
+ , fieldList = /{([\w ]*?)}/
+ , optionalFieldList = /(?:{([\w ]*?)})?/
+
+
+ // foo or *
+ , jsonPathNamedNodeInObjectNotation = jsonPathClause(
+ possiblyCapturing,
+ namedNode,
+ optionalFieldList
+ )
+
+ // ["foo"]
+ , jsonPathNamedNodeInArrayNotation = jsonPathClause(
+ possiblyCapturing,
+ nodeInArrayNotation,
+ optionalFieldList
+ )
+
+ // [2] or [*]
+ , jsonPathNumberedNodeInArrayNotation = jsonPathClause(
+ possiblyCapturing,
+ numberedNodeInArrayNotation,
+ optionalFieldList
+ )
+
+ // {a b c}
+ , jsonPathPureDuckTyping = jsonPathClause(
+ possiblyCapturing,
+ namePlaceholder,
+ fieldList
+ )
+
+ // ..
+ , jsonPathDoubleDot = jsonPathClause(/\.\./)
+
+ // .
+ , jsonPathDot = jsonPathClause(/\./)
+
+ // !
+ , jsonPathBang = jsonPathClause(
+ possiblyCapturing,
+ /!/
+ )
+
+ // nada!
+ , emptyString = jsonPathClause(/$/)
+
+ ;
+
+
+ /* We export only a single function. When called, this function injects
+ into another function the descriptors from above.
+ */
+ return function (fn){
+ return fn(
+ lazyUnion(
+ jsonPathNamedNodeInObjectNotation
+ , jsonPathNamedNodeInArrayNotation
+ , jsonPathNumberedNodeInArrayNotation
+ , jsonPathPureDuckTyping
+ )
+ , jsonPathDoubleDot
+ , jsonPathDot
+ , jsonPathBang
+ , emptyString
+ );
+ };
+
+}());
+/**
+ * Get a new key->node mapping
+ *
+ * @param {String|Number} key
+ * @param {Object|Array|String|Number|null} node a value found in the json
+ */
+function namedNode(key, node) {
+ return {key:key, node:node};
+}
+
+/** get the key of a namedNode */
+var keyOf = attr('key');
+
+/** get the node from a namedNode */
+var nodeOf = attr('node');
+/**
+ * This file provides various listeners which can be used to build up
+ * a changing ascent based on the callbacks provided by Clarinet. It listens
+ * to the low-level events from Clarinet and emits higher-level ones.
+ *
+ * The building up is stateless so to track a JSON file
+ * ascentManager.js is required to store the ascent state
+ * between calls.
+ */
+
+
+
+/**
+ * A special value to use in the path list to represent the path 'to' a root
+ * object (which doesn't really have any path). This prevents the need for
+ * special-casing detection of the root object and allows it to be treated
+ * like any other object. We might think of this as being similar to the
+ * 'unnamed root' domain ".", eg if I go to
+ * http://en.wikipedia.org./wiki/En/Main_page the dot after 'org' deliminates
+ * the unnamed root of the DNS.
+ *
+ * This is kept as an object to take advantage that in Javascript's OO objects
+ * are guaranteed to be distinct, therefore no other object can possibly clash
+ * with this one. Strings, numbers etc provide no such guarantee.
+ **/
+var ROOT_PATH = {};
+
+
+/**
+ * Create a new set of handlers for clarinet's events, bound to the emit
+ * function given.
+ */
+function incrementalContentBuilder( oboeBus ) {
+
+ var emitNodeOpened = oboeBus(NODE_OPENED).emit,
+ emitNodeClosed = oboeBus(NODE_CLOSED).emit,
+ emitRootOpened = oboeBus(ROOT_PATH_FOUND).emit,
+ emitRootClosed = oboeBus(ROOT_NODE_FOUND).emit;
+
+ function arrayIndicesAreKeys( possiblyInconsistentAscent, newDeepestNode) {
+
+ /* for values in arrays we aren't pre-warned of the coming paths
+ (Clarinet gives no call to onkey like it does for values in objects)
+ so if we are in an array we need to create this path ourselves. The
+ key will be len(parentNode) because array keys are always sequential
+ numbers. */
+
+ var parentNode = nodeOf( head( possiblyInconsistentAscent));
+
+ return isOfType( Array, parentNode)
+ ?
+ keyFound( possiblyInconsistentAscent,
+ len(parentNode),
+ newDeepestNode
+ )
+ :
+ // nothing needed, return unchanged
+ possiblyInconsistentAscent
+ ;
+ }
+
+ function nodeOpened( ascent, newDeepestNode ) {
+
+ if( !ascent ) {
+ // we discovered the root node,
+ emitRootOpened( newDeepestNode);
+
+ return keyFound( ascent, ROOT_PATH, newDeepestNode);
+ }
+
+ // we discovered a non-root node
+
+ var arrayConsistentAscent = arrayIndicesAreKeys( ascent, newDeepestNode),
+ ancestorBranches = tail( arrayConsistentAscent),
+ previouslyUnmappedName = keyOf( head( arrayConsistentAscent));
+
+ appendBuiltContent(
+ ancestorBranches,
+ previouslyUnmappedName,
+ newDeepestNode
+ );
+
+ return cons(
+ namedNode( previouslyUnmappedName, newDeepestNode ),
+ ancestorBranches
+ );
+ }
+
+
+ /**
+ * Add a new value to the object we are building up to represent the
+ * parsed JSON
+ */
+ function appendBuiltContent( ancestorBranches, key, node ){
+
+ nodeOf( head( ancestorBranches))[key] = node;
+ }
+
+
+ /**
+ * For when we find a new key in the json.
+ *
+ * @param {String|Number|Object} newDeepestName the key. If we are in an
+ * array will be a number, otherwise a string. May take the special
+ * value ROOT_PATH if the root node has just been found
+ *
+ * @param {String|Number|Object|Array|Null|undefined} [maybeNewDeepestNode]
+ * usually this won't be known so can be undefined. Can't use null
+ * to represent unknown because null is a valid value in JSON
+ **/
+ function keyFound(ascent, newDeepestName, maybeNewDeepestNode) {
+
+ if( ascent ) { // if not root
+
+ // If we have the key but (unless adding to an array) no known value
+ // yet. Put that key in the output but against no defined value:
+ appendBuiltContent( ascent, newDeepestName, maybeNewDeepestNode );
+ }
+
+ var ascentWithNewPath = cons(
+ namedNode( newDeepestName,
+ maybeNewDeepestNode),
+ ascent
+ );
+
+ emitNodeOpened( ascentWithNewPath);
+
+ return ascentWithNewPath;
+ }
+
+
+ /**
+ * For when the current node ends.
+ */
+ function nodeClosed( ascent ) {
+
+ emitNodeClosed( ascent);
+
+ return tail( ascent) ||
+ // If there are no nodes left in the ascent the root node
+ // just closed. Emit a special event for this:
+ emitRootClosed(nodeOf(head(ascent)));
+ }
+
+ var contentBuilderHandlers = {};
+ contentBuilderHandlers[SAX_VALUE_OPEN] = nodeOpened;
+ contentBuilderHandlers[SAX_VALUE_CLOSE] = nodeClosed;
+ contentBuilderHandlers[SAX_KEY] = keyFound;
+ return contentBuilderHandlers;
+}
+
+/**
+ * The jsonPath evaluator compiler used for Oboe.js.
+ *
+ * One function is exposed. This function takes a String JSONPath spec and
+ * returns a function to test candidate ascents for matches.
+ *
+ * String jsonPath -> (List ascent) -> Boolean|Object
+ *
+ * This file is coded in a pure functional style. That is, no function has
+ * side effects, every function evaluates to the same value for the same
+ * arguments and no variables are reassigned.
+ */
+// the call to jsonPathSyntax injects the token syntaxes that are needed
+// inside the compiler
+var jsonPathCompiler = jsonPathSyntax(function (pathNodeSyntax,
+ doubleDotSyntax,
+ dotSyntax,
+ bangSyntax,
+ emptySyntax ) {
+
+ var CAPTURING_INDEX = 1;
+ var NAME_INDEX = 2;
+ var FIELD_LIST_INDEX = 3;
+
+ var headKey = compose2(keyOf, head),
+ headNode = compose2(nodeOf, head);
+
+ /**
+ * Create an evaluator function for a named path node, expressed in the
+ * JSONPath like:
+ * foo
+ * ["bar"]
+ * [2]
+ */
+ function nameClause(previousExpr, detection ) {
+
+ var name = detection[NAME_INDEX],
+
+ matchesName = ( !name || name == '*' )
+ ? always
+ : function(ascent){return headKey(ascent) == name};
+
+
+ return lazyIntersection(matchesName, previousExpr);
+ }
+
+ /**
+ * Create an evaluator function for a a duck-typed node, expressed like:
+ *
+ * {spin, taste, colour}
+ * .particle{spin, taste, colour}
+ * *{spin, taste, colour}
+ */
+ function duckTypeClause(previousExpr, detection) {
+
+ var fieldListStr = detection[FIELD_LIST_INDEX];
+
+ if (!fieldListStr)
+ return previousExpr; // don't wrap at all, return given expr as-is
+
+ var hasAllrequiredFields = partialComplete(
+ hasAllProperties,
+ arrayAsList(fieldListStr.split(/\W+/))
+ ),
+
+ isMatch = compose2(
+ hasAllrequiredFields,
+ headNode
+ );
+
+ return lazyIntersection(isMatch, previousExpr);
+ }
+
+ /**
+ * Expression for $, returns the evaluator function
+ */
+ function capture( previousExpr, detection ) {
+
+ // extract meaning from the detection
+ var capturing = !!detection[CAPTURING_INDEX];
+
+ if (!capturing)
+ return previousExpr; // don't wrap at all, return given expr as-is
+
+ return lazyIntersection(previousExpr, head);
+
+ }
+
+ /**
+ * Create an evaluator function that moves onto the next item on the
+ * lists. This function is the place where the logic to move up a
+ * level in the ascent exists.
+ *
+ * Eg, for JSONPath ".foo" we need skip1(nameClause(always, [,'foo']))
+ */
+ function skip1(previousExpr) {
+
+
+ if( previousExpr == always ) {
+ /* If there is no previous expression this consume command
+ is at the start of the jsonPath.
+ Since JSONPath specifies what we'd like to find but not
+ necessarily everything leading down to it, when running
+ out of JSONPath to check against we default to true */
+ return always;
+ }
+
+ /** return true if the ascent we have contains only the JSON root,
+ * false otherwise
+ */
+ function notAtRoot(ascent){
+ return headKey(ascent) != ROOT_PATH;
+ }
+
+ return lazyIntersection(
+ /* If we're already at the root but there are more
+ expressions to satisfy, can't consume any more. No match.
+
+ This check is why none of the other exprs have to be able
+ to handle empty lists; skip1 is the only evaluator that
+ moves onto the next token and it refuses to do so once it
+ reaches the last item in the list. */
+ notAtRoot,
+
+ /* We are not at the root of the ascent yet.
+ Move to the next level of the ascent by handing only
+ the tail to the previous expression */
+ compose2(previousExpr, tail)
+ );
+
+ }
+
+ /**
+ * Create an evaluator function for the .. (double dot) token. Consumes
+ * zero or more levels of the ascent, the fewest that are required to find
+ * a match when given to previousExpr.
+ */
+ function skipMany(previousExpr) {
+
+ if( previousExpr == always ) {
+ /* If there is no previous expression this consume command
+ is at the start of the jsonPath.
+ Since JSONPath specifies what we'd like to find but not
+ necessarily everything leading down to it, when running
+ out of JSONPath to check against we default to true */
+ return always;
+ }
+
+ var
+ // In JSONPath .. is equivalent to !.. so if .. reaches the root
+ // the match has succeeded. Ie, we might write ..foo or !..foo
+ // and both should match identically.
+ terminalCaseWhenArrivingAtRoot = rootExpr(),
+ terminalCaseWhenPreviousExpressionIsSatisfied = previousExpr,
+ recursiveCase = skip1(function(ascent) {
+ return cases(ascent);
+ }),
+
+ cases = lazyUnion(
+ terminalCaseWhenArrivingAtRoot
+ , terminalCaseWhenPreviousExpressionIsSatisfied
+ , recursiveCase
+ );
+
+ return cases;
+ }
+
+ /**
+ * Generate an evaluator for ! - matches only the root element of the json
+ * and ignores any previous expressions since nothing may precede !.
+ */
+ function rootExpr() {
+
+ return function(ascent){
+ return headKey(ascent) == ROOT_PATH;
+ };
+ }
+
+ /**
+ * Generate a statement wrapper to sit around the outermost
+ * clause evaluator.
+ *
+ * Handles the case where the capturing is implicit because the JSONPath
+ * did not contain a '$' by returning the last node.
+ */
+ function statementExpr(lastClause) {
+
+ return function(ascent) {
+
+ // kick off the evaluation by passing through to the last clause
+ var exprMatch = lastClause(ascent);
+
+ return exprMatch === true ? head(ascent) : exprMatch;
+ };
+ }
+
+ /**
+ * For when a token has been found in the JSONPath input.
+ * Compiles the parser for that token and returns in combination with the
+ * parser already generated.
+ *
+ * @param {Function} exprs a list of the clause evaluator generators for
+ * the token that was found
+ * @param {Function} parserGeneratedSoFar the parser already found
+ * @param {Array} detection the match given by the regex engine when
+ * the feature was found
+ */
+ function expressionsReader( exprs, parserGeneratedSoFar, detection ) {
+
+ // if exprs is zero-length foldR will pass back the
+ // parserGeneratedSoFar as-is so we don't need to treat
+ // this as a special case
+
+ return foldR(
+ function( parserGeneratedSoFar, expr ){
+
+ return expr(parserGeneratedSoFar, detection);
+ },
+ parserGeneratedSoFar,
+ exprs
+ );
+
+ }
+
+ /**
+ * If jsonPath matches the given detector function, creates a function which
+ * evaluates against every clause in the clauseEvaluatorGenerators. The
+ * created function is propagated to the onSuccess function, along with
+ * the remaining unparsed JSONPath substring.
+ *
+ * The intended use is to create a clauseMatcher by filling in
+ * the first two arguments, thus providing a function that knows
+ * some syntax to match and what kind of generator to create if it
+ * finds it. The parameter list once completed is:
+ *
+ * (jsonPath, parserGeneratedSoFar, onSuccess)
+ *
+ * onSuccess may be compileJsonPathToFunction, to recursively continue
+ * parsing after finding a match or returnFoundParser to stop here.
+ */
+ function generateClauseReaderIfTokenFound (
+
+ tokenDetector, clauseEvaluatorGenerators,
+
+ jsonPath, parserGeneratedSoFar, onSuccess) {
+
+ var detected = tokenDetector(jsonPath);
+
+ if(detected) {
+ var compiledParser = expressionsReader(
+ clauseEvaluatorGenerators,
+ parserGeneratedSoFar,
+ detected
+ ),
+
+ remainingUnparsedJsonPath = jsonPath.substr(len(detected[0]));
+
+ return onSuccess(remainingUnparsedJsonPath, compiledParser);
+ }
+ }
+
+ /**
+ * Partially completes generateClauseReaderIfTokenFound above.
+ */
+ function clauseMatcher(tokenDetector, exprs) {
+
+ return partialComplete(
+ generateClauseReaderIfTokenFound,
+ tokenDetector,
+ exprs
+ );
+ }
+
+ /**
+ * clauseForJsonPath is a function which attempts to match against
+ * several clause matchers in order until one matches. If non match the
+ * jsonPath expression is invalid and an error is thrown.
+ *
+ * The parameter list is the same as a single clauseMatcher:
+ *
+ * (jsonPath, parserGeneratedSoFar, onSuccess)
+ */
+ var clauseForJsonPath = lazyUnion(
+
+ clauseMatcher(pathNodeSyntax , list( capture,
+ duckTypeClause,
+ nameClause,
+ skip1 ))
+
+ , clauseMatcher(doubleDotSyntax , list( skipMany))
+
+ // dot is a separator only (like whitespace in other languages) but
+ // rather than make it a special case, use an empty list of
+ // expressions when this token is found
+ , clauseMatcher(dotSyntax , list() )
+
+ , clauseMatcher(bangSyntax , list( capture,
+ rootExpr))
+
+ , clauseMatcher(emptySyntax , list( statementExpr))
+
+ , function (jsonPath) {
+ throw Error('"' + jsonPath + '" could not be tokenised')
+ }
+ );
+
+
+ /**
+ * One of two possible values for the onSuccess argument of
+ * generateClauseReaderIfTokenFound.
+ *
+ * When this function is used, generateClauseReaderIfTokenFound simply
+ * returns the compiledParser that it made, regardless of if there is
+ * any remaining jsonPath to be compiled.
+ */
+ function returnFoundParser(_remainingJsonPath, compiledParser){
+ return compiledParser
+ }
+
+ /**
+ * Recursively compile a JSONPath expression.
+ *
+ * This function serves as one of two possible values for the onSuccess
+ * argument of generateClauseReaderIfTokenFound, meaning continue to
+ * recursively compile. Otherwise, returnFoundParser is given and
+ * compilation terminates.
+ */
+ function compileJsonPathToFunction( uncompiledJsonPath,
+ parserGeneratedSoFar ) {
+
+ /**
+ * On finding a match, if there is remaining text to be compiled
+ * we want to either continue parsing using a recursive call to
+ * compileJsonPathToFunction. Otherwise, we want to stop and return
+ * the parser that we have found so far.
+ */
+ var onFind = uncompiledJsonPath
+ ? compileJsonPathToFunction
+ : returnFoundParser;
+
+ return clauseForJsonPath(
+ uncompiledJsonPath,
+ parserGeneratedSoFar,
+ onFind
+ );
+ }
+
+ /**
+ * This is the function that we expose to the rest of the library.
+ */
+ return function(jsonPath){
+
+ try {
+ // Kick off the recursive parsing of the jsonPath
+ return compileJsonPathToFunction(jsonPath, always);
+
+ } catch( e ) {
+ throw Error( 'Could not compile "' + jsonPath +
+ '" because ' + e.message
+ );
+ }
+ }
+
+});
+
+/**
+ * A pub/sub which is responsible for a single event type. A
+ * multi-event type event bus is created by pubSub by collecting
+ * several of these.
+ *
+ * @param {String} eventType
+ * the name of the events managed by this singleEventPubSub
+ * @param {singleEventPubSub} [newListener]
+ * place to notify of new listeners
+ * @param {singleEventPubSub} [removeListener]
+ * place to notify of when listeners are removed
+ */
+function singleEventPubSub(eventType, newListener, removeListener){
+
+ /** we are optimised for emitting events over firing them.
+ * As well as the tuple list which stores event ids and
+ * listeners there is a list with just the listeners which
+ * can be iterated more quickly when we are emitting
+ */
+ var listenerTupleList,
+ listenerList;
+
+ function hasId(id){
+ return function(tuple) {
+ return tuple.id == id;
+ };
+ }
+
+ return {
+
+ /**
+ * @param {Function} listener
+ * @param {*} listenerId
+ * an id that this listener can later by removed by.
+ * Can be of any type, to be compared to other ids using ==
+ */
+ on:function( listener, listenerId ) {
+
+ var tuple = {
+ listener: listener
+ , id: listenerId || listener // when no id is given use the
+ // listener function as the id
+ };
+
+ if( newListener ) {
+ newListener.emit(eventType, listener, tuple.id);
+ }
+
+ listenerTupleList = cons( tuple, listenerTupleList );
+ listenerList = cons( listener, listenerList );
+
+ return this; // chaining
+ },
+
+ emit:function () {
+ applyEach( listenerList, arguments );
+ },
+
+ un: function( listenerId ) {
+
+ var removed;
+
+ listenerTupleList = without(
+ listenerTupleList,
+ hasId(listenerId),
+ function(tuple){
+ removed = tuple;
+ }
+ );
+
+ if( removed ) {
+ listenerList = without( listenerList, function(listener){
+ return listener == removed.listener;
+ });
+
+ if( removeListener ) {
+ removeListener.emit(eventType, removed.listener, removed.id);
+ }
+ }
+ },
+
+ listeners: function(){
+ // differs from Node EventEmitter: returns list, not array
+ return listenerList;
+ },
+
+ hasListener: function(listenerId){
+ var test = listenerId? hasId(listenerId) : always;
+
+ return defined(first( test, listenerTupleList));
+ }
+ };
+}
+
+/**
+ * pubSub is a curried interface for listening to and emitting
+ * events.
+ *
+ * If we get a bus:
+ *
+ * var bus = pubSub();
+ *
+ * We can listen to event 'foo' like:
+ *
+ * bus('foo').on(myCallback)
+ *
+ * And emit event foo like:
+ *
+ * bus('foo').emit()
+ *
+ * or, with a parameter:
+ *
+ * bus('foo').emit('bar')
+ *
+ * All functions can be cached and don't need to be
+ * bound. Ie:
+ *
+ * var fooEmitter = bus('foo').emit
+ * fooEmitter('bar'); // emit an event
+ * fooEmitter('baz'); // emit another
+ *
+ * There's also an uncurried[1] shortcut for .emit and .on:
+ *
+ * bus.on('foo', callback)
+ * bus.emit('foo', 'bar')
+ *
+ * [1]: http://zvon.org/other/haskell/Outputprelude/uncurry_f.html
+ */
+function pubSub(){
+
+ var singles = {},
+ newListener = newSingle('newListener'),
+ removeListener = newSingle('removeListener');
+
+ function newSingle(eventName) {
+ return singles[eventName] = singleEventPubSub(
+ eventName,
+ newListener,
+ removeListener
+ );
+ }
+
+ /** pubSub instances are functions */
+ function pubSubInstance( eventName ){
+
+ return singles[eventName] || newSingle( eventName );
+ }
+
+ // add convenience EventEmitter-style uncurried form of 'emit' and 'on'
+ ['emit', 'on', 'un'].forEach(function(methodName){
+
+ pubSubInstance[methodName] = varArgs(function(eventName, parameters){
+ apply( parameters, pubSubInstance( eventName )[methodName]);
+ });
+ });
+
+ return pubSubInstance;
+}
+
+/**
+ * This file declares some constants to use as names for event types.
+ */
+
+var // the events which are never exported are kept as
+ // the smallest possible representation, in numbers:
+ _S = 1,
+
+ // fired whenever a new node starts in the JSON stream:
+ NODE_OPENED = _S++,
+
+ // fired whenever a node closes in the JSON stream:
+ NODE_CLOSED = _S++,
+
+ // called if a .node callback returns a value -
+ NODE_SWAP = _S++,
+ NODE_DROP = _S++,
+
+ FAIL_EVENT = 'fail',
+
+ ROOT_NODE_FOUND = _S++,
+ ROOT_PATH_FOUND = _S++,
+
+ HTTP_START = 'start',
+ STREAM_DATA = 'data',
+ STREAM_END = 'end',
+ ABORTING = _S++,
+
+ // SAX events butchered from Clarinet
+ SAX_KEY = _S++,
+ SAX_VALUE_OPEN = _S++,
+ SAX_VALUE_CLOSE = _S++;
+
+function errorReport(statusCode, body, error) {
+ try{
+ var jsonBody = JSON.parse(body);
+ }catch(e){}
+
+ return {
+ statusCode:statusCode,
+ body:body,
+ jsonBody:jsonBody,
+ thrown:error
+ };
+}
+
+/**
+ * The pattern adaptor listens for newListener and removeListener
+ * events. When patterns are added or removed it compiles the JSONPath
+ * and wires them up.
+ *
+ * When nodes and paths are found it emits the fully-qualified match
+ * events with parameters ready to ship to the outside world
+ */
+
+function patternAdapter(oboeBus, jsonPathCompiler) {
+
+ var predicateEventMap = {
+ node:oboeBus(NODE_CLOSED)
+ , path:oboeBus(NODE_OPENED)
+ };
+
+ function emitMatchingNode(emitMatch, node, ascent) {
+
+ /*
+ We're now calling to the outside world where Lisp-style
+ lists will not be familiar. Convert to standard arrays.
+
+ Also, reverse the order because it is more common to
+ list paths "root to leaf" than "leaf to root" */
+ var descent = reverseList(ascent);
+
+ emitMatch(
+ node,
+
+ // To make a path, strip off the last item which is the special
+ // ROOT_PATH token for the 'path' to the root node
+ listAsArray(tail(map(keyOf,descent))), // path
+ listAsArray(map(nodeOf, descent)) // ancestors
+ );
+ }
+
+ /*
+ * Set up the catching of events such as NODE_CLOSED and NODE_OPENED and, if
+ * matching the specified pattern, propagate to pattern-match events such as
+ * oboeBus('node:!')
+ *
+ *
+ *
+ * @param {Function} predicateEvent
+ * either oboeBus(NODE_CLOSED) or oboeBus(NODE_OPENED).
+ * @param {Function} compiledJsonPath
+ */
+ function addUnderlyingListener( fullEventName, predicateEvent, compiledJsonPath ){
+
+ var emitMatch = oboeBus(fullEventName).emit;
+
+ predicateEvent.on( function (ascent) {
+
+ var maybeMatchingMapping = compiledJsonPath(ascent);
+
+ /* Possible values for maybeMatchingMapping are now:
+
+ false:
+ we did not match
+
+ an object/array/string/number/null:
+ we matched and have the node that matched.
+ Because nulls are valid json values this can be null.
+
+ undefined:
+ we matched but don't have the matching node yet.
+ ie, we know there is an upcoming node that matches but we
+ can't say anything else about it.
+ */
+ if (maybeMatchingMapping !== false) {
+
+ emitMatchingNode(
+ emitMatch,
+ nodeOf(maybeMatchingMapping),
+ ascent
+ );
+ }
+ }, fullEventName);
+
+ oboeBus('removeListener').on( function(removedEventName){
+
+ // if the fully qualified match event listener is later removed, clean up
+ // by removing the underlying listener if it was the last using that pattern:
+
+ if( removedEventName == fullEventName ) {
+
+ if( !oboeBus(removedEventName).listeners( )) {
+ predicateEvent.un( fullEventName );
+ }
+ }
+ });
+ }
+
+ oboeBus('newListener').on( function(fullEventName){
+
+ var match = /(node|path):(.*)/.exec(fullEventName);
+
+ if( match ) {
+ var predicateEvent = predicateEventMap[match[1]];
+
+ if( !predicateEvent.hasListener( fullEventName) ) {
+
+ addUnderlyingListener(
+ fullEventName,
+ predicateEvent,
+ jsonPathCompiler( match[2] )
+ );
+ }
+ }
+ })
+
+}
+
+/**
+ * The instance API is the thing that is returned when oboe() is called.
+ * it allows:
+ *
+ * - listeners for various events to be added and removed
+ * - the http response header/headers to be read
+ */
+function instanceApi(oboeBus, contentSource){
+
+ var oboeApi,
+ fullyQualifiedNamePattern = /^(node|path):./,
+ rootNodeFinishedEvent = oboeBus(ROOT_NODE_FOUND),
+ emitNodeDrop = oboeBus(NODE_DROP).emit,
+ emitNodeSwap = oboeBus(NODE_SWAP).emit,
+
+ /**
+ * Add any kind of listener that the instance api exposes
+ */
+ addListener = varArgs(function( eventId, parameters ){
+
+ if( oboeApi[eventId] ) {
+
+ // for events added as .on(event, callback), if there is a
+ // .event() equivalent with special behaviour , pass through
+ // to that:
+ apply(parameters, oboeApi[eventId]);
+ } else {
+
+ // we have a standard Node.js EventEmitter 2-argument call.
+ // The first parameter is the listener.
+ var event = oboeBus(eventId),
+ listener = parameters[0];
+
+ if( fullyQualifiedNamePattern.test(eventId) ) {
+
+ // allow fully-qualified node/path listeners
+ // to be added
+ addForgettableCallback(event, listener);
+ } else {
+
+ // the event has no special handling, pass through
+ // directly onto the event bus:
+ event.on( listener);
+ }
+ }
+
+ return oboeApi; // chaining
+ }),
+
+ /**
+ * Remove any kind of listener that the instance api exposes
+ */
+ removeListener = function( eventId, p2, p3 ){
+
+ if( eventId == 'done' ) {
+
+ rootNodeFinishedEvent.un(p2);
+
+ } else if( eventId == 'node' || eventId == 'path' ) {
+
+ // allow removal of node and path
+ oboeBus.un(eventId + ':' + p2, p3);
+ } else {
+
+ // we have a standard Node.js EventEmitter 2-argument call.
+ // The second parameter is the listener. This may be a call
+ // to remove a fully-qualified node/path listener but requires
+ // no special handling
+ var listener = p2;
+
+ oboeBus(eventId).un(listener);
+ }
+
+ return oboeApi; // chaining
+ };
+
+ /**
+ * Add a callback, wrapped in a try/catch so as to not break the
+ * execution of Oboe if an exception is thrown (fail events are
+ * fired instead)
+ *
+ * The callback is used as the listener id so that it can later be
+ * removed using .un(callback)
+ */
+ function addProtectedCallback(eventName, callback) {
+ oboeBus(eventName).on(protectedCallback(callback), callback);
+ return oboeApi; // chaining
+ }
+
+ /**
+ * Add a callback where, if .forget() is called during the callback's
+ * execution, the callback will be de-registered
+ */
+ function addForgettableCallback(event, callback, listenerId) {
+
+ // listenerId is optional and if not given, the original
+ // callback will be used
+ listenerId = listenerId || callback;
+
+ var safeCallback = protectedCallback(callback);
+
+ event.on( function() {
+
+ var discard = false;
+
+ oboeApi.forget = function(){
+ discard = true;
+ };
+
+ apply( arguments, safeCallback );
+
+ delete oboeApi.forget;
+
+ if( discard ) {
+ event.un(listenerId);
+ }
+ }, listenerId);
+
+ return oboeApi; // chaining
+ }
+
+ /**
+ * wrap a callback so that if it throws, Oboe.js doesn't crash but instead
+ * throw the error in another event loop
+ */
+ function protectedCallback( callback ) {
+ return function() {
+ try{
+ return callback.apply(oboeApi, arguments);
+ }catch(e) {
+ setTimeout(function() {
+ throw new Error(e.message);
+ });
+ }
+ }
+ }
+
+ /**
+ * Return the fully qualified event for when a pattern matches
+ * either a node or a path
+ *
+ * @param type {String} either 'node' or 'path'
+ */
+ function fullyQualifiedPatternMatchEvent(type, pattern) {
+ return oboeBus(type + ':' + pattern);
+ }
+
+ function wrapCallbackToSwapNodeIfSomethingReturned( callback ) {
+ return function() {
+ var returnValueFromCallback = callback.apply(this, arguments);
+
+ if( defined(returnValueFromCallback) ) {
+
+ if( returnValueFromCallback == oboe.drop ) {
+ emitNodeDrop();
+ } else {
+ emitNodeSwap(returnValueFromCallback);
+ }
+ }
+ }
+ }
+
+ function addSingleNodeOrPathListener(eventId, pattern, callback) {
+
+ var effectiveCallback;
+
+ if( eventId == 'node' ) {
+ effectiveCallback = wrapCallbackToSwapNodeIfSomethingReturned(callback);
+ } else {
+ effectiveCallback = callback;
+ }
+
+ addForgettableCallback(
+ fullyQualifiedPatternMatchEvent(eventId, pattern),
+ effectiveCallback,
+ callback
+ );
+ }
+
+ /**
+ * Add several listeners at a time, from a map
+ */
+ function addMultipleNodeOrPathListeners(eventId, listenerMap) {
+
+ for( var pattern in listenerMap ) {
+ addSingleNodeOrPathListener(eventId, pattern, listenerMap[pattern]);
+ }
+ }
+
+ /**
+ * implementation behind .onPath() and .onNode()
+ */
+ function addNodeOrPathListenerApi( eventId, jsonPathOrListenerMap, callback ){
+
+ if( isString(jsonPathOrListenerMap) ) {
+ addSingleNodeOrPathListener(eventId, jsonPathOrListenerMap, callback);
+
+ } else {
+ addMultipleNodeOrPathListeners(eventId, jsonPathOrListenerMap);
+ }
+
+ return oboeApi; // chaining
+ }
+
+
+ // some interface methods are only filled in after we receive
+ // values and are noops before that:
+ oboeBus(ROOT_PATH_FOUND).on( function(rootNode) {
+ oboeApi.root = functor(rootNode);
+ });
+
+ /**
+ * When content starts make the headers readable through the
+ * instance API
+ */
+ oboeBus(HTTP_START).on( function(_statusCode, headers) {
+
+ oboeApi.header = function(name) {
+ return name ? headers[name]
+ : headers
+ ;
+ }
+ });
+
+ /**
+ * Construct and return the public API of the Oboe instance to be
+ * returned to the calling application
+ */
+ return oboeApi = {
+ on : addListener,
+ addListener : addListener,
+ removeListener : removeListener,
+ emit : oboeBus.emit,
+
+ node : partialComplete(addNodeOrPathListenerApi, 'node'),
+ path : partialComplete(addNodeOrPathListenerApi, 'path'),
+
+ done : partialComplete(addForgettableCallback, rootNodeFinishedEvent),
+ start : partialComplete(addProtectedCallback, HTTP_START ),
+
+ // fail doesn't use protectedCallback because
+ // could lead to non-terminating loops
+ fail : oboeBus(FAIL_EVENT).on,
+
+ // public api calling abort fires the ABORTING event
+ abort : oboeBus(ABORTING).emit,
+
+ // for manually feeding data
+ write : oboeBus(STREAM_DATA).emit,
+ finish : oboeBus(STREAM_END).emit,
+
+ // initially return nothing for header and root
+ header : noop,
+ root : noop,
+
+ source : contentSource
+ };
+}
+
+/**
+ * This file sits just behind the API which is used to attain a new
+ * Oboe instance. It creates the new components that are required
+ * and introduces them to each other.
+ */
+
+function wire (httpMethodName, contentSource, body, headers, withCredentials){
+
+ var oboeBus = pubSub();
+
+ // Wire the input stream in if we are given a content source.
+ // This will usually be the case. If not, the instance created
+ // will have to be passed content from an external source.
+
+ if( contentSource ) {
+
+ streamingHttp( oboeBus,
+ httpTransport(),
+ httpMethodName,
+ contentSource,
+ body,
+ headers,
+ withCredentials
+ );
+ }
+
+ clarinet(oboeBus);
+
+ ascentManager(oboeBus, incrementalContentBuilder(oboeBus));
+
+ patternAdapter(oboeBus, jsonPathCompiler);
+
+ return instanceApi(oboeBus, contentSource);
+}
+
+function applyDefaults( passthrough, url, httpMethodName, body, headers, withCredentials, cached ){
+
+ headers = headers ?
+ // Shallow-clone the headers array. This allows it to be
+ // modified without side effects to the caller. We don't
+ // want to change objects that the user passes in.
+ JSON.parse(JSON.stringify(headers))
+ : {};
+
+ if( body ) {
+ if( !isString(body) ) {
+
+ // If the body is not a string, stringify it. This allows objects to
+ // be given which will be sent as JSON.
+ body = JSON.stringify(body);
+
+ // Default Content-Type to JSON unless given otherwise.
+ headers['Content-Type'] = headers['Content-Type'] || 'application/json';
+ }
+ } else {
+ body = null;
+ }
+
+ // support cache busting like jQuery.ajax({cache:false})
+ function modifiedUrl(baseUrl, cached) {
+
+ if( cached === false ) {
+
+ if( baseUrl.indexOf('?') == -1 ) {
+ baseUrl += '?';
+ } else {
+ baseUrl += '&';
+ }
+
+ baseUrl += '_=' + new Date().getTime();
+ }
+ return baseUrl;
+ }
+
+ return passthrough( httpMethodName || 'GET', modifiedUrl(url, cached), body, headers, withCredentials || false );
+}
+
+// export public API
+function oboe(arg1) {
+
+ // We use duck-typing to detect if the parameter given is a stream, with the
+ // below list of parameters.
+ // Unpipe and unshift would normally be present on a stream but this breaks
+ // compatibility with Request streams.
+ // See https://github.com/jimhigson/oboe.js/issues/65
+
+ var nodeStreamMethodNames = list('resume', 'pause', 'pipe'),
+ isStream = partialComplete(
+ hasAllProperties
+ , nodeStreamMethodNames
+ );
+
+ if( arg1 ) {
+ if (isStream(arg1) || isString(arg1)) {
+
+ // simple version for GETs. Signature is:
+ // oboe( url )
+ // or, under node:
+ // oboe( readableStream )
+ return applyDefaults(
+ wire,
+ arg1 // url
+ );
+
+ } else {
+
+ // method signature is:
+ // oboe({method:m, url:u, body:b, headers:{...}})
+
+ return applyDefaults(
+ wire,
+ arg1.url,
+ arg1.method,
+ arg1.body,
+ arg1.headers,
+ arg1.withCredentials,
+ arg1.cached
+ );
+
+ }
+ } else {
+ // wire up a no-AJAX, no-stream Oboe. Will have to have content
+ // fed in externally and using .emit.
+ return wire();
+ }
+}
+
+/* oboe.drop is a special value. If a node callback returns this value the
+ parsed node is deleted from the JSON
+ */
+oboe.drop = function() {
+ return oboe.drop;
+};
+
+
+ return oboe;
+})();
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/index.js b/chromium/third_party/catapult/tracing/third_party/oboe/index.js
new file mode 100644
index 00000000000..d1cc947676f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/index.js
@@ -0,0 +1,3 @@
+var oboe = require('./dist/oboe-node');
+
+module.exports = oboe;
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/jasmine.json b/chromium/third_party/catapult/tracing/third_party/oboe/jasmine.json
new file mode 100644
index 00000000000..de5e5a91d00
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/jasmine.json
@@ -0,0 +1,8 @@
+{
+ "spec_dir": "test",
+ "spec_files": [
+ "specs/oboe.*.spec.js"
+ ],
+ "stopSpecOnExpectationFailure": false,
+ "random": false
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/logo.png b/chromium/third_party/catapult/tracing/third_party/oboe/logo.png
new file mode 100644
index 00000000000..ecc5e5396f7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/logo.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/package.json b/chromium/third_party/catapult/tracing/third_party/oboe/package.json
new file mode 100644
index 00000000000..310fa98effb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "oboe",
+ "title": "Oboe.js",
+ "version": "2.1.3",
+ "description": "Oboe.js reads json, giving you the objects as they are found without waiting for the stream to finish",
+ "main": "./dist/oboe-node.js",
+ "browser": "./dist/oboe-browser.js",
+ "scripts": {
+ "test": "node ./node_modules/grunt-cli/bin/grunt headless-mode default",
+ "test-start-server": "node ./node_modules/grunt-cli/bin/grunt test-start-server",
+ "test-run": "node ./node_modules/grunt-cli/bin/grunt test-run",
+ "test-node": "jasmine JASMINE_CONFIG_PATH=jasmine.json",
+ "browser-test-auto-run": "node ./node_modules/grunt-cli/bin/grunt test-auto-run",
+ "node-test-auto-run": "node ./node_modules/grunt-cli/bin/grunt node-test-auto-run",
+ "dist-sizes": "node ./node_modules/grunt-cli/bin/grunt dist-sizes"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jimhigson/oboe.js.git"
+ },
+ "keywords": [
+ "json",
+ "parser",
+ "stream",
+ "progressive",
+ "http",
+ "sax",
+ "event",
+ "emitter",
+ "async",
+ "browser"
+ ],
+ "homepage": "http://oboejs.com",
+ "author": "Jim Higson",
+ "license": "BSD",
+ "readmeFilename": "README.md",
+ "devDependencies": {
+ "color": "~0.4.4",
+ "cors": "~2.1.1",
+ "doctoc": "~0.4.3",
+ "express": "~3.4.3",
+ "get-json": "0.0.1",
+ "grunt": "~0.4.1",
+ "grunt-clear": "~0.2.1",
+ "grunt-cli": "~0.1.9",
+ "grunt-concurrent": "~0.3.1",
+ "grunt-contrib-clean": "~0.5.0",
+ "grunt-contrib-concat": "~0.1.3",
+ "grunt-contrib-copy": "~0.4.1",
+ "grunt-contrib-uglify": "~0.2.0",
+ "grunt-contrib-watch": "~0.5.1",
+ "grunt-exec": "~0.4.2",
+ "grunt-karma": "2.0.0",
+ "grunt-micro": "~0.1.0",
+ "grunt-wrap": "~0.2.0",
+ "jasmine": "2.5.2",
+ "jasmine-core": "2.5.2",
+ "jasmine-node": "~1.11.0",
+ "karma": "1.3.0",
+ "karma-coverage": "1.1.1",
+ "karma-firefox-launcher": "1.0.0",
+ "karma-jasmine": "1.0.2",
+ "karma-phantomjs-launcher": "1.0.2",
+ "karma-safari-launcher": "1.0.0",
+ "matchdep": "~0.1.2",
+ "request": "^2.55.0",
+ "sinon": "=1.17.3"
+ },
+ "dependencies": {
+ "http-https": "^1.0.0"
+ },
+ "jam": {
+ "main": "dist/oboe-browser.js",
+ "include": [
+ "dist/oboe-browser.js",
+ "LICENCE",
+ "package.json",
+ "README.md"
+ ],
+ "dependencies": {},
+ "categories": [
+ "AJAX & Websockets",
+ "Parsers & Compilers"
+ ]
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/LICENCE.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/LICENCE.js
new file mode 100644
index 00000000000..b2a60bf9fb2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/LICENCE.js
@@ -0,0 +1,30 @@
+/*
+
+Copyright (c) 2013, Jim Higson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+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
+HOLDER 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.
+
+*/
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/ascent.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/ascent.js
new file mode 100644
index 00000000000..c5cec4e44bd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/ascent.js
@@ -0,0 +1,15 @@
+/**
+ * Get a new key->node mapping
+ *
+ * @param {String|Number} key
+ * @param {Object|Array|String|Number|null} node a value found in the json
+ */
+function namedNode(key, node) {
+ return {key:key, node:node};
+}
+
+/** get the key of a namedNode */
+var keyOf = attr('key');
+
+/** get the node from a namedNode */
+var nodeOf = attr('node'); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/ascentManager.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/ascentManager.js
new file mode 100644
index 00000000000..672fb251ea2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/ascentManager.js
@@ -0,0 +1,62 @@
+
+/**
+ * A bridge used to assign stateless functions to listen to clarinet.
+ *
+ * As well as the parameter from clarinet, each callback will also be passed
+ * the result of the last callback.
+ *
+ * This may also be used to clear all listeners by assigning zero handlers:
+ *
+ * ascentManager( clarinet, {} )
+ */
+function ascentManager(oboeBus, handlers){
+ "use strict";
+
+ var listenerId = {},
+ ascent;
+
+ function stateAfter(handler) {
+ return function(param){
+ ascent = handler( ascent, param);
+ }
+ }
+
+ for( var eventName in handlers ) {
+
+ oboeBus(eventName).on(stateAfter(handlers[eventName]), listenerId);
+ }
+
+ oboeBus(NODE_SWAP).on(function(newNode) {
+
+ var oldHead = head(ascent),
+ key = keyOf(oldHead),
+ ancestors = tail(ascent),
+ parentNode;
+
+ if( ancestors ) {
+ parentNode = nodeOf(head(ancestors));
+ parentNode[key] = newNode;
+ }
+ });
+
+ oboeBus(NODE_DROP).on(function() {
+
+ var oldHead = head(ascent),
+ key = keyOf(oldHead),
+ ancestors = tail(ascent),
+ parentNode;
+
+ if( ancestors ) {
+ parentNode = nodeOf(head(ancestors));
+
+ delete parentNode[key];
+ }
+ });
+
+ oboeBus(ABORTING).on(function(){
+
+ for( var eventName in handlers ) {
+ oboeBus(eventName).un(listenerId);
+ }
+ });
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/defaults.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/defaults.js
new file mode 100644
index 00000000000..95131daf354
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/defaults.js
@@ -0,0 +1,41 @@
+function applyDefaults( passthrough, url, httpMethodName, body, headers, withCredentials, cached ){
+
+ headers = headers ?
+ // Shallow-clone the headers array. This allows it to be
+ // modified without side effects to the caller. We don't
+ // want to change objects that the user passes in.
+ JSON.parse(JSON.stringify(headers))
+ : {};
+
+ if( body ) {
+ if( !isString(body) ) {
+
+ // If the body is not a string, stringify it. This allows objects to
+ // be given which will be sent as JSON.
+ body = JSON.stringify(body);
+
+ // Default Content-Type to JSON unless given otherwise.
+ headers['Content-Type'] = headers['Content-Type'] || 'application/json';
+ }
+ } else {
+ body = null;
+ }
+
+ // support cache busting like jQuery.ajax({cache:false})
+ function modifiedUrl(baseUrl, cached) {
+
+ if( cached === false ) {
+
+ if( baseUrl.indexOf('?') == -1 ) {
+ baseUrl += '?';
+ } else {
+ baseUrl += '&';
+ }
+
+ baseUrl += '_=' + new Date().getTime();
+ }
+ return baseUrl;
+ }
+
+ return passthrough( httpMethodName || 'GET', modifiedUrl(url, cached), body, headers, withCredentials || false );
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/detectCrossOrigin.browser.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/detectCrossOrigin.browser.js
new file mode 100644
index 00000000000..af3ffca7906
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/detectCrossOrigin.browser.js
@@ -0,0 +1,65 @@
+/**
+ * Detect if a given URL is cross-origin in the scope of the
+ * current page.
+ *
+ * Browser only (since cross-origin has no meaning in Node.js)
+ *
+ * @param {Object} pageLocation - as in window.location
+ * @param {Object} ajaxHost - an object like window.location describing the
+ * origin of the url that we want to ajax in
+ */
+function isCrossOrigin(pageLocation, ajaxHost) {
+
+ /*
+ * NB: defaultPort only knows http and https.
+ * Returns undefined otherwise.
+ */
+ function defaultPort(protocol) {
+ return {'http:':80, 'https:':443}[protocol];
+ }
+
+ function portOf(location) {
+ // pageLocation should always have a protocol. ajaxHost if no port or
+ // protocol is specified, should use the port of the containing page
+
+ return location.port || defaultPort(location.protocol||pageLocation.protocol);
+ }
+
+ // if ajaxHost doesn't give a domain, port is the same as pageLocation
+ // it can't give a protocol but not a domain
+ // it can't give a port but not a domain
+
+ return !!( (ajaxHost.protocol && (ajaxHost.protocol != pageLocation.protocol)) ||
+ (ajaxHost.host && (ajaxHost.host != pageLocation.host)) ||
+ (ajaxHost.host && (portOf(ajaxHost) != portOf(pageLocation)))
+ );
+}
+
+/* turn any url into an object like window.location */
+function parseUrlOrigin(url) {
+ // url could be domain-relative
+ // url could give a domain
+
+ // cross origin means:
+ // same domain
+ // same port
+ // some protocol
+ // so, same everything up to the first (single) slash
+ // if such is given
+ //
+ // can ignore everything after that
+
+ var URL_HOST_PATTERN = /(\w+:)?(?:\/\/)([\w.-]+)?(?::(\d+))?\/?/,
+
+ // if no match, use an empty array so that
+ // subexpressions 1,2,3 are all undefined
+ // and will ultimately return all empty
+ // strings as the parse result:
+ urlHostMatch = URL_HOST_PATTERN.exec(url) || [];
+
+ return {
+ protocol: urlHostMatch[1] || '',
+ host: urlHostMatch[2] || '',
+ port: urlHostMatch[3] || ''
+ };
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/events.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/events.js
new file mode 100644
index 00000000000..ac8e5d29427
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/events.js
@@ -0,0 +1,45 @@
+/**
+ * This file declares some constants to use as names for event types.
+ */
+
+var // the events which are never exported are kept as
+ // the smallest possible representation, in numbers:
+ _S = 1,
+
+ // fired whenever a new node starts in the JSON stream:
+ NODE_OPENED = _S++,
+
+ // fired whenever a node closes in the JSON stream:
+ NODE_CLOSED = _S++,
+
+ // called if a .node callback returns a value -
+ NODE_SWAP = _S++,
+ NODE_DROP = _S++,
+
+ FAIL_EVENT = 'fail',
+
+ ROOT_NODE_FOUND = _S++,
+ ROOT_PATH_FOUND = _S++,
+
+ HTTP_START = 'start',
+ STREAM_DATA = 'data',
+ STREAM_END = 'end',
+ ABORTING = _S++,
+
+ // SAX events butchered from Clarinet
+ SAX_KEY = _S++,
+ SAX_VALUE_OPEN = _S++,
+ SAX_VALUE_CLOSE = _S++;
+
+function errorReport(statusCode, body, error) {
+ try{
+ var jsonBody = JSON.parse(body);
+ }catch(e){}
+
+ return {
+ statusCode:statusCode,
+ body:body,
+ jsonBody:jsonBody,
+ thrown:error
+ };
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/functional.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/functional.js
new file mode 100644
index 00000000000..f4277454120
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/functional.js
@@ -0,0 +1,250 @@
+/**
+ * Partially complete a function.
+ *
+ * var add3 = partialComplete( function add(a,b){return a+b}, 3 );
+ *
+ * add3(4) // gives 7
+ *
+ * function wrap(left, right, cen){return left + " " + cen + " " + right;}
+ *
+ * var pirateGreeting = partialComplete( wrap , "I'm", ", a mighty pirate!" );
+ *
+ * pirateGreeting("Guybrush Threepwood");
+ * // gives "I'm Guybrush Threepwood, a mighty pirate!"
+ */
+var partialComplete = varArgs(function( fn, args ) {
+
+ // this isn't the shortest way to write this but it does
+ // avoid creating a new array each time to pass to fn.apply,
+ // otherwise could just call boundArgs.concat(callArgs)
+
+ var numBoundArgs = args.length;
+
+ return varArgs(function( callArgs ) {
+
+ for (var i = 0; i < callArgs.length; i++) {
+ args[numBoundArgs + i] = callArgs[i];
+ }
+
+ args.length = numBoundArgs + callArgs.length;
+
+ return fn.apply(this, args);
+ });
+ }),
+
+/**
+ * Compose zero or more functions:
+ *
+ * compose(f1, f2, f3)(x) = f1(f2(f3(x))))
+ *
+ * The last (inner-most) function may take more than one parameter:
+ *
+ * compose(f1, f2, f3)(x,y) = f1(f2(f3(x,y))))
+ */
+ compose = varArgs(function(fns) {
+
+ var fnsList = arrayAsList(fns);
+
+ function next(params, curFn) {
+ return [apply(params, curFn)];
+ }
+
+ return varArgs(function(startParams){
+
+ return foldR(next, startParams, fnsList)[0];
+ });
+ });
+
+/**
+ * A more optimised version of compose that takes exactly two functions
+ * @param f1
+ * @param f2
+ */
+function compose2(f1, f2){
+ return function(){
+ return f1.call(this,f2.apply(this,arguments));
+ }
+}
+
+/**
+ * Generic form for a function to get a property from an object
+ *
+ * var o = {
+ * foo:'bar'
+ * }
+ *
+ * var getFoo = attr('foo')
+ *
+ * fetFoo(o) // returns 'bar'
+ *
+ * @param {String} key the property name
+ */
+function attr(key) {
+ return function(o) { return o[key]; };
+}
+
+/**
+ * Call a list of functions with the same args until one returns a
+ * truthy result. Similar to the || operator.
+ *
+ * So:
+ * lazyUnion([f1,f2,f3 ... fn])( p1, p2 ... pn )
+ *
+ * Is equivalent to:
+ * apply([p1, p2 ... pn], f1) ||
+ * apply([p1, p2 ... pn], f2) ||
+ * apply([p1, p2 ... pn], f3) ... apply(fn, [p1, p2 ... pn])
+ *
+ * @returns the first return value that is given that is truthy.
+ */
+ var lazyUnion = varArgs(function(fns) {
+
+ return varArgs(function(params){
+
+ var maybeValue;
+
+ for (var i = 0; i < len(fns); i++) {
+
+ maybeValue = apply(params, fns[i]);
+
+ if( maybeValue ) {
+ return maybeValue;
+ }
+ }
+ });
+ });
+
+/**
+ * This file declares various pieces of functional programming.
+ *
+ * This isn't a general purpose functional library, to keep things small it
+ * has just the parts useful for Oboe.js.
+ */
+
+
+/**
+ * Call a single function with the given arguments array.
+ * Basically, a functional-style version of the OO-style Function#apply for
+ * when we don't care about the context ('this') of the call.
+ *
+ * The order of arguments allows partial completion of the arguments array
+ */
+function apply(args, fn) {
+ return fn.apply(undefined, args);
+}
+
+/**
+ * Define variable argument functions but cut out all that tedious messing about
+ * with the arguments object. Delivers the variable-length part of the arguments
+ * list as an array.
+ *
+ * Eg:
+ *
+ * var myFunction = varArgs(
+ * function( fixedArgument, otherFixedArgument, variableNumberOfArguments ){
+ * console.log( variableNumberOfArguments );
+ * }
+ * )
+ *
+ * myFunction('a', 'b', 1, 2, 3); // logs [1,2,3]
+ *
+ * var myOtherFunction = varArgs(function( variableNumberOfArguments ){
+ * console.log( variableNumberOfArguments );
+ * })
+ *
+ * myFunction(1, 2, 3); // logs [1,2,3]
+ *
+ */
+function varArgs(fn){
+
+ var numberOfFixedArguments = fn.length -1,
+ slice = Array.prototype.slice;
+
+
+ if( numberOfFixedArguments == 0 ) {
+ // an optimised case for when there are no fixed args:
+
+ return function(){
+ return fn.call(this, slice.call(arguments));
+ }
+
+ } else if( numberOfFixedArguments == 1 ) {
+ // an optimised case for when there are is one fixed args:
+
+ return function(){
+ return fn.call(this, arguments[0], slice.call(arguments, 1));
+ }
+ }
+
+ // general case
+
+ // we know how many arguments fn will always take. Create a
+ // fixed-size array to hold that many, to be re-used on
+ // every call to the returned function
+ var argsHolder = Array(fn.length);
+
+ return function(){
+
+ for (var i = 0; i < numberOfFixedArguments; i++) {
+ argsHolder[i] = arguments[i];
+ }
+
+ argsHolder[numberOfFixedArguments] =
+ slice.call(arguments, numberOfFixedArguments);
+
+ return fn.apply( this, argsHolder);
+ }
+}
+
+
+/**
+ * Swap the order of parameters to a binary function
+ *
+ * A bit like this flip: http://zvon.org/other/haskell/Outputprelude/flip_f.html
+ */
+function flip(fn){
+ return function(a, b){
+ return fn(b,a);
+ }
+}
+
+
+/**
+ * Create a function which is the intersection of two other functions.
+ *
+ * Like the && operator, if the first is truthy, the second is never called,
+ * otherwise the return value from the second is returned.
+ */
+function lazyIntersection(fn1, fn2) {
+
+ return function (param) {
+
+ return fn1(param) && fn2(param);
+ };
+}
+
+/**
+ * A function which does nothing
+ */
+function noop(){}
+
+/**
+ * A function which is always happy
+ */
+function always(){return true}
+
+/**
+ * Create a function which always returns the same
+ * value
+ *
+ * var return3 = functor(3);
+ *
+ * return3() // gives 3
+ * return3() // still gives 3
+ * return3() // will always give 3
+ */
+function functor(val){
+ return function(){
+ return val;
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/incrementalContentBuilder.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/incrementalContentBuilder.js
new file mode 100644
index 00000000000..381a73dd451
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/incrementalContentBuilder.js
@@ -0,0 +1,150 @@
+/**
+ * This file provides various listeners which can be used to build up
+ * a changing ascent based on the callbacks provided by Clarinet. It listens
+ * to the low-level events from Clarinet and emits higher-level ones.
+ *
+ * The building up is stateless so to track a JSON file
+ * ascentManager.js is required to store the ascent state
+ * between calls.
+ */
+
+
+
+/**
+ * A special value to use in the path list to represent the path 'to' a root
+ * object (which doesn't really have any path). This prevents the need for
+ * special-casing detection of the root object and allows it to be treated
+ * like any other object. We might think of this as being similar to the
+ * 'unnamed root' domain ".", eg if I go to
+ * http://en.wikipedia.org./wiki/En/Main_page the dot after 'org' deliminates
+ * the unnamed root of the DNS.
+ *
+ * This is kept as an object to take advantage that in Javascript's OO objects
+ * are guaranteed to be distinct, therefore no other object can possibly clash
+ * with this one. Strings, numbers etc provide no such guarantee.
+ **/
+var ROOT_PATH = {};
+
+
+/**
+ * Create a new set of handlers for clarinet's events, bound to the emit
+ * function given.
+ */
+function incrementalContentBuilder( oboeBus ) {
+
+ var emitNodeOpened = oboeBus(NODE_OPENED).emit,
+ emitNodeClosed = oboeBus(NODE_CLOSED).emit,
+ emitRootOpened = oboeBus(ROOT_PATH_FOUND).emit,
+ emitRootClosed = oboeBus(ROOT_NODE_FOUND).emit;
+
+ function arrayIndicesAreKeys( possiblyInconsistentAscent, newDeepestNode) {
+
+ /* for values in arrays we aren't pre-warned of the coming paths
+ (Clarinet gives no call to onkey like it does for values in objects)
+ so if we are in an array we need to create this path ourselves. The
+ key will be len(parentNode) because array keys are always sequential
+ numbers. */
+
+ var parentNode = nodeOf( head( possiblyInconsistentAscent));
+
+ return isOfType( Array, parentNode)
+ ?
+ keyFound( possiblyInconsistentAscent,
+ len(parentNode),
+ newDeepestNode
+ )
+ :
+ // nothing needed, return unchanged
+ possiblyInconsistentAscent
+ ;
+ }
+
+ function nodeOpened( ascent, newDeepestNode ) {
+
+ if( !ascent ) {
+ // we discovered the root node,
+ emitRootOpened( newDeepestNode);
+
+ return keyFound( ascent, ROOT_PATH, newDeepestNode);
+ }
+
+ // we discovered a non-root node
+
+ var arrayConsistentAscent = arrayIndicesAreKeys( ascent, newDeepestNode),
+ ancestorBranches = tail( arrayConsistentAscent),
+ previouslyUnmappedName = keyOf( head( arrayConsistentAscent));
+
+ appendBuiltContent(
+ ancestorBranches,
+ previouslyUnmappedName,
+ newDeepestNode
+ );
+
+ return cons(
+ namedNode( previouslyUnmappedName, newDeepestNode ),
+ ancestorBranches
+ );
+ }
+
+
+ /**
+ * Add a new value to the object we are building up to represent the
+ * parsed JSON
+ */
+ function appendBuiltContent( ancestorBranches, key, node ){
+
+ nodeOf( head( ancestorBranches))[key] = node;
+ }
+
+
+ /**
+ * For when we find a new key in the json.
+ *
+ * @param {String|Number|Object} newDeepestName the key. If we are in an
+ * array will be a number, otherwise a string. May take the special
+ * value ROOT_PATH if the root node has just been found
+ *
+ * @param {String|Number|Object|Array|Null|undefined} [maybeNewDeepestNode]
+ * usually this won't be known so can be undefined. Can't use null
+ * to represent unknown because null is a valid value in JSON
+ **/
+ function keyFound(ascent, newDeepestName, maybeNewDeepestNode) {
+
+ if( ascent ) { // if not root
+
+ // If we have the key but (unless adding to an array) no known value
+ // yet. Put that key in the output but against no defined value:
+ appendBuiltContent( ascent, newDeepestName, maybeNewDeepestNode );
+ }
+
+ var ascentWithNewPath = cons(
+ namedNode( newDeepestName,
+ maybeNewDeepestNode),
+ ascent
+ );
+
+ emitNodeOpened( ascentWithNewPath);
+
+ return ascentWithNewPath;
+ }
+
+
+ /**
+ * For when the current node ends.
+ */
+ function nodeClosed( ascent ) {
+
+ emitNodeClosed( ascent);
+
+ return tail( ascent) ||
+ // If there are no nodes left in the ascent the root node
+ // just closed. Emit a special event for this:
+ emitRootClosed(nodeOf(head(ascent)));
+ }
+
+ var contentBuilderHandlers = {};
+ contentBuilderHandlers[SAX_VALUE_OPEN] = nodeOpened;
+ contentBuilderHandlers[SAX_VALUE_CLOSE] = nodeClosed;
+ contentBuilderHandlers[SAX_KEY] = keyFound;
+ return contentBuilderHandlers;
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/instanceApi.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/instanceApi.js
new file mode 100644
index 00000000000..2c14ebab3ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/instanceApi.js
@@ -0,0 +1,254 @@
+/**
+ * The instance API is the thing that is returned when oboe() is called.
+ * it allows:
+ *
+ * - listeners for various events to be added and removed
+ * - the http response header/headers to be read
+ */
+function instanceApi(oboeBus, contentSource){
+
+ var oboeApi,
+ fullyQualifiedNamePattern = /^(node|path):./,
+ rootNodeFinishedEvent = oboeBus(ROOT_NODE_FOUND),
+ emitNodeDrop = oboeBus(NODE_DROP).emit,
+ emitNodeSwap = oboeBus(NODE_SWAP).emit,
+
+ /**
+ * Add any kind of listener that the instance api exposes
+ */
+ addListener = varArgs(function( eventId, parameters ){
+
+ if( oboeApi[eventId] ) {
+
+ // for events added as .on(event, callback), if there is a
+ // .event() equivalent with special behaviour , pass through
+ // to that:
+ apply(parameters, oboeApi[eventId]);
+ } else {
+
+ // we have a standard Node.js EventEmitter 2-argument call.
+ // The first parameter is the listener.
+ var event = oboeBus(eventId),
+ listener = parameters[0];
+
+ if( fullyQualifiedNamePattern.test(eventId) ) {
+
+ // allow fully-qualified node/path listeners
+ // to be added
+ addForgettableCallback(event, listener);
+ } else {
+
+ // the event has no special handling, pass through
+ // directly onto the event bus:
+ event.on( listener);
+ }
+ }
+
+ return oboeApi; // chaining
+ }),
+
+ /**
+ * Remove any kind of listener that the instance api exposes
+ */
+ removeListener = function( eventId, p2, p3 ){
+
+ if( eventId == 'done' ) {
+
+ rootNodeFinishedEvent.un(p2);
+
+ } else if( eventId == 'node' || eventId == 'path' ) {
+
+ // allow removal of node and path
+ oboeBus.un(eventId + ':' + p2, p3);
+ } else {
+
+ // we have a standard Node.js EventEmitter 2-argument call.
+ // The second parameter is the listener. This may be a call
+ // to remove a fully-qualified node/path listener but requires
+ // no special handling
+ var listener = p2;
+
+ oboeBus(eventId).un(listener);
+ }
+
+ return oboeApi; // chaining
+ };
+
+ /**
+ * Add a callback, wrapped in a try/catch so as to not break the
+ * execution of Oboe if an exception is thrown (fail events are
+ * fired instead)
+ *
+ * The callback is used as the listener id so that it can later be
+ * removed using .un(callback)
+ */
+ function addProtectedCallback(eventName, callback) {
+ oboeBus(eventName).on(protectedCallback(callback), callback);
+ return oboeApi; // chaining
+ }
+
+ /**
+ * Add a callback where, if .forget() is called during the callback's
+ * execution, the callback will be de-registered
+ */
+ function addForgettableCallback(event, callback, listenerId) {
+
+ // listenerId is optional and if not given, the original
+ // callback will be used
+ listenerId = listenerId || callback;
+
+ var safeCallback = protectedCallback(callback);
+
+ event.on( function() {
+
+ var discard = false;
+
+ oboeApi.forget = function(){
+ discard = true;
+ };
+
+ apply( arguments, safeCallback );
+
+ delete oboeApi.forget;
+
+ if( discard ) {
+ event.un(listenerId);
+ }
+ }, listenerId);
+
+ return oboeApi; // chaining
+ }
+
+ /**
+ * wrap a callback so that if it throws, Oboe.js doesn't crash but instead
+ * throw the error in another event loop
+ */
+ function protectedCallback( callback ) {
+ return function() {
+ try{
+ return callback.apply(oboeApi, arguments);
+ }catch(e) {
+ setTimeout(function() {
+ throw new Error(e.message);
+ });
+ }
+ }
+ }
+
+ /**
+ * Return the fully qualified event for when a pattern matches
+ * either a node or a path
+ *
+ * @param type {String} either 'node' or 'path'
+ */
+ function fullyQualifiedPatternMatchEvent(type, pattern) {
+ return oboeBus(type + ':' + pattern);
+ }
+
+ function wrapCallbackToSwapNodeIfSomethingReturned( callback ) {
+ return function() {
+ var returnValueFromCallback = callback.apply(this, arguments);
+
+ if( defined(returnValueFromCallback) ) {
+
+ if( returnValueFromCallback == oboe.drop ) {
+ emitNodeDrop();
+ } else {
+ emitNodeSwap(returnValueFromCallback);
+ }
+ }
+ }
+ }
+
+ function addSingleNodeOrPathListener(eventId, pattern, callback) {
+
+ var effectiveCallback;
+
+ if( eventId == 'node' ) {
+ effectiveCallback = wrapCallbackToSwapNodeIfSomethingReturned(callback);
+ } else {
+ effectiveCallback = callback;
+ }
+
+ addForgettableCallback(
+ fullyQualifiedPatternMatchEvent(eventId, pattern),
+ effectiveCallback,
+ callback
+ );
+ }
+
+ /**
+ * Add several listeners at a time, from a map
+ */
+ function addMultipleNodeOrPathListeners(eventId, listenerMap) {
+
+ for( var pattern in listenerMap ) {
+ addSingleNodeOrPathListener(eventId, pattern, listenerMap[pattern]);
+ }
+ }
+
+ /**
+ * implementation behind .onPath() and .onNode()
+ */
+ function addNodeOrPathListenerApi( eventId, jsonPathOrListenerMap, callback ){
+
+ if( isString(jsonPathOrListenerMap) ) {
+ addSingleNodeOrPathListener(eventId, jsonPathOrListenerMap, callback);
+
+ } else {
+ addMultipleNodeOrPathListeners(eventId, jsonPathOrListenerMap);
+ }
+
+ return oboeApi; // chaining
+ }
+
+
+ // some interface methods are only filled in after we receive
+ // values and are noops before that:
+ oboeBus(ROOT_PATH_FOUND).on( function(rootNode) {
+ oboeApi.root = functor(rootNode);
+ });
+
+ /**
+ * When content starts make the headers readable through the
+ * instance API
+ */
+ oboeBus(HTTP_START).on( function(_statusCode, headers) {
+
+ oboeApi.header = function(name) {
+ return name ? headers[name]
+ : headers
+ ;
+ }
+ });
+
+ /**
+ * Construct and return the public API of the Oboe instance to be
+ * returned to the calling application
+ */
+ return oboeApi = {
+ on : addListener,
+ addListener : addListener,
+ removeListener : removeListener,
+ emit : oboeBus.emit,
+
+ node : partialComplete(addNodeOrPathListenerApi, 'node'),
+ path : partialComplete(addNodeOrPathListenerApi, 'path'),
+
+ done : partialComplete(addForgettableCallback, rootNodeFinishedEvent),
+ start : partialComplete(addProtectedCallback, HTTP_START ),
+
+ // fail doesn't use protectedCallback because
+ // could lead to non-terminating loops
+ fail : oboeBus(FAIL_EVENT).on,
+
+ // public api calling abort fires the ABORTING event
+ abort : oboeBus(ABORTING).emit,
+
+ // initially return nothing for header and root
+ header : noop,
+ root : noop,
+
+ source : contentSource
+ };
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPath.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPath.js
new file mode 100644
index 00000000000..59b1434b422
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPath.js
@@ -0,0 +1,364 @@
+/**
+ * The jsonPath evaluator compiler used for Oboe.js.
+ *
+ * One function is exposed. This function takes a String JSONPath spec and
+ * returns a function to test candidate ascents for matches.
+ *
+ * String jsonPath -> (List ascent) -> Boolean|Object
+ *
+ * This file is coded in a pure functional style. That is, no function has
+ * side effects, every function evaluates to the same value for the same
+ * arguments and no variables are reassigned.
+ */
+// the call to jsonPathSyntax injects the token syntaxes that are needed
+// inside the compiler
+var jsonPathCompiler = jsonPathSyntax(function (pathNodeSyntax,
+ doubleDotSyntax,
+ dotSyntax,
+ bangSyntax,
+ emptySyntax ) {
+
+ var CAPTURING_INDEX = 1;
+ var NAME_INDEX = 2;
+ var FIELD_LIST_INDEX = 3;
+
+ var headKey = compose2(keyOf, head),
+ headNode = compose2(nodeOf, head);
+
+ /**
+ * Create an evaluator function for a named path node, expressed in the
+ * JSONPath like:
+ * foo
+ * ["bar"]
+ * [2]
+ */
+ function nameClause(previousExpr, detection ) {
+
+ var name = detection[NAME_INDEX],
+
+ matchesName = ( !name || name == '*' )
+ ? always
+ : function(ascent){return headKey(ascent) == name};
+
+
+ return lazyIntersection(matchesName, previousExpr);
+ }
+
+ /**
+ * Create an evaluator function for a a duck-typed node, expressed like:
+ *
+ * {spin, taste, colour}
+ * .particle{spin, taste, colour}
+ * *{spin, taste, colour}
+ */
+ function duckTypeClause(previousExpr, detection) {
+
+ var fieldListStr = detection[FIELD_LIST_INDEX];
+
+ if (!fieldListStr)
+ return previousExpr; // don't wrap at all, return given expr as-is
+
+ var hasAllrequiredFields = partialComplete(
+ hasAllProperties,
+ arrayAsList(fieldListStr.split(/\W+/))
+ ),
+
+ isMatch = compose2(
+ hasAllrequiredFields,
+ headNode
+ );
+
+ return lazyIntersection(isMatch, previousExpr);
+ }
+
+ /**
+ * Expression for $, returns the evaluator function
+ */
+ function capture( previousExpr, detection ) {
+
+ // extract meaning from the detection
+ var capturing = !!detection[CAPTURING_INDEX];
+
+ if (!capturing)
+ return previousExpr; // don't wrap at all, return given expr as-is
+
+ return lazyIntersection(previousExpr, head);
+
+ }
+
+ /**
+ * Create an evaluator function that moves onto the next item on the
+ * lists. This function is the place where the logic to move up a
+ * level in the ascent exists.
+ *
+ * Eg, for JSONPath ".foo" we need skip1(nameClause(always, [,'foo']))
+ */
+ function skip1(previousExpr) {
+
+
+ if( previousExpr == always ) {
+ /* If there is no previous expression this consume command
+ is at the start of the jsonPath.
+ Since JSONPath specifies what we'd like to find but not
+ necessarily everything leading down to it, when running
+ out of JSONPath to check against we default to true */
+ return always;
+ }
+
+ /** return true if the ascent we have contains only the JSON root,
+ * false otherwise
+ */
+ function notAtRoot(ascent){
+ return headKey(ascent) != ROOT_PATH;
+ }
+
+ return lazyIntersection(
+ /* If we're already at the root but there are more
+ expressions to satisfy, can't consume any more. No match.
+
+ This check is why none of the other exprs have to be able
+ to handle empty lists; skip1 is the only evaluator that
+ moves onto the next token and it refuses to do so once it
+ reaches the last item in the list. */
+ notAtRoot,
+
+ /* We are not at the root of the ascent yet.
+ Move to the next level of the ascent by handing only
+ the tail to the previous expression */
+ compose2(previousExpr, tail)
+ );
+
+ }
+
+ /**
+ * Create an evaluator function for the .. (double dot) token. Consumes
+ * zero or more levels of the ascent, the fewest that are required to find
+ * a match when given to previousExpr.
+ */
+ function skipMany(previousExpr) {
+
+ if( previousExpr == always ) {
+ /* If there is no previous expression this consume command
+ is at the start of the jsonPath.
+ Since JSONPath specifies what we'd like to find but not
+ necessarily everything leading down to it, when running
+ out of JSONPath to check against we default to true */
+ return always;
+ }
+
+ var
+ // In JSONPath .. is equivalent to !.. so if .. reaches the root
+ // the match has succeeded. Ie, we might write ..foo or !..foo
+ // and both should match identically.
+ terminalCaseWhenArrivingAtRoot = rootExpr(),
+ terminalCaseWhenPreviousExpressionIsSatisfied = previousExpr,
+ recursiveCase = skip1(function(ascent) {
+ return cases(ascent);
+ }),
+
+ cases = lazyUnion(
+ terminalCaseWhenArrivingAtRoot
+ , terminalCaseWhenPreviousExpressionIsSatisfied
+ , recursiveCase
+ );
+
+ return cases;
+ }
+
+ /**
+ * Generate an evaluator for ! - matches only the root element of the json
+ * and ignores any previous expressions since nothing may precede !.
+ */
+ function rootExpr() {
+
+ return function(ascent){
+ return headKey(ascent) == ROOT_PATH;
+ };
+ }
+
+ /**
+ * Generate a statement wrapper to sit around the outermost
+ * clause evaluator.
+ *
+ * Handles the case where the capturing is implicit because the JSONPath
+ * did not contain a '$' by returning the last node.
+ */
+ function statementExpr(lastClause) {
+
+ return function(ascent) {
+
+ // kick off the evaluation by passing through to the last clause
+ var exprMatch = lastClause(ascent);
+
+ return exprMatch === true ? head(ascent) : exprMatch;
+ };
+ }
+
+ /**
+ * For when a token has been found in the JSONPath input.
+ * Compiles the parser for that token and returns in combination with the
+ * parser already generated.
+ *
+ * @param {Function} exprs a list of the clause evaluator generators for
+ * the token that was found
+ * @param {Function} parserGeneratedSoFar the parser already found
+ * @param {Array} detection the match given by the regex engine when
+ * the feature was found
+ */
+ function expressionsReader( exprs, parserGeneratedSoFar, detection ) {
+
+ // if exprs is zero-length foldR will pass back the
+ // parserGeneratedSoFar as-is so we don't need to treat
+ // this as a special case
+
+ return foldR(
+ function( parserGeneratedSoFar, expr ){
+
+ return expr(parserGeneratedSoFar, detection);
+ },
+ parserGeneratedSoFar,
+ exprs
+ );
+
+ }
+
+ /**
+ * If jsonPath matches the given detector function, creates a function which
+ * evaluates against every clause in the clauseEvaluatorGenerators. The
+ * created function is propagated to the onSuccess function, along with
+ * the remaining unparsed JSONPath substring.
+ *
+ * The intended use is to create a clauseMatcher by filling in
+ * the first two arguments, thus providing a function that knows
+ * some syntax to match and what kind of generator to create if it
+ * finds it. The parameter list once completed is:
+ *
+ * (jsonPath, parserGeneratedSoFar, onSuccess)
+ *
+ * onSuccess may be compileJsonPathToFunction, to recursively continue
+ * parsing after finding a match or returnFoundParser to stop here.
+ */
+ function generateClauseReaderIfTokenFound (
+
+ tokenDetector, clauseEvaluatorGenerators,
+
+ jsonPath, parserGeneratedSoFar, onSuccess) {
+
+ var detected = tokenDetector(jsonPath);
+
+ if(detected) {
+ var compiledParser = expressionsReader(
+ clauseEvaluatorGenerators,
+ parserGeneratedSoFar,
+ detected
+ ),
+
+ remainingUnparsedJsonPath = jsonPath.substr(len(detected[0]));
+
+ return onSuccess(remainingUnparsedJsonPath, compiledParser);
+ }
+ }
+
+ /**
+ * Partially completes generateClauseReaderIfTokenFound above.
+ */
+ function clauseMatcher(tokenDetector, exprs) {
+
+ return partialComplete(
+ generateClauseReaderIfTokenFound,
+ tokenDetector,
+ exprs
+ );
+ }
+
+ /**
+ * clauseForJsonPath is a function which attempts to match against
+ * several clause matchers in order until one matches. If non match the
+ * jsonPath expression is invalid and an error is thrown.
+ *
+ * The parameter list is the same as a single clauseMatcher:
+ *
+ * (jsonPath, parserGeneratedSoFar, onSuccess)
+ */
+ var clauseForJsonPath = lazyUnion(
+
+ clauseMatcher(pathNodeSyntax , list( capture,
+ duckTypeClause,
+ nameClause,
+ skip1 ))
+
+ , clauseMatcher(doubleDotSyntax , list( skipMany))
+
+ // dot is a separator only (like whitespace in other languages) but
+ // rather than make it a special case, use an empty list of
+ // expressions when this token is found
+ , clauseMatcher(dotSyntax , list() )
+
+ , clauseMatcher(bangSyntax , list( capture,
+ rootExpr))
+
+ , clauseMatcher(emptySyntax , list( statementExpr))
+
+ , function (jsonPath) {
+ throw Error('"' + jsonPath + '" could not be tokenised')
+ }
+ );
+
+
+ /**
+ * One of two possible values for the onSuccess argument of
+ * generateClauseReaderIfTokenFound.
+ *
+ * When this function is used, generateClauseReaderIfTokenFound simply
+ * returns the compiledParser that it made, regardless of if there is
+ * any remaining jsonPath to be compiled.
+ */
+ function returnFoundParser(_remainingJsonPath, compiledParser){
+ return compiledParser
+ }
+
+ /**
+ * Recursively compile a JSONPath expression.
+ *
+ * This function serves as one of two possible values for the onSuccess
+ * argument of generateClauseReaderIfTokenFound, meaning continue to
+ * recursively compile. Otherwise, returnFoundParser is given and
+ * compilation terminates.
+ */
+ function compileJsonPathToFunction( uncompiledJsonPath,
+ parserGeneratedSoFar ) {
+
+ /**
+ * On finding a match, if there is remaining text to be compiled
+ * we want to either continue parsing using a recursive call to
+ * compileJsonPathToFunction. Otherwise, we want to stop and return
+ * the parser that we have found so far.
+ */
+ var onFind = uncompiledJsonPath
+ ? compileJsonPathToFunction
+ : returnFoundParser;
+
+ return clauseForJsonPath(
+ uncompiledJsonPath,
+ parserGeneratedSoFar,
+ onFind
+ );
+ }
+
+ /**
+ * This is the function that we expose to the rest of the library.
+ */
+ return function(jsonPath){
+
+ try {
+ // Kick off the recursive parsing of the jsonPath
+ return compileJsonPathToFunction(jsonPath, always);
+
+ } catch( e ) {
+ throw Error( 'Could not compile "' + jsonPath +
+ '" because ' + e.message
+ );
+ }
+ }
+
+});
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPathSyntax.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPathSyntax.js
new file mode 100644
index 00000000000..8c54ed334e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/jsonPathSyntax.js
@@ -0,0 +1,115 @@
+var jsonPathSyntax = (function() {
+
+ var
+
+ /**
+ * Export a regular expression as a simple function by exposing just
+ * the Regex#exec. This allows regex tests to be used under the same
+ * interface as differently implemented tests, or for a user of the
+ * tests to not concern themselves with their implementation as regular
+ * expressions.
+ *
+ * This could also be expressed point-free as:
+ * Function.prototype.bind.bind(RegExp.prototype.exec),
+ *
+ * But that's far too confusing! (and not even smaller once minified
+ * and gzipped)
+ */
+ regexDescriptor = function regexDescriptor(regex) {
+ return regex.exec.bind(regex);
+ }
+
+ /**
+ * Join several regular expressions and express as a function.
+ * This allows the token patterns to reuse component regular expressions
+ * instead of being expressed in full using huge and confusing regular
+ * expressions.
+ */
+ , jsonPathClause = varArgs(function( componentRegexes ) {
+
+ // The regular expressions all start with ^ because we
+ // only want to find matches at the start of the
+ // JSONPath fragment we are inspecting
+ componentRegexes.unshift(/^/);
+
+ return regexDescriptor(
+ RegExp(
+ componentRegexes.map(attr('source')).join('')
+ )
+ );
+ })
+
+ , possiblyCapturing = /(\$?)/
+ , namedNode = /([\w-_]+|\*)/
+ , namePlaceholder = /()/
+ , nodeInArrayNotation = /\["([^"]+)"\]/
+ , numberedNodeInArrayNotation = /\[(\d+|\*)\]/
+ , fieldList = /{([\w ]*?)}/
+ , optionalFieldList = /(?:{([\w ]*?)})?/
+
+
+ // foo or *
+ , jsonPathNamedNodeInObjectNotation = jsonPathClause(
+ possiblyCapturing,
+ namedNode,
+ optionalFieldList
+ )
+
+ // ["foo"]
+ , jsonPathNamedNodeInArrayNotation = jsonPathClause(
+ possiblyCapturing,
+ nodeInArrayNotation,
+ optionalFieldList
+ )
+
+ // [2] or [*]
+ , jsonPathNumberedNodeInArrayNotation = jsonPathClause(
+ possiblyCapturing,
+ numberedNodeInArrayNotation,
+ optionalFieldList
+ )
+
+ // {a b c}
+ , jsonPathPureDuckTyping = jsonPathClause(
+ possiblyCapturing,
+ namePlaceholder,
+ fieldList
+ )
+
+ // ..
+ , jsonPathDoubleDot = jsonPathClause(/\.\./)
+
+ // .
+ , jsonPathDot = jsonPathClause(/\./)
+
+ // !
+ , jsonPathBang = jsonPathClause(
+ possiblyCapturing,
+ /!/
+ )
+
+ // nada!
+ , emptyString = jsonPathClause(/$/)
+
+ ;
+
+
+ /* We export only a single function. When called, this function injects
+ into another function the descriptors from above.
+ */
+ return function (fn){
+ return fn(
+ lazyUnion(
+ jsonPathNamedNodeInObjectNotation
+ , jsonPathNamedNodeInArrayNotation
+ , jsonPathNumberedNodeInArrayNotation
+ , jsonPathPureDuckTyping
+ )
+ , jsonPathDoubleDot
+ , jsonPathDot
+ , jsonPathBang
+ , emptyString
+ );
+ };
+
+}()); \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/libs/clarinet.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/libs/clarinet.js
new file mode 100644
index 00000000000..c67bc83034d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/libs/clarinet.js
@@ -0,0 +1,501 @@
+/*
+ This is a slightly hacked-up browser only version of clarinet
+
+ * some features removed to help keep browser Oboe under
+ the 5k micro-library limit
+ * plug directly into event bus
+
+ For the original go here:
+ https://github.com/dscape/clarinet
+
+ We receive the events:
+ STREAM_DATA
+ STREAM_END
+
+ We emit the events:
+ SAX_KEY
+ SAX_VALUE_OPEN
+ SAX_VALUE_CLOSE
+ FAIL_EVENT
+ */
+
+function clarinet(eventBus) {
+ "use strict";
+
+ var
+ // shortcut some events on the bus
+ emitSaxKey = eventBus(SAX_KEY).emit,
+ emitValueOpen = eventBus(SAX_VALUE_OPEN).emit,
+ emitValueClose = eventBus(SAX_VALUE_CLOSE).emit,
+ emitFail = eventBus(FAIL_EVENT).emit,
+
+ MAX_BUFFER_LENGTH = 64 * 1024
+ , stringTokenPattern = /[\\"\n]/g
+ , _n = 0
+
+ // states
+ , BEGIN = _n++
+ , VALUE = _n++ // general stuff
+ , OPEN_OBJECT = _n++ // {
+ , CLOSE_OBJECT = _n++ // }
+ , OPEN_ARRAY = _n++ // [
+ , CLOSE_ARRAY = _n++ // ]
+ , STRING = _n++ // ""
+ , OPEN_KEY = _n++ // , "a"
+ , CLOSE_KEY = _n++ // :
+ , TRUE = _n++ // r
+ , TRUE2 = _n++ // u
+ , TRUE3 = _n++ // e
+ , FALSE = _n++ // a
+ , FALSE2 = _n++ // l
+ , FALSE3 = _n++ // s
+ , FALSE4 = _n++ // e
+ , NULL = _n++ // u
+ , NULL2 = _n++ // l
+ , NULL3 = _n++ // l
+ , NUMBER_DECIMAL_POINT = _n++ // .
+ , NUMBER_DIGIT = _n // [0-9]
+
+ // setup initial parser values
+ , bufferCheckPosition = MAX_BUFFER_LENGTH
+ , latestError
+ , c
+ , p
+ , textNode = undefined
+ , numberNode = ""
+ , slashed = false
+ , closed = false
+ , state = BEGIN
+ , stack = []
+ , unicodeS = null
+ , unicodeI = 0
+ , depth = 0
+ , position = 0
+ , column = 0 //mostly for error reporting
+ , line = 1
+ ;
+
+ function checkBufferLength () {
+
+ var maxActual = 0;
+
+ if (textNode !== undefined && textNode.length > MAX_BUFFER_LENGTH) {
+ emitError("Max buffer length exceeded: textNode");
+ maxActual = Math.max(maxActual, textNode.length);
+ }
+ if (numberNode.length > MAX_BUFFER_LENGTH) {
+ emitError("Max buffer length exceeded: numberNode");
+ maxActual = Math.max(maxActual, numberNode.length);
+ }
+
+ bufferCheckPosition = (MAX_BUFFER_LENGTH - maxActual)
+ + position;
+ }
+
+ eventBus(STREAM_DATA).on(handleData);
+
+ /* At the end of the http content close the clarinet
+ This will provide an error if the total content provided was not
+ valid json, ie if not all arrays, objects and Strings closed properly */
+ eventBus(STREAM_END).on(handleStreamEnd);
+
+ function emitError (errorString) {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+
+ latestError = Error(errorString + "\nLn: "+line+
+ "\nCol: "+column+
+ "\nChr: "+c);
+
+ emitFail(errorReport(undefined, undefined, latestError));
+ }
+
+ function handleStreamEnd() {
+ if( state == BEGIN ) {
+ // Handle the case where the stream closes without ever receiving
+ // any input. This isn't an error - response bodies can be blank,
+ // particularly for 204 http responses
+
+ // Because of how Oboe is currently implemented, we parse a
+ // completely empty stream as containing an empty object.
+ // This is because Oboe's done event is only fired when the
+ // root object of the JSON stream closes.
+
+ // This should be decoupled and attached instead to the input stream
+ // from the http (or whatever) resource ending.
+ // If this decoupling could happen the SAX parser could simply emit
+ // zero events on a completely empty input.
+ emitValueOpen({});
+ emitValueClose();
+
+ closed = true;
+ return;
+ }
+
+ if (state !== VALUE || depth !== 0)
+ emitError("Unexpected end");
+
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+
+ closed = true;
+ }
+
+ function whitespace(c){
+ return c == '\r' || c == '\n' || c == ' ' || c == '\t';
+ }
+
+ function handleData (chunk) {
+
+ // this used to throw the error but inside Oboe we will have already
+ // gotten the error when it was emitted. The important thing is to
+ // not continue with the parse.
+ if (latestError)
+ return;
+
+ if (closed) {
+ return emitError("Cannot write after close");
+ }
+
+ var i = 0;
+ c = chunk[0];
+
+ while (c) {
+ p = c;
+ c = chunk[i++];
+ if(!c) break;
+
+ position ++;
+ if (c == "\n") {
+ line ++;
+ column = 0;
+ } else column ++;
+ switch (state) {
+
+ case BEGIN:
+ if (c === "{") state = OPEN_OBJECT;
+ else if (c === "[") state = OPEN_ARRAY;
+ else if (!whitespace(c))
+ return emitError("Non-whitespace before {[.");
+ continue;
+
+ case OPEN_KEY:
+ case OPEN_OBJECT:
+ if (whitespace(c)) continue;
+ if(state === OPEN_KEY) stack.push(CLOSE_KEY);
+ else {
+ if(c === '}') {
+ emitValueOpen({});
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ continue;
+ } else stack.push(CLOSE_OBJECT);
+ }
+ if(c === '"')
+ state = STRING;
+ else
+ return emitError("Malformed object key should start with \" ");
+ continue;
+
+ case CLOSE_KEY:
+ case CLOSE_OBJECT:
+ if (whitespace(c)) continue;
+
+ if(c===':') {
+ if(state === CLOSE_OBJECT) {
+ stack.push(CLOSE_OBJECT);
+
+ if (textNode !== undefined) {
+ // was previously (in upstream Clarinet) one event
+ // - object open came with the text of the first
+ emitValueOpen({});
+ emitSaxKey(textNode);
+ textNode = undefined;
+ }
+ depth++;
+ } else {
+ if (textNode !== undefined) {
+ emitSaxKey(textNode);
+ textNode = undefined;
+ }
+ }
+ state = VALUE;
+ } else if (c==='}') {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ } else if(c===',') {
+ if(state === CLOSE_OBJECT)
+ stack.push(CLOSE_OBJECT);
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ state = OPEN_KEY;
+ } else
+ return emitError('Bad object');
+ continue;
+
+ case OPEN_ARRAY: // after an array there always a value
+ case VALUE:
+ if (whitespace(c)) continue;
+ if(state===OPEN_ARRAY) {
+ emitValueOpen([]);
+ depth++;
+ state = VALUE;
+ if(c === ']') {
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ continue;
+ } else {
+ stack.push(CLOSE_ARRAY);
+ }
+ }
+ if(c === '"') state = STRING;
+ else if(c === '{') state = OPEN_OBJECT;
+ else if(c === '[') state = OPEN_ARRAY;
+ else if(c === 't') state = TRUE;
+ else if(c === 'f') state = FALSE;
+ else if(c === 'n') state = NULL;
+ else if(c === '-') { // keep and continue
+ numberNode += c;
+ } else if(c==='0') {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else if('123456789'.indexOf(c) !== -1) {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else
+ return emitError("Bad value");
+ continue;
+
+ case CLOSE_ARRAY:
+ if(c===',') {
+ stack.push(CLOSE_ARRAY);
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ state = VALUE;
+ } else if (c===']') {
+ if (textNode !== undefined) {
+ emitValueOpen(textNode);
+ emitValueClose();
+ textNode = undefined;
+ }
+ emitValueClose();
+ depth--;
+ state = stack.pop() || VALUE;
+ } else if (whitespace(c))
+ continue;
+ else
+ return emitError('Bad array');
+ continue;
+
+ case STRING:
+ if (textNode === undefined) {
+ textNode = "";
+ }
+
+ // thanks thejh, this is an about 50% performance improvement.
+ var starti = i-1;
+
+ STRING_BIGLOOP: while (true) {
+
+ // zero means "no unicode active". 1-4 mean "parse some more". end after 4.
+ while (unicodeI > 0) {
+ unicodeS += c;
+ c = chunk.charAt(i++);
+ if (unicodeI === 4) {
+ // TODO this might be slow? well, probably not used too often anyway
+ textNode += String.fromCharCode(parseInt(unicodeS, 16));
+ unicodeI = 0;
+ starti = i-1;
+ } else {
+ unicodeI++;
+ }
+ // we can just break here: no stuff we skipped that still has to be sliced out or so
+ if (!c) break STRING_BIGLOOP;
+ }
+ if (c === '"' && !slashed) {
+ state = stack.pop() || VALUE;
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ if (c === '\\' && !slashed) {
+ slashed = true;
+ textNode += chunk.substring(starti, i-1);
+ c = chunk.charAt(i++);
+ if (!c) break;
+ }
+ if (slashed) {
+ slashed = false;
+ if (c === 'n') { textNode += '\n'; }
+ else if (c === 'r') { textNode += '\r'; }
+ else if (c === 't') { textNode += '\t'; }
+ else if (c === 'f') { textNode += '\f'; }
+ else if (c === 'b') { textNode += '\b'; }
+ else if (c === 'u') {
+ // \uxxxx. meh!
+ unicodeI = 1;
+ unicodeS = '';
+ } else {
+ textNode += c;
+ }
+ c = chunk.charAt(i++);
+ starti = i-1;
+ if (!c) break;
+ else continue;
+ }
+
+ stringTokenPattern.lastIndex = i;
+ var reResult = stringTokenPattern.exec(chunk);
+ if (!reResult) {
+ i = chunk.length+1;
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ i = reResult.index+1;
+ c = chunk.charAt(reResult.index);
+ if (!c) {
+ textNode += chunk.substring(starti, i-1);
+ break;
+ }
+ }
+ continue;
+
+ case TRUE:
+ if (!c) continue; // strange buffers
+ if (c==='r') state = TRUE2;
+ else
+ return emitError( 'Invalid true started with t'+ c);
+ continue;
+
+ case TRUE2:
+ if (!c) continue;
+ if (c==='u') state = TRUE3;
+ else
+ return emitError('Invalid true started with tr'+ c);
+ continue;
+
+ case TRUE3:
+ if (!c) continue;
+ if(c==='e') {
+ emitValueOpen(true);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid true started with tru'+ c);
+ continue;
+
+ case FALSE:
+ if (!c) continue;
+ if (c==='a') state = FALSE2;
+ else
+ return emitError('Invalid false started with f'+ c);
+ continue;
+
+ case FALSE2:
+ if (!c) continue;
+ if (c==='l') state = FALSE3;
+ else
+ return emitError('Invalid false started with fa'+ c);
+ continue;
+
+ case FALSE3:
+ if (!c) continue;
+ if (c==='s') state = FALSE4;
+ else
+ return emitError('Invalid false started with fal'+ c);
+ continue;
+
+ case FALSE4:
+ if (!c) continue;
+ if (c==='e') {
+ emitValueOpen(false);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid false started with fals'+ c);
+ continue;
+
+ case NULL:
+ if (!c) continue;
+ if (c==='u') state = NULL2;
+ else
+ return emitError('Invalid null started with n'+ c);
+ continue;
+
+ case NULL2:
+ if (!c) continue;
+ if (c==='l') state = NULL3;
+ else
+ return emitError('Invalid null started with nu'+ c);
+ continue;
+
+ case NULL3:
+ if (!c) continue;
+ if(c==='l') {
+ emitValueOpen(null);
+ emitValueClose();
+ state = stack.pop() || VALUE;
+ } else
+ return emitError('Invalid null started with nul'+ c);
+ continue;
+
+ case NUMBER_DECIMAL_POINT:
+ if(c==='.') {
+ numberNode += c;
+ state = NUMBER_DIGIT;
+ } else
+ return emitError('Leading zero not followed by .');
+ continue;
+
+ case NUMBER_DIGIT:
+ if('0123456789'.indexOf(c) !== -1) numberNode += c;
+ else if (c==='.') {
+ if(numberNode.indexOf('.')!==-1)
+ return emitError('Invalid number has two dots');
+ numberNode += c;
+ } else if (c==='e' || c==='E') {
+ if(numberNode.indexOf('e')!==-1 ||
+ numberNode.indexOf('E')!==-1 )
+ return emitError('Invalid number has two exponential');
+ numberNode += c;
+ } else if (c==="+" || c==="-") {
+ if(!(p==='e' || p==='E'))
+ return emitError('Invalid symbol in number');
+ numberNode += c;
+ } else {
+ if (numberNode) {
+ emitValueOpen(parseFloat(numberNode));
+ emitValueClose();
+ numberNode = "";
+ }
+ i--; // go back one
+ state = stack.pop() || VALUE;
+ }
+ continue;
+
+ default:
+ return emitError("Unknown state: " + state);
+ }
+ }
+ if (position >= bufferCheckPosition)
+ checkBufferLength();
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/lists.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/lists.js
new file mode 100644
index 00000000000..17b3a4f7efe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/lists.js
@@ -0,0 +1,192 @@
+/**
+ * Like cons in Lisp
+ */
+function cons(x, xs) {
+
+ /* Internally lists are linked 2-element Javascript arrays.
+
+ Ideally the return here would be Object.freeze([x,xs])
+ so that bugs related to mutation are found fast.
+ However, cons is right on the critical path for
+ performance and this slows oboe-mark down by
+ ~25%. Under theoretical future JS engines that freeze more
+ efficiently (possibly even use immutability to
+ run faster) this should be considered for
+ restoration.
+ */
+
+ return [x,xs];
+}
+
+/**
+ * The empty list
+ */
+var emptyList = null,
+
+/**
+ * Get the head of a list.
+ *
+ * Ie, head(cons(a,b)) = a
+ */
+ head = attr(0),
+
+/**
+ * Get the tail of a list.
+ *
+ * Ie, tail(cons(a,b)) = b
+ */
+ tail = attr(1);
+
+
+/**
+ * Converts an array to a list
+ *
+ * asList([a,b,c])
+ *
+ * is equivalent to:
+ *
+ * cons(a, cons(b, cons(c, emptyList)))
+ **/
+function arrayAsList(inputArray){
+
+ return reverseList(
+ inputArray.reduce(
+ flip(cons),
+ emptyList
+ )
+ );
+}
+
+/**
+ * A varargs version of arrayAsList. Works a bit like list
+ * in LISP.
+ *
+ * list(a,b,c)
+ *
+ * is equivalent to:
+ *
+ * cons(a, cons(b, cons(c, emptyList)))
+ */
+var list = varArgs(arrayAsList);
+
+/**
+ * Convert a list back to a js native array
+ */
+function listAsArray(list){
+
+ return foldR( function(arraySoFar, listItem){
+
+ arraySoFar.unshift(listItem);
+ return arraySoFar;
+
+ }, [], list );
+
+}
+
+/**
+ * Map a function over a list
+ */
+function map(fn, list) {
+
+ return list
+ ? cons(fn(head(list)), map(fn,tail(list)))
+ : emptyList
+ ;
+}
+
+/**
+ * foldR implementation. Reduce a list down to a single value.
+ *
+ * @pram {Function} fn (rightEval, curVal) -> result
+ */
+function foldR(fn, startValue, list) {
+
+ return list
+ ? fn(foldR(fn, startValue, tail(list)), head(list))
+ : startValue
+ ;
+}
+
+/**
+ * foldR implementation. Reduce a list down to a single value.
+ *
+ * @pram {Function} fn (rightEval, curVal) -> result
+ */
+function foldR1(fn, list) {
+
+ return tail(list)
+ ? fn(foldR1(fn, tail(list)), head(list))
+ : head(list)
+ ;
+}
+
+
+/**
+ * Return a list like the one given but with the first instance equal
+ * to item removed
+ */
+function without(list, test, removedFn) {
+
+ return withoutInner(list, removedFn || noop);
+
+ function withoutInner(subList, removedFn) {
+ return subList
+ ? ( test(head(subList))
+ ? (removedFn(head(subList)), tail(subList))
+ : cons(head(subList), withoutInner(tail(subList), removedFn))
+ )
+ : emptyList
+ ;
+ }
+}
+
+/**
+ * Returns true if the given function holds for every item in
+ * the list, false otherwise
+ */
+function all(fn, list) {
+
+ return !list ||
+ ( fn(head(list)) && all(fn, tail(list)) );
+}
+
+/**
+ * Call every function in a list of functions with the same arguments
+ *
+ * This doesn't make any sense if we're doing pure functional because
+ * it doesn't return anything. Hence, this is only really useful if the
+ * functions being called have side-effects.
+ */
+function applyEach(fnList, args) {
+
+ if( fnList ) {
+ head(fnList).apply(null, args);
+
+ applyEach(tail(fnList), args);
+ }
+}
+
+/**
+ * Reverse the order of a list
+ */
+function reverseList(list){
+
+ // js re-implementation of 3rd solution from:
+ // http://www.haskell.org/haskellwiki/99_questions/Solutions/5
+ function reverseInner( list, reversedAlready ) {
+ if( !list ) {
+ return reversedAlready;
+ }
+
+ return reverseInner(tail(list), cons(head(list), reversedAlready))
+ }
+
+ return reverseInner(list, emptyList);
+}
+
+function first(test, list) {
+ return list &&
+ (test(head(list))
+ ? head(list)
+ : first(test,tail(list)));
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/parseResponseHeaders.browser.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/parseResponseHeaders.browser.js
new file mode 100644
index 00000000000..8a2d27b6a50
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/parseResponseHeaders.browser.js
@@ -0,0 +1,24 @@
+// based on gist https://gist.github.com/monsur/706839
+
+/**
+ * XmlHttpRequest's getAllResponseHeaders() method returns a string of response
+ * headers according to the format described here:
+ * http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method
+ * This method parses that string into a user-friendly key/value pair object.
+ */
+function parseResponseHeaders(headerStr) {
+ var headers = {};
+
+ headerStr && headerStr.split('\u000d\u000a')
+ .forEach(function(headerPair){
+
+ // Can't use split() here because it does the wrong thing
+ // if the header value has the string ": " in it.
+ var index = headerPair.indexOf('\u003a\u0020');
+
+ headers[headerPair.substring(0, index)]
+ = headerPair.substring(index + 2);
+ });
+
+ return headers;
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/patternAdapter.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/patternAdapter.js
new file mode 100644
index 00000000000..f51670e68ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/patternAdapter.js
@@ -0,0 +1,112 @@
+/**
+ * The pattern adaptor listens for newListener and removeListener
+ * events. When patterns are added or removed it compiles the JSONPath
+ * and wires them up.
+ *
+ * When nodes and paths are found it emits the fully-qualified match
+ * events with parameters ready to ship to the outside world
+ */
+
+function patternAdapter(oboeBus, jsonPathCompiler) {
+
+ var predicateEventMap = {
+ node:oboeBus(NODE_CLOSED)
+ , path:oboeBus(NODE_OPENED)
+ };
+
+ function emitMatchingNode(emitMatch, node, ascent) {
+
+ /*
+ We're now calling to the outside world where Lisp-style
+ lists will not be familiar. Convert to standard arrays.
+
+ Also, reverse the order because it is more common to
+ list paths "root to leaf" than "leaf to root" */
+ var descent = reverseList(ascent);
+
+ emitMatch(
+ node,
+
+ // To make a path, strip off the last item which is the special
+ // ROOT_PATH token for the 'path' to the root node
+ listAsArray(tail(map(keyOf,descent))), // path
+ listAsArray(map(nodeOf, descent)) // ancestors
+ );
+ }
+
+ /*
+ * Set up the catching of events such as NODE_CLOSED and NODE_OPENED and, if
+ * matching the specified pattern, propagate to pattern-match events such as
+ * oboeBus('node:!')
+ *
+ *
+ *
+ * @param {Function} predicateEvent
+ * either oboeBus(NODE_CLOSED) or oboeBus(NODE_OPENED).
+ * @param {Function} compiledJsonPath
+ */
+ function addUnderlyingListener( fullEventName, predicateEvent, compiledJsonPath ){
+
+ var emitMatch = oboeBus(fullEventName).emit;
+
+ predicateEvent.on( function (ascent) {
+
+ var maybeMatchingMapping = compiledJsonPath(ascent);
+
+ /* Possible values for maybeMatchingMapping are now:
+
+ false:
+ we did not match
+
+ an object/array/string/number/null:
+ we matched and have the node that matched.
+ Because nulls are valid json values this can be null.
+
+ undefined:
+ we matched but don't have the matching node yet.
+ ie, we know there is an upcoming node that matches but we
+ can't say anything else about it.
+ */
+ if (maybeMatchingMapping !== false) {
+
+ emitMatchingNode(
+ emitMatch,
+ nodeOf(maybeMatchingMapping),
+ ascent
+ );
+ }
+ }, fullEventName);
+
+ oboeBus('removeListener').on( function(removedEventName){
+
+ // if the fully qualified match event listener is later removed, clean up
+ // by removing the underlying listener if it was the last using that pattern:
+
+ if( removedEventName == fullEventName ) {
+
+ if( !oboeBus(removedEventName).listeners( )) {
+ predicateEvent.un( fullEventName );
+ }
+ }
+ });
+ }
+
+ oboeBus('newListener').on( function(fullEventName){
+
+ var match = /(node|path):(.*)/.exec(fullEventName);
+
+ if( match ) {
+ var predicateEvent = predicateEventMap[match[1]];
+
+ if( !predicateEvent.hasListener( fullEventName) ) {
+
+ addUnderlyingListener(
+ fullEventName,
+ predicateEvent,
+ jsonPathCompiler( match[2] )
+ );
+ }
+ }
+ })
+
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/pubSub.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/pubSub.js
new file mode 100644
index 00000000000..7d9d3f3c510
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/pubSub.js
@@ -0,0 +1,64 @@
+/**
+ * pubSub is a curried interface for listening to and emitting
+ * events.
+ *
+ * If we get a bus:
+ *
+ * var bus = pubSub();
+ *
+ * We can listen to event 'foo' like:
+ *
+ * bus('foo').on(myCallback)
+ *
+ * And emit event foo like:
+ *
+ * bus('foo').emit()
+ *
+ * or, with a parameter:
+ *
+ * bus('foo').emit('bar')
+ *
+ * All functions can be cached and don't need to be
+ * bound. Ie:
+ *
+ * var fooEmitter = bus('foo').emit
+ * fooEmitter('bar'); // emit an event
+ * fooEmitter('baz'); // emit another
+ *
+ * There's also an uncurried[1] shortcut for .emit and .on:
+ *
+ * bus.on('foo', callback)
+ * bus.emit('foo', 'bar')
+ *
+ * [1]: http://zvon.org/other/haskell/Outputprelude/uncurry_f.html
+ */
+function pubSub(){
+
+ var singles = {},
+ newListener = newSingle('newListener'),
+ removeListener = newSingle('removeListener');
+
+ function newSingle(eventName) {
+ return singles[eventName] = singleEventPubSub(
+ eventName,
+ newListener,
+ removeListener
+ );
+ }
+
+ /** pubSub instances are functions */
+ function pubSubInstance( eventName ){
+
+ return singles[eventName] || newSingle( eventName );
+ }
+
+ // add convenience EventEmitter-style uncurried form of 'emit' and 'on'
+ ['emit', 'on', 'un'].forEach(function(methodName){
+
+ pubSubInstance[methodName] = varArgs(function(eventName, parameters){
+ apply( parameters, pubSubInstance( eventName )[methodName]);
+ });
+ });
+
+ return pubSubInstance;
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/publicApi.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/publicApi.js
new file mode 100644
index 00000000000..2b375729832
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/publicApi.js
@@ -0,0 +1,56 @@
+// export public API
+function oboe(arg1) {
+
+ // We use duck-typing to detect if the parameter given is a stream, with the
+ // below list of parameters.
+ // Unpipe and unshift would normally be present on a stream but this breaks
+ // compatibility with Request streams.
+ // See https://github.com/jimhigson/oboe.js/issues/65
+
+ var nodeStreamMethodNames = list('resume', 'pause', 'pipe'),
+ isStream = partialComplete(
+ hasAllProperties
+ , nodeStreamMethodNames
+ );
+
+ if( arg1 ) {
+ if (isStream(arg1) || isString(arg1)) {
+
+ // simple version for GETs. Signature is:
+ // oboe( url )
+ // or, under node:
+ // oboe( readableStream )
+ return applyDefaults(
+ wire,
+ arg1 // url
+ );
+
+ } else {
+
+ // method signature is:
+ // oboe({method:m, url:u, body:b, headers:{...}})
+
+ return applyDefaults(
+ wire,
+ arg1.url,
+ arg1.method,
+ arg1.body,
+ arg1.headers,
+ arg1.withCredentials,
+ arg1.cached
+ );
+
+ }
+ } else {
+ // wire up a no-AJAX, no-stream Oboe. Will have to have content
+ // fed in externally and using .emit.
+ return wire();
+ }
+}
+
+/* oboe.drop is a special value. If a node callback returns this value the
+ parsed node is deleted from the JSON
+ */
+oboe.drop = function() {
+ return oboe.drop;
+};
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/singleEventPubSub.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/singleEventPubSub.js
new file mode 100644
index 00000000000..62b2ec43e99
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/singleEventPubSub.js
@@ -0,0 +1,93 @@
+/**
+ * A pub/sub which is responsible for a single event type. A
+ * multi-event type event bus is created by pubSub by collecting
+ * several of these.
+ *
+ * @param {String} eventType
+ * the name of the events managed by this singleEventPubSub
+ * @param {singleEventPubSub} [newListener]
+ * place to notify of new listeners
+ * @param {singleEventPubSub} [removeListener]
+ * place to notify of when listeners are removed
+ */
+function singleEventPubSub(eventType, newListener, removeListener){
+
+ /** we are optimised for emitting events over firing them.
+ * As well as the tuple list which stores event ids and
+ * listeners there is a list with just the listeners which
+ * can be iterated more quickly when we are emitting
+ */
+ var listenerTupleList,
+ listenerList;
+
+ function hasId(id){
+ return function(tuple) {
+ return tuple.id == id;
+ };
+ }
+
+ return {
+
+ /**
+ * @param {Function} listener
+ * @param {*} listenerId
+ * an id that this listener can later by removed by.
+ * Can be of any type, to be compared to other ids using ==
+ */
+ on:function( listener, listenerId ) {
+
+ var tuple = {
+ listener: listener
+ , id: listenerId || listener // when no id is given use the
+ // listener function as the id
+ };
+
+ if( newListener ) {
+ newListener.emit(eventType, listener, tuple.id);
+ }
+
+ listenerTupleList = cons( tuple, listenerTupleList );
+ listenerList = cons( listener, listenerList );
+
+ return this; // chaining
+ },
+
+ emit:function () {
+ applyEach( listenerList, arguments );
+ },
+
+ un: function( listenerId ) {
+
+ var removed;
+
+ listenerTupleList = without(
+ listenerTupleList,
+ hasId(listenerId),
+ function(tuple){
+ removed = tuple;
+ }
+ );
+
+ if( removed ) {
+ listenerList = without( listenerList, function(listener){
+ return listener == removed.listener;
+ });
+
+ if( removeListener ) {
+ removeListener.emit(eventType, removed.listener, removed.id);
+ }
+ }
+ },
+
+ listeners: function(){
+ // differs from Node EventEmitter: returns list, not array
+ return listenerList;
+ },
+
+ hasListener: function(listenerId){
+ var test = listenerId? hasId(listenerId) : always;
+
+ return defined(first( test, listenerTupleList));
+ }
+ };
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.browser.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.browser.js
new file mode 100644
index 00000000000..0f6a5229f05
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.browser.js
@@ -0,0 +1,149 @@
+function httpTransport(){
+ return new XMLHttpRequest();
+}
+
+/**
+ * A wrapper around the browser XmlHttpRequest object that raises an
+ * event whenever a new part of the response is available.
+ *
+ * In older browsers progressive reading is impossible so all the
+ * content is given in a single call. For newer ones several events
+ * should be raised, allowing progressive interpretation of the response.
+ *
+ * @param {Function} oboeBus an event bus local to this Oboe instance
+ * @param {XMLHttpRequest} xhr the xhr to use as the transport. Under normal
+ * operation, will have been created using httpTransport() above
+ * but for tests a stub can be provided instead.
+ * @param {String} method one of 'GET' 'POST' 'PUT' 'PATCH' 'DELETE'
+ * @param {String} url the url to make a request to
+ * @param {String|Null} data some content to be sent with the request.
+ * Only valid if method is POST or PUT.
+ * @param {Object} [headers] the http request headers to send
+ * @param {boolean} withCredentials the XHR withCredentials property will be
+ * set to this value
+ */
+function streamingHttp(oboeBus, xhr, method, url, data, headers, withCredentials) {
+
+ "use strict";
+
+ var emitStreamData = oboeBus(STREAM_DATA).emit,
+ emitFail = oboeBus(FAIL_EVENT).emit,
+ numberOfCharsAlreadyGivenToCallback = 0,
+ stillToSendStartEvent = true;
+
+ // When an ABORTING message is put on the event bus abort
+ // the ajax request
+ oboeBus( ABORTING ).on( function(){
+
+ // if we keep the onreadystatechange while aborting the XHR gives
+ // a callback like a successful call so first remove this listener
+ // by assigning null:
+ xhr.onreadystatechange = null;
+
+ xhr.abort();
+ });
+
+ /**
+ * Handle input from the underlying xhr: either a state change,
+ * the progress event or the request being complete.
+ */
+ function handleProgress() {
+
+ var textSoFar = xhr.responseText,
+ newText = textSoFar.substr(numberOfCharsAlreadyGivenToCallback);
+
+
+ /* Raise the event for new text.
+
+ On older browsers, the new text is the whole response.
+ On newer/better ones, the fragment part that we got since
+ last progress. */
+
+ if( newText ) {
+ emitStreamData( newText );
+ }
+
+ numberOfCharsAlreadyGivenToCallback = len(textSoFar);
+ }
+
+
+ if('onprogress' in xhr){ // detect browser support for progressive delivery
+ xhr.onprogress = handleProgress;
+ }
+
+ xhr.onreadystatechange = function() {
+
+ function sendStartIfNotAlready() {
+ // Internet Explorer is very unreliable as to when xhr.status etc can
+ // be read so has to be protected with try/catch and tried again on
+ // the next readyState if it fails
+ try{
+ stillToSendStartEvent && oboeBus( HTTP_START ).emit(
+ xhr.status,
+ parseResponseHeaders(xhr.getAllResponseHeaders()) );
+ stillToSendStartEvent = false;
+ } catch(e){/* do nothing, will try again on next readyState*/}
+ }
+
+ switch( xhr.readyState ) {
+
+ case 2: // HEADERS_RECEIVED
+ case 3: // LOADING
+ return sendStartIfNotAlready();
+
+ case 4: // DONE
+ sendStartIfNotAlready(); // if xhr.status hasn't been available yet, it must be NOW, huh IE?
+
+ // is this a 2xx http code?
+ var successful = String(xhr.status)[0] == 2;
+
+ if( successful ) {
+ // In Chrome 29 (not 28) no onprogress is emitted when a response
+ // is complete before the onload. We need to always do handleInput
+ // in case we get the load but have not had a final progress event.
+ // This looks like a bug and may change in future but let's take
+ // the safest approach and assume we might not have received a
+ // progress event for each part of the response
+ handleProgress();
+
+ oboeBus(STREAM_END).emit();
+ } else {
+
+ emitFail( errorReport(
+ xhr.status,
+ xhr.responseText
+ ));
+ }
+ }
+ };
+
+ try{
+
+ xhr.open(method, url, true);
+
+ for( var headerName in headers ){
+ xhr.setRequestHeader(headerName, headers[headerName]);
+ }
+
+ if( !isCrossOrigin(window.location, parseUrlOrigin(url)) ) {
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ }
+
+ xhr.withCredentials = withCredentials;
+
+ xhr.send(data);
+
+ } catch( e ) {
+
+ // To keep a consistent interface with Node, we can't emit an event here.
+ // Node's streaming http adaptor receives the error as an asynchronous
+ // event rather than as an exception. If we emitted now, the Oboe user
+ // has had no chance to add a .fail listener so there is no way
+ // the event could be useful. For both these reasons defer the
+ // firing to the next JS frame.
+ window.setTimeout(
+ partialComplete(emitFail, errorReport(undefined, undefined, e))
+ , 0
+ );
+ }
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.node.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.node.js
new file mode 100644
index 00000000000..cc2f4212c04
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/streamingHttp.node.js
@@ -0,0 +1,135 @@
+var httpTransport = functor(require('http-https'));
+
+/**
+ * A wrapper around the browser XmlHttpRequest object that raises an
+ * event whenever a new part of the response is available.
+ *
+ * In older browsers progressive reading is impossible so all the
+ * content is given in a single call. For newer ones several events
+ * should be raised, allowing progressive interpretation of the response.
+ *
+ * @param {Function} oboeBus an event bus local to this Oboe instance
+ * @param {XMLHttpRequest} transport the http implementation to use as the transport. Under normal
+ * operation, will have been created using httpTransport() above
+ * and therefore be Node's http
+ * but for tests a stub may be provided instead.
+ * @param {String} method one of 'GET' 'POST' 'PUT' 'PATCH' 'DELETE'
+ * @param {String} contentSource the url to make a request to, or a stream to read from
+ * @param {String|Null} data some content to be sent with the request.
+ * Only valid if method is POST or PUT.
+ * @param {Object} [headers] the http request headers to send
+ */
+function streamingHttp(oboeBus, transport, method, contentSource, data, headers) {
+ "use strict";
+
+ /* receiving data after calling .abort on Node's http has been observed in the
+ wild. Keep aborted as state so that if the request has been aborted we
+ can ignore new data from that point on */
+ var aborted = false;
+
+ function readStreamToEventBus(readableStream) {
+
+ // use stream in flowing mode
+ readableStream.on('data', function (chunk) {
+
+ // avoid reading the stream after aborting the request
+ if( !aborted ) {
+ oboeBus(STREAM_DATA).emit(chunk.toString());
+ }
+ });
+
+ readableStream.on('end', function() {
+
+ // avoid reading the stream after aborting the request
+ if( !aborted ) {
+ oboeBus(STREAM_END).emit();
+ }
+ });
+ }
+
+ function readStreamToEnd(readableStream, callback){
+ var content = '';
+
+ readableStream.on('data', function (chunk) {
+
+ content += chunk.toString();
+ });
+
+ readableStream.on('end', function() {
+
+ callback( content );
+ });
+ }
+
+ function openUrlAsStream( url ) {
+
+ var parsedUrl = require('url').parse(url);
+
+ return transport.request({
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port,
+ path: parsedUrl.path,
+ method: method,
+ headers: headers,
+ protocol: parsedUrl.protocol
+ });
+ }
+
+ function fetchUrl() {
+ if( !contentSource.match(/https?:\/\//) ) {
+ throw new Error(
+ 'Supported protocols when passing a URL into Oboe are http and https. ' +
+ 'If you wish to use another protocol, please pass a ReadableStream ' +
+ '(http://nodejs.org/api/stream.html#stream_class_stream_readable) like ' +
+ 'oboe(fs.createReadStream("my_file")). I was given the URL: ' +
+ contentSource
+ );
+ }
+
+ var req = openUrlAsStream(contentSource);
+
+ req.on('response', function(res){
+ var statusCode = res.statusCode,
+ successful = String(statusCode)[0] == 2;
+
+ oboeBus(HTTP_START).emit( res.statusCode, res.headers);
+
+ if( successful ) {
+
+ readStreamToEventBus(res)
+
+ } else {
+ readStreamToEnd(res, function(errorBody){
+ oboeBus(FAIL_EVENT).emit(
+ errorReport( statusCode, errorBody )
+ );
+ });
+ }
+ });
+
+ req.on('error', function(e) {
+ oboeBus(FAIL_EVENT).emit(
+ errorReport(undefined, undefined, e )
+ );
+ });
+
+ oboeBus(ABORTING).on( function(){
+ aborted = true;
+ req.abort();
+ });
+
+ if( data ) {
+ req.write(data);
+ }
+
+ req.end();
+ }
+
+ if( isString(contentSource) ) {
+ fetchUrl(contentSource);
+ } else {
+ // contentsource is a stream
+ readStreamToEventBus(contentSource);
+ }
+
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/util.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/util.js
new file mode 100644
index 00000000000..cf6d2cfd800
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/util.js
@@ -0,0 +1,44 @@
+/**
+ * This file defines some loosely associated syntactic sugar for
+ * Javascript programming
+ */
+
+
+/**
+ * Returns true if the given candidate is of type T
+ */
+function isOfType(T, maybeSomething){
+ return maybeSomething && maybeSomething.constructor === T;
+}
+
+var len = attr('length'),
+ isString = partialComplete(isOfType, String);
+
+/**
+ * I don't like saying this:
+ *
+ * foo !=== undefined
+ *
+ * because of the double-negative. I find this:
+ *
+ * defined(foo)
+ *
+ * easier to read.
+ */
+function defined( value ) {
+ return value !== undefined;
+}
+
+/**
+ * Returns true if object o has a key named like every property in
+ * the properties array. Will give false if any are missing, or if o
+ * is not an object.
+ */
+function hasAllProperties(fieldList, o) {
+
+ return (o instanceof Object)
+ &&
+ all(function (field) {
+ return (field in o);
+ }, fieldList);
+} \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/wire.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/wire.js
new file mode 100644
index 00000000000..712f5516586
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/wire.js
@@ -0,0 +1,34 @@
+/**
+ * This file sits just behind the API which is used to attain a new
+ * Oboe instance. It creates the new components that are required
+ * and introduces them to each other.
+ */
+
+function wire (httpMethodName, contentSource, body, headers, withCredentials){
+
+ var oboeBus = pubSub();
+
+ // Wire the input stream in if we are given a content source.
+ // This will usually be the case. If not, the instance created
+ // will have to be passed content from an external source.
+
+ if( contentSource ) {
+
+ streamingHttp( oboeBus,
+ httpTransport(),
+ httpMethodName,
+ contentSource,
+ body,
+ headers,
+ withCredentials
+ );
+ }
+
+ clarinet(oboeBus);
+
+ ascentManager(oboeBus, incrementalContentBuilder(oboeBus));
+
+ patternAdapter(oboeBus, jsonPathCompiler);
+
+ return instanceApi(oboeBus, contentSource);
+}
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.browser.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.browser.js
new file mode 100644
index 00000000000..2a7dd9d12f7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.browser.js
@@ -0,0 +1,24 @@
+// This file is the concatenation of many js files.
+// See http://github.com/jimhigson/oboe.js for the raw source
+
+// having a local undefined, window, Object etc allows slightly better minification:
+(function (window, Object, Array, Error, JSON, undefined ) {
+
+ // ---contents--- //
+
+ if ( typeof define === "function" && define.amd ) {
+ define( "oboe", [], function () { return oboe; } );
+ } else if (typeof exports === 'object') {
+ module.exports = oboe;
+ } else {
+ window.oboe = oboe;
+ }
+})((function(){
+ // Access to the window object throws an exception in HTML5 web workers so
+ // point it to "self" if it runs in a web worker
+ try {
+ return window;
+ } catch (e) {
+ return self;
+ }
+ }()), Object, Array, Error, JSON);
diff --git a/chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.node.js b/chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.node.js
new file mode 100644
index 00000000000..38c93a52e6c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/oboe/src/wrapper.node.js
@@ -0,0 +1,9 @@
+// this file is the concatenation of several js files. See http://github.com/jimhigson/oboe.js
+// for the unconcatenated source
+
+module.exports = (function () {
+
+ // ---contents--- //
+
+ return oboe;
+})();
diff --git a/chromium/third_party/catapult/tracing/third_party/pako/LICENSE b/chromium/third_party/catapult/tracing/third_party/pako/LICENSE
new file mode 100644
index 00000000000..a934ef8db47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/pako/LICENSE
@@ -0,0 +1,21 @@
+(The MIT License)
+
+Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/chromium/third_party/catapult/tracing/third_party/pako/README.chromium b/chromium/third_party/catapult/tracing/third_party/pako/README.chromium
new file mode 100644
index 00000000000..b335916b62e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/pako/README.chromium
@@ -0,0 +1,15 @@
+Name: pako
+Short Name: pako
+URL: https://github.com/nodeca/pako
+Version: 1.0.6
+Revision: 893381abcafa10fa2081ce60dae7d4d8e873a658
+Date: Thu Sep 14 14:38:19 2017 +0300
+License: MIT
+License File: LICENSE
+Security Critical: no
+
+Description:
+high speed zlib port to javascript, works in browser & node.js http://nodeca.github.io/pako/
+
+Local Modifications:
+Took only the minified JS file and associated license.
diff --git a/chromium/third_party/catapult/tracing/third_party/pako/pako.min.js b/chromium/third_party/catapult/tracing/third_party/pako/pako.min.js
new file mode 100644
index 00000000000..73024f7a68c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/pako/pako.min.js
@@ -0,0 +1 @@
+!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pako=t()}}(function(){return function t(e,a,i){function n(s,o){if(!a[s]){if(!e[s]){var l="function"==typeof require&&require;if(!o&&l)return l(s,!0);if(r)return r(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var d=a[s]={exports:{}};e[s][0].call(d.exports,function(t){var a=e[s][1][t];return n(a||t)},d,d.exports,t,e,a,i)}return a[s].exports}for(var r="function"==typeof require&&require,s=0;s<i.length;s++)n(i[s]);return n}({1:[function(t,e,a){"use strict";function i(t){if(!(this instanceof i))return new i(t);this.options=s.assign({level:_,method:c,chunkSize:16384,windowBits:15,memLevel:8,strategy:u,to:""},t||{});var e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new h,this.strm.avail_out=0;var a=r.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==f)throw new Error(l[a]);if(e.header&&r.deflateSetHeader(this.strm,e.header),e.dictionary){var n;if(n="string"==typeof e.dictionary?o.string2buf(e.dictionary):"[object ArrayBuffer]"===d.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,(a=r.deflateSetDictionary(this.strm,n))!==f)throw new Error(l[a]);this._dict_set=!0}}function n(t,e){var a=new i(e);if(a.push(t,!0),a.err)throw a.msg||l[a.err];return a.result}var r=t("./zlib/deflate"),s=t("./utils/common"),o=t("./utils/strings"),l=t("./zlib/messages"),h=t("./zlib/zstream"),d=Object.prototype.toString,f=0,_=-1,u=0,c=8;i.prototype.push=function(t,e){var a,i,n=this.strm,l=this.options.chunkSize;if(this.ended)return!1;i=e===~~e?e:!0===e?4:0,"string"==typeof t?n.input=o.string2buf(t):"[object ArrayBuffer]"===d.call(t)?n.input=new Uint8Array(t):n.input=t,n.next_in=0,n.avail_in=n.input.length;do{if(0===n.avail_out&&(n.output=new s.Buf8(l),n.next_out=0,n.avail_out=l),1!==(a=r.deflate(n,i))&&a!==f)return this.onEnd(a),this.ended=!0,!1;0!==n.avail_out&&(0!==n.avail_in||4!==i&&2!==i)||("string"===this.options.to?this.onData(o.buf2binstring(s.shrinkBuf(n.output,n.next_out))):this.onData(s.shrinkBuf(n.output,n.next_out)))}while((n.avail_in>0||0===n.avail_out)&&1!==a);return 4===i?(a=r.deflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===f):2!==i||(this.onEnd(f),n.avail_out=0,!0)},i.prototype.onData=function(t){this.chunks.push(t)},i.prototype.onEnd=function(t){t===f&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=s.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Deflate=i,a.deflate=n,a.deflateRaw=function(t,e){return e=e||{},e.raw=!0,n(t,e)},a.gzip=function(t,e){return e=e||{},e.gzip=!0,n(t,e)}},{"./utils/common":3,"./utils/strings":4,"./zlib/deflate":8,"./zlib/messages":13,"./zlib/zstream":15}],2:[function(t,e,a){"use strict";function i(t){if(!(this instanceof i))return new i(t);this.options=s.assign({chunkSize:16384,windowBits:0,to:""},t||{});var e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new d,this.strm.avail_out=0;var a=r.inflateInit2(this.strm,e.windowBits);if(a!==l.Z_OK)throw new Error(h[a]);this.header=new f,r.inflateGetHeader(this.strm,this.header)}function n(t,e){var a=new i(e);if(a.push(t,!0),a.err)throw a.msg||h[a.err];return a.result}var r=t("./zlib/inflate"),s=t("./utils/common"),o=t("./utils/strings"),l=t("./zlib/constants"),h=t("./zlib/messages"),d=t("./zlib/zstream"),f=t("./zlib/gzheader"),_=Object.prototype.toString;i.prototype.push=function(t,e){var a,i,n,h,d,f,u=this.strm,c=this.options.chunkSize,b=this.options.dictionary,g=!1;if(this.ended)return!1;i=e===~~e?e:!0===e?l.Z_FINISH:l.Z_NO_FLUSH,"string"==typeof t?u.input=o.binstring2buf(t):"[object ArrayBuffer]"===_.call(t)?u.input=new Uint8Array(t):u.input=t,u.next_in=0,u.avail_in=u.input.length;do{if(0===u.avail_out&&(u.output=new s.Buf8(c),u.next_out=0,u.avail_out=c),(a=r.inflate(u,l.Z_NO_FLUSH))===l.Z_NEED_DICT&&b&&(f="string"==typeof b?o.string2buf(b):"[object ArrayBuffer]"===_.call(b)?new Uint8Array(b):b,a=r.inflateSetDictionary(this.strm,f)),a===l.Z_BUF_ERROR&&!0===g&&(a=l.Z_OK,g=!1),a!==l.Z_STREAM_END&&a!==l.Z_OK)return this.onEnd(a),this.ended=!0,!1;u.next_out&&(0!==u.avail_out&&a!==l.Z_STREAM_END&&(0!==u.avail_in||i!==l.Z_FINISH&&i!==l.Z_SYNC_FLUSH)||("string"===this.options.to?(n=o.utf8border(u.output,u.next_out),h=u.next_out-n,d=o.buf2string(u.output,n),u.next_out=h,u.avail_out=c-h,h&&s.arraySet(u.output,u.output,n,h,0),this.onData(d)):this.onData(s.shrinkBuf(u.output,u.next_out)))),0===u.avail_in&&0===u.avail_out&&(g=!0)}while((u.avail_in>0||0===u.avail_out)&&a!==l.Z_STREAM_END);return a===l.Z_STREAM_END&&(i=l.Z_FINISH),i===l.Z_FINISH?(a=r.inflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===l.Z_OK):i!==l.Z_SYNC_FLUSH||(this.onEnd(l.Z_OK),u.avail_out=0,!0)},i.prototype.onData=function(t){this.chunks.push(t)},i.prototype.onEnd=function(t){t===l.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=s.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Inflate=i,a.inflate=n,a.inflateRaw=function(t,e){return e=e||{},e.raw=!0,n(t,e)},a.ungzip=n},{"./utils/common":3,"./utils/strings":4,"./zlib/constants":6,"./zlib/gzheader":9,"./zlib/inflate":11,"./zlib/messages":13,"./zlib/zstream":15}],3:[function(t,e,a){"use strict";function i(t,e){return Object.prototype.hasOwnProperty.call(t,e)}var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;a.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(var n in a)i(a,n)&&(t[n]=a[n])}}return t},a.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var r={arraySet:function(t,e,a,i,n){if(e.subarray&&t.subarray)t.set(e.subarray(a,a+i),n);else for(var r=0;r<i;r++)t[n+r]=e[a+r]},flattenChunks:function(t){var e,a,i,n,r,s;for(i=0,e=0,a=t.length;e<a;e++)i+=t[e].length;for(s=new Uint8Array(i),n=0,e=0,a=t.length;e<a;e++)r=t[e],s.set(r,n),n+=r.length;return s}},s={arraySet:function(t,e,a,i,n){for(var r=0;r<i;r++)t[n+r]=e[a+r]},flattenChunks:function(t){return[].concat.apply([],t)}};a.setTyped=function(t){t?(a.Buf8=Uint8Array,a.Buf16=Uint16Array,a.Buf32=Int32Array,a.assign(a,r)):(a.Buf8=Array,a.Buf16=Array,a.Buf32=Array,a.assign(a,s))},a.setTyped(n)},{}],4:[function(t,e,a){"use strict";function i(t,e){if(e<65537&&(t.subarray&&s||!t.subarray&&r))return String.fromCharCode.apply(null,n.shrinkBuf(t,e));for(var a="",i=0;i<e;i++)a+=String.fromCharCode(t[i]);return a}var n=t("./common"),r=!0,s=!0;try{String.fromCharCode.apply(null,[0])}catch(t){r=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(t){s=!1}for(var o=new n.Buf8(256),l=0;l<256;l++)o[l]=l>=252?6:l>=248?5:l>=240?4:l>=224?3:l>=192?2:1;o[254]=o[254]=1,a.string2buf=function(t){var e,a,i,r,s,o=t.length,l=0;for(r=0;r<o;r++)55296==(64512&(a=t.charCodeAt(r)))&&r+1<o&&56320==(64512&(i=t.charCodeAt(r+1)))&&(a=65536+(a-55296<<10)+(i-56320),r++),l+=a<128?1:a<2048?2:a<65536?3:4;for(e=new n.Buf8(l),s=0,r=0;s<l;r++)55296==(64512&(a=t.charCodeAt(r)))&&r+1<o&&56320==(64512&(i=t.charCodeAt(r+1)))&&(a=65536+(a-55296<<10)+(i-56320),r++),a<128?e[s++]=a:a<2048?(e[s++]=192|a>>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},a.buf2binstring=function(t){return i(t,t.length)},a.binstring2buf=function(t){for(var e=new n.Buf8(t.length),a=0,i=e.length;a<i;a++)e[a]=t.charCodeAt(a);return e},a.buf2string=function(t,e){var a,n,r,s,l=e||t.length,h=new Array(2*l);for(n=0,a=0;a<l;)if((r=t[a++])<128)h[n++]=r;else if((s=o[r])>4)h[n++]=65533,a+=s-1;else{for(r&=2===s?31:3===s?15:7;s>1&&a<l;)r=r<<6|63&t[a++],s--;s>1?h[n++]=65533:r<65536?h[n++]=r:(r-=65536,h[n++]=55296|r>>10&1023,h[n++]=56320|1023&r)}return i(h,n)},a.utf8border=function(t,e){var a;for((e=e||t.length)>t.length&&(e=t.length),a=e-1;a>=0&&128==(192&t[a]);)a--;return a<0?e:0===a?e:a+o[t[a]]>e?a:e}},{"./common":3}],5:[function(t,e,a){"use strict";e.exports=function(t,e,a,i){for(var n=65535&t|0,r=t>>>16&65535|0,s=0;0!==a;){a-=s=a>2e3?2e3:a;do{r=r+(n=n+e[i++]|0)|0}while(--s);n%=65521,r%=65521}return n|r<<16|0}},{}],6:[function(t,e,a){"use strict";e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],7:[function(t,e,a){"use strict";var i=function(){for(var t,e=[],a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e}();e.exports=function(t,e,a,n){var r=i,s=n+a;t^=-1;for(var o=n;o<s;o++)t=t>>>8^r[255&(t^e[o])];return-1^t}},{}],8:[function(t,e,a){"use strict";function i(t,e){return t.msg=A[e],e}function n(t){return(t<<1)-(t>4?9:0)}function r(t){for(var e=t.length;--e>=0;)t[e]=0}function s(t){var e=t.state,a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(z.arraySet(t.output,e.pending_buf,e.pending_out,a,t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))}function o(t,e){B._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,s(t.strm)}function l(t,e){t.pending_buf[t.pending++]=e}function h(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function d(t,e,a,i){var n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,z.arraySet(e,t.input,t.next_in,n,a),1===t.state.wrap?t.adler=S(t.adler,e,n,a):2===t.state.wrap&&(t.adler=E(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)}function f(t,e){var a,i,n=t.max_chain_length,r=t.strstart,s=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-it?t.strstart-(t.w_size-it):0,h=t.window,d=t.w_mask,f=t.prev,_=t.strstart+at,u=h[r+s-1],c=h[r+s];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+s]===c&&h[a+s-1]===u&&h[a]===h[r]&&h[++a]===h[r+1]){r+=2,a++;do{}while(h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&r<_);if(i=at-(_-r),r=_-at,i>s){if(t.match_start=e,s=i,i>=o)break;u=h[r+s-1],c=h[r+s]}}}while((e=f[e&d])>l&&0!=--n);return s<=t.lookahead?s:t.lookahead}function _(t){var e,a,i,n,r,s=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=s+(s-it)){z.arraySet(t.window,t.window,s,s,0),t.match_start-=s,t.strstart-=s,t.block_start-=s,e=a=t.hash_size;do{i=t.head[--e],t.head[e]=i>=s?i-s:0}while(--a);e=a=s;do{i=t.prev[--e],t.prev[e]=i>=s?i-s:0}while(--a);n+=s}if(0===t.strm.avail_in)break;if(a=d(t.strm,t.window,t.strstart+t.lookahead,n),t.lookahead+=a,t.lookahead+t.insert>=et)for(r=t.strstart-t.insert,t.ins_h=t.window[r],t.ins_h=(t.ins_h<<t.hash_shift^t.window[r+1])&t.hash_mask;t.insert&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[r+et-1])&t.hash_mask,t.prev[r&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=r,r++,t.insert--,!(t.lookahead+t.insert<et)););}while(t.lookahead<it&&0!==t.strm.avail_in)}function u(t,e){for(var a,i;;){if(t.lookahead<it){if(_(t),t.lookahead<it&&e===Z)return _t;if(0===t.lookahead)break}if(a=0,t.lookahead>=et&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+et-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-it&&(t.match_length=f(t,a)),t.match_length>=et)if(i=B._tr_tally(t,t.strstart-t.match_start,t.match_length-et),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=et){t.match_length--;do{t.strstart++,t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+et-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+1])&t.hash_mask;else i=B._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(o(t,!1),0===t.strm.avail_out))return _t}return t.insert=t.strstart<et-1?t.strstart:et-1,e===N?(o(t,!0),0===t.strm.avail_out?ct:bt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?_t:ut}function c(t,e){for(var a,i,n;;){if(t.lookahead<it){if(_(t),t.lookahead<it&&e===Z)return _t;if(0===t.lookahead)break}if(a=0,t.lookahead>=et&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+et-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=et-1,0!==a&&t.prev_length<t.max_lazy_match&&t.strstart-a<=t.w_size-it&&(t.match_length=f(t,a),t.match_length<=5&&(t.strategy===H||t.match_length===et&&t.strstart-t.match_start>4096)&&(t.match_length=et-1)),t.prev_length>=et&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-et,i=B._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-et),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+et-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=et-1,t.strstart++,i&&(o(t,!1),0===t.strm.avail_out))return _t}else if(t.match_available){if((i=B._tr_tally(t,0,t.window[t.strstart-1]))&&o(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return _t}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=B._tr_tally(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<et-1?t.strstart:et-1,e===N?(o(t,!0),0===t.strm.avail_out?ct:bt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?_t:ut}function b(t,e){for(var a,i,n,r,s=t.window;;){if(t.lookahead<=at){if(_(t),t.lookahead<=at&&e===Z)return _t;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=et&&t.strstart>0&&(n=t.strstart-1,(i=s[n])===s[++n]&&i===s[++n]&&i===s[++n])){r=t.strstart+at;do{}while(i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&n<r);t.match_length=at-(r-n),t.match_length>t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=et?(a=B._tr_tally(t,1,t.match_length-et),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=B._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(o(t,!1),0===t.strm.avail_out))return _t}return t.insert=0,e===N?(o(t,!0),0===t.strm.avail_out?ct:bt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?_t:ut}function g(t,e){for(var a;;){if(0===t.lookahead&&(_(t),0===t.lookahead)){if(e===Z)return _t;break}if(t.match_length=0,a=B._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(o(t,!1),0===t.strm.avail_out))return _t}return t.insert=0,e===N?(o(t,!0),0===t.strm.avail_out?ct:bt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?_t:ut}function m(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}function w(t){t.window_size=2*t.w_size,r(t.head),t.max_lazy_match=x[t.level].max_lazy,t.good_match=x[t.level].good_length,t.nice_match=x[t.level].nice_length,t.max_chain_length=x[t.level].max_chain,t.strstart=0,t.block_start=0,t.lookahead=0,t.insert=0,t.match_length=t.prev_length=et-1,t.match_available=0,t.ins_h=0}function p(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=q,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new z.Buf16(2*$),this.dyn_dtree=new z.Buf16(2*(2*Q+1)),this.bl_tree=new z.Buf16(2*(2*V+1)),r(this.dyn_ltree),r(this.dyn_dtree),r(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new z.Buf16(tt+1),this.heap=new z.Buf16(2*J+1),r(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new z.Buf16(2*J+1),r(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function v(t){var e;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=Y,e=t.state,e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?rt:dt,t.adler=2===e.wrap?0:1,e.last_flush=Z,B._tr_init(e),D):i(t,U)}function k(t){var e=v(t);return e===D&&w(t.state),e}function y(t,e,a,n,r,s){if(!t)return U;var o=1;if(e===L&&(e=6),n<0?(o=0,n=-n):n>15&&(o=2,n-=16),r<1||r>G||a!==q||n<8||n>15||e<0||e>9||s<0||s>M)return i(t,U);8===n&&(n=9);var l=new p;return t.state=l,l.strm=t,l.wrap=o,l.gzhead=null,l.w_bits=n,l.w_size=1<<l.w_bits,l.w_mask=l.w_size-1,l.hash_bits=r+7,l.hash_size=1<<l.hash_bits,l.hash_mask=l.hash_size-1,l.hash_shift=~~((l.hash_bits+et-1)/et),l.window=new z.Buf8(2*l.w_size),l.head=new z.Buf16(l.hash_size),l.prev=new z.Buf16(l.w_size),l.lit_bufsize=1<<r+6,l.pending_buf_size=4*l.lit_bufsize,l.pending_buf=new z.Buf8(l.pending_buf_size),l.d_buf=1*l.lit_bufsize,l.l_buf=3*l.lit_bufsize,l.level=e,l.strategy=s,l.method=a,k(t)}var x,z=t("../utils/common"),B=t("./trees"),S=t("./adler32"),E=t("./crc32"),A=t("./messages"),Z=0,R=1,C=3,N=4,O=5,D=0,I=1,U=-2,T=-3,F=-5,L=-1,H=1,j=2,K=3,M=4,P=0,Y=2,q=8,G=9,X=15,W=8,J=286,Q=30,V=19,$=2*J+1,tt=15,et=3,at=258,it=at+et+1,nt=32,rt=42,st=69,ot=73,lt=91,ht=103,dt=113,ft=666,_t=1,ut=2,ct=3,bt=4,gt=3;x=[new m(0,0,0,0,function(t,e){var a=65535;for(a>t.pending_buf_size-5&&(a=t.pending_buf_size-5);;){if(t.lookahead<=1){if(_(t),0===t.lookahead&&e===Z)return _t;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+a;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,o(t,!1),0===t.strm.avail_out))return _t;if(t.strstart-t.block_start>=t.w_size-it&&(o(t,!1),0===t.strm.avail_out))return _t}return t.insert=0,e===N?(o(t,!0),0===t.strm.avail_out?ct:bt):(t.strstart>t.block_start&&(o(t,!1),t.strm.avail_out),_t)}),new m(4,4,8,4,u),new m(4,5,16,8,u),new m(4,6,32,32,u),new m(4,4,16,16,c),new m(8,16,32,32,c),new m(8,16,128,128,c),new m(8,32,128,256,c),new m(32,128,258,1024,c),new m(32,258,258,4096,c)],a.deflateInit=function(t,e){return y(t,e,q,X,W,P)},a.deflateInit2=y,a.deflateReset=k,a.deflateResetKeep=v,a.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?U:(t.state.gzhead=e,D):U},a.deflate=function(t,e){var a,o,d,f;if(!t||!t.state||e>O||e<0)return t?i(t,U):U;if(o=t.state,!t.output||!t.input&&0!==t.avail_in||o.status===ft&&e!==N)return i(t,0===t.avail_out?F:U);if(o.strm=t,a=o.last_flush,o.last_flush=e,o.status===rt)if(2===o.wrap)t.adler=0,l(o,31),l(o,139),l(o,8),o.gzhead?(l(o,(o.gzhead.text?1:0)+(o.gzhead.hcrc?2:0)+(o.gzhead.extra?4:0)+(o.gzhead.name?8:0)+(o.gzhead.comment?16:0)),l(o,255&o.gzhead.time),l(o,o.gzhead.time>>8&255),l(o,o.gzhead.time>>16&255),l(o,o.gzhead.time>>24&255),l(o,9===o.level?2:o.strategy>=j||o.level<2?4:0),l(o,255&o.gzhead.os),o.gzhead.extra&&o.gzhead.extra.length&&(l(o,255&o.gzhead.extra.length),l(o,o.gzhead.extra.length>>8&255)),o.gzhead.hcrc&&(t.adler=E(t.adler,o.pending_buf,o.pending,0)),o.gzindex=0,o.status=st):(l(o,0),l(o,0),l(o,0),l(o,0),l(o,0),l(o,9===o.level?2:o.strategy>=j||o.level<2?4:0),l(o,gt),o.status=dt);else{var _=q+(o.w_bits-8<<4)<<8;_|=(o.strategy>=j||o.level<2?0:o.level<6?1:6===o.level?2:3)<<6,0!==o.strstart&&(_|=nt),_+=31-_%31,o.status=dt,h(o,_),0!==o.strstart&&(h(o,t.adler>>>16),h(o,65535&t.adler)),t.adler=1}if(o.status===st)if(o.gzhead.extra){for(d=o.pending;o.gzindex<(65535&o.gzhead.extra.length)&&(o.pending!==o.pending_buf_size||(o.gzhead.hcrc&&o.pending>d&&(t.adler=E(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending!==o.pending_buf_size));)l(o,255&o.gzhead.extra[o.gzindex]),o.gzindex++;o.gzhead.hcrc&&o.pending>d&&(t.adler=E(t.adler,o.pending_buf,o.pending-d,d)),o.gzindex===o.gzhead.extra.length&&(o.gzindex=0,o.status=ot)}else o.status=ot;if(o.status===ot)if(o.gzhead.name){d=o.pending;do{if(o.pending===o.pending_buf_size&&(o.gzhead.hcrc&&o.pending>d&&(t.adler=E(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending===o.pending_buf_size)){f=1;break}f=o.gzindex<o.gzhead.name.length?255&o.gzhead.name.charCodeAt(o.gzindex++):0,l(o,f)}while(0!==f);o.gzhead.hcrc&&o.pending>d&&(t.adler=E(t.adler,o.pending_buf,o.pending-d,d)),0===f&&(o.gzindex=0,o.status=lt)}else o.status=lt;if(o.status===lt)if(o.gzhead.comment){d=o.pending;do{if(o.pending===o.pending_buf_size&&(o.gzhead.hcrc&&o.pending>d&&(t.adler=E(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending===o.pending_buf_size)){f=1;break}f=o.gzindex<o.gzhead.comment.length?255&o.gzhead.comment.charCodeAt(o.gzindex++):0,l(o,f)}while(0!==f);o.gzhead.hcrc&&o.pending>d&&(t.adler=E(t.adler,o.pending_buf,o.pending-d,d)),0===f&&(o.status=ht)}else o.status=ht;if(o.status===ht&&(o.gzhead.hcrc?(o.pending+2>o.pending_buf_size&&s(t),o.pending+2<=o.pending_buf_size&&(l(o,255&t.adler),l(o,t.adler>>8&255),t.adler=0,o.status=dt)):o.status=dt),0!==o.pending){if(s(t),0===t.avail_out)return o.last_flush=-1,D}else if(0===t.avail_in&&n(e)<=n(a)&&e!==N)return i(t,F);if(o.status===ft&&0!==t.avail_in)return i(t,F);if(0!==t.avail_in||0!==o.lookahead||e!==Z&&o.status!==ft){var u=o.strategy===j?g(o,e):o.strategy===K?b(o,e):x[o.level].func(o,e);if(u!==ct&&u!==bt||(o.status=ft),u===_t||u===ct)return 0===t.avail_out&&(o.last_flush=-1),D;if(u===ut&&(e===R?B._tr_align(o):e!==O&&(B._tr_stored_block(o,0,0,!1),e===C&&(r(o.head),0===o.lookahead&&(o.strstart=0,o.block_start=0,o.insert=0))),s(t),0===t.avail_out))return o.last_flush=-1,D}return e!==N?D:o.wrap<=0?I:(2===o.wrap?(l(o,255&t.adler),l(o,t.adler>>8&255),l(o,t.adler>>16&255),l(o,t.adler>>24&255),l(o,255&t.total_in),l(o,t.total_in>>8&255),l(o,t.total_in>>16&255),l(o,t.total_in>>24&255)):(h(o,t.adler>>>16),h(o,65535&t.adler)),s(t),o.wrap>0&&(o.wrap=-o.wrap),0!==o.pending?D:I)},a.deflateEnd=function(t){var e;return t&&t.state?(e=t.state.status)!==rt&&e!==st&&e!==ot&&e!==lt&&e!==ht&&e!==dt&&e!==ft?i(t,U):(t.state=null,e===dt?i(t,T):D):U},a.deflateSetDictionary=function(t,e){var a,i,n,s,o,l,h,d,f=e.length;if(!t||!t.state)return U;if(a=t.state,2===(s=a.wrap)||1===s&&a.status!==rt||a.lookahead)return U;for(1===s&&(t.adler=S(t.adler,e,f,0)),a.wrap=0,f>=a.w_size&&(0===s&&(r(a.head),a.strstart=0,a.block_start=0,a.insert=0),d=new z.Buf8(a.w_size),z.arraySet(d,e,f-a.w_size,a.w_size,0),e=d,f=a.w_size),o=t.avail_in,l=t.next_in,h=t.input,t.avail_in=f,t.next_in=0,t.input=e,_(a);a.lookahead>=et;){i=a.strstart,n=a.lookahead-(et-1);do{a.ins_h=(a.ins_h<<a.hash_shift^a.window[i+et-1])&a.hash_mask,a.prev[i&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=i,i++}while(--n);a.strstart=i,a.lookahead=et-1,_(a)}return a.strstart+=a.lookahead,a.block_start=a.strstart,a.insert=a.lookahead,a.lookahead=0,a.match_length=a.prev_length=et-1,a.match_available=0,t.next_in=l,t.input=h,t.avail_in=o,a.wrap=s,D},a.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":3,"./adler32":5,"./crc32":7,"./messages":13,"./trees":14}],9:[function(t,e,a){"use strict";e.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],10:[function(t,e,a){"use strict";e.exports=function(t,e){var a,i,n,r,s,o,l,h,d,f,_,u,c,b,g,m,w,p,v,k,y,x,z,B,S;a=t.state,i=t.next_in,B=t.input,n=i+(t.avail_in-5),r=t.next_out,S=t.output,s=r-(e-t.avail_out),o=r+(t.avail_out-257),l=a.dmax,h=a.wsize,d=a.whave,f=a.wnext,_=a.window,u=a.hold,c=a.bits,b=a.lencode,g=a.distcode,m=(1<<a.lenbits)-1,w=(1<<a.distbits)-1;t:do{c<15&&(u+=B[i++]<<c,c+=8,u+=B[i++]<<c,c+=8),p=b[u&m];e:for(;;){if(v=p>>>24,u>>>=v,c-=v,0===(v=p>>>16&255))S[r++]=65535&p;else{if(!(16&v)){if(0==(64&v)){p=b[(65535&p)+(u&(1<<v)-1)];continue e}if(32&v){a.mode=12;break t}t.msg="invalid literal/length code",a.mode=30;break t}k=65535&p,(v&=15)&&(c<v&&(u+=B[i++]<<c,c+=8),k+=u&(1<<v)-1,u>>>=v,c-=v),c<15&&(u+=B[i++]<<c,c+=8,u+=B[i++]<<c,c+=8),p=g[u&w];a:for(;;){if(v=p>>>24,u>>>=v,c-=v,!(16&(v=p>>>16&255))){if(0==(64&v)){p=g[(65535&p)+(u&(1<<v)-1)];continue a}t.msg="invalid distance code",a.mode=30;break t}if(y=65535&p,v&=15,c<v&&(u+=B[i++]<<c,(c+=8)<v&&(u+=B[i++]<<c,c+=8)),(y+=u&(1<<v)-1)>l){t.msg="invalid distance too far back",a.mode=30;break t}if(u>>>=v,c-=v,v=r-s,y>v){if((v=y-v)>d&&a.sane){t.msg="invalid distance too far back",a.mode=30;break t}if(x=0,z=_,0===f){if(x+=h-v,v<k){k-=v;do{S[r++]=_[x++]}while(--v);x=r-y,z=S}}else if(f<v){if(x+=h+f-v,(v-=f)<k){k-=v;do{S[r++]=_[x++]}while(--v);if(x=0,f<k){k-=v=f;do{S[r++]=_[x++]}while(--v);x=r-y,z=S}}}else if(x+=f-v,v<k){k-=v;do{S[r++]=_[x++]}while(--v);x=r-y,z=S}for(;k>2;)S[r++]=z[x++],S[r++]=z[x++],S[r++]=z[x++],k-=3;k&&(S[r++]=z[x++],k>1&&(S[r++]=z[x++]))}else{x=r-y;do{S[r++]=S[x++],S[r++]=S[x++],S[r++]=S[x++],k-=3}while(k>2);k&&(S[r++]=S[x++],k>1&&(S[r++]=S[x++]))}break}}break}}while(i<n&&r<o);i-=k=c>>3,u&=(1<<(c-=k<<3))-1,t.next_in=i,t.next_out=r,t.avail_in=i<n?n-i+5:5-(i-n),t.avail_out=r<o?o-r+257:257-(r-o),a.hold=u,a.bits=c}},{}],11:[function(t,e,a){"use strict";function i(t){return(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function n(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new u.Buf16(320),this.work=new u.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function r(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=N,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new u.Buf32(dt),e.distcode=e.distdyn=new u.Buf32(ft),e.sane=1,e.back=-1,z):E}function s(t){var e;return t&&t.state?(e=t.state,e.wsize=0,e.whave=0,e.wnext=0,r(t)):E}function o(t,e){var a,i;return t&&t.state?(i=t.state,e<0?(a=0,e=-e):(a=1+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?E:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,s(t))):E}function l(t,e){var a,i;return t?(i=new n,t.state=i,i.window=null,(a=o(t,e))!==z&&(t.state=null),a):E}function h(t){if(ut){var e;for(f=new u.Buf32(512),_=new u.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(m(p,t.lens,0,288,f,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;m(v,t.lens,0,32,_,0,t.work,{bits:5}),ut=!1}t.lencode=f,t.lenbits=9,t.distcode=_,t.distbits=5}function d(t,e,a,i){var n,r=t.state;return null===r.window&&(r.wsize=1<<r.wbits,r.wnext=0,r.whave=0,r.window=new u.Buf8(r.wsize)),i>=r.wsize?(u.arraySet(r.window,e,a-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):((n=r.wsize-r.wnext)>i&&(n=i),u.arraySet(r.window,e,a-i,n,r.wnext),(i-=n)?(u.arraySet(r.window,e,a-i,i,0),r.wnext=i,r.whave=r.wsize):(r.wnext+=n,r.wnext===r.wsize&&(r.wnext=0),r.whave<r.wsize&&(r.whave+=n))),0}var f,_,u=t("../utils/common"),c=t("./adler32"),b=t("./crc32"),g=t("./inffast"),m=t("./inftrees"),w=0,p=1,v=2,k=4,y=5,x=6,z=0,B=1,S=2,E=-2,A=-3,Z=-4,R=-5,C=8,N=1,O=2,D=3,I=4,U=5,T=6,F=7,L=8,H=9,j=10,K=11,M=12,P=13,Y=14,q=15,G=16,X=17,W=18,J=19,Q=20,V=21,$=22,tt=23,et=24,at=25,it=26,nt=27,rt=28,st=29,ot=30,lt=31,ht=32,dt=852,ft=592,_t=15,ut=!0;a.inflateReset=s,a.inflateReset2=o,a.inflateResetKeep=r,a.inflateInit=function(t){return l(t,_t)},a.inflateInit2=l,a.inflate=function(t,e){var a,n,r,s,o,l,f,_,dt,ft,_t,ut,ct,bt,gt,mt,wt,pt,vt,kt,yt,xt,zt,Bt,St=0,Et=new u.Buf8(4),At=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!t||!t.state||!t.output||!t.input&&0!==t.avail_in)return E;(a=t.state).mode===M&&(a.mode=P),o=t.next_out,r=t.output,f=t.avail_out,s=t.next_in,n=t.input,l=t.avail_in,_=a.hold,dt=a.bits,ft=l,_t=f,xt=z;t:for(;;)switch(a.mode){case N:if(0===a.wrap){a.mode=P;break}for(;dt<16;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(2&a.wrap&&35615===_){a.check=0,Et[0]=255&_,Et[1]=_>>>8&255,a.check=b(a.check,Et,2,0),_=0,dt=0,a.mode=O;break}if(a.flags=0,a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&_)<<8)+(_>>8))%31){t.msg="incorrect header check",a.mode=ot;break}if((15&_)!==C){t.msg="unknown compression method",a.mode=ot;break}if(_>>>=4,dt-=4,yt=8+(15&_),0===a.wbits)a.wbits=yt;else if(yt>a.wbits){t.msg="invalid window size",a.mode=ot;break}a.dmax=1<<yt,t.adler=a.check=1,a.mode=512&_?j:M,_=0,dt=0;break;case O:for(;dt<16;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(a.flags=_,(255&a.flags)!==C){t.msg="unknown compression method",a.mode=ot;break}if(57344&a.flags){t.msg="unknown header flags set",a.mode=ot;break}a.head&&(a.head.text=_>>8&1),512&a.flags&&(Et[0]=255&_,Et[1]=_>>>8&255,a.check=b(a.check,Et,2,0)),_=0,dt=0,a.mode=D;case D:for(;dt<32;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}a.head&&(a.head.time=_),512&a.flags&&(Et[0]=255&_,Et[1]=_>>>8&255,Et[2]=_>>>16&255,Et[3]=_>>>24&255,a.check=b(a.check,Et,4,0)),_=0,dt=0,a.mode=I;case I:for(;dt<16;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}a.head&&(a.head.xflags=255&_,a.head.os=_>>8),512&a.flags&&(Et[0]=255&_,Et[1]=_>>>8&255,a.check=b(a.check,Et,2,0)),_=0,dt=0,a.mode=U;case U:if(1024&a.flags){for(;dt<16;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}a.length=_,a.head&&(a.head.extra_len=_),512&a.flags&&(Et[0]=255&_,Et[1]=_>>>8&255,a.check=b(a.check,Et,2,0)),_=0,dt=0}else a.head&&(a.head.extra=null);a.mode=T;case T:if(1024&a.flags&&((ut=a.length)>l&&(ut=l),ut&&(a.head&&(yt=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Array(a.head.extra_len)),u.arraySet(a.head.extra,n,s,ut,yt)),512&a.flags&&(a.check=b(a.check,n,ut,s)),l-=ut,s+=ut,a.length-=ut),a.length))break t;a.length=0,a.mode=F;case F:if(2048&a.flags){if(0===l)break t;ut=0;do{yt=n[s+ut++],a.head&&yt&&a.length<65536&&(a.head.name+=String.fromCharCode(yt))}while(yt&&ut<l);if(512&a.flags&&(a.check=b(a.check,n,ut,s)),l-=ut,s+=ut,yt)break t}else a.head&&(a.head.name=null);a.length=0,a.mode=L;case L:if(4096&a.flags){if(0===l)break t;ut=0;do{yt=n[s+ut++],a.head&&yt&&a.length<65536&&(a.head.comment+=String.fromCharCode(yt))}while(yt&&ut<l);if(512&a.flags&&(a.check=b(a.check,n,ut,s)),l-=ut,s+=ut,yt)break t}else a.head&&(a.head.comment=null);a.mode=H;case H:if(512&a.flags){for(;dt<16;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(_!==(65535&a.check)){t.msg="header crc mismatch",a.mode=ot;break}_=0,dt=0}a.head&&(a.head.hcrc=a.flags>>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=M;break;case j:for(;dt<32;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}t.adler=a.check=i(_),_=0,dt=0,a.mode=K;case K:if(0===a.havedict)return t.next_out=o,t.avail_out=f,t.next_in=s,t.avail_in=l,a.hold=_,a.bits=dt,S;t.adler=a.check=1,a.mode=M;case M:if(e===y||e===x)break t;case P:if(a.last){_>>>=7&dt,dt-=7&dt,a.mode=nt;break}for(;dt<3;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}switch(a.last=1&_,_>>>=1,dt-=1,3&_){case 0:a.mode=Y;break;case 1:if(h(a),a.mode=Q,e===x){_>>>=2,dt-=2;break t}break;case 2:a.mode=X;break;case 3:t.msg="invalid block type",a.mode=ot}_>>>=2,dt-=2;break;case Y:for(_>>>=7&dt,dt-=7&dt;dt<32;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if((65535&_)!=(_>>>16^65535)){t.msg="invalid stored block lengths",a.mode=ot;break}if(a.length=65535&_,_=0,dt=0,a.mode=q,e===x)break t;case q:a.mode=G;case G:if(ut=a.length){if(ut>l&&(ut=l),ut>f&&(ut=f),0===ut)break t;u.arraySet(r,n,s,ut,o),l-=ut,s+=ut,f-=ut,o+=ut,a.length-=ut;break}a.mode=M;break;case X:for(;dt<14;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(a.nlen=257+(31&_),_>>>=5,dt-=5,a.ndist=1+(31&_),_>>>=5,dt-=5,a.ncode=4+(15&_),_>>>=4,dt-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=ot;break}a.have=0,a.mode=W;case W:for(;a.have<a.ncode;){for(;dt<3;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}a.lens[At[a.have++]]=7&_,_>>>=3,dt-=3}for(;a.have<19;)a.lens[At[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,zt={bits:a.lenbits},xt=m(w,a.lens,0,19,a.lencode,0,a.work,zt),a.lenbits=zt.bits,xt){t.msg="invalid code lengths set",a.mode=ot;break}a.have=0,a.mode=J;case J:for(;a.have<a.nlen+a.ndist;){for(;St=a.lencode[_&(1<<a.lenbits)-1],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(gt<=dt);){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(wt<16)_>>>=gt,dt-=gt,a.lens[a.have++]=wt;else{if(16===wt){for(Bt=gt+2;dt<Bt;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(_>>>=gt,dt-=gt,0===a.have){t.msg="invalid bit length repeat",a.mode=ot;break}yt=a.lens[a.have-1],ut=3+(3&_),_>>>=2,dt-=2}else if(17===wt){for(Bt=gt+3;dt<Bt;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}dt-=gt,yt=0,ut=3+(7&(_>>>=gt)),_>>>=3,dt-=3}else{for(Bt=gt+7;dt<Bt;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}dt-=gt,yt=0,ut=11+(127&(_>>>=gt)),_>>>=7,dt-=7}if(a.have+ut>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=ot;break}for(;ut--;)a.lens[a.have++]=yt}}if(a.mode===ot)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=ot;break}if(a.lenbits=9,zt={bits:a.lenbits},xt=m(p,a.lens,0,a.nlen,a.lencode,0,a.work,zt),a.lenbits=zt.bits,xt){t.msg="invalid literal/lengths set",a.mode=ot;break}if(a.distbits=6,a.distcode=a.distdyn,zt={bits:a.distbits},xt=m(v,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,zt),a.distbits=zt.bits,xt){t.msg="invalid distances set",a.mode=ot;break}if(a.mode=Q,e===x)break t;case Q:a.mode=V;case V:if(l>=6&&f>=258){t.next_out=o,t.avail_out=f,t.next_in=s,t.avail_in=l,a.hold=_,a.bits=dt,g(t,_t),o=t.next_out,r=t.output,f=t.avail_out,s=t.next_in,n=t.input,l=t.avail_in,_=a.hold,dt=a.bits,a.mode===M&&(a.back=-1);break}for(a.back=0;St=a.lencode[_&(1<<a.lenbits)-1],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(gt<=dt);){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(mt&&0==(240&mt)){for(pt=gt,vt=mt,kt=wt;St=a.lencode[kt+((_&(1<<pt+vt)-1)>>pt)],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(pt+gt<=dt);){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}_>>>=pt,dt-=pt,a.back+=pt}if(_>>>=gt,dt-=gt,a.back+=gt,a.length=wt,0===mt){a.mode=it;break}if(32&mt){a.back=-1,a.mode=M;break}if(64&mt){t.msg="invalid literal/length code",a.mode=ot;break}a.extra=15&mt,a.mode=$;case $:if(a.extra){for(Bt=a.extra;dt<Bt;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}a.length+=_&(1<<a.extra)-1,_>>>=a.extra,dt-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=tt;case tt:for(;St=a.distcode[_&(1<<a.distbits)-1],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(gt<=dt);){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(0==(240&mt)){for(pt=gt,vt=mt,kt=wt;St=a.distcode[kt+((_&(1<<pt+vt)-1)>>pt)],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(pt+gt<=dt);){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}_>>>=pt,dt-=pt,a.back+=pt}if(_>>>=gt,dt-=gt,a.back+=gt,64&mt){t.msg="invalid distance code",a.mode=ot;break}a.offset=wt,a.extra=15&mt,a.mode=et;case et:if(a.extra){for(Bt=a.extra;dt<Bt;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}a.offset+=_&(1<<a.extra)-1,_>>>=a.extra,dt-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=ot;break}a.mode=at;case at:if(0===f)break t;if(ut=_t-f,a.offset>ut){if((ut=a.offset-ut)>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=ot;break}ut>a.wnext?(ut-=a.wnext,ct=a.wsize-ut):ct=a.wnext-ut,ut>a.length&&(ut=a.length),bt=a.window}else bt=r,ct=o-a.offset,ut=a.length;ut>f&&(ut=f),f-=ut,a.length-=ut;do{r[o++]=bt[ct++]}while(--ut);0===a.length&&(a.mode=V);break;case it:if(0===f)break t;r[o++]=a.length,f--,a.mode=V;break;case nt:if(a.wrap){for(;dt<32;){if(0===l)break t;l--,_|=n[s++]<<dt,dt+=8}if(_t-=f,t.total_out+=_t,a.total+=_t,_t&&(t.adler=a.check=a.flags?b(a.check,r,_t,o-_t):c(a.check,r,_t,o-_t)),_t=f,(a.flags?_:i(_))!==a.check){t.msg="incorrect data check",a.mode=ot;break}_=0,dt=0}a.mode=rt;case rt:if(a.wrap&&a.flags){for(;dt<32;){if(0===l)break t;l--,_+=n[s++]<<dt,dt+=8}if(_!==(4294967295&a.total)){t.msg="incorrect length check",a.mode=ot;break}_=0,dt=0}a.mode=st;case st:xt=B;break t;case ot:xt=A;break t;case lt:return Z;case ht:default:return E}return t.next_out=o,t.avail_out=f,t.next_in=s,t.avail_in=l,a.hold=_,a.bits=dt,(a.wsize||_t!==t.avail_out&&a.mode<ot&&(a.mode<nt||e!==k))&&d(t,t.output,t.next_out,_t-t.avail_out)?(a.mode=lt,Z):(ft-=t.avail_in,_t-=t.avail_out,t.total_in+=ft,t.total_out+=_t,a.total+=_t,a.wrap&&_t&&(t.adler=a.check=a.flags?b(a.check,r,_t,t.next_out-_t):c(a.check,r,_t,t.next_out-_t)),t.data_type=a.bits+(a.last?64:0)+(a.mode===M?128:0)+(a.mode===Q||a.mode===q?256:0),(0===ft&&0===_t||e===k)&&xt===z&&(xt=R),xt)},a.inflateEnd=function(t){if(!t||!t.state)return E;var e=t.state;return e.window&&(e.window=null),t.state=null,z},a.inflateGetHeader=function(t,e){var a;return t&&t.state?0==(2&(a=t.state).wrap)?E:(a.head=e,e.done=!1,z):E},a.inflateSetDictionary=function(t,e){var a,i,n=e.length;return t&&t.state?0!==(a=t.state).wrap&&a.mode!==K?E:a.mode===K&&(i=1,(i=c(i,e,n,0))!==a.check)?A:d(t,e,n,n)?(a.mode=lt,Z):(a.havedict=1,z):E},a.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":3,"./adler32":5,"./crc32":7,"./inffast":10,"./inftrees":12}],12:[function(t,e,a){"use strict";var i=t("../utils/common"),n=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],r=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],s=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],o=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];e.exports=function(t,e,a,l,h,d,f,_){var u,c,b,g,m,w,p,v,k,y=_.bits,x=0,z=0,B=0,S=0,E=0,A=0,Z=0,R=0,C=0,N=0,O=null,D=0,I=new i.Buf16(16),U=new i.Buf16(16),T=null,F=0;for(x=0;x<=15;x++)I[x]=0;for(z=0;z<l;z++)I[e[a+z]]++;for(E=y,S=15;S>=1&&0===I[S];S--);if(E>S&&(E=S),0===S)return h[d++]=20971520,h[d++]=20971520,_.bits=1,0;for(B=1;B<S&&0===I[B];B++);for(E<B&&(E=B),R=1,x=1;x<=15;x++)if(R<<=1,(R-=I[x])<0)return-1;if(R>0&&(0===t||1!==S))return-1;for(U[1]=0,x=1;x<15;x++)U[x+1]=U[x]+I[x];for(z=0;z<l;z++)0!==e[a+z]&&(f[U[e[a+z]]++]=z);if(0===t?(O=T=f,w=19):1===t?(O=n,D-=257,T=r,F-=257,w=256):(O=s,T=o,w=-1),N=0,z=0,x=B,m=d,A=E,Z=0,b=-1,C=1<<E,g=C-1,1===t&&C>852||2===t&&C>592)return 1;for(;;){p=x-Z,f[z]<w?(v=0,k=f[z]):f[z]>w?(v=T[F+f[z]],k=O[D+f[z]]):(v=96,k=0),u=1<<x-Z,B=c=1<<A;do{h[m+(N>>Z)+(c-=u)]=p<<24|v<<16|k|0}while(0!==c);for(u=1<<x-1;N&u;)u>>=1;if(0!==u?(N&=u-1,N+=u):N=0,z++,0==--I[x]){if(x===S)break;x=e[a+f[z]]}if(x>E&&(N&g)!==b){for(0===Z&&(Z=E),m+=B,R=1<<(A=x-Z);A+Z<S&&!((R-=I[A+Z])<=0);)A++,R<<=1;if(C+=1<<A,1===t&&C>852||2===t&&C>592)return 1;h[b=N&g]=E<<24|A<<16|m-d|0}}return 0!==N&&(h[m+N]=x-Z<<24|64<<16|0),_.bits=E,0}},{"../utils/common":3}],13:[function(t,e,a){"use strict";e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],14:[function(t,e,a){"use strict";function i(t){for(var e=t.length;--e>=0;)t[e]=0}function n(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}function r(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function s(t){return t<256?et[t]:et[256+(t>>>7)]}function o(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function l(t,e,a){t.bi_valid>M-a?(t.bi_buf|=e<<t.bi_valid&65535,o(t,t.bi_buf),t.bi_buf=e>>M-t.bi_valid,t.bi_valid+=a-M):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=a)}function h(t,e,a){l(t,a[2*e],a[2*e+1])}function d(t,e){var a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1}function f(t){16===t.bi_valid?(o(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}function _(t,e){var a,i,n,r,s,o,l=e.dyn_tree,h=e.max_code,d=e.stat_desc.static_tree,f=e.stat_desc.has_stree,_=e.stat_desc.extra_bits,u=e.stat_desc.extra_base,c=e.stat_desc.max_length,b=0;for(r=0;r<=K;r++)t.bl_count[r]=0;for(l[2*t.heap[t.heap_max]+1]=0,a=t.heap_max+1;a<j;a++)(r=l[2*l[2*(i=t.heap[a])+1]+1]+1)>c&&(r=c,b++),l[2*i+1]=r,i>h||(t.bl_count[r]++,s=0,i>=u&&(s=_[i-u]),o=l[2*i],t.opt_len+=o*(r+s),f&&(t.static_len+=o*(d[2*i+1]+s)));if(0!==b){do{for(r=c-1;0===t.bl_count[r];)r--;t.bl_count[r]--,t.bl_count[r+1]+=2,t.bl_count[c]--,b-=2}while(b>0);for(r=c;0!==r;r--)for(i=t.bl_count[r];0!==i;)(n=t.heap[--a])>h||(l[2*n+1]!==r&&(t.opt_len+=(r-l[2*n+1])*l[2*n],l[2*n+1]=r),i--)}}function u(t,e,a){var i,n,r=new Array(K+1),s=0;for(i=1;i<=K;i++)r[i]=s=s+a[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=d(r[o]++,o))}}function c(){var t,e,a,i,r,s=new Array(K+1);for(a=0,i=0;i<U-1;i++)for(it[i]=a,t=0;t<1<<W[i];t++)at[a++]=i;for(at[a-1]=i,r=0,i=0;i<16;i++)for(nt[i]=r,t=0;t<1<<J[i];t++)et[r++]=i;for(r>>=7;i<L;i++)for(nt[i]=r<<7,t=0;t<1<<J[i]-7;t++)et[256+r++]=i;for(e=0;e<=K;e++)s[e]=0;for(t=0;t<=143;)$[2*t+1]=8,t++,s[8]++;for(;t<=255;)$[2*t+1]=9,t++,s[9]++;for(;t<=279;)$[2*t+1]=7,t++,s[7]++;for(;t<=287;)$[2*t+1]=8,t++,s[8]++;for(u($,F+1,s),t=0;t<L;t++)tt[2*t+1]=5,tt[2*t]=d(t,5);rt=new n($,W,T+1,F,K),st=new n(tt,J,0,L,K),ot=new n(new Array(0),Q,0,H,P)}function b(t){var e;for(e=0;e<F;e++)t.dyn_ltree[2*e]=0;for(e=0;e<L;e++)t.dyn_dtree[2*e]=0;for(e=0;e<H;e++)t.bl_tree[2*e]=0;t.dyn_ltree[2*Y]=1,t.opt_len=t.static_len=0,t.last_lit=t.matches=0}function g(t){t.bi_valid>8?o(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function m(t,e,a,i){g(t),i&&(o(t,a),o(t,~a)),A.arraySet(t.pending_buf,t.window,e,a,t.pending),t.pending+=a}function w(t,e,a,i){var n=2*e,r=2*a;return t[n]<t[r]||t[n]===t[r]&&i[e]<=i[a]}function p(t,e,a){for(var i=t.heap[a],n=a<<1;n<=t.heap_len&&(n<t.heap_len&&w(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!w(e,i,t.heap[n],t.depth));)t.heap[a]=t.heap[n],a=n,n<<=1;t.heap[a]=i}function v(t,e,a){var i,n,r,o,d=0;if(0!==t.last_lit)do{i=t.pending_buf[t.d_buf+2*d]<<8|t.pending_buf[t.d_buf+2*d+1],n=t.pending_buf[t.l_buf+d],d++,0===i?h(t,n,e):(h(t,(r=at[n])+T+1,e),0!==(o=W[r])&&l(t,n-=it[r],o),h(t,r=s(--i),a),0!==(o=J[r])&&l(t,i-=nt[r],o))}while(d<t.last_lit);h(t,Y,e)}function k(t,e){var a,i,n,r=e.dyn_tree,s=e.stat_desc.static_tree,o=e.stat_desc.has_stree,l=e.stat_desc.elems,h=-1;for(t.heap_len=0,t.heap_max=j,a=0;a<l;a++)0!==r[2*a]?(t.heap[++t.heap_len]=h=a,t.depth[a]=0):r[2*a+1]=0;for(;t.heap_len<2;)r[2*(n=t.heap[++t.heap_len]=h<2?++h:0)]=1,t.depth[n]=0,t.opt_len--,o&&(t.static_len-=s[2*n+1]);for(e.max_code=h,a=t.heap_len>>1;a>=1;a--)p(t,r,a);n=l;do{a=t.heap[1],t.heap[1]=t.heap[t.heap_len--],p(t,r,1),i=t.heap[1],t.heap[--t.heap_max]=a,t.heap[--t.heap_max]=i,r[2*n]=r[2*a]+r[2*i],t.depth[n]=(t.depth[a]>=t.depth[i]?t.depth[a]:t.depth[i])+1,r[2*a+1]=r[2*i+1]=n,t.heap[1]=n++,p(t,r,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],_(t,e),u(r,h,t.bl_count)}function y(t,e,a){var i,n,r=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=s,s=e[2*(i+1)+1],++o<l&&n===s||(o<h?t.bl_tree[2*n]+=o:0!==n?(n!==r&&t.bl_tree[2*n]++,t.bl_tree[2*q]++):o<=10?t.bl_tree[2*G]++:t.bl_tree[2*X]++,o=0,r=n,0===s?(l=138,h=3):n===s?(l=6,h=3):(l=7,h=4))}function x(t,e,a){var i,n,r=-1,s=e[1],o=0,d=7,f=4;for(0===s&&(d=138,f=3),i=0;i<=a;i++)if(n=s,s=e[2*(i+1)+1],!(++o<d&&n===s)){if(o<f)do{h(t,n,t.bl_tree)}while(0!=--o);else 0!==n?(n!==r&&(h(t,n,t.bl_tree),o--),h(t,q,t.bl_tree),l(t,o-3,2)):o<=10?(h(t,G,t.bl_tree),l(t,o-3,3)):(h(t,X,t.bl_tree),l(t,o-11,7));o=0,r=n,0===s?(d=138,f=3):n===s?(d=6,f=3):(d=7,f=4)}}function z(t){var e;for(y(t,t.dyn_ltree,t.l_desc.max_code),y(t,t.dyn_dtree,t.d_desc.max_code),k(t,t.bl_desc),e=H-1;e>=3&&0===t.bl_tree[2*V[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}function B(t,e,a,i){var n;for(l(t,e-257,5),l(t,a-1,5),l(t,i-4,4),n=0;n<i;n++)l(t,t.bl_tree[2*V[n]+1],3);x(t,t.dyn_ltree,e-1),x(t,t.dyn_dtree,a-1)}function S(t){var e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return R;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return C;for(e=32;e<T;e++)if(0!==t.dyn_ltree[2*e])return C;return R}function E(t,e,a,i){l(t,(O<<1)+(i?1:0),3),m(t,e,a,!0)}var A=t("../utils/common"),Z=4,R=0,C=1,N=2,O=0,D=1,I=2,U=29,T=256,F=T+1+U,L=30,H=19,j=2*F+1,K=15,M=16,P=7,Y=256,q=16,G=17,X=18,W=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],J=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Q=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],V=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],$=new Array(2*(F+2));i($);var tt=new Array(2*L);i(tt);var et=new Array(512);i(et);var at=new Array(256);i(at);var it=new Array(U);i(it);var nt=new Array(L);i(nt);var rt,st,ot,lt=!1;a._tr_init=function(t){lt||(c(),lt=!0),t.l_desc=new r(t.dyn_ltree,rt),t.d_desc=new r(t.dyn_dtree,st),t.bl_desc=new r(t.bl_tree,ot),t.bi_buf=0,t.bi_valid=0,b(t)},a._tr_stored_block=E,a._tr_flush_block=function(t,e,a,i){var n,r,s=0;t.level>0?(t.strm.data_type===N&&(t.strm.data_type=S(t)),k(t,t.l_desc),k(t,t.d_desc),s=z(t),n=t.opt_len+3+7>>>3,(r=t.static_len+3+7>>>3)<=n&&(n=r)):n=r=a+5,a+4<=n&&-1!==e?E(t,e,a,i):t.strategy===Z||r===n?(l(t,(D<<1)+(i?1:0),3),v(t,$,tt)):(l(t,(I<<1)+(i?1:0),3),B(t,t.l_desc.max_code+1,t.d_desc.max_code+1,s+1),v(t,t.dyn_ltree,t.dyn_dtree)),b(t),i&&g(t)},a._tr_tally=function(t,e,a){return t.pending_buf[t.d_buf+2*t.last_lit]=e>>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&a,t.last_lit++,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(at[a]+T+1)]++,t.dyn_dtree[2*s(e)]++),t.last_lit===t.lit_bufsize-1},a._tr_align=function(t){l(t,D<<1,3),h(t,Y,$),f(t)}},{"../utils/common":3}],15:[function(t,e,a){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],"/":[function(t,e,a){"use strict";var i={};(0,t("./lib/utils/common").assign)(i,t("./lib/deflate"),t("./lib/inflate"),t("./lib/zlib/constants")),e.exports=i},{"./lib/deflate":1,"./lib/inflate":2,"./lib/utils/common":3,"./lib/zlib/constants":6}]},{},[])("/")});
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/README.chromium b/chromium/third_party/catapult/tracing/third_party/symbols/README.chromium
new file mode 100644
index 00000000000..b50ed33a8e4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/README.chromium
@@ -0,0 +1,21 @@
+Name: symbols module from Chromium
+URL: https://chromium.googlesource.com/chromium/src.git
+License: BSD
+
+Description:
+
+Originally added to src/build/android/pylib/symbols by Primiano Tucci:
+
+commit eb3ad0de43537c5bff297326b11b121c77cc18e6
+Author: primiano@chromium.org
+Date: Mon Feb 24 19:23:59 2014 +0000
+
+ Add fast ELF Symbolizer to memory_inspector.
+
+ This CL introduces a multiprocess, pipelined and asynchronous ELF
+ symbolizer (based on addr2line) which gives honor to a bulkly workstation
+ when symbolizing large batches of symbols.
+
+ BUG=340294,339059
+
+ Review URL: https://codereview.chromium.org/167893009
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/symbols/PRESUBMIT.py b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/PRESUBMIT.py
new file mode 100644
index 00000000000..b4d94ae225b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/PRESUBMIT.py
@@ -0,0 +1,21 @@
+# Copyright 2014 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.
+
+def CommonChecks(input_api, output_api):
+ output = []
+ output.extend(input_api.canned_checks.RunPylint(input_api, output_api))
+ output.extend(input_api.canned_checks.RunUnitTestsInDirectory(
+ input_api,
+ output_api,
+ input_api.PresubmitLocalPath(),
+ whitelist=[r'^.+_unittest\.py$']))
+ return output
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return CommonChecks(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return CommonChecks(input_api, output_api) \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/symbols/__init__.py b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/__init__.py
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer.py b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer.py
new file mode 100644
index 00000000000..9deec868368
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer.py
@@ -0,0 +1,470 @@
+# Copyright 2014 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 collections
+import datetime
+import logging
+import multiprocessing
+import os
+import posixpath
+import re
+import subprocess
+import sys
+import threading
+import time
+try:
+ import Queue
+except ImportError:
+ import queue as Queue
+
+
+# addr2line builds a possibly infinite memory cache that can exhaust
+# the computer's memory if allowed to grow for too long. This constant
+# controls how many lookups we do before restarting the process. 4000
+# gives near peak performance without extreme memory usage.
+ADDR2LINE_RECYCLE_LIMIT = 4000
+
+
+class ELFSymbolizer(object):
+ """An uber-fast (multiprocessing, pipelined and asynchronous) ELF symbolizer.
+
+ This class is a frontend for addr2line (part of GNU binutils), designed to
+ symbolize batches of large numbers of symbols for a given ELF file. It
+ supports sharding symbolization against many addr2line instances and
+ pipelining of multiple requests per each instance (in order to hide addr2line
+ internals and OS pipe latencies).
+
+ The interface exhibited by this class is a very simple asynchronous interface,
+ which is based on the following three methods:
+ - SymbolizeAsync(): used to request (enqueue) resolution of a given address.
+ - The |callback| method: used to communicated back the symbol information.
+ - Join(): called to conclude the batch to gather the last outstanding results.
+ In essence, before the Join method returns, this class will have issued as
+ many callbacks as the number of SymbolizeAsync() calls. In this regard, note
+ that due to multiprocess sharding, callbacks can be delivered out of order.
+
+ Some background about addr2line:
+ - it is invoked passing the elf path in the cmdline, piping the addresses in
+ its stdin and getting results on its stdout.
+ - it has pretty large response times for the first requests, but it
+ works very well in streaming mode once it has been warmed up.
+ - it doesn't scale by itself (on more cores). However, spawning multiple
+ instances at the same time on the same file is pretty efficient as they
+ keep hitting the pagecache and become mostly CPU bound.
+ - it might hang or crash, mostly for OOM. This class deals with both of these
+ problems.
+
+ Despite the "scary" imports and the multi* words above, (almost) no multi-
+ threading/processing is involved from the python viewpoint. Concurrency
+ here is achieved by spawning several addr2line subprocesses and handling their
+ output pipes asynchronously. Therefore, all the code here (with the exception
+ of the Queue instance in Addr2Line) should be free from mind-blowing
+ thread-safety concerns.
+
+ The multiprocess sharding works as follows:
+ The symbolizer tries to use the lowest number of addr2line instances as
+ possible (with respect of |max_concurrent_jobs|) and enqueue all the requests
+ in a single addr2line instance. For few symbols (i.e. dozens) sharding isn't
+ worth the startup cost.
+ The multiprocess logic kicks in as soon as the queues for the existing
+ instances grow. Specifically, once all the existing instances reach the
+ |max_queue_size| bound, a new addr2line instance is kicked in.
+ In the case of a very eager producer (i.e. all |max_concurrent_jobs| instances
+ have a backlog of |max_queue_size|), back-pressure is applied on the caller by
+ blocking the SymbolizeAsync method.
+
+ This module has been deliberately designed to be dependency free (w.r.t. of
+ other modules in this project), to allow easy reuse in external projects.
+ """
+
+ def __init__(self, elf_file_path, addr2line_path, callback, inlines=False,
+ max_concurrent_jobs=None, addr2line_timeout=30, max_queue_size=50,
+ source_root_path=None, strip_base_path=None):
+ """Args:
+ elf_file_path: path of the elf file to be symbolized.
+ addr2line_path: path of the toolchain's addr2line binary.
+ callback: a callback which will be invoked for each resolved symbol with
+ the two args (sym_info, callback_arg). The former is an instance of
+ |ELFSymbolInfo| and contains the symbol information. The latter is an
+ embedder-provided argument which is passed to SymbolizeAsync().
+ inlines: when True, the ELFSymbolInfo will contain also the details about
+ the outer inlining functions. When False, only the innermost function
+ will be provided.
+ max_concurrent_jobs: Max number of addr2line instances spawned.
+ Parallelize responsibly, addr2line is a memory and I/O monster.
+ max_queue_size: Max number of outstanding requests per addr2line instance.
+ addr2line_timeout: Max time (in seconds) to wait for a addr2line response.
+ After the timeout, the instance will be considered hung and respawned.
+ source_root_path: In some toolchains only the name of the source file is
+ is output, without any path information; disambiguation searches
+ through the source directory specified by |source_root_path| argument
+ for files whose name matches, adding the full path information to the
+ output. For example, if the toolchain outputs "unicode.cc" and there
+ is a file called "unicode.cc" located under |source_root_path|/foo,
+ the tool will replace "unicode.cc" with
+ "|source_root_path|/foo/unicode.cc". If there are multiple files with
+ the same name, disambiguation will fail because the tool cannot
+ determine which of the files was the source of the symbol.
+ strip_base_path: Rebases the symbols source paths onto |source_root_path|
+ (i.e replace |strip_base_path| with |source_root_path).
+ """
+ assert(os.path.isfile(addr2line_path)), 'Cannot find ' + addr2line_path
+ self.elf_file_path = elf_file_path
+ self.addr2line_path = addr2line_path
+ self.callback = callback
+ self.inlines = inlines
+ self.max_concurrent_jobs = (max_concurrent_jobs or
+ min(multiprocessing.cpu_count(), 4))
+ self.max_queue_size = max_queue_size
+ self.addr2line_timeout = addr2line_timeout
+ self.requests_counter = 0 # For generating monotonic request IDs.
+ self._a2l_instances = [] # Up to |max_concurrent_jobs| _Addr2Line inst.
+
+ # If necessary, create disambiguation lookup table
+ self.disambiguate = source_root_path is not None
+ self.disambiguation_table = {}
+ self.strip_base_path = strip_base_path
+ if(self.disambiguate):
+ self.source_root_path = os.path.abspath(source_root_path)
+ self._CreateDisambiguationTable()
+
+ # Create one addr2line instance. More instances will be created on demand
+ # (up to |max_concurrent_jobs|) depending on the rate of the requests.
+ self._CreateNewA2LInstance()
+
+ def SymbolizeAsync(self, addr, callback_arg=None):
+ """Requests symbolization of a given address.
+
+ This method is not guaranteed to return immediately. It generally does, but
+ in some scenarios (e.g. all addr2line instances have full queues) it can
+ block to create back-pressure.
+
+ Args:
+ addr: address to symbolize.
+ callback_arg: optional argument which will be passed to the |callback|."""
+ assert(isinstance(addr, int))
+
+ # Process all the symbols that have been resolved in the meanwhile.
+ # Essentially, this drains all the addr2line(s) out queues.
+ for a2l_to_purge in self._a2l_instances:
+ a2l_to_purge.ProcessAllResolvedSymbolsInQueue()
+ a2l_to_purge.RecycleIfNecessary()
+
+ # Find the best instance according to this logic:
+ # 1. Find an existing instance with the shortest queue.
+ # 2. If all of instances' queues are full, but there is room in the pool,
+ # (i.e. < |max_concurrent_jobs|) create a new instance.
+ # 3. If there were already |max_concurrent_jobs| instances and all of them
+ # had full queues, make back-pressure.
+
+ # 1.
+ def _SortByQueueSizeAndReqID(a2l):
+ return (a2l.queue_size, a2l.first_request_id)
+ a2l = min(self._a2l_instances, key=_SortByQueueSizeAndReqID)
+
+ # 2.
+ if (a2l.queue_size >= self.max_queue_size and
+ len(self._a2l_instances) < self.max_concurrent_jobs):
+ a2l = self._CreateNewA2LInstance()
+
+ # 3.
+ if a2l.queue_size >= self.max_queue_size:
+ a2l.WaitForNextSymbolInQueue()
+
+ a2l.EnqueueRequest(addr, callback_arg)
+
+ def Join(self):
+ """Waits for all the outstanding requests to complete and terminates."""
+ for a2l in self._a2l_instances:
+ a2l.WaitForIdle()
+ a2l.Terminate()
+
+ def _CreateNewA2LInstance(self):
+ assert(len(self._a2l_instances) < self.max_concurrent_jobs)
+ a2l = ELFSymbolizer.Addr2Line(self)
+ self._a2l_instances.append(a2l)
+ return a2l
+
+ def _CreateDisambiguationTable(self):
+ """ Non-unique file names will result in None entries"""
+ start_time = time.time()
+ logging.info('Collecting information about available source files...')
+ self.disambiguation_table = {}
+
+ for root, _, filenames in os.walk(self.source_root_path):
+ for f in filenames:
+ self.disambiguation_table[f] = os.path.join(root, f) if (f not in
+ self.disambiguation_table) else None
+ logging.info('Finished collecting information about '
+ 'possible files (took %.1f s).',
+ (time.time() - start_time))
+
+
+ class Addr2Line(object):
+ """A python wrapper around an addr2line instance.
+
+ The communication with the addr2line process looks as follows:
+ [STDIN] [STDOUT] (from addr2line's viewpoint)
+ > f001111
+ > f002222
+ < Symbol::Name(foo, bar) for f001111
+ < /path/to/source/file.c:line_number
+ > f003333
+ < Symbol::Name2() for f002222
+ < /path/to/source/file.c:line_number
+ < Symbol::Name3() for f003333
+ < /path/to/source/file.c:line_number
+ """
+
+ SYM_ADDR_RE = re.compile(r'([^:]+):(\?|\d+).*')
+
+ def __init__(self, symbolizer):
+ self._symbolizer = symbolizer
+ self._lib_file_name = posixpath.basename(symbolizer.elf_file_path)
+
+ # The request queue (i.e. addresses pushed to addr2line's stdin and not
+ # yet retrieved on stdout)
+ self._request_queue = collections.deque()
+
+ # This is essentially len(self._request_queue). It has been optimized to a
+ # separate field because turned out to be a perf hot-spot.
+ self.queue_size = 0
+
+ # Keep track of the number of symbols a process has processed to
+ # avoid a single process growing too big and using all the memory.
+ self._processed_symbols_count = 0
+
+ # Objects required to handle the addr2line subprocess.
+ self._proc = None # Subprocess.Popen(...) instance.
+ self._thread = None # Threading.thread instance.
+ self._out_queue = None # Queue.Queue instance (for buffering a2l stdout).
+ self._RestartAddr2LineProcess()
+
+ def EnqueueRequest(self, addr, callback_arg):
+ """Pushes an address to addr2line's stdin (and keeps track of it)."""
+ self._symbolizer.requests_counter += 1 # For global "age" of requests.
+ req_idx = self._symbolizer.requests_counter
+ self._request_queue.append((addr, callback_arg, req_idx))
+ self.queue_size += 1
+ self._WriteToA2lStdin(addr)
+
+ def WaitForIdle(self):
+ """Waits until all the pending requests have been symbolized."""
+ while self.queue_size > 0:
+ self.WaitForNextSymbolInQueue()
+
+ def WaitForNextSymbolInQueue(self):
+ """Waits for the next pending request to be symbolized."""
+ if not self.queue_size:
+ return
+
+ # This outer loop guards against a2l hanging (detecting stdout timeout).
+ while True:
+ start_time = datetime.datetime.now()
+ timeout = datetime.timedelta(seconds=self._symbolizer.addr2line_timeout)
+
+ # The inner loop guards against a2l crashing (checking if it exited).
+ while (datetime.datetime.now() - start_time < timeout):
+ # poll() returns !None if the process exited. a2l should never exit.
+ if self._proc.poll():
+ logging.warning('addr2line crashed, respawning (lib: %s).' %
+ self._lib_file_name)
+ self._RestartAddr2LineProcess()
+ # TODO(primiano): the best thing to do in this case would be
+ # shrinking the pool size as, very likely, addr2line is crashed
+ # due to low memory (and the respawned one will die again soon).
+
+ try:
+ lines = self._out_queue.get(block=True, timeout=0.25)
+ except Queue.Empty:
+ # On timeout (1/4 s.) repeat the inner loop and check if either the
+ # addr2line process did crash or we waited its output for too long.
+ continue
+
+ # In nominal conditions, we get straight to this point.
+ self._ProcessSymbolOutput(lines)
+ return
+
+ # If this point is reached, we waited more than |addr2line_timeout|.
+ logging.warning('Hung addr2line process, respawning (lib: %s).' %
+ self._lib_file_name)
+ self._RestartAddr2LineProcess()
+
+ def ProcessAllResolvedSymbolsInQueue(self):
+ """Consumes all the addr2line output lines produced (without blocking)."""
+ if not self.queue_size:
+ return
+ while True:
+ try:
+ lines = self._out_queue.get_nowait()
+ except Queue.Empty:
+ break
+ self._ProcessSymbolOutput(lines)
+
+ def RecycleIfNecessary(self):
+ """Restarts the process if it has been used for too long.
+
+ A long running addr2line process will consume excessive amounts
+ of memory without any gain in performance."""
+ if self._processed_symbols_count >= ADDR2LINE_RECYCLE_LIMIT:
+ self._RestartAddr2LineProcess()
+
+
+ def Terminate(self):
+ """Kills the underlying addr2line process.
+
+ The poller |_thread| will terminate as well due to the broken pipe."""
+ try:
+ self._proc.kill()
+ self._proc.communicate() # Essentially wait() without risking deadlock.
+ except Exception: # An exception while terminating? How interesting.
+ pass
+ self._proc = None
+
+ def _WriteToA2lStdin(self, addr):
+ self._proc.stdin.write('%s\n' % hex(addr))
+ if self._symbolizer.inlines:
+ # In the case of inlines we output an extra blank line, which causes
+ # addr2line to emit a (??,??:0) tuple that we use as a boundary marker.
+ self._proc.stdin.write('\n')
+ self._proc.stdin.flush()
+
+ def _ProcessSymbolOutput(self, lines):
+ """Parses an addr2line symbol output and triggers the client callback."""
+ (_, callback_arg, _) = self._request_queue.popleft()
+ self.queue_size -= 1
+
+ innermost_sym_info = None
+ sym_info = None
+ for (line1, line2) in lines:
+ prev_sym_info = sym_info
+ name = line1 if not line1.startswith('?') else None
+ source_path = None
+ source_line = None
+ m = ELFSymbolizer.Addr2Line.SYM_ADDR_RE.match(line2)
+ if m:
+ if not m.group(1).startswith('?'):
+ source_path = m.group(1)
+ if not m.group(2).startswith('?'):
+ source_line = int(m.group(2))
+ else:
+ logging.warning('Got invalid symbol path from addr2line: %s' % line2)
+
+ # In case disambiguation is on, and needed
+ was_ambiguous = False
+ disambiguated = False
+ if self._symbolizer.disambiguate:
+ if source_path and not posixpath.isabs(source_path):
+ path = self._symbolizer.disambiguation_table.get(source_path)
+ was_ambiguous = True
+ disambiguated = path is not None
+ source_path = path if disambiguated else source_path
+
+ # Use absolute paths (so that paths are consistent, as disambiguation
+ # uses absolute paths)
+ if source_path and not was_ambiguous:
+ source_path = os.path.abspath(source_path)
+
+ if source_path and self._symbolizer.strip_base_path:
+ # Strip the base path
+ source_path = re.sub('^' + self._symbolizer.strip_base_path,
+ self._symbolizer.source_root_path or '', source_path)
+
+ sym_info = ELFSymbolInfo(name, source_path, source_line, was_ambiguous,
+ disambiguated)
+ if prev_sym_info:
+ prev_sym_info.inlined_by = sym_info
+ if not innermost_sym_info:
+ innermost_sym_info = sym_info
+
+ self._processed_symbols_count += 1
+ self._symbolizer.callback(innermost_sym_info, callback_arg)
+
+ def _RestartAddr2LineProcess(self):
+ if self._proc:
+ self.Terminate()
+
+ # The only reason of existence of this Queue (and the corresponding
+ # Thread below) is the lack of a subprocess.stdout.poll_avail_lines().
+ # Essentially this is a pipe able to extract a couple of lines atomically.
+ self._out_queue = Queue.Queue()
+
+ # Start the underlying addr2line process in line buffered mode.
+
+ cmd = [self._symbolizer.addr2line_path, '--functions', '--demangle',
+ '--exe=' + self._symbolizer.elf_file_path]
+ if self._symbolizer.inlines:
+ cmd += ['--inlines']
+ self._proc = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE, stderr=sys.stderr, close_fds=True)
+
+ # Start the poller thread, which simply moves atomically the lines read
+ # from the addr2line's stdout to the |_out_queue|.
+ self._thread = threading.Thread(
+ target=ELFSymbolizer.Addr2Line.StdoutReaderThread,
+ args=(self._proc.stdout, self._out_queue, self._symbolizer.inlines))
+ self._thread.daemon = True # Don't prevent early process exit.
+ self._thread.start()
+
+ self._processed_symbols_count = 0
+
+ # Replay the pending requests on the new process (only for the case
+ # of a hung addr2line timing out during the game).
+ for (addr, _, _) in self._request_queue:
+ self._WriteToA2lStdin(addr)
+
+ @staticmethod
+ def StdoutReaderThread(process_pipe, queue, inlines):
+ """The poller thread fn, which moves the addr2line stdout to the |queue|.
+
+ This is the only piece of code not running on the main thread. It merely
+ writes to a Queue, which is thread-safe. In the case of inlines, it
+ detects the ??,??:0 marker and sends the lines atomically, such that the
+ main thread always receives all the lines corresponding to one symbol in
+ one shot."""
+ try:
+ lines_for_one_symbol = []
+ while True:
+ line1 = process_pipe.readline().rstrip('\r\n')
+ line2 = process_pipe.readline().rstrip('\r\n')
+ if not line1 or not line2:
+ break
+ inline_has_more_lines = inlines and (len(lines_for_one_symbol) == 0 or
+ (line1 != '??' and line2 != '??:0'))
+ if not inlines or inline_has_more_lines:
+ lines_for_one_symbol += [(line1, line2)]
+ if inline_has_more_lines:
+ continue
+ queue.put(lines_for_one_symbol)
+ lines_for_one_symbol = []
+ process_pipe.close()
+
+ # Every addr2line processes will die at some point, please die silently.
+ except (IOError, OSError):
+ pass
+
+ @property
+ def first_request_id(self):
+ """Returns the request_id of the oldest pending request in the queue."""
+ return self._request_queue[0][2] if self._request_queue else 0
+
+
+class ELFSymbolInfo(object):
+ """The result of the symbolization passed as first arg. of each callback."""
+
+ def __init__(self, name, source_path, source_line, was_ambiguous=False,
+ disambiguated=False):
+ """All the fields here can be None (if addr2line replies with '??')."""
+ self.name = name
+ self.source_path = source_path
+ self.source_line = source_line
+ # In the case of |inlines|=True, the |inlined_by| points to the outer
+ # function inlining the current one (and so on, to form a chain).
+ self.inlined_by = None
+ self.disambiguated = disambiguated
+ self.was_ambiguous = was_ambiguous
+
+ def __str__(self):
+ return '%s [%s:%d]' % (
+ self.name or '??', self.source_path or '??', self.source_line or 0)
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer_unittest.py b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer_unittest.py
new file mode 100755
index 00000000000..d7978aba0f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/elf_symbolizer_unittest.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python
+# Copyright 2014 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 os
+import sys
+import unittest
+
+sys.path.insert(0, os.path.dirname(__file__))
+# pylint: disable=relative-import
+import elf_symbolizer
+import mock_addr2line
+
+
+_MOCK_A2L_PATH = os.path.join(os.path.dirname(mock_addr2line.__file__),
+ 'mock_addr2line')
+_INCOMPLETE_MOCK_ADDR = 1024 * 1024
+_UNKNOWN_MOCK_ADDR = 2 * 1024 * 1024
+_INLINE_MOCK_ADDR = 3 * 1024 * 1024
+
+
+class ELFSymbolizerTest(unittest.TestCase):
+ def setUp(self):
+ self._callback = functools.partial(
+ ELFSymbolizerTest._SymbolizeCallback, self)
+ self._resolved_addresses = set()
+ # Mute warnings, we expect them due to the crash/hang tests.
+ logging.getLogger().setLevel(logging.ERROR)
+
+ def testParallelism1(self):
+ self._RunTest(max_concurrent_jobs=1, num_symbols=100)
+
+ def testParallelism4(self):
+ self._RunTest(max_concurrent_jobs=4, num_symbols=100)
+
+ def testParallelism8(self):
+ self._RunTest(max_concurrent_jobs=8, num_symbols=100)
+
+ def testCrash(self):
+ os.environ['MOCK_A2L_CRASH_EVERY'] = '99'
+ self._RunTest(max_concurrent_jobs=1, num_symbols=100)
+ os.environ['MOCK_A2L_CRASH_EVERY'] = '0'
+
+ def testHang(self):
+ os.environ['MOCK_A2L_HANG_EVERY'] = '99'
+ self._RunTest(max_concurrent_jobs=1, num_symbols=100)
+ os.environ['MOCK_A2L_HANG_EVERY'] = '0'
+
+ def testInlines(self):
+ """Stimulate the inline processing logic."""
+ symbolizer = elf_symbolizer.ELFSymbolizer(
+ elf_file_path='/path/doesnt/matter/mock_lib1.so',
+ addr2line_path=_MOCK_A2L_PATH,
+ callback=self._callback,
+ inlines=True,
+ max_concurrent_jobs=4)
+
+ for addr in xrange(1000):
+ exp_inline = False
+ exp_unknown = False
+
+ # First 100 addresses with inlines.
+ if addr < 100:
+ addr += _INLINE_MOCK_ADDR
+ exp_inline = True
+
+ # Followed by 100 without inlines.
+ elif addr < 200:
+ pass
+
+ # Followed by 100 interleaved inlines and not inlines.
+ elif addr < 300:
+ if addr & 1:
+ addr += _INLINE_MOCK_ADDR
+ exp_inline = True
+
+ # Followed by 100 interleaved inlines and unknonwn.
+ elif addr < 400:
+ if addr & 1:
+ addr += _INLINE_MOCK_ADDR
+ exp_inline = True
+ else:
+ addr += _UNKNOWN_MOCK_ADDR
+ exp_unknown = True
+
+ exp_name = 'mock_sym_for_addr_%d' % addr if not exp_unknown else None
+ exp_source_path = 'mock_src/mock_lib1.so.c' if not exp_unknown else None
+ exp_source_line = addr if not exp_unknown else None
+ cb_arg = (addr, exp_name, exp_source_path, exp_source_line, exp_inline)
+ symbolizer.SymbolizeAsync(addr, cb_arg)
+
+ symbolizer.Join()
+
+ def testIncompleteSyminfo(self):
+ """Stimulate the symbol-not-resolved logic."""
+ symbolizer = elf_symbolizer.ELFSymbolizer(
+ elf_file_path='/path/doesnt/matter/mock_lib1.so',
+ addr2line_path=_MOCK_A2L_PATH,
+ callback=self._callback,
+ max_concurrent_jobs=1)
+
+ # Test symbols with valid name but incomplete path.
+ addr = _INCOMPLETE_MOCK_ADDR
+ exp_name = 'mock_sym_for_addr_%d' % addr
+ exp_source_path = None
+ exp_source_line = None
+ cb_arg = (addr, exp_name, exp_source_path, exp_source_line, False)
+ symbolizer.SymbolizeAsync(addr, cb_arg)
+
+ # Test symbols with no name or sym info.
+ addr = _UNKNOWN_MOCK_ADDR
+ exp_name = None
+ exp_source_path = None
+ exp_source_line = None
+ cb_arg = (addr, exp_name, exp_source_path, exp_source_line, False)
+ symbolizer.SymbolizeAsync(addr, cb_arg)
+
+ symbolizer.Join()
+
+ def _RunTest(self, max_concurrent_jobs, num_symbols):
+ symbolizer = elf_symbolizer.ELFSymbolizer(
+ elf_file_path='/path/doesnt/matter/mock_lib1.so',
+ addr2line_path=_MOCK_A2L_PATH,
+ callback=self._callback,
+ max_concurrent_jobs=max_concurrent_jobs,
+ addr2line_timeout=0.5)
+
+ for addr in xrange(num_symbols):
+ exp_name = 'mock_sym_for_addr_%d' % addr
+ exp_source_path = 'mock_src/mock_lib1.so.c'
+ exp_source_line = addr
+ cb_arg = (addr, exp_name, exp_source_path, exp_source_line, False)
+ symbolizer.SymbolizeAsync(addr, cb_arg)
+
+ symbolizer.Join()
+
+ # Check that all the expected callbacks have been received.
+ for addr in xrange(num_symbols):
+ self.assertIn(addr, self._resolved_addresses)
+ self._resolved_addresses.remove(addr)
+
+ # Check for unexpected callbacks.
+ self.assertEqual(len(self._resolved_addresses), 0)
+
+ def _SymbolizeCallback(self, sym_info, cb_arg):
+ self.assertTrue(isinstance(sym_info, elf_symbolizer.ELFSymbolInfo))
+ self.assertTrue(isinstance(cb_arg, tuple))
+ self.assertEqual(len(cb_arg), 5)
+
+ # Unpack expectations from the callback extra argument.
+ (addr, exp_name, exp_source_path, exp_source_line, exp_inlines) = cb_arg
+ if exp_name is None:
+ self.assertIsNone(sym_info.name)
+ else:
+ self.assertTrue(sym_info.name.startswith(exp_name))
+ self.assertEqual(sym_info.source_path, exp_source_path)
+ self.assertEqual(sym_info.source_line, exp_source_line)
+
+ if exp_inlines:
+ self.assertEqual(sym_info.name, exp_name + '_inner')
+ self.assertEqual(sym_info.inlined_by.name, exp_name + '_middle')
+ self.assertEqual(sym_info.inlined_by.inlined_by.name,
+ exp_name + '_outer')
+
+ # Check against duplicate callbacks.
+ self.assertNotIn(addr, self._resolved_addresses)
+ self._resolved_addresses.add(addr)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/__init__.py b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/__init__.py
diff --git a/chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/mock_addr2line b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/mock_addr2line
new file mode 100755
index 00000000000..5544359ad60
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/third_party/symbols/symbols/mock_addr2line/mock_addr2line
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+# Copyright 2014 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.
+
+"""Simple mock for addr2line.
+
+Outputs mock symbol information, with each symbol being a function of the
+original address (so it is easy to double-check consistency in unittests).
+"""
+
+import optparse
+import os
+import posixpath
+import sys
+import time
+
+
+def main(argv):
+ parser = optparse.OptionParser()
+ parser.add_option('-e', '--exe', dest='exe') # Path of the debug-library.so.
+ # Silently swallow the other unnecessary arguments.
+ parser.add_option('-C', '--demangle', action='store_true')
+ parser.add_option('-f', '--functions', action='store_true')
+ parser.add_option('-i', '--inlines', action='store_true')
+ options, _ = parser.parse_args(argv[1:])
+ lib_file_name = posixpath.basename(options.exe)
+ processed_sym_count = 0
+ crash_every = int(os.environ.get('MOCK_A2L_CRASH_EVERY', 0))
+ hang_every = int(os.environ.get('MOCK_A2L_HANG_EVERY', 0))
+
+ while(True):
+ line = sys.stdin.readline().rstrip('\r')
+ if not line:
+ break
+
+ # An empty line should generate '??,??:0' (is used as marker for inlines).
+ if line == '\n':
+ print '??'
+ print '??:0'
+ sys.stdout.flush()
+ continue
+
+ addr = int(line, 16)
+ processed_sym_count += 1
+ if crash_every and processed_sym_count % crash_every == 0:
+ sys.exit(1)
+ if hang_every and processed_sym_count % hang_every == 0:
+ time.sleep(1)
+
+ # Addresses < 1M will return good mock symbol information.
+ if addr < 1024 * 1024:
+ print 'mock_sym_for_addr_%d' % addr
+ print 'mock_src/%s.c:%d' % (lib_file_name, addr)
+
+ # Addresses 1M <= x < 2M will return symbols with a name but a missing path.
+ elif addr < 2 * 1024 * 1024:
+ print 'mock_sym_for_addr_%d' % addr
+ print '??:0'
+
+ # Addresses 2M <= x < 3M will return unknown symbol information.
+ elif addr < 3 * 1024 * 1024:
+ print '??'
+ print '??'
+
+ # Addresses 3M <= x < 4M will return inlines.
+ elif addr < 4 * 1024 * 1024:
+ print 'mock_sym_for_addr_%d_inner' % addr
+ print 'mock_src/%s.c:%d' % (lib_file_name, addr)
+ print 'mock_sym_for_addr_%d_middle' % addr
+ print 'mock_src/%s.c:%d' % (lib_file_name, addr)
+ print 'mock_sym_for_addr_%d_outer' % addr
+ print 'mock_src/%s.c:%d' % (lib_file_name, addr)
+
+ else:
+ print 'mock_sym_for_addr_%d' % addr
+ print 'mock_src/%s.c:%d' % (lib_file_name, addr)
+
+ sys.stdout.flush()
+
+
+if __name__ == '__main__':
+ main(sys.argv) \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/__init__.py b/chromium/third_party/catapult/tracing/tracing/__init__.py
new file mode 100644
index 00000000000..76063aae790
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) 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 tracing_project
+tracing_project.UpdateSysPathIfNeeded()
diff --git a/chromium/third_party/catapult/tracing/tracing/base/assert_utils.html b/chromium/third_party/catapult/tracing/tracing/base/assert_utils.html
new file mode 100644
index 00000000000..1f0a9972d07
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/assert_utils.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<script>
+'use strict';
+
+// TODO(charliea): Investigate whether we could make this a Chai plugin.
+/**
+ * @fileoverview Provides assert functions that are not available on the Chai
+ * assert module.
+ */
+tr.exportTo('tr.b', function() {
+ /**
+ * Throws if the range |actual| is not equal to the range |expected| (using
+ * tr.b.math.Range.equals).
+ */
+ function assertRangeEquals(actual, expected) {
+ assert(actual.equals(expected),
+ 'Expected Range(' + actual.min + ', ' + actual.max + ') to be ' +
+ 'Range(' + expected.min + ', ' + expected.max + ')');
+ }
+
+ return {
+ assertRangeEquals,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/base.html b/chromium/third_party/catapult/tracing/tracing/base/base.html
new file mode 100644
index 00000000000..c313389b4d0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/base.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<script>
+'use strict';
+
+/**
+ * The global object.
+ * @type {!Object}
+ * @const
+ */
+const global = this.window || this.global;
+
+/** Platform, package, object property, and Event support. */
+this.tr = (function() {
+ if (global.tr) return global.tr;
+
+ /**
+ * Builds an object structure for the provided namespace path,
+ * ensuring that names that already exist are not overwritten. For
+ * example:
+ * 'a.b.c' -> a = {};a.b={};a.b.c={};
+ * @param {string} name Name of the object that this file defines.
+ * @private
+ */
+ function exportPath(name) {
+ const parts = name.split('.');
+ let cur = global;
+
+ for (let part; parts.length && (part = parts.shift());) {
+ if (part in cur) {
+ cur = cur[part];
+ } else {
+ cur = cur[part] = {};
+ }
+ }
+ return cur;
+ }
+
+ function isExported(name) {
+ const parts = name.split('.');
+ let cur = global;
+
+ for (let part; parts.length && (part = parts.shift());) {
+ if (part in cur) {
+ cur = cur[part];
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function isDefined(name) {
+ const parts = name.split('.');
+
+ let curObject = global;
+
+ for (let i = 0; i < parts.length; i++) {
+ const partName = parts[i];
+ const nextObject = curObject[partName];
+ if (nextObject === undefined) return false;
+ curObject = nextObject;
+ }
+ return true;
+ }
+
+ let panicElement = undefined;
+ const rawPanicMessages = [];
+ function showPanicElementIfNeeded() {
+ if (panicElement) return;
+
+ const panicOverlay = document.createElement('div');
+ panicOverlay.style.backgroundColor = 'white';
+ panicOverlay.style.border = '3px solid red';
+ panicOverlay.style.boxSizing = 'border-box';
+ panicOverlay.style.color = 'black';
+ panicOverlay.style.display = 'flex';
+ panicOverlay.style.height = '100%';
+ panicOverlay.style.left = 0;
+ panicOverlay.style.padding = '8px';
+ panicOverlay.style.position = 'fixed';
+ panicOverlay.style.top = 0;
+ panicOverlay.style.webkitFlexDirection = 'column';
+ panicOverlay.style.width = '100%';
+
+ panicElement = document.createElement('div');
+ panicElement.style.webkitFlex = '1 1 auto';
+ panicElement.style.overflow = 'auto';
+ panicOverlay.appendChild(panicElement);
+
+ if (!document.body) {
+ setTimeout(function() {
+ document.body.appendChild(panicOverlay);
+ }, 150);
+ } else {
+ document.body.appendChild(panicOverlay);
+ }
+ }
+
+ function showPanic(panicTitle, panicDetails) {
+ if (tr.isHeadless) {
+ if (panicDetails instanceof Error) throw panicDetails;
+ throw new Error('Panic: ' + panicTitle + ':\n' + panicDetails);
+ }
+
+ if (panicDetails instanceof Error) {
+ panicDetails = panicDetails.stack;
+ }
+
+ showPanicElementIfNeeded();
+ const panicMessageEl = document.createElement('div');
+ panicMessageEl.innerHTML =
+ '<h2 id="message"></h2>' +
+ '<pre id="details"></pre>';
+ panicMessageEl.querySelector('#message').textContent = panicTitle;
+ panicMessageEl.querySelector('#details').textContent = panicDetails;
+ panicElement.appendChild(panicMessageEl);
+
+ rawPanicMessages.push({
+ title: panicTitle,
+ details: panicDetails
+ });
+ }
+
+ function hasPanic() {
+ return rawPanicMessages.length !== 0;
+ }
+ function getPanicText() {
+ return rawPanicMessages.map(function(msg) {
+ return msg.title;
+ }).join(', ');
+ }
+
+ function exportTo(namespace, fn) {
+ const obj = exportPath(namespace);
+ const exports = fn();
+
+ for (const propertyName in exports) {
+ // Maybe we should check the prototype chain here? The current usage
+ // pattern is always using an object literal so we only care about own
+ // properties.
+ const propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
+ propertyName);
+ if (propertyDescriptor) {
+ Object.defineProperty(obj, propertyName, propertyDescriptor);
+ }
+ }
+ }
+
+ /**
+ * Initialization which must be deferred until run-time.
+ */
+ function initialize() {
+ if (global.isVinn) {
+ tr.isVinn = true;
+ } else if (global.process && global.process.versions.node) {
+ tr.isNode = true;
+ } else {
+ tr.isVinn = false;
+ tr.isNode = false;
+ tr.doc = document;
+
+ tr.isMac = /Mac/.test(navigator.platform);
+ tr.isWindows = /Win/.test(navigator.platform);
+ tr.isChromeOS = /CrOS/.test(navigator.userAgent);
+ tr.isLinux = /Linux/.test(navigator.userAgent);
+ }
+ tr.isHeadless = tr.isVinn || tr.isNode;
+ }
+
+ return {
+ initialize,
+
+ exportTo,
+ isExported,
+ isDefined,
+
+ showPanic,
+ hasPanic,
+ getPanicText,
+ };
+})();
+
+tr.initialize();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/base64.html b/chromium/third_party/catapult/tracing/tracing/base/base64.html
new file mode 100644
index 00000000000..4e78cd9e5e6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/base64.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ function Base64() {
+ }
+
+ function b64ToUint6(nChr) {
+ if (nChr > 64 && nChr < 91) return nChr - 65;
+ if (nChr > 96 && nChr < 123) return nChr - 71;
+ if (nChr > 47 && nChr < 58) return nChr + 4;
+ if (nChr === 43) return 62;
+ if (nChr === 47) return 63;
+ return 0;
+ }
+
+ Base64.getDecodedBufferLength = function(input) {
+ let pad = 0;
+ if (input.substr(-2) === '==') {
+ pad = 2;
+ } else if (input.substr(-1) === '=') {
+ pad = 1;
+ }
+ return ((input.length * 3 + 1) >> 2) - pad;
+ };
+
+ Base64.EncodeArrayBufferToString = function(input) {
+ // http://stackoverflow.com/questions/9267899/
+ let binary = '';
+ const bytes = new Uint8Array(input);
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+ };
+
+ Base64.DecodeToTypedArray = function(input, output) {
+ const nInLen = input.length;
+ const nOutLen = Base64.getDecodedBufferLength(input);
+ let nMod3 = 0;
+ let nMod4 = 0;
+ let nUint24 = 0;
+ let nOutIdx = 0;
+
+ if (nOutLen > output.byteLength) {
+ throw new Error('Output buffer too small to decode.');
+ }
+
+ for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= b64ToUint6(input.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
+ output.setUint8(nOutIdx, nUint24 >>> (16 >>> nMod3 & 24) & 255);
+ }
+ nUint24 = 0;
+ }
+ }
+ return nOutLen;
+ };
+
+ /*
+ * Wrapper of btoa
+ * The reason is that window object has a builtin btoa,
+ * but we also want to use btoa when it is headless.
+ * For example we want to use it in a mapper
+ */
+ Base64.btoa = function(input) {
+ return btoa(input);
+ };
+
+ /*
+ * Wrapper of atob
+ * The reason is that window object has a builtin atob,
+ * but we also want to use atob when it is headless.
+ * For example we want to use it in a mapper
+ */
+ Base64.atob = function(input) {
+ return atob(input);
+ };
+
+ return {
+ Base64,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/base64_test.html b/chromium/third_party/catapult/tracing/tracing/base/base64_test.html
new file mode 100644
index 00000000000..44970675a8b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/base64_test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base64.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('getDecodedLength', function() {
+ assert.strictEqual(tr.b.Base64.getDecodedBufferLength('YQ=='), 1);
+ assert.strictEqual(tr.b.Base64.getDecodedBufferLength('YWI='), 2);
+ assert.strictEqual(tr.b.Base64.getDecodedBufferLength('YWJj='), 3);
+ assert.strictEqual(tr.b.Base64.getDecodedBufferLength('YWJjZA=='), 4);
+ assert.strictEqual(tr.b.Base64.getDecodedBufferLength('YWJjZGU='), 5);
+ assert.strictEqual(tr.b.Base64.getDecodedBufferLength('YWJjZGVm'), 6);
+ });
+
+ test('DecodeToTypedArray', function() {
+ const buffer = new DataView(new ArrayBuffer(256));
+ tr.b.Base64.DecodeToTypedArray('YQ==', buffer);
+ assert.strictEqual(buffer.getInt8(0), 97);
+
+ tr.b.Base64.DecodeToTypedArray('YWJjZA==', buffer);
+ for (let i = 0; i < 4; i++) {
+ assert.strictEqual(buffer.getInt8(i), 97 + i);
+ }
+
+ tr.b.Base64.DecodeToTypedArray('YWJjZGVm', buffer);
+ for (let i = 0; i < 4; i++) {
+ assert.strictEqual(buffer.getInt8(i), 97 + i);
+ }
+ });
+
+ test('DecodeLengthReturn', function() {
+ const buffer = new DataView(new ArrayBuffer(256));
+ let len = tr.b.Base64.DecodeToTypedArray(btoa('h'), buffer);
+ assert.strictEqual(len, 1);
+ len = tr.b.Base64.DecodeToTypedArray(btoa('he'), buffer);
+ assert.strictEqual(len, 2);
+ len = tr.b.Base64.DecodeToTypedArray(btoa('hel'), buffer);
+ assert.strictEqual(len, 3);
+ len = tr.b.Base64.DecodeToTypedArray(btoa('hell'), buffer);
+ assert.strictEqual(len, 4);
+ len = tr.b.Base64.DecodeToTypedArray(btoa('hello'), buffer);
+ assert.strictEqual(len, 5);
+ len = tr.b.Base64.DecodeToTypedArray(btoa('hello!'), buffer);
+ assert.strictEqual(len, 6);
+ });
+
+ test('Base64.atob', function() {
+ const output = tr.b.Base64.atob('aGVsbG8gd29ybGQ=');
+ assert.strictEqual(output, 'hello world');
+ });
+
+ test('Base64.btoa', function() {
+ const output = tr.b.Base64.btoa('hello world');
+ assert.strictEqual(output, 'aGVsbG8gd29ybGQ=');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/category_util.html b/chromium/third_party/catapult/tracing/tracing/base/category_util.html
new file mode 100644
index 00000000000..e10ea4f4b8d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/category_util.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper code for working with tracing categories.
+ *
+ */
+tr.exportTo('tr.b', function() {
+ // Cached values for getCategoryParts.
+ const categoryPartsFor = {};
+
+ /**
+ * Categories are stored in comma-separated form, e.g: 'a,b' meaning
+ * that the event is part of the a and b category.
+ *
+ * This function returns the category split by string, caching the
+ * array for performance.
+ *
+ * Do not mutate the returned array!!!!
+ */
+ function getCategoryParts(category) {
+ let parts = categoryPartsFor[category];
+ if (parts !== undefined) return parts;
+ parts = category.split(',');
+ categoryPartsFor[category] = parts;
+ return parts;
+ }
+
+ return {
+ getCategoryParts,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/color.html b/chromium/third_party/catapult/tracing/tracing/base/color.html
new file mode 100644
index 00000000000..0c2fd52f1b5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/color.html
@@ -0,0 +1,260 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ function clamp01(value) {
+ return Math.max(0, Math.min(1, value));
+ }
+
+ function Color(opt_r, opt_g, opt_b, opt_a) {
+ this.r = Math.floor(opt_r) || 0;
+ this.g = Math.floor(opt_g) || 0;
+ this.b = Math.floor(opt_b) || 0;
+ this.a = opt_a;
+ }
+
+ Color.fromString = function(str) {
+ let tmp;
+ let values;
+ if (str.substr(0, 4) === 'rgb(') {
+ tmp = str.substr(4, str.length - 5);
+ values = tmp.split(',').map(function(v) {
+ return v.replace(/^\s+/, '', 'g');
+ });
+ if (values.length !== 3) {
+ throw new Error('Malformatted rgb-expression');
+ }
+ return new Color(
+ parseInt(values[0]),
+ parseInt(values[1]),
+ parseInt(values[2]));
+ }
+ if (str.substr(0, 5) === 'rgba(') {
+ tmp = str.substr(5, str.length - 6);
+ values = tmp.split(',').map(function(v) {
+ return v.replace(/^\s+/, '', 'g');
+ });
+ if (values.length !== 4) {
+ throw new Error('Malformatted rgb-expression');
+ }
+ return new Color(
+ parseInt(values[0]),
+ parseInt(values[1]),
+ parseInt(values[2]),
+ parseFloat(values[3]));
+ }
+ if (str[0] === '#' && str.length === 7) {
+ return new Color(
+ parseInt(str.substr(1, 2), 16),
+ parseInt(str.substr(3, 2), 16),
+ parseInt(str.substr(5, 2), 16));
+ }
+ throw new Error('Unrecognized string format.');
+ };
+
+ Color.lerp = function(a, b, percent) {
+ if (a.a !== undefined && b.a !== undefined) {
+ return Color.lerpRGBA(a, b, percent);
+ }
+ return Color.lerpRGB(a, b, percent);
+ };
+
+ Color.lerpRGB = function(a, b, percent) {
+ return new Color(
+ ((b.r - a.r) * percent) + a.r,
+ ((b.g - a.g) * percent) + a.g,
+ ((b.b - a.b) * percent) + a.b);
+ };
+
+ Color.lerpRGBA = function(a, b, percent) {
+ return new Color(
+ ((b.r - a.r) * percent) + a.r,
+ ((b.g - a.g) * percent) + a.g,
+ ((b.b - a.b) * percent) + a.b,
+ ((b.a - a.a) * percent) + a.a);
+ };
+
+ Color.fromDict = function(dict) {
+ return new Color(dict.r, dict.g, dict.b, dict.a);
+ };
+
+ /**
+ * Converts an HSL triplet with alpha to an RGB color.
+ * |h| Hue value in [0, 1].
+ * |s| Saturation value in [0, 1].
+ * |l| Lightness in [0, 1].
+ * |a| Alpha in [0, 1]
+ */
+ Color.fromHSLExplicit = function(h, s, l, a) {
+ let r;
+ let g;
+ let b;
+ function hue2rgb(p, q, t) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ }
+
+ if (s === 0) {
+ r = g = b = l;
+ } else {
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+
+ return new Color(Math.floor(r * 255),
+ Math.floor(g * 255),
+ Math.floor(b * 255), a);
+ };
+
+ Color.fromHSL = function(hsl) {
+ return Color.fromHSLExplicit(hsl.h, hsl.s, hsl.l, hsl.a);
+ };
+
+ Color.prototype = {
+ clone() {
+ const c = new Color();
+ c.r = this.r;
+ c.g = this.g;
+ c.b = this.b;
+ c.a = this.a;
+ return c;
+ },
+
+ blendOver(bgColor) {
+ const oneMinusThisAlpha = 1 - this.a;
+ const outA = this.a + bgColor.a * oneMinusThisAlpha;
+ const bgBlend = (bgColor.a * oneMinusThisAlpha) / bgColor.a;
+ return new Color(
+ this.r * this.a + bgColor.r * bgBlend,
+ this.g * this.a + bgColor.g * bgBlend,
+ this.b * this.a + bgColor.b * bgBlend,
+ outA);
+ },
+
+ brighten(opt_k) {
+ const k = opt_k || 0.45;
+
+ return new Color(
+ Math.min(255, this.r + Math.floor(this.r * k)),
+ Math.min(255, this.g + Math.floor(this.g * k)),
+ Math.min(255, this.b + Math.floor(this.b * k)),
+ this.a);
+ },
+
+ lighten(k, opt_maxL) {
+ const maxL = opt_maxL !== undefined ? opt_maxL : 1.0;
+ const hsl = this.toHSL();
+ hsl.l = Math.min(hsl.l + k, maxL);
+ return Color.fromHSL(hsl);
+ },
+
+ darken(opt_k) {
+ let k;
+ if (opt_k !== undefined) {
+ k = opt_k;
+ } else {
+ k = 0.45;
+ }
+
+ return new Color(
+ Math.min(255, this.r - Math.floor(this.r * k)),
+ Math.min(255, this.g - Math.floor(this.g * k)),
+ Math.min(255, this.b - Math.floor(this.b * k)),
+ this.a);
+ },
+
+ desaturate(opt_desaturateFactor) {
+ let desaturateFactor;
+ if (opt_desaturateFactor !== undefined) {
+ desaturateFactor = opt_desaturateFactor;
+ } else {
+ desaturateFactor = 1;
+ }
+
+ const hsl = this.toHSL();
+ hsl.s = clamp01(hsl.s * (1 - desaturateFactor));
+ return Color.fromHSL(hsl);
+ },
+
+ withAlpha(a) {
+ return new Color(this.r, this.g, this.b, a);
+ },
+
+ toString() {
+ if (this.a !== undefined) {
+ return 'rgba(' +
+ this.r + ',' + this.g + ',' +
+ this.b + ',' + this.a + ')';
+ }
+ return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';
+ },
+
+ /**
+ * Returns a dict {h, s, l, a} with:
+ * |h| Hue value in [0, 1].
+ * |s| Saturation value in [0, 1].
+ * |l| Lightness in [0, 1].
+ * |a| Alpha in [0, 1]
+ */
+ toHSL() {
+ const r = this.r / 255;
+ const g = this.g / 255;
+ const b = this.b / 255;
+
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+
+ let h;
+ let s;
+ const l = (max + min) / 2;
+ if (min === max) {
+ h = 0;
+ s = 0;
+ } else {
+ const delta = max - min;
+ if (l > 0.5) {
+ s = delta / (2 - max - min);
+ } else {
+ s = delta / (max + min);
+ }
+
+ if (r === max) {
+ h = (g - b) / delta;
+ if (g < b) h += 6;
+ } else if (g === max) {
+ h = 2 + ((b - r) / delta);
+ } else {
+ h = 4 + ((r - g) / delta);
+ }
+ h /= 6;
+ }
+
+ return {h, s, l, a: this.a};
+ },
+
+ toStringWithAlphaOverride(alpha) {
+ return 'rgba(' +
+ this.r + ',' + this.g + ',' +
+ this.b + ',' + alpha + ')';
+ }
+ };
+
+ return {
+ Color,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/color_scheme.html b/chromium/third_party/catapult/tracing/tracing/base/color_scheme.html
new file mode 100644
index 00000000000..fbd22506647
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/color_scheme.html
@@ -0,0 +1,237 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/sinebow_color_generator.html">
+<link rel="import" href="/tracing/base/utils.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides color scheme related functions.
+ */
+tr.exportTo('tr.b', function() {
+ // Basic constants...
+ 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)
+ };
+
+ // Some constants we'll need for later lookups.
+ const numReservedColorIds = Object.keys(reservedColorsByName).length;
+ const numColorsPerVariant = numGeneralPurposeColorIds + numReservedColorIds;
+
+ function ColorScheme() {
+ }
+
+ /*
+ * A flat array of tr.b.Color values of the palette, and their variants.
+ *
+ * This array is made up of a set of base colors, repeated N times to form
+ * a set of variants on that base color.
+ *
+ * Within the base colors, there are "general purpose" colors,
+ * which can be used for random color selection, and
+ * reserved colors, which are used when specific colors
+ * need to be used, e.g. where red is desired.
+ *
+ * The variants are automatically generated from the base colors. The 0th
+ * variant is the default apeparance of the color, and the varaiants are
+ * mutations of that color, e.g. several brightening levels and desaturations.
+ *
+ * For example, a very simple version of this array looks like the following:
+ * 0: Generic Color 0
+ * 1: Generic Color 1
+ * 2: Named Color 'foo'
+ * 3: Brightened Generic Color 0
+ * 4: Brightened Generic Color 1
+ * 5: Brightened Named Color 'foo'
+ */
+ 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);
+ }
+
+ // Basic colors.
+ pushVariant(function(c) { return c; });
+
+ // Brightened variants.
+ ColorScheme.properties.brightenedOffsets = [];
+ ColorScheme.properties.brightenedOffsets.push(ColorScheme.colors.length);
+ pushVariant(function(c) {
+ return c.lighten(0.3, 0.8);
+ });
+
+ ColorScheme.properties.brightenedOffsets.push(ColorScheme.colors.length);
+ pushVariant(function(c) {
+ return c.lighten(0.48, 0.85);
+ });
+
+ ColorScheme.properties.brightenedOffsets.push(ColorScheme.colors.length);
+ pushVariant(function(c) {
+ return c.lighten(0.65, 0.9);
+ });
+
+
+ // Desaturated variants.
+ ColorScheme.properties.dimmedOffsets = [];
+ ColorScheme.properties.dimmedOffsets.push(ColorScheme.colors.length);
+ pushVariant(function(c) {
+ return c.desaturate();
+ });
+ ColorScheme.properties.dimmedOffsets.push(ColorScheme.colors.length);
+ pushVariant(function(c) {
+ return c.desaturate(0.5);
+ });
+ ColorScheme.properties.dimmedOffsets.push(ColorScheme.colors.length);
+ pushVariant(function(c) {
+ return c.desaturate(0.3);
+ });
+
+ /**
+ * A toString'd representation of ColorScheme.colors.
+ */
+ ColorScheme.colorsAsStrings = ColorScheme.colors.map(function(c) {
+ return c.toString();
+ });
+
+ // Build reservedColorNameToIdMap.
+ const reservedColorNameToIdMap = (function() {
+ const m = new Map();
+ let i = generalPurposeColors.length;
+ for (const key of Object.keys(reservedColorsByName)) {
+ m.set(key, i++);
+ }
+ return m;
+ })();
+
+ /**
+ * @param {String} name The color name.
+ * @return {Number} The color ID for the given color name.
+ */
+ ColorScheme.getColorIdForReservedName = function(name) {
+ const id = reservedColorNameToIdMap.get(name);
+ if (id === undefined) {
+ throw new Error('Unrecognized color ' + name);
+ }
+ return id;
+ };
+
+ ColorScheme.getColorForReservedNameAsString = function(reservedName) {
+ const id = ColorScheme.getColorIdForReservedName(reservedName);
+ return ColorScheme.colorsAsStrings[id];
+ };
+
+ /**
+ * Computes a simplistic hashcode of the provide name. Used to chose colors
+ * for slices.
+ * @param {string} name The string to hash.
+ */
+ ColorScheme.getStringHash = function(name) {
+ let hash = 0;
+ for (let i = 0; i < name.length; ++i) {
+ hash = (hash + 37 * hash + 11 * name.charCodeAt(i)) % 0xFFFFFFFF;
+ }
+ return hash;
+ };
+
+ // Previously computed string color IDs. They are based on a stable hash, so
+ // it is safe to save them throughout the program time.
+ const stringColorIdCache = new Map();
+
+ /**
+ * @return {Number} A color ID that is stably associated to the provided via
+ * the getStringHash method. The color ID will be chosen from the general
+ * purpose ID space only, e.g. no reserved ID will be used.
+ */
+ ColorScheme.getColorIdForGeneralPurposeString = function(string) {
+ if (stringColorIdCache.get(string) === undefined) {
+ const hash = ColorScheme.getStringHash(string);
+ stringColorIdCache.set(string, hash % numGeneralPurposeColorIds);
+ }
+ return stringColorIdCache.get(string);
+ };
+
+ /**
+ * @return {Number} A color id generated consistently from a |colorId| and
+ * number |n|.
+ */
+ ColorScheme.getAnotherColorId = function(colorId, n) {
+ return (colorId + n) % numColorsPerVariant;
+ };
+
+ /**
+ * @return {Number} A color ID that the |offset| variant of |colorId|.
+ */
+ ColorScheme.getVariantColorId = function(colorId, offset) {
+ return colorId + offset;
+ };
+
+ return {
+ ColorScheme,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/color_scheme_test.html b/chromium/third_party/catapult/tracing/tracing/base/color_scheme_test.html
new file mode 100644
index 00000000000..7c418671e3c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/color_scheme_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ function isValidColorId(colorId) {
+ return typeof(colorId) === 'number' &&
+ colorId >= 0 &&
+ colorId <= 200;
+ }
+
+ test('getColorIdForReservedName', function() {
+ assert.isTrue(isValidColorId(
+ ColorScheme.getColorIdForReservedName('black')));
+ assert.throws(ColorScheme.getColorIdForReservedName.bind(
+ undefined, 'NOT_A_RESERVED_NAME'));
+ assert.throws(ColorScheme.getColorIdForReservedName.bind(
+ undefined, 'constructor'));
+ });
+
+ test('getColorForReservedNameAsString', function() {
+ assert.strictEqual('rgb(0,0,0)',
+ ColorScheme.getColorForReservedNameAsString('black'));
+ assert.throws(ColorScheme.getColorForReservedNameAsString.bind(
+ undefined, 'NOT_A_RESERVED_NAME'));
+ assert.throws(ColorScheme.getColorForReservedNameAsString.bind(
+ undefined, 'constructor'));
+ });
+
+ test('getColorIdForGeneralPurposeString', function() {
+ assert.isTrue(isValidColorId(
+ ColorScheme.getColorIdForGeneralPurposeString('black')));
+ assert.isTrue(isValidColorId(
+ ColorScheme.getColorIdForGeneralPurposeString('NOT_A_RESERVED_NAME')));
+ assert.isTrue(isValidColorId(
+ ColorScheme.getColorIdForGeneralPurposeString('constructor')));
+ });
+
+ test('getAnotherColorId', function() {
+ const colorId = ColorScheme.getColorIdForGeneralPurposeString('black');
+ assert.isTrue(isValidColorId(
+ ColorScheme.getAnotherColorId(colorId, 300)));
+ });
+
+ test('getVariantColorId', function() {
+ const colorId = ColorScheme.getColorIdForGeneralPurposeString('black');
+ const offset = ColorScheme.properties.brightenedOffsets[0];
+ assert.isTrue(isValidColorId(
+ ColorScheme.getVariantColorId(colorId, offset)));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/color_test.html b/chromium/third_party/catapult/tracing/tracing/base/color_test.html
new file mode 100644
index 00000000000..f92ed6977a3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/color_test.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/color.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('fromRGB', function() {
+ const c = tr.b.Color.fromString('rgb(1, 2, 3)');
+ assert.strictEqual(c.r, 1);
+ assert.strictEqual(c.g, 2);
+ assert.strictEqual(c.b, 3);
+ assert.isUndefined(c.a);
+ });
+
+ test('fromRGBA', function() {
+ const c = tr.b.Color.fromString('rgba(1, 2, 3, 0.5)');
+ assert.strictEqual(c.r, 1);
+ assert.strictEqual(c.g, 2);
+ assert.strictEqual(c.b, 3);
+ assert.strictEqual(c.a, 0.5);
+ });
+
+ test('fromHex', function() {
+ const c = tr.b.Color.fromString('#010203');
+ assert.strictEqual(c.r, 1);
+ assert.strictEqual(c.g, 2);
+ assert.strictEqual(c.b, 3);
+ assert.isUndefined(c.a);
+ });
+
+ test('toStringRGB', function() {
+ const c = new tr.b.Color(1, 2, 3);
+ assert.strictEqual(c.toString(), 'rgb(1,2,3)');
+ });
+
+ test('toStringRGBA', function() {
+ const c = new tr.b.Color(1, 2, 3, 0.5);
+ assert.strictEqual(c.toString(), 'rgba(1,2,3,0.5)');
+ });
+
+ test('lerpRGB', function() {
+ const a = new tr.b.Color(0, 127, 191);
+ const b = new tr.b.Color(255, 255, 255);
+ const x = tr.b.Color.lerpRGB(a, b, 0.25);
+ assert.strictEqual(x.r, 63);
+ assert.strictEqual(x.g, 159);
+ assert.strictEqual(x.b, 207);
+ });
+
+ test('lerpRGBA', function() {
+ const a = new tr.b.Color(0, 127, 191, 0.5);
+ const b = new tr.b.Color(255, 255, 255, 1);
+ const x = tr.b.Color.lerpRGBA(a, b, 0.25);
+ assert.strictEqual(x.r, 63);
+ assert.strictEqual(x.g, 159);
+ assert.strictEqual(x.b, 207);
+ assert.strictEqual(x.a, 0.625);
+ });
+
+ test('blendRGBA', function() {
+ const red = new tr.b.Color(255, 0, 0, 0.5);
+ const white = new tr.b.Color(255, 255, 255, 1);
+ const x = red.blendOver(white);
+ assert.strictEqual(x.r, 255);
+ assert.strictEqual(x.g, 127);
+ assert.strictEqual(x.b, 127);
+ assert.strictEqual(x.a, 1);
+ });
+
+ test('fromHSL', function() {
+ const reddish = tr.b.Color.fromHSLExplicit(.994, 0.644, .484, 0.8);
+ assert.strictEqual(reddish.r, 202);
+ assert.strictEqual(reddish.g, 43);
+ assert.strictEqual(reddish.b, 49);
+ assert.strictEqual(reddish.a, 0.8);
+
+ const gray = tr.b.Color.fromHSLExplicit(0, 0, .50, 1.0);
+ assert.strictEqual(gray.r, 127);
+ assert.strictEqual(gray.g, 127);
+ assert.strictEqual(gray.b, 127);
+ assert.strictEqual(gray.a, 1.0);
+ });
+
+ test('toHSL', function() {
+ const reddish = new tr.b.Color(203, 44, 49, 0.8).toHSL();
+ assert.closeTo(reddish.h, .994, 0.01);
+ assert.closeTo(reddish.s, 0.644, 0.01);
+ assert.closeTo(reddish.l, 0.484, 0.01);
+ assert.strictEqual(reddish.a, 0.8);
+
+ const gray = new tr.b.Color(128, 128, 128, 1.0).toHSL();
+ assert.strictEqual(gray.h, 0);
+ assert.strictEqual(gray.s, 0);
+ assert.closeTo(gray.l, .50, 0.01);
+ assert.strictEqual(gray.a, 1.0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/event.html b/chromium/third_party/catapult/tracing/tracing/base/event.html
new file mode 100644
index 00000000000..00e543a21fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/event.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ let Event;
+ if (tr.isHeadless) {
+ /**
+ * Creates a new event to be used with tr.b.EventTarget or DOM EventTarget
+ * objects.
+ * @param {string} type The name of the event.
+ * @param {boolean=} opt_bubbles Whether the event bubbles.
+ * Default is false.
+ * @param {boolean=} opt_preventable Whether the default action of the event
+ * can be prevented.
+ * @constructor
+ * @extends {Event}
+ */
+ function HeadlessEvent(type, opt_bubbles, opt_preventable) {
+ this.type = type;
+ this.bubbles = (opt_bubbles !== undefined ?
+ !!opt_bubbles : false);
+ this.cancelable = (opt_preventable !== undefined ?
+ !!opt_preventable : false);
+
+ this.defaultPrevented = false;
+ this.cancelBubble = false;
+ }
+
+ HeadlessEvent.prototype = {
+ preventDefault() {
+ this.defaultPrevented = true;
+ },
+
+ stopPropagation() {
+ this.cancelBubble = true;
+ }
+ };
+ Event = HeadlessEvent;
+ } else {
+ /**
+ * Creates a new event to be used with tr.b.EventTarget or DOM EventTarget
+ * objects.
+ * @param {string} type The name of the event.
+ * @param {boolean=} opt_bubbles Whether the event bubbles.
+ * Default is false.
+ * @param {boolean=} opt_preventable Whether the default action of the event
+ * can be prevented.
+ * @constructor
+ * @extends {Event}
+ */
+ function TrEvent(type, opt_bubbles, opt_preventable) {
+ const e = tr.doc.createEvent('Event');
+ e.initEvent(type, !!opt_bubbles, !!opt_preventable);
+ e.__proto__ = global.Event.prototype;
+ return e;
+ }
+
+ TrEvent.prototype = {
+ __proto__: global.Event.prototype
+ };
+ Event = TrEvent;
+ }
+
+ /**
+ * Dispatches a simple event on an event target.
+ * @param {!EventTarget} target The event target to dispatch the event on.
+ * @param {string} type The type of the event.
+ * @param {boolean=} opt_bubbles Whether the event bubbles or not.
+ * @param {boolean=} opt_cancelable Whether the default action of the event
+ * can be prevented.
+ * @param {!Object=} opt_fields
+ *
+ * @return {boolean} If any of the listeners called {@code preventDefault}
+ * during the dispatch this will return false.
+ */
+ function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable,
+ opt_fields) {
+ const e = new tr.b.Event(type, opt_bubbles, opt_cancelable);
+ Object.assign(e, opt_fields);
+ return target.dispatchEvent(e);
+ }
+
+ async function dispatchSimpleEventAsync(target, type, opt_fields) {
+ const e = new tr.b.Event(type, false, false);
+ Object.assign(e, opt_fields);
+ return await target.dispatchAsync(e);
+ }
+
+ return {
+ Event,
+ dispatchSimpleEvent,
+ dispatchSimpleEventAsync,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/event_target.html b/chromium/third_party/catapult/tracing/tracing/base/event_target.html
new file mode 100644
index 00000000000..c3a3025d0fc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/event_target.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This contains an implementation of the EventTarget interface
+ * as defined by DOM Level 2 Events.
+ */
+tr.exportTo('tr.b', function() {
+ /**
+ * Creates a new EventTarget. This class implements the DOM level 2
+ * EventTarget interface and can be used wherever those are used.
+ * @constructor
+ */
+ function EventTarget() {
+ }
+ EventTarget.decorate = function(target) {
+ for (const k in EventTarget.prototype) {
+ if (k === 'decorate') continue;
+ const v = EventTarget.prototype[k];
+ if (typeof v !== 'function') continue;
+ target[k] = v;
+ }
+ };
+
+ EventTarget.prototype = {
+
+ /**
+ * Adds an event listener to the target.
+ * @param {string} type The name of the event.
+ * @param {!Function|{handleEvent:Function}} handler The handler for the
+ * event. This is called when the event is dispatched.
+ */
+ addEventListener(type, handler) {
+ if (!this.listeners_) {
+ this.listeners_ = Object.create(null);
+ }
+ if (!(type in this.listeners_)) {
+ this.listeners_[type] = [handler];
+ } else {
+ const handlers = this.listeners_[type];
+ if (handlers.indexOf(handler) < 0) {
+ handlers.push(handler);
+ }
+ }
+ },
+
+ /**
+ * Removes an event listener from the target.
+ * @param {string} type The name of the event.
+ * @param {!Function|{handleEvent:Function}} handler The handler for the
+ * event.
+ */
+ removeEventListener(type, handler) {
+ if (!this.listeners_) return;
+ if (type in this.listeners_) {
+ const handlers = this.listeners_[type];
+ const index = handlers.indexOf(handler);
+ if (index >= 0) {
+ // Clean up if this was the last listener.
+ if (handlers.length === 1) {
+ delete this.listeners_[type];
+ } else {
+ handlers.splice(index, 1);
+ }
+ }
+ }
+ },
+
+ /**
+ * Dispatches an event and calls all the listeners that are listening to
+ * the type of the event.
+ * @param {!cr.event.Event} event The event to dispatch.
+ * @return {boolean} Whether the default action was prevented. If someone
+ * calls preventDefault on the event object then this returns false.
+ */
+ dispatchEvent(event) {
+ if (!this.listeners_) return true;
+
+ // Since we are using DOM Event objects we need to override some of the
+ // properties and methods so that we can emulate this correctly.
+ event.__defineGetter__('target', () => this);
+ const realPreventDefault = event.preventDefault;
+ event.preventDefault = function() {
+ realPreventDefault.call(this);
+ this.rawReturnValue = false;
+ };
+
+ const type = event.type;
+ let prevented = 0;
+ if (type in this.listeners_) {
+ // Clone to prevent removal during dispatch
+ const handlers = this.listeners_[type].concat();
+ for (let i = 0, handler; handler = handlers[i]; i++) {
+ if (handler.handleEvent) {
+ prevented |= handler.handleEvent.call(handler, event) === false;
+ } else {
+ prevented |= handler.call(this, event) === false;
+ }
+ }
+ }
+
+ return !prevented && event.rawReturnValue;
+ },
+
+ async dispatchAsync(event) {
+ if (!this.listeners_) return true;
+
+ const listeners = this.listeners_[event.type];
+ if (listeners === undefined) return;
+
+ // Clone to prevent removal during dispatch
+ await Promise.all(listeners.slice().map(listener => {
+ if (listener.handleEvent) {
+ return listener.handleEvent.call(listener, event);
+ }
+ return listener.call(this, event);
+ }));
+ },
+
+ hasEventListener(type) {
+ return (this.listeners_ !== undefined &&
+ this.listeners_[type] !== undefined);
+ }
+ };
+
+ return {
+ EventTarget,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/event_target_test.html b/chromium/third_party/catapult/tracing/tracing/base/event_target_test.html
new file mode 100644
index 00000000000..149f8d35495
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/event_target_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/event_target.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('sync', function() {
+ let listenerCallCount = 0;
+ function listener() { listenerCallCount++; }
+
+ const div = new tr.b.EventTarget();
+ tr.b.EventTarget.decorate(div);
+
+ assert.isFalse(div.hasEventListener('foo'));
+
+ div.addEventListener('foo', listener);
+ assert.isTrue(div.hasEventListener('foo'));
+
+ tr.b.dispatchSimpleEvent(div, 'foo');
+ assert.strictEqual(listenerCallCount, 1);
+
+ div.removeEventListener('foo', listener);
+
+ tr.b.dispatchSimpleEvent(div, 'foo');
+ assert.strictEqual(listenerCallCount, 1);
+
+ assert.isFalse(div.hasEventListener('foo'));
+ });
+
+ test('async', function() {
+ let listenerCallCount = 0;
+ async function listener() {
+ listenerCallCount++;
+ await Promise.resolve();
+ }
+
+ const div = new tr.b.EventTarget();
+ tr.b.EventTarget.decorate(div);
+
+ assert.isFalse(div.hasEventListener('foo'));
+
+ div.addEventListener('foo', listener);
+ assert.isTrue(div.hasEventListener('foo'));
+
+ tr.b.dispatchSimpleEventAsync(div, 'foo');
+ assert.strictEqual(listenerCallCount, 1);
+
+ div.removeEventListener('foo', listener);
+
+ tr.b.dispatchSimpleEventAsync(div, 'foo');
+ assert.strictEqual(listenerCallCount, 1);
+
+ assert.isFalse(div.hasEventListener('foo'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/extension_registry.html b/chromium/third_party/catapult/tracing/tracing/base/extension_registry.html
new file mode 100644
index 00000000000..754af87c715
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/extension_registry.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/extension_registry_base.html">
+<link rel="import" href="/tracing/base/extension_registry_basic.html">
+<link rel="import" href="/tracing/base/extension_registry_type_based.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper code for defining extension registries, which can be
+ * used to make a part of trace-viewer extensible.
+ *
+ * This file provides two basic types of extension registries:
+ * - Generic: register a type with metadata, query for those types based on
+ * a predicate
+ *
+ * - TypeName-based: register a type that handles some combination
+ * of tracing categories or typeNames, then query
+ * for it based on a category, typeName or both.
+ *
+ * When you register subtypes, you pass the constructor for the
+ * subtype, and any metadata you want associated with the subtype. Use metadata
+ * instead of stuffing fields onto the constructor. E.g.:
+ * registry.register(MySubclass, {titleWhenShownInTabStrip: 'MySub'})
+ *
+ * Some registries want a default object that is returned when a more precise
+ * subtype has not been registered. To provide one, set the defaultConstructor
+ * option on the registry options.
+ *
+ * TODO: Extension registry used to make reference to mandatoryBaseType but it
+ * was never enforced. We may want to add it back in the future in order to
+ * enforce the types that can be put into a given registry.
+ */
+tr.exportTo('tr.b', function() {
+ function decorateExtensionRegistry(registry, registryOptions) {
+ if (registry.register) {
+ throw new Error('Already has registry');
+ }
+
+ registryOptions.freeze();
+ if (registryOptions.mode === tr.b.BASIC_REGISTRY_MODE) {
+ tr.b._decorateBasicExtensionRegistry(registry, registryOptions);
+ } else if (registryOptions.mode === tr.b.TYPE_BASED_REGISTRY_MODE) {
+ tr.b._decorateTypeBasedExtensionRegistry(registry, registryOptions);
+ } else {
+ throw new Error('Unrecognized mode');
+ }
+
+ // Make it an event target.
+ if (registry.addEventListener === undefined) {
+ tr.b.EventTarget.decorate(registry);
+ }
+ }
+
+ return {
+ decorateExtensionRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/extension_registry_base.html b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_base.html
new file mode 100644
index 00000000000..99f12f30f1e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_base.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ function RegisteredTypeInfo(constructor, metadata) {
+ this.constructor = constructor;
+ this.metadata = metadata;
+ }
+
+ const BASIC_REGISTRY_MODE = 'BASIC_REGISTRY_MODE';
+ const TYPE_BASED_REGISTRY_MODE = 'TYPE_BASED_REGISTRY_MODE';
+ const ALL_MODES = {BASIC_REGISTRY_MODE: true, TYPE_BASED_REGISTRY_MODE: true};
+
+ function ExtensionRegistryOptions(mode) {
+ if (mode === undefined) {
+ throw new Error('Mode is required');
+ }
+ if (!ALL_MODES[mode]) {
+ throw new Error('Not a mode.');
+ }
+
+ this.mode_ = mode;
+ this.defaultMetadata_ = {};
+ this.defaultConstructor_ = undefined;
+ this.defaultTypeInfo_ = undefined;
+ this.frozen_ = false;
+ }
+ ExtensionRegistryOptions.prototype = {
+ freeze() {
+ if (this.frozen_) {
+ throw new Error('Frozen');
+ }
+ this.frozen_ = true;
+ },
+
+ get mode() {
+ return this.mode_;
+ },
+
+ get defaultMetadata() {
+ return this.defaultMetadata_;
+ },
+
+ set defaultMetadata(defaultMetadata) {
+ if (this.frozen_) {
+ throw new Error('Frozen');
+ }
+ this.defaultMetadata_ = defaultMetadata;
+ this.defaultTypeInfo_ = undefined;
+ },
+
+ get defaultConstructor() {
+ return this.defaultConstructor_;
+ },
+
+ set defaultConstructor(defaultConstructor) {
+ if (this.frozen_) {
+ throw new Error('Frozen');
+ }
+ this.defaultConstructor_ = defaultConstructor;
+ this.defaultTypeInfo_ = undefined;
+ },
+
+ get defaultTypeInfo() {
+ if (this.defaultTypeInfo_ === undefined && this.defaultConstructor_) {
+ this.defaultTypeInfo_ = new RegisteredTypeInfo(
+ this.defaultConstructor,
+ this.defaultMetadata);
+ }
+ return this.defaultTypeInfo_;
+ },
+
+ validateConstructor(constructor) {
+ if (!this.mandatoryBaseClass) return;
+ let curProto = constructor.prototype.__proto__;
+ let ok = false;
+ while (curProto) {
+ if (curProto === this.mandatoryBaseClass.prototype) {
+ ok = true;
+ break;
+ }
+ curProto = curProto.__proto__;
+ }
+ if (!ok) {
+ throw new Error(constructor + 'must be subclass of ' + registry);
+ }
+ }
+ };
+
+ return {
+ BASIC_REGISTRY_MODE,
+ TYPE_BASED_REGISTRY_MODE,
+
+ ExtensionRegistryOptions,
+ RegisteredTypeInfo,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/extension_registry_basic.html b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_basic.html
new file mode 100644
index 00000000000..f0351be38f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_basic.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/extension_registry_base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const RegisteredTypeInfo = tr.b.RegisteredTypeInfo;
+ const ExtensionRegistryOptions = tr.b.ExtensionRegistryOptions;
+
+ function decorateBasicExtensionRegistry(registry, extensionRegistryOptions) {
+ const savedStateStack = [];
+ registry.registeredTypeInfos_ = [];
+
+ registry.register = function(constructor,
+ opt_metadata) {
+ if (registry.findIndexOfRegisteredConstructor(
+ constructor) !== undefined) {
+ throw new Error('Handler already registered for ' + constructor);
+ }
+
+ extensionRegistryOptions.validateConstructor(constructor);
+
+ const metadata = {};
+ for (const k in extensionRegistryOptions.defaultMetadata) {
+ metadata[k] = extensionRegistryOptions.defaultMetadata[k];
+ }
+ if (opt_metadata) {
+ for (const k in opt_metadata) {
+ metadata[k] = opt_metadata[k];
+ }
+ }
+
+ const typeInfo = new RegisteredTypeInfo(
+ constructor,
+ metadata);
+
+ let e = new tr.b.Event('will-register');
+ e.typeInfo = typeInfo;
+ registry.dispatchEvent(e);
+
+ registry.registeredTypeInfos_.push(typeInfo);
+
+ e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.pushCleanStateBeforeTest = function() {
+ savedStateStack.push(registry.registeredTypeInfos_);
+ registry.registeredTypeInfos_ = [];
+
+ const e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+ registry.popCleanStateAfterTest = function() {
+ registry.registeredTypeInfos_ = savedStateStack[0];
+ savedStateStack.splice(0, 1);
+
+ const e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.findIndexOfRegisteredConstructor = function(constructor) {
+ for (let i = 0; i < registry.registeredTypeInfos_.length; i++) {
+ if (registry.registeredTypeInfos_[i].constructor === constructor) {
+ return i;
+ }
+ }
+ return undefined;
+ };
+
+ registry.unregister = function(constructor) {
+ const foundIndex = registry.findIndexOfRegisteredConstructor(constructor);
+ if (foundIndex === undefined) {
+ throw new Error(constructor + ' not registered');
+ }
+ registry.registeredTypeInfos_.splice(foundIndex, 1);
+
+ const e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.getAllRegisteredTypeInfos = function() {
+ return registry.registeredTypeInfos_;
+ };
+
+ registry.findTypeInfo = function(constructor) {
+ const foundIndex = this.findIndexOfRegisteredConstructor(constructor);
+ if (foundIndex !== undefined) {
+ return this.registeredTypeInfos_[foundIndex];
+ }
+ return undefined;
+ };
+
+ registry.findTypeInfoMatching = function(predicate, opt_this) {
+ opt_this = opt_this ? opt_this : undefined;
+ for (let i = 0; i < registry.registeredTypeInfos_.length; ++i) {
+ const typeInfo = registry.registeredTypeInfos_[i];
+ if (predicate.call(opt_this, typeInfo)) {
+ return typeInfo;
+ }
+ }
+ return extensionRegistryOptions.defaultTypeInfo;
+ };
+
+ registry.findTypeInfoWithName = function(name) {
+ if (typeof(name) !== 'string') {
+ throw new Error('Name is not a string.');
+ }
+ const typeInfo = registry.findTypeInfoMatching(function(ti) {
+ return ti.constructor.name === name;
+ });
+ if (typeInfo) return typeInfo;
+ return undefined;
+ };
+ }
+
+ return {
+ _decorateBasicExtensionRegistry: decorateBasicExtensionRegistry
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/extension_registry_test.html b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_test.html
new file mode 100644
index 00000000000..27c64f300df
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_test.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('tberSimpleNamedRegistration', function() {
+ function DummyEvent() {
+ }
+ DummyEvent.prototype = {
+ };
+
+ function DummyEventSubclass() {
+ }
+ DummyEventSubclass.prototype = {
+ __proto__: DummyEvent.prototype
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = DummyEvent;
+ options.defaultConstructor = DummyEvent;
+ tr.b.decorateExtensionRegistry(
+ DummyEvent, options);
+
+ DummyEvent.register(DummyEventSubclass, {typeName: 'dummy-name'});
+ assert.strictEqual(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+ assert.strictEqual(
+ DummyEvent.getConstructor('dummy', 'dummy-name'), DummyEventSubclass);
+ DummyEvent.unregister(DummyEventSubclass);
+ assert.strictEqual(
+ DummyEvent, DummyEvent.getConstructor('dummy', 'dummy-name'));
+ });
+
+ test('tberSimpleCategoryRegistration', function() {
+ function DummyEvent() {
+ }
+ DummyEvent.prototype = {
+ };
+
+ function DummyEventSubclass() {
+ }
+ DummyEventSubclass.prototype = {
+ __proto__: DummyEvent.prototype
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = DummyEvent;
+ options.defaultConstructor = DummyEvent;
+ tr.b.decorateExtensionRegistry(
+ DummyEvent, options);
+
+ DummyEvent.register(
+ DummyEventSubclass,
+ {categoryParts: ['dummy']
+ });
+ assert.strictEqual(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+ assert.strictEqual(DummyEvent.getConstructor('dummy', 'dummy-name'),
+ DummyEventSubclass);
+ DummyEvent.unregister(DummyEventSubclass);
+ assert.strictEqual(
+ DummyEvent, DummyEvent.getConstructor('dummy', 'dummy-name'));
+ });
+
+ test('tberSimpleCompoundCategory', function() {
+ function DummyEvent() {
+ }
+ DummyEvent.prototype = {
+ };
+
+ function DummyEventSubclass() {
+ }
+ DummyEventSubclass.prototype = {
+ __proto__: DummyEvent.prototype
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = DummyEvent;
+ options.defaultConstructor = DummyEvent;
+ tr.b.decorateExtensionRegistry(
+ DummyEvent, options);
+
+ DummyEvent.register(
+ DummyEventSubclass,
+ {
+ categoryParts: ['dummy']
+ });
+ assert.strictEqual(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+ assert.strictEqual(DummyEventSubclass,
+ DummyEvent.getConstructor('dummy,something-else',
+ 'dummy-name'));
+ });
+
+ test('tberDefaultType', function() {
+ function DummyEvent() {
+ }
+ DummyEvent.prototype = {
+ };
+
+ function DummyEventSubclass() {
+ }
+ DummyEventSubclass.prototype = {
+ __proto__: DummyEvent.prototype
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = DummyEvent;
+ options.defaultConstructor = DummyEvent;
+ tr.b.decorateExtensionRegistry(
+ DummyEvent, options);
+
+ DummyEvent.register(
+ DummyEventSubclass,
+ {
+ categoryParts: ['dummy']
+ });
+ assert.strictEqual(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+ assert.strictEqual(DummyEventSubclass,
+ DummyEvent.getConstructor('dummy,something-else',
+ 'dummy-name'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/extension_registry_type_based.html b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_type_based.html
new file mode 100644
index 00000000000..b9b30d2aa81
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/extension_registry_type_based.html
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/category_util.html">
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/extension_registry_base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const getCategoryParts = tr.b.getCategoryParts;
+
+ const RegisteredTypeInfo = tr.b.RegisteredTypeInfo;
+ const ExtensionRegistryOptions = tr.b.ExtensionRegistryOptions;
+
+
+ function decorateTypeBasedExtensionRegistry(registry,
+ extensionRegistryOptions) {
+ const savedStateStack = [];
+
+ registry.registeredTypeInfos_ = [];
+
+ registry.categoryPartToTypeInfoMap_ = new Map();
+ registry.typeNameToTypeInfoMap_ = new Map();
+
+ registry.register = function(constructor,
+ metadata) {
+ extensionRegistryOptions.validateConstructor(constructor);
+
+ const typeInfo = new RegisteredTypeInfo(
+ constructor,
+ metadata || extensionRegistryOptions.defaultMetadata);
+
+ typeInfo.typeNames = [];
+ typeInfo.categoryParts = [];
+ if (metadata && metadata.typeName) {
+ typeInfo.typeNames.push(metadata.typeName);
+ }
+ if (metadata && metadata.typeNames) {
+ typeInfo.typeNames.push.apply(
+ typeInfo.typeNames, metadata.typeNames);
+ }
+ if (metadata && metadata.categoryParts) {
+ typeInfo.categoryParts.push.apply(
+ typeInfo.categoryParts, metadata.categoryParts);
+ }
+
+ if (typeInfo.typeNames.length === 0 &&
+ typeInfo.categoryParts.length === 0) {
+ throw new Error('typeName or typeNames must be provided');
+ }
+
+ // Sanity checks...
+ typeInfo.typeNames.forEach(function(typeName) {
+ if (registry.typeNameToTypeInfoMap_.has(typeName)) {
+ throw new Error('typeName ' + typeName + ' already registered');
+ }
+ });
+ typeInfo.categoryParts.forEach(function(categoryPart) {
+ if (registry.categoryPartToTypeInfoMap_.has(categoryPart)) {
+ throw new Error('categoryPart ' + categoryPart +
+ ' already registered');
+ }
+ });
+
+ let e = new tr.b.Event('will-register');
+ e.typeInfo = typeInfo;
+ registry.dispatchEvent(e);
+
+ // Actual registration.
+ typeInfo.typeNames.forEach(function(typeName) {
+ registry.typeNameToTypeInfoMap_.set(typeName, typeInfo);
+ });
+ typeInfo.categoryParts.forEach(function(categoryPart) {
+ registry.categoryPartToTypeInfoMap_.set(categoryPart, typeInfo);
+ });
+ registry.registeredTypeInfos_.push(typeInfo);
+
+ e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.pushCleanStateBeforeTest = function() {
+ savedStateStack.push({
+ registeredTypeInfos: registry.registeredTypeInfos_,
+ typeNameToTypeInfoMap: registry.typeNameToTypeInfoMap_,
+ categoryPartToTypeInfoMap: registry.categoryPartToTypeInfoMap_
+ });
+ registry.registeredTypeInfos_ = [];
+ registry.typeNameToTypeInfoMap_ = new Map();
+ registry.categoryPartToTypeInfoMap_ = new Map();
+ const e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.popCleanStateAfterTest = function() {
+ const state = savedStateStack[0];
+ savedStateStack.splice(0, 1);
+
+ registry.registeredTypeInfos_ = state.registeredTypeInfos;
+ registry.typeNameToTypeInfoMap_ = state.typeNameToTypeInfoMap;
+ registry.categoryPartToTypeInfoMap_ = state.categoryPartToTypeInfoMap;
+ const e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.unregister = function(constructor) {
+ let typeInfoIndex = -1;
+ for (let i = 0; i < registry.registeredTypeInfos_.length; i++) {
+ if (registry.registeredTypeInfos_[i].constructor === constructor) {
+ typeInfoIndex = i;
+ break;
+ }
+ }
+ if (typeInfoIndex === -1) {
+ throw new Error(constructor + ' not registered');
+ }
+
+ const typeInfo = registry.registeredTypeInfos_[typeInfoIndex];
+ registry.registeredTypeInfos_.splice(typeInfoIndex, 1);
+ typeInfo.typeNames.forEach(function(typeName) {
+ registry.typeNameToTypeInfoMap_.delete(typeName);
+ });
+ typeInfo.categoryParts.forEach(function(categoryPart) {
+ registry.categoryPartToTypeInfoMap_.delete(categoryPart);
+ });
+ const e = new tr.b.Event('registry-changed');
+ registry.dispatchEvent(e);
+ };
+
+ registry.getTypeInfo = function(category, typeName) {
+ if (category) {
+ const categoryParts = getCategoryParts(category);
+ for (let i = 0; i < categoryParts.length; i++) {
+ const categoryPart = categoryParts[i];
+ const typeInfo = registry.categoryPartToTypeInfoMap_.get(
+ categoryPart);
+ if (typeInfo !== undefined) return typeInfo;
+ }
+ }
+ const typeInfo = registry.typeNameToTypeInfoMap_.get(typeName);
+ if (typeInfo !== undefined) return typeInfo;
+
+ return extensionRegistryOptions.defaultTypeInfo;
+ };
+
+ // TODO(nduca): Remove or rename.
+ registry.getConstructor = function(category, typeName) {
+ const typeInfo = registry.getTypeInfo(category, typeName);
+ if (typeInfo) return typeInfo.constructor;
+ return undefined;
+ };
+ }
+
+ return {
+ _decorateTypeBasedExtensionRegistry: decorateTypeBasedExtensionRegistry
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme.html b/chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme.html
new file mode 100644
index 00000000000..a540db8fbb4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/base/sinebow_color_generator.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ class FixedColorScheme {
+ /**
+ * @param {!Map} namesToColors
+ * @constructor
+ */
+ constructor(namesToColors) {
+ this.namesToColors_ = namesToColors;
+ }
+
+ /**
+ * Create a color scheme where each name in names gets assigned a fixed
+ * color. This color is arbitrary but unique within the color scheme.
+ * @param {!Array<string>} names
+ * @return {tr.b.FixedColorScheme}
+ */
+ 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);
+ }
+
+ /**
+ * Return color associated with |name|.
+ * @param {!string} name
+ * @return {tr.b.Color}
+ */
+ getColor(name) {
+ const color = this.namesToColors_.get(name);
+ if (color === undefined) throw new Error('Unknown color: ' + name);
+ return color;
+ }
+ }
+
+ const MemoryColumnColorScheme = new FixedColorScheme(new Map([
+ ['used_memory_column', new tr.b.Color(0, 0, 255)],
+ ['older_used_memory_column', new tr.b.Color(153, 204, 255)],
+ ['tracing_memory_column', new tr.b.Color(153, 153, 153)]
+ ]));
+
+ function FixedColorSchemeRegistry() {}
+ FixedColorSchemeRegistry.lookUp = function(name) {
+ const info = this.findTypeInfoMatching(info => info.metadata.name === name);
+ if (!info) return undefined;
+ return info.constructor();
+ };
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(FixedColorSchemeRegistry, options);
+
+ return {
+ MemoryColumnColorScheme,
+ FixedColorScheme,
+ FixedColorSchemeRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme_test.html b/chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme_test.html
new file mode 100644
index 00000000000..283de53d2d1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/fixed_color_scheme_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/fixed_color_scheme.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function isColor(color) {
+ return color.toString().startsWith('rgb');
+ }
+
+ test('canSpecifyColors', function() {
+ const scheme = new tr.b.FixedColorScheme(new Map([
+ ['red', new tr.b.Color(255, 0, 0)],
+ ['green', new tr.b.Color(0, 255, 0)],
+ ['blue', new tr.b.Color(0, 0, 255)],
+ ]));
+ assert.strictEqual(scheme.getColor('red').toString(),
+ (new tr.b.Color(255, 0, 0)).toString());
+ assert.strictEqual(scheme.getColor('green').toString(),
+ (new tr.b.Color(0, 255, 0)).toString());
+ assert.strictEqual(scheme.getColor('blue').toString(),
+ (new tr.b.Color(0, 0, 255)).toString());
+ });
+
+ test('namesGetColors', function() {
+ const scheme = tr.b.FixedColorScheme.fromNames(['foo', 'bar', 'baz']);
+ assert.isTrue(isColor(scheme.getColor('foo')));
+ assert.isTrue(isColor(scheme.getColor('bar')));
+ assert.isTrue(isColor(scheme.getColor('baz')));
+ });
+
+ test('namesGetDifferentColors', function() {
+ const scheme = tr.b.FixedColorScheme.fromNames(['foo', 'bar', 'baz']);
+ assert.notEqual(scheme.getColor('foo'), scheme.getColor('bar'));
+ assert.notEqual(scheme.getColor('bar'), scheme.getColor('baz'));
+ assert.notEqual(scheme.getColor('baz'), scheme.getColor('foo'));
+ });
+
+ test('sameNamesGetSameColors', function() {
+ const scheme = tr.b.FixedColorScheme.fromNames(['foo', 'bar', 'baz']);
+ assert.strictEqual(scheme.getColor('foo'), scheme.getColor('foo'));
+ assert.strictEqual(scheme.getColor('bar'), scheme.getColor('bar'));
+ assert.strictEqual(scheme.getColor('baz'), scheme.getColor('baz'));
+ });
+
+ test('differentSchemesGiveTheSameColorsForTheSameNames', function() {
+ const a = tr.b.FixedColorScheme.fromNames(['foo', 'bar', 'baz']);
+ const b = tr.b.FixedColorScheme.fromNames(['foo', 'bar', 'baz']);
+ assert.strictEqual(
+ a.getColor('foo').toString(), b.getColor('foo').toString());
+ assert.strictEqual(
+ a.getColor('bar').toString(), b.getColor('bar').toString());
+ assert.strictEqual(
+ a.getColor('baz').toString(), b.getColor('baz').toString());
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/guid.html b/chromium/third_party/catapult/tracing/tracing/base/guid.html
new file mode 100644
index 00000000000..eb73e695aee
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/guid.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ let nextGUID = 1;
+
+ const UUID4_PATTERN = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
+
+ const GUID = {
+ /* Allocate an integer GUID.
+ *
+ * These GUIDs are not unique between loads, but are fast to generate, and
+ * consume very little memory.
+ *
+ * @return {number} globally unique id.
+ */
+ allocateSimple() {
+ return nextGUID++;
+ },
+
+ /* Return the last GUID allocated without allocating a new one.
+ *
+ * @return {number} last guid.
+ */
+ getLastSimpleGuid() {
+ return nextGUID - 1;
+ },
+
+ /* Generate a random string UUID.
+ *
+ * Version 4 random UUIDs are practically guaranteed to be unique between
+ * loads, so they can be serialized and compared with results from other
+ * loads. These are slower to generate and consume more memory than simple
+ * GUIDs.
+ *
+ * Background on using Math.random() for allocating identifiers:
+ * https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.n5b6vgrsh
+ * https://v8project.blogspot.com/2015/12/theres-mathrandom-and-then-theres.html
+ *
+ * @return {string} universally unique id.
+ */
+ allocateUUID4() {
+ return UUID4_PATTERN.replace(/[xy]/g, function(c) {
+ let r = parseInt(Math.random() * 16);
+ if (c === 'y') r = (r & 3) + 8;
+ return r.toString(16);
+ });
+ }
+ };
+
+ return {
+ GUID,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/headless_tests.html b/chromium/third_party/catapult/tracing/tracing/base/headless_tests.html
new file mode 100644
index 00000000000..5731e4ff155
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/headless_tests.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unittest.html">
+<link rel="import" href="/tracing/base/unittest/test_runner.html">
+<link rel="import" href="/tracing/base/unittest/text_test_results.html">
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+tr.exportTo('tr.b.unittest', function() {
+ if (!tr.isHeadless) {
+ throw new Error('headless_tests.html only works in headless mode');
+ }
+ function quit(errCode) {
+ if (tr.isVinn) {
+ global.quit(errCode);
+ } else {
+ process.exit(errCode);
+ }
+ }
+
+ function printSpacer() {
+ console.log('\n\n------------------------------------------------------' +
+ '----------------');
+ }
+ function loadAndRunTests(suiteRelpathsToLoad) {
+ const results = new tr.b.unittest.TextTestResults();
+
+ const loader = new tr.b.unittest.SuiteLoader(suiteRelpathsToLoad);
+
+ let p = loader.allSuitesLoadedPromise;
+
+ p = p.then(
+ function didAllSuitesLoad() {
+ const tests = loader.getAllTests().filter(function(testCase) {
+ if (testCase instanceof tr.b.unittest.PerfTestCase) {
+ return false;
+ }
+ return true;
+ });
+ if (tests.length === 0) {
+ printSpacer();
+ console.log('FAILED: No tests to run.');
+ console.log(err.stack);
+ quit(1);
+ }
+ const runner = new tr.b.unittest.TestRunner(results, tests);
+ return runner.beginRunning();
+ },
+ function suiteLoadingFailure(err) {
+ printSpacer();
+ console.log('FAILED: A test suite failed to load.');
+ console.log(err.stack);
+ quit(1);
+ });
+
+ p = p.then(
+ function didAllTestRun() {
+ if (results.numTestsThatFailed > 0) {
+ quit(1);
+ } else {
+ quit(0);
+ }
+ },
+ function testHarnessError(e) {
+ console.log('FAILED: A test harness error has ocurred.');
+ console.log(e.stack);
+ quit(1);
+ });
+ return p;
+ }
+
+ return {
+ loadAndRunTests,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream.html b/chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream.html
new file mode 100644
index 00000000000..69b02015c44
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/trace_stream.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const MAX_FUNCTION_ARGS_COUNT = Math.pow(2, 15) - 1;
+
+ class InMemoryTraceStream extends tr.b.TraceStream {
+ constructor(buffer, isBinary, opt_headerSize) {
+ super();
+ if (!buffer instanceof Uint8Array) {
+ throw new Error('buffer should be a Uint8Array');
+ }
+ const headerSize = opt_headerSize || tr.b.TraceStream.HEADER_SIZE;
+
+ this.data_ = buffer;
+ this.isBinary_ = isBinary;
+ this.header_ = InMemoryTraceStream.uint8ArrayToString_(
+ this.data_.subarray(0, headerSize));
+ this.cursor_ = 0;
+ }
+
+ get isBinary() {
+ return this.isBinary_;
+ }
+
+ get hasData() {
+ return this.cursor_ < this.data_.length;
+ }
+
+ get header() {
+ return this.header_;
+ }
+
+ get data() {
+ return this.data_;
+ }
+
+ toString() {
+ this.rewind();
+ return this.readNumBytes(Number.MAX_VALUE);
+ }
+
+ readUntilDelimiter(delim) {
+ if (delim.length !== 1) {
+ throw new Error('delim must be exactly one character');
+ }
+ const offset = this.data_.indexOf(delim.charCodeAt(0), this.cursor_) + 1;
+ return this.readToOffset_(
+ offset > 0 ? Math.min(offset, this.data_.length) : this.data_.length);
+ }
+
+ readNumBytes(opt_size) {
+ if (opt_size !== undefined && opt_size <= 0) {
+ throw new Error(
+ `readNumBytes expects a positive size (${opt_size} given)`);
+ }
+
+ const size = opt_size || tr.b.TraceStream.CHUNK_SIZE;
+ const offset = Math.min(this.cursor_ + size, this.data_.length);
+ return this.readToOffset_(offset);
+ }
+
+ rewind() {
+ this.cursor_ = 0;
+ }
+
+ // The underlying buffer is not copied.
+ substream(startOffset, opt_endOffset, opt_headerSize) {
+ return new InMemoryTraceStream(
+ this.data_.subarray(startOffset, opt_endOffset), this.isBinary_,
+ opt_headerSize);
+ }
+
+ /**
+ * @returns {string} The contents of the stream between the current cursor
+ * location (inclusive) and |offset| (exclusive). The cursor location is
+ * moved forward to |offset|.
+ */
+ readToOffset_(offset) {
+ const out = InMemoryTraceStream.uint8ArrayToString_(
+ this.data_.subarray(this.cursor_, offset));
+ this.cursor_ = offset;
+ return out;
+ }
+
+ static uint8ArrayToString_(arr) {
+ if (typeof TextDecoder !== 'undefined') {
+ const decoder = new TextDecoder('utf-8');
+ return decoder.decode(arr);
+ }
+ const c = [];
+ for (let i = 0; i < arr.length; i += MAX_FUNCTION_ARGS_COUNT) {
+ c.push(String.fromCharCode(...arr.subarray(
+ i, i + MAX_FUNCTION_ARGS_COUNT)));
+ }
+ return c.join('');
+ }
+ }
+
+ return {
+ InMemoryTraceStream,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream_test.html b/chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream_test.html
new file mode 100644
index 00000000000..fbc838f4877
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/in_memory_trace_stream_test.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function stringToUint8Array(str) {
+ const buffer = new ArrayBuffer(str.length);
+ const bufferView = new Uint8Array(buffer);
+ for (let i = 0; i < bufferView.length; i++) {
+ bufferView[i] = str.charCodeAt(i);
+ }
+ return bufferView;
+ }
+
+ test('readUntilDelimiter', function() {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array('line 1\nline 2\n'), false);
+ assert.strictEqual(stream.readUntilDelimiter('\n'), 'line 1\n');
+ assert.strictEqual(stream.readUntilDelimiter('\n'), 'line 2\n');
+ assert.isFalse(stream.hasData);
+ });
+
+ test('readUntilDelimiter_noDelimiter', function() {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array('line 1'), false);
+ assert.isTrue(stream.hasData);
+ assert.strictEqual(stream.readUntilDelimiter('\n'), 'line 1');
+ assert.isFalse(stream.hasData);
+ });
+
+ test('readUntilDelimiter_noData', function() {
+ const stream = new tr.b.InMemoryTraceStream(new Uint8Array(0), false);
+ assert.isFalse(stream.hasData);
+ assert.strictEqual(stream.readUntilDelimiter('\n'), '');
+ assert.isFalse(stream.hasData);
+ });
+
+ test('readUntilDelimiter_multiCharacterDelimiterThrows', function() {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array('Line 1'), false);
+ assert.throws(stream.readUntilDelimiter.bind(stream, 'xy'));
+ });
+
+ test('readNumBytes', function() {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array('first block second large block and the rest'),
+ false);
+ assert.isTrue(stream.hasData);
+ assert.strictEqual(stream.readNumBytes(12), 'first block ');
+ assert.strictEqual(stream.readNumBytes(19), 'second large block ');
+ // Read a chunk of default length.
+ assert.strictEqual(stream.readNumBytes(), 'and the rest');
+ assert.isFalse(stream.hasData);
+ });
+
+ test('readNumBytes_noData', function() {
+ const stream = new tr.b.InMemoryTraceStream(new Uint8Array(0), false);
+ assert.isFalse(stream.hasData);
+ assert.strictEqual(stream.readNumBytes(10), '');
+ assert.isFalse(stream.hasData);
+ });
+
+ test('readNumBytes_notEnoughData', function() {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array('bla'), false);
+ assert.isTrue(stream.hasData);
+ assert.strictEqual(stream.readNumBytes(10), 'bla');
+ assert.isFalse(stream.hasData);
+ });
+
+ test('readNumBytes_negativeSize', function() {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array('bla'), false);
+ assert.throws(stream.readNumBytes.bind(stream, -10));
+ });
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/interval_tree.html b/chromium/third_party/catapult/tracing/tracing/base/interval_tree.html
new file mode 100644
index 00000000000..b0874802fe7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/interval_tree.html
@@ -0,0 +1,350 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ function max(a, b) {
+ if (a === undefined) return b;
+ if (b === undefined) return a;
+ return Math.max(a, b);
+ }
+
+ /**
+ * This class implements an interval tree.
+ * See: http://wikipedia.org/wiki/Interval_tree
+ *
+ * Internally the tree is a Red-Black tree. The insertion/colour is done using
+ * the Left-leaning Red-Black Trees algorithm as described in:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * @param {function} beginPositionCb Callback to retrieve the begin position.
+ * @param {function} endPositionCb Callback to retrieve the end position.
+ *
+ * @constructor
+ */
+ function IntervalTree(beginPositionCb, endPositionCb) {
+ this.beginPositionCb_ = beginPositionCb;
+ this.endPositionCb_ = endPositionCb;
+
+ this.root_ = undefined;
+ this.size_ = 0;
+ }
+
+ IntervalTree.prototype = {
+ /**
+ * Insert events into the interval tree.
+ *
+ * @param {Object} datum The object to insert.
+ */
+ insert(datum) {
+ const startPosition = this.beginPositionCb_(datum);
+ const endPosition = this.endPositionCb_(datum);
+
+ const node = new IntervalTreeNode(datum,
+ startPosition, endPosition);
+ this.size_++;
+
+ this.root_ = this.insertNode_(this.root_, node);
+ this.root_.colour = Colour.BLACK;
+ return datum;
+ },
+
+ insertNode_(root, node) {
+ if (root === undefined) return node;
+
+ if (root.leftNode && root.leftNode.isRed &&
+ root.rightNode && root.rightNode.isRed) {
+ this.flipNodeColour_(root);
+ }
+
+ if (node.key < root.key) {
+ root.leftNode = this.insertNode_(root.leftNode, node);
+ } else if (node.key === root.key) {
+ root.merge(node);
+ } else {
+ root.rightNode = this.insertNode_(root.rightNode, node);
+ }
+
+ if (root.rightNode && root.rightNode.isRed &&
+ (root.leftNode === undefined || !root.leftNode.isRed)) {
+ root = this.rotateLeft_(root);
+ }
+
+ if (root.leftNode && root.leftNode.isRed &&
+ root.leftNode.leftNode && root.leftNode.leftNode.isRed) {
+ root = this.rotateRight_(root);
+ }
+
+ return root;
+ },
+
+ rotateRight_(node) {
+ const sibling = node.leftNode;
+ node.leftNode = sibling.rightNode;
+ sibling.rightNode = node;
+ sibling.colour = node.colour;
+ node.colour = Colour.RED;
+ return sibling;
+ },
+
+ rotateLeft_(node) {
+ const sibling = node.rightNode;
+ node.rightNode = sibling.leftNode;
+ sibling.leftNode = node;
+ sibling.colour = node.colour;
+ node.colour = Colour.RED;
+ return sibling;
+ },
+
+ flipNodeColour_(node) {
+ node.colour = this.flipColour_(node.colour);
+ node.leftNode.colour = this.flipColour_(node.leftNode.colour);
+ node.rightNode.colour = this.flipColour_(node.rightNode.colour);
+ },
+
+ flipColour_(colour) {
+ return colour === Colour.RED ? Colour.BLACK : Colour.RED;
+ },
+
+ /* The high values are used to find intersection. It should be called after
+ * all of the nodes are inserted. Doing it each insert is _slow_. */
+ updateHighValues() {
+ this.updateHighValues_(this.root_);
+ },
+
+ /* There is probably a smarter way to do this by starting from the inserted
+ * node, but need to handle the rotations correctly. Went the easy route
+ * for now. */
+ updateHighValues_(node) {
+ if (node === undefined) return undefined;
+
+ node.maxHighLeft = this.updateHighValues_(node.leftNode);
+ node.maxHighRight = this.updateHighValues_(node.rightNode);
+
+ return max(max(node.maxHighLeft, node.highValue), node.maxHighRight);
+ },
+
+ validateFindArguments_(queryLow, queryHigh) {
+ if (queryLow === undefined || queryHigh === undefined) {
+ throw new Error('queryLow and queryHigh must be defined');
+ }
+ if ((typeof queryLow !== 'number') || (typeof queryHigh !== 'number')) {
+ throw new Error('queryLow and queryHigh must be numbers');
+ }
+ },
+
+ /**
+ * Retrieve all overlapping intervals.
+ *
+ * @param {number} queryLow The low value for the intersection interval.
+ * @param {number} queryHigh The high value for the intersection interval.
+ * @return {Array} All [begin, end] pairs inside intersecting intervals.
+ */
+ findIntersection(queryLow, queryHigh) {
+ this.validateFindArguments_(queryLow, queryHigh);
+ if (this.root_ === undefined) return [];
+
+ const ret = [];
+ this.root_.appendIntersectionsInto_(ret, queryLow, queryHigh);
+ return ret;
+ },
+
+ /**
+ * Returns the number of nodes in the tree.
+ */
+ get size() {
+ return this.size_;
+ },
+
+ /**
+ * Returns the root node in the tree.
+ */
+ get root() {
+ return this.root_;
+ },
+
+ /**
+ * Dumps out the [lowValue, highValue] pairs for each node in depth-first
+ * order.
+ */
+ dump_() {
+ if (this.root_ === undefined) return [];
+ return this.root_.dump();
+ }
+ };
+
+ const Colour = {
+ RED: 'red',
+ BLACK: 'black'
+ };
+
+ function IntervalTreeNode(datum, lowValue, highValue) {
+ this.lowValue_ = lowValue;
+
+ this.data_ = [{
+ datum,
+ high: highValue,
+ low: lowValue
+ }];
+
+ this.colour_ = Colour.RED;
+
+ this.parentNode_ = undefined;
+ this.leftNode_ = undefined;
+ this.rightNode_ = undefined;
+
+ this.maxHighLeft_ = undefined;
+ this.maxHighRight_ = undefined;
+ }
+
+ IntervalTreeNode.prototype = {
+ appendIntersectionsInto_(ret, queryLow, queryHigh) {
+ /* This node starts has a start point at or further right then queryHigh
+ * so we know this node is out and all right children are out. Just need
+ * to check left */
+ if (this.lowValue_ >= queryHigh) {
+ if (!this.leftNode_) return;
+ return this.leftNode_.appendIntersectionsInto_(
+ ret, queryLow, queryHigh);
+ }
+
+ /* If we have a maximum left high value that is bigger then queryLow we
+ * need to check left for matches */
+ if (this.maxHighLeft_ > queryLow) {
+ this.leftNode_.appendIntersectionsInto_(ret, queryLow, queryHigh);
+ }
+
+ /* We know that this node starts before queryHigh, if any of it's data
+ * ends after queryLow we need to add those nodes */
+ if (this.highValue > queryLow) {
+ for (let i = (this.data.length - 1); i >= 0; --i) {
+ /* data nodes are sorted by high value, so as soon as we see one
+ * before low value we're done. */
+ if (this.data[i].high < queryLow) break;
+
+ ret.push(this.data[i].datum);
+ }
+ }
+
+ /* check for matches in the right tree */
+ if (this.rightNode_) {
+ this.rightNode_.appendIntersectionsInto_(ret, queryLow, queryHigh);
+ }
+ },
+
+ get colour() {
+ return this.colour_;
+ },
+
+ set colour(colour) {
+ this.colour_ = colour;
+ },
+
+ get key() {
+ return this.lowValue_;
+ },
+
+ get lowValue() {
+ return this.lowValue_;
+ },
+
+ get highValue() {
+ return this.data_[this.data_.length - 1].high;
+ },
+
+ set leftNode(left) {
+ this.leftNode_ = left;
+ },
+
+ get leftNode() {
+ return this.leftNode_;
+ },
+
+ get hasLeftNode() {
+ return this.leftNode_ !== undefined;
+ },
+
+ set rightNode(right) {
+ this.rightNode_ = right;
+ },
+
+ get rightNode() {
+ return this.rightNode_;
+ },
+
+ get hasRightNode() {
+ return this.rightNode_ !== undefined;
+ },
+
+ set parentNode(parent) {
+ this.parentNode_ = parent;
+ },
+
+ get parentNode() {
+ return this.parentNode_;
+ },
+
+ get isRootNode() {
+ return this.parentNode_ === undefined;
+ },
+
+ set maxHighLeft(high) {
+ this.maxHighLeft_ = high;
+ },
+
+ get maxHighLeft() {
+ return this.maxHighLeft_;
+ },
+
+ set maxHighRight(high) {
+ this.maxHighRight_ = high;
+ },
+
+ get maxHighRight() {
+ return this.maxHighRight_;
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ get isRed() {
+ return this.colour_ === Colour.RED;
+ },
+
+ merge(node) {
+ for (let i = 0; i < node.data.length; i++) {
+ this.data_.push(node.data[i]);
+ }
+ this.data_.sort(function(a, b) {
+ return a.high - b.high;
+ });
+ },
+
+ dump() {
+ const ret = {};
+ if (this.leftNode_) {
+ ret.left = this.leftNode_.dump();
+ }
+
+ ret.data = this.data_.map(function(d) { return [d.low, d.high]; });
+
+ if (this.rightNode_) {
+ ret.right = this.rightNode_.dump();
+ }
+
+ return ret;
+ }
+ };
+
+ return {
+ IntervalTree,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/interval_tree_test.html b/chromium/third_party/catapult/tracing/tracing/base/interval_tree_test.html
new file mode 100644
index 00000000000..01c9e930f26
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/interval_tree_test.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/interval_tree.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function SimpleIntervalTree() {
+ tr.b.IntervalTree.call(this,
+ function(s) { return s.start; },
+ function(s) { return s.end; });
+ return this;
+ }
+ SimpleIntervalTree.prototype = {
+ __proto__: tr.b.IntervalTree.prototype
+ };
+
+ function buildSimpleTree() {
+ const tree = new SimpleIntervalTree();
+ tree.v0 = tree.insert({start: 2, end: 6});
+ tree.v1 = tree.insert({start: 1, end: 3});
+ tree.v2 = tree.insert({start: 5, end: 7});
+ tree.v3 = tree.insert({start: 1, end: 5});
+ tree.v4 = tree.insert({start: 3, end: 5});
+ tree.v5 = tree.insert({start: 3, end: 5});
+ tree.v6 = tree.insert({start: 3, end: 6});
+ tree.v7 = tree.insert({start: 1, end: 1});
+ tree.v8 = tree.insert({start: 4, end: 8});
+ tree.v9 = tree.insert({start: 0, end: 2});
+
+ tree.updateHighValues();
+
+ return tree;
+ }
+
+ function sortSimpleResults(intersection) {
+ intersection.sort(function(a, b) {
+ if (a.start === b.start) return a.end - b.end;
+ return a.start - b.start;
+ });
+ }
+
+ test('findIntersection', function() {
+ const tree = buildSimpleTree();
+ const intersection = tree.findIntersection(2, 4);
+ sortSimpleResults(intersection);
+
+ const expected = [tree.v1, tree.v3, tree.v0, tree.v4, tree.v5, tree.v6];
+ assert.strictEqual(intersection.length, 6);
+ assert.deepEqual(intersection, expected);
+ });
+
+ test('findIntersection_zeroDuration', function() {
+ const tree = buildSimpleTree();
+ const intersection = tree.findIntersection(2, 2);
+ sortSimpleResults(intersection);
+
+ const expected = [tree.v1, tree.v3];
+ assert.strictEqual(intersection.length, 2);
+ assert.deepEqual(intersection, expected);
+ });
+
+ test('findIntersection_noMatching', function() {
+ const tree = buildSimpleTree();
+ const intersection = tree.findIntersection(9, 10);
+ assert.deepEqual(intersection, []);
+ });
+
+ test('findIntersection_emptyTree', function() {
+ const tree = new tr.b.IntervalTree();
+ tree.updateHighValues();
+
+ const intersection = tree.findIntersection(2, 4);
+ assert.deepEqual(intersection, []);
+ });
+
+ test('findIntersection_emptyInterval', function() {
+ const tree = new tr.b.IntervalTree();
+ tree.updateHighValues();
+
+ assert.throws(function() {
+ tree.findIntersection();
+ });
+ assert.throws(function() {
+ tree.findIntersection(1);
+ });
+ assert.throws(function() {
+ tree.findIntersection('a', 'b');
+ });
+ });
+
+ test('insert', function() {
+ const tree = new tr.b.IntervalTree(
+ function(s) { return s.start; },
+ function(s) { return s.end; });
+
+ assert.strictEqual(tree.size, 0);
+
+ tree.insert({start: 1, end: 4});
+ tree.insert({start: 3, end: 5});
+ tree.updateHighValues();
+
+ const outTree = {
+ 'left': {
+ 'data': [[1, 4]]
+ },
+ 'data': [[3, 5]]
+ };
+
+ assert.strictEqual(tree.size, 2);
+ assert.deepEqual(tree.dump_(), outTree);
+ });
+
+ test('insert_withoutEnd', function() {
+ const tree = new tr.b.IntervalTree(
+ function(s) { return s.start; },
+ function(s) { return s.end; });
+
+ assert.strictEqual(tree.size, 0);
+
+ tree.insert({start: 3, end: 5});
+ tree.insert({start: 1, end: 4});
+ tree.updateHighValues();
+
+ const outTree = {
+ 'left': {
+ 'data': [[1, 4]]
+ },
+ 'data': [[3, 5]]
+ };
+
+ assert.strictEqual(tree.size, 2);
+ assert.deepEqual(tree.dump_(), outTree);
+ });
+
+ test('insert_balancesTree', function() {
+ const tree = new tr.b.IntervalTree(
+ function(s) { return s.start; },
+ function(s) { return s.end; });
+
+ assert.strictEqual(tree.size, 0);
+
+ for (let i = 0; i < 10; ++i) {
+ tree.insert({start: i, end: 5});
+ }
+ tree.updateHighValues();
+
+ const outTree = {
+ 'left': {
+ 'left': {
+ 'data': [[0, 5]]
+ },
+ 'data': [[1, 5]],
+ 'right': {
+ 'data': [[2, 5]]
+ }
+ },
+ 'data': [[3, 5]],
+ 'right': {
+ 'left': {
+ 'left': {
+ 'data': [[4, 5]]
+ },
+ 'data': [[5, 5]],
+ 'right': {
+ 'data': [[6, 5]]
+ }
+ },
+ 'data': [[7, 5]],
+ 'right': {
+ 'left': {
+ 'data': [[8, 5]]
+ },
+ 'data': [[9, 5]]
+ }
+ }
+ };
+
+ assert.deepEqual(tree.dump_(), outTree);
+ });
+
+ test('insert_withDuplicateIntervals', function() {
+ const tree = new tr.b.IntervalTree(
+ function(s) { return s.start; },
+ function(s) { return s.end; });
+
+ assert.strictEqual(tree.size, 0);
+
+ tree.insert({start: 1, end: 4});
+ tree.insert({start: 3, end: 5});
+ tree.insert({start: 3, end: 5});
+ tree.insert({start: 3, end: 6});
+ tree.updateHighValues();
+
+ const outTree = {
+ 'left': {
+ 'data': [[1, 4]]
+ },
+ 'data': [[3, 5], [3, 5], [3, 6]]
+ };
+
+ assert.strictEqual(tree.size, 4);
+ assert.deepEqual(tree.dump_(), outTree);
+ });
+
+ test('insert_updatesHighValues', function() {
+ const tree = buildSimpleTree();
+
+ const expected = [
+ [undefined, undefined],
+ [2, undefined],
+ [5, 8],
+ [undefined, undefined],
+ [6, 7],
+ [undefined, undefined]
+ ];
+
+ const result = [];
+ function walk(node) {
+ if (node === undefined) return;
+
+ walk(node.leftNode);
+ result.push([node.maxHighLeft, node.maxHighRight]);
+ walk(node.rightNode);
+ }
+ walk(tree.root);
+
+ assert.deepEqual(result, expected);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/bbox2.html b/chromium/third_party/catapult/tracing/tracing/base/math/bbox2.html
new file mode 100644
index 00000000000..bca11eb0ce9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/bbox2.html
@@ -0,0 +1,156 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview 2D bounding box computations.
+ */
+tr.exportTo('tr.b.math', function() {
+ /**
+ * Tracks a 2D bounding box.
+ * @constructor
+ */
+ function BBox2() {
+ this.isEmpty_ = true;
+ this.min_ = undefined;
+ this.max_ = undefined;
+ }
+
+ BBox2.prototype = {
+ __proto__: Object.prototype,
+
+ reset() {
+ this.isEmpty_ = true;
+ this.min_ = undefined;
+ this.max_ = undefined;
+ },
+
+ get isEmpty() {
+ return this.isEmpty_;
+ },
+
+ addBBox2(bbox2) {
+ if (bbox2.isEmpty) return;
+ this.addVec2(bbox2.min_);
+ this.addVec2(bbox2.max_);
+ },
+
+ clone() {
+ const bbox = new BBox2();
+ bbox.addBBox2(this);
+ return bbox;
+ },
+
+ /**
+ * Adds x, y to the range.
+ */
+ addXY(x, y) {
+ if (this.isEmpty_) {
+ this.max_ = vec2.create();
+ this.min_ = vec2.create();
+ vec2.set(this.max_, x, y);
+ vec2.set(this.min_, x, y);
+ this.isEmpty_ = false;
+ return;
+ }
+ this.max_[0] = Math.max(this.max_[0], x);
+ this.max_[1] = Math.max(this.max_[1], y);
+ this.min_[0] = Math.min(this.min_[0], x);
+ this.min_[1] = Math.min(this.min_[1], y);
+ },
+
+ /**
+ * Adds value_x, value_y in the form [value_x,value_y] to the range.
+ */
+ addVec2(value) {
+ if (this.isEmpty_) {
+ this.max_ = vec2.create();
+ this.min_ = vec2.create();
+ vec2.set(this.max_, value[0], value[1]);
+ vec2.set(this.min_, value[0], value[1]);
+ this.isEmpty_ = false;
+ return;
+ }
+ this.max_[0] = Math.max(this.max_[0], value[0]);
+ this.max_[1] = Math.max(this.max_[1], value[1]);
+ this.min_[0] = Math.min(this.min_[0], value[0]);
+ this.min_[1] = Math.min(this.min_[1], value[1]);
+ },
+
+ addQuad(quad) {
+ this.addVec2(quad.p1);
+ this.addVec2(quad.p2);
+ this.addVec2(quad.p3);
+ this.addVec2(quad.p4);
+ },
+
+ get minVec2() {
+ if (this.isEmpty_) return undefined;
+ return this.min_;
+ },
+
+ get maxVec2() {
+ if (this.isEmpty_) return undefined;
+ return this.max_;
+ },
+
+ get sizeAsVec2() {
+ if (this.isEmpty_) {
+ throw new Error('Empty BBox2 has no size');
+ }
+ const size = vec2.create();
+ vec2.subtract(size, this.max_, this.min_);
+ return size;
+ },
+
+ get size() {
+ if (this.isEmpty_) {
+ throw new Error('Empty BBox2 has no size');
+ }
+ return {width: this.max_[0] - this.min_[0],
+ height: this.max_[1] - this.min_[1]};
+ },
+
+ get width() {
+ if (this.isEmpty_) {
+ throw new Error('Empty BBox2 has no width');
+ }
+ return this.max_[0] - this.min_[0];
+ },
+
+ get height() {
+ if (this.isEmpty_) {
+ throw new Error('Empty BBox2 has no width');
+ }
+ return this.max_[1] - this.min_[1];
+ },
+
+ toString() {
+ if (this.isEmpty_) return 'empty';
+ return 'min=(' + this.min_[0] + ',' + this.min_[1] + ') ' +
+ 'max=(' + this.max_[0] + ',' + this.max_[1] + ')';
+ },
+
+ asRect() {
+ return tr.b.math.Rect.fromXYWH(
+ this.min_[0],
+ this.min_[1],
+ this.max_[0] - this.min_[0],
+ this.max_[1] - this.min_[1]);
+ }
+ };
+
+ return {
+ BBox2,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/bbox2_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/bbox2_test.html
new file mode 100644
index 00000000000..90b4be3ccb3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/bbox2_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/bbox2.html">
+<script>
+// Copyright (c) 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.
+
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('addVec2', function() {
+ const bbox = new tr.b.math.BBox2();
+ const x = vec2.create();
+ vec2.set(x, 10, 10);
+ bbox.addVec2(x);
+ assert.strictEqual(bbox.minVec2[0], 10);
+ assert.strictEqual(bbox.minVec2[1], 10);
+ assert.strictEqual(bbox.maxVec2[0], 10);
+ assert.strictEqual(bbox.maxVec2[1], 10);
+
+ // Mutate x.
+ vec2.set(x, 11, 11);
+
+ // Bbox shouldn't have changed.
+ assert.strictEqual(bbox.minVec2[0], 10);
+ assert.strictEqual(bbox.minVec2[1], 10);
+ assert.strictEqual(bbox.maxVec2[0], 10);
+ assert.strictEqual(bbox.maxVec2[1], 10);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/math.html b/chromium/third_party/catapult/tracing/tracing/base/math/math.html
new file mode 100644
index 00000000000..260cc4ac7bd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/math.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script src="/gl-matrix-min.js"></script>
+
+<script>
+'use strict';
+
+// In node, the script-src for gl-matrix-min above brings in glmatrix into
+// a module, instead of into the global scope. Whereas, Tracing code
+// assumes that glMatrix is in the global scope. So, in Node only, we
+// require() it in, and then take all its exports and shove them into the
+// global scope by hand.
+(function(global) {
+ if (tr.isNode) {
+ const glMatrixAbsPath = HTMLImportsLoader.hrefToAbsolutePath(
+ '/gl-matrix-min.js');
+ const glMatrixModule = require(glMatrixAbsPath);
+ for (const exportName in glMatrixModule) {
+ global[exportName] = glMatrixModule[exportName];
+ }
+ }
+})(this);
+</script>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b.math', function() {
+ const PREFERRED_NUMBER_SERIES_MULTIPLIERS = [1, 2, 5, 10];
+
+ /* Returns true when x and y are within delta of each other. */
+ function approximately(x, y, delta) {
+ if (delta === undefined) delta = 1e-9;
+ return Math.abs(x - y) < delta;
+ }
+
+ function clamp(x, lo, hi) {
+ return Math.min(Math.max(x, lo), hi);
+ }
+
+ function lerp(percentage, lo, hi) {
+ const range = hi - lo;
+ return lo + percentage * range;
+ }
+
+ function normalize(value, lo, hi) {
+ return (value - lo) / (hi - lo);
+ }
+
+ function deg2rad(deg) {
+ return (Math.PI * deg) / 180.0;
+ }
+
+ /* The Gauss error function gives the probability that a measurement (which is
+ * under the influence of normally distributed errors with standard deviation
+ * sigma = 1) is less than x from the mean value of the standard normal
+ * distribution.
+ * https://www.desmos.com/calculator/t1v4bdpske
+ *
+ * @param {number} x A tolerance for error.
+ * @return {number} The probability that a measurement is less than |x| from
+ * the mean value of the standard normal distribution.
+ */
+ function erf(x) {
+ // save the sign of x
+ // erf(-x) = -erf(x);
+ const sign = (x >= 0) ? 1 : -1;
+ x = Math.abs(x);
+
+ // constants
+ const a1 = 0.254829592;
+ const a2 = -0.284496736;
+ const a3 = 1.421413741;
+ const a4 = -1.453152027;
+ const a5 = 1.061405429;
+ const p = 0.3275911;
+
+ // Abramowitz and Stegun formula 7.1.26
+ // maximum error: 1.5e-7
+ const t = 1.0 / (1.0 + p * x);
+ const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t *
+ Math.exp(-x * x);
+ return sign * y;
+ }
+
+ const tmpVec2 = vec2.create();
+ const tmpVec2b = vec2.create();
+ const tmpVec4 = vec4.create();
+ const tmpMat2d = mat2d.create();
+
+ vec2.createFromArray = function(arr) {
+ if (arr.length !== 2) throw new Error('Should be length 2');
+ const v = vec2.create();
+ vec2.set(v, arr[0], arr[1]);
+ return v;
+ };
+
+ vec2.createXY = function(x, y) {
+ const v = vec2.create();
+ vec2.set(v, x, y);
+ return v;
+ };
+
+ vec2.toString = function(a) {
+ return '[' + a[0] + ', ' + a[1] + ']';
+ };
+
+ vec2.addTwoScaledUnitVectors = function(out, u1, scale1, u2, scale2) {
+ // out = u1 * scale1 + u2 * scale2
+ vec2.scale(tmpVec2, u1, scale1);
+ vec2.scale(tmpVec2b, u2, scale2);
+ vec2.add(out, tmpVec2, tmpVec2b);
+ };
+
+ vec2.interpolatePiecewiseFunction = function(points, x) {
+ if (x < points[0][0]) return points[0][1];
+ for (let i = 1; i < points.length; ++i) {
+ if (x < points[i][0]) {
+ const percent = normalize(x, points[i - 1][0], points[i][0]);
+ return lerp(percent, points[i - 1][1], points[i][1]);
+ }
+ }
+ return points[points.length - 1][1];
+ };
+
+ vec3.createXYZ = function(x, y, z) {
+ const v = vec3.create();
+ vec3.set(v, x, y, z);
+ return v;
+ };
+
+ vec3.toString = function(a) {
+ return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')';
+ };
+
+ mat2d.translateXY = function(out, x, y) {
+ vec2.set(tmpVec2, x, y);
+ mat2d.translate(out, out, tmpVec2);
+ };
+
+ mat2d.scaleXY = function(out, x, y) {
+ vec2.set(tmpVec2, x, y);
+ mat2d.scale(out, out, tmpVec2);
+ };
+
+ vec4.unitize = function(out, a) {
+ out[0] = a[0] / a[3];
+ out[1] = a[1] / a[3];
+ out[2] = a[2] / a[3];
+ out[3] = 1;
+ return out;
+ };
+
+ vec2.copyFromVec4 = function(out, a) {
+ vec4.unitize(tmpVec4, a);
+ vec2.copy(out, tmpVec4);
+ };
+
+ /**
+ * @param {number} x
+ * @param {number=} opt_base Defaults to 10
+ * @return {number}
+ */
+ function logOrLog10(x, base) {
+ if (base === 10) return Math.log10(x);
+ return Math.log(x) / Math.log(base);
+ }
+
+ /**
+ * @param {number} x
+ * @param {number=} opt_base Defaults to 10
+ * @return {number}
+ */
+ function lesserPower(x, opt_base) {
+ const base = opt_base || 10;
+ return Math.pow(base, Math.floor(logOrLog10(x, base)));
+ }
+
+ /**
+ * @param {number} x
+ * @param {number=} opt_base Defaults to 10
+ * @return {number}
+ */
+ 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;
+ }
+
+ /**
+ * Uses the 1-2-5 series to find the closest prefered number to min
+ * whose absolute value is at least the absolute value of |min|.
+ * https://en.wikipedia.org/wiki/Preferred_number
+ */
+ function preferredNumberLargerThanMin(min) {
+ const absMin = Math.abs(min);
+ // The conservative guess is the largest power of 10 less than
+ // or equal to |absMin|.
+ 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,
+ truncate,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/math_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/math_test.html
new file mode 100644
index 00000000000..70e5405cd22
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/math_test.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/math.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('truncate', function() {
+ assert.strictEqual(3, tr.b.math.truncate(10 / 3, 0));
+ assert.strictEqual(3.3, tr.b.math.truncate(10 / 3, 1));
+ assert.strictEqual(3.33, tr.b.math.truncate(10 / 3, 2));
+ assert.strictEqual(3.333, tr.b.math.truncate(10 / 3, 3));
+ assert.strictEqual(3.3333, tr.b.math.truncate(10 / 3, 4));
+ });
+
+ test('erf', function() {
+ assert.closeTo(-1, tr.b.math.erf(-1e10), 1e-6);
+ assert.closeTo(-0.8427, tr.b.math.erf(-1), 1e-6);
+ assert.closeTo(-0.5205, tr.b.math.erf(-0.5), 1e-6);
+ assert.closeTo(0, tr.b.math.erf(0), 1e-6);
+ assert.closeTo(0.5205, tr.b.math.erf(0.5), 1e-6);
+ assert.closeTo(0.8427, tr.b.math.erf(1), 1e-6);
+ assert.closeTo(1, tr.b.math.erf(1e10), 1e-6);
+ });
+
+ test('clamping', function() {
+ assert.strictEqual(tr.b.math.clamp(2, 1, 3), 2);
+ assert.strictEqual(tr.b.math.clamp(1, 1, 3), 1);
+ assert.strictEqual(tr.b.math.clamp(0, 1, 3), 1);
+ assert.strictEqual(tr.b.math.clamp(3, 1, 3), 3);
+ assert.strictEqual(tr.b.math.clamp(4, 1, 3), 3);
+ });
+
+ test('interpolatePiecewiseFunction', function() {
+ const points = [[0, 0], [0.1, 0.5], [1, 1]];
+ assert.strictEqual(0, vec2.interpolatePiecewiseFunction(points, -1));
+ assert.strictEqual(0, vec2.interpolatePiecewiseFunction(points, 0));
+ assert.strictEqual(0.25, vec2.interpolatePiecewiseFunction(points, 0.05));
+ assert.strictEqual(0.5, vec2.interpolatePiecewiseFunction(points, 0.1));
+ assert.strictEqual(0.75, vec2.interpolatePiecewiseFunction(points, 0.55));
+ assert.strictEqual(1, vec2.interpolatePiecewiseFunction(points, 1));
+ assert.strictEqual(1, vec2.interpolatePiecewiseFunction(points, 2));
+ });
+
+ test('powers', function() {
+ assert.strictEqual(0.01, tr.b.math.lesserPower(0.05));
+ assert.strictEqual(0.1, tr.b.math.greaterPower(0.05));
+ assert.strictEqual(0.1, tr.b.math.lesserPower(0.5));
+ assert.strictEqual(1, tr.b.math.greaterPower(0.5));
+ assert.strictEqual(1, tr.b.math.lesserPower(5));
+ assert.strictEqual(10, tr.b.math.greaterPower(5));
+ assert.strictEqual(10, tr.b.math.lesserPower(50));
+ assert.strictEqual(100, tr.b.math.greaterPower(50));
+
+ assert.strictEqual(0, tr.b.math.lesserPower(0));
+ assert.strictEqual(0, tr.b.math.greaterPower(0));
+ assert.isTrue(isNaN(tr.b.math.lesserPower(-1)));
+ assert.isTrue(isNaN(tr.b.math.greaterPower(-1)));
+
+ assert.strictEqual(0.25, tr.b.math.lesserPower(0.3, 2));
+ assert.strictEqual(0.5, tr.b.math.greaterPower(0.3, 2));
+ assert.strictEqual(0.5, tr.b.math.lesserPower(0.8, 2));
+ assert.strictEqual(1, tr.b.math.greaterPower(0.8, 2));
+ assert.strictEqual(1, tr.b.math.lesserPower(1.5, 2));
+ assert.strictEqual(2, tr.b.math.greaterPower(1.5, 2));
+ assert.strictEqual(2, tr.b.math.lesserPower(3, 2));
+ assert.strictEqual(4, tr.b.math.greaterPower(3, 2));
+ assert.strictEqual(4, tr.b.math.lesserPower(5, 2));
+ assert.strictEqual(8, tr.b.math.greaterPower(5, 2));
+
+ assert.strictEqual(0, tr.b.math.lesserPower(0, 2));
+ assert.strictEqual(0, tr.b.math.greaterPower(0, 2));
+ assert.isTrue(isNaN(tr.b.math.lesserPower(-1, 2)));
+ assert.isTrue(isNaN(tr.b.math.greaterPower(-1, 2)));
+ });
+
+ test('lesserWholeNumber', function() {
+ // Use powers of 2 less than 10 to prevent float rounding errors from
+ // breaking Math.floor().
+ for (const i of [1, 2, 4, 8]) {
+ assert.strictEqual(-i, tr.b.math.lesserWholeNumber(-i));
+ assert.strictEqual(-i * 10, tr.b.math.lesserWholeNumber(-i * 10));
+ assert.strictEqual(-i / 10, tr.b.math.lesserWholeNumber(-i / 10));
+ assert.strictEqual(-i * 100, tr.b.math.lesserWholeNumber(-i * 100));
+ assert.strictEqual(-i / 100, tr.b.math.lesserWholeNumber(-i / 100));
+
+ assert.strictEqual(i, tr.b.math.lesserWholeNumber(i));
+ assert.strictEqual(i * 10, tr.b.math.lesserWholeNumber(i * 10));
+ assert.strictEqual(i / 10, tr.b.math.lesserWholeNumber(i / 10));
+ assert.strictEqual(i * 100, tr.b.math.lesserWholeNumber(i * 100));
+ assert.strictEqual(i / 100, tr.b.math.lesserWholeNumber(i / 100));
+
+ const x = i * 1.01;
+ assert.strictEqual(-i, tr.b.math.lesserWholeNumber(-x));
+ assert.strictEqual(-i * 10, tr.b.math.lesserWholeNumber(-x * 10));
+ assert.strictEqual(-i / 10, tr.b.math.lesserWholeNumber(-x / 10));
+ assert.strictEqual(-i * 100, tr.b.math.lesserWholeNumber(-x * 100));
+ assert.strictEqual(-i / 100, tr.b.math.lesserWholeNumber(-x / 100));
+
+ assert.strictEqual(i, tr.b.math.lesserWholeNumber(x));
+ assert.strictEqual(i * 10, tr.b.math.lesserWholeNumber(x * 10));
+ assert.strictEqual(i / 10, tr.b.math.lesserWholeNumber(x / 10));
+ assert.strictEqual(i * 100, tr.b.math.lesserWholeNumber(x * 100));
+ assert.strictEqual(i / 100, tr.b.math.lesserWholeNumber(x / 100));
+ }
+ });
+
+ test('greaterWholeNumber', function() {
+ // Use powers of 2 great than 10 to prevent float rounding errors from
+ // breaking Math.floor().
+ for (const i of [1, 2, 4, 8]) {
+ assert.strictEqual(-i, tr.b.math.greaterWholeNumber(-i));
+ assert.strictEqual(-i * 10, tr.b.math.greaterWholeNumber(-i * 10));
+ assert.strictEqual(-i / 10, tr.b.math.greaterWholeNumber(-i / 10));
+ assert.strictEqual(-i * 100, tr.b.math.greaterWholeNumber(-i * 100));
+ assert.strictEqual(-i / 100, tr.b.math.greaterWholeNumber(-i / 100));
+
+ assert.strictEqual(i, tr.b.math.greaterWholeNumber(i));
+ assert.strictEqual(i * 10, tr.b.math.greaterWholeNumber(i * 10));
+ assert.strictEqual(i / 10, tr.b.math.greaterWholeNumber(i / 10));
+ assert.strictEqual(i * 100, tr.b.math.greaterWholeNumber(i * 100));
+ assert.strictEqual(i / 100, tr.b.math.greaterWholeNumber(i / 100));
+
+ const x = i * 0.99;
+ assert.strictEqual(-i, tr.b.math.greaterWholeNumber(-x));
+ assert.strictEqual(-i * 10, tr.b.math.greaterWholeNumber(-x * 10));
+ assert.strictEqual(-i / 10, tr.b.math.greaterWholeNumber(-x / 10));
+ assert.strictEqual(-i * 100, tr.b.math.greaterWholeNumber(-x * 100));
+ assert.strictEqual(-i / 100, tr.b.math.greaterWholeNumber(-x / 100));
+
+ assert.strictEqual(i, tr.b.math.greaterWholeNumber(x));
+ assert.strictEqual(i * 10, tr.b.math.greaterWholeNumber(x * 10));
+ assert.strictEqual(i / 10, tr.b.math.greaterWholeNumber(x / 10));
+ assert.strictEqual(i * 100, tr.b.math.greaterWholeNumber(x * 100));
+ assert.strictEqual(i / 100, tr.b.math.greaterWholeNumber(x / 100));
+ }
+ });
+
+ test('preferedNumberLargerThanMin', function() {
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(0), 0);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(1), 1);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(2), 2);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(3), 5);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(7), 10);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(0.03), 0.05);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(-1), -1);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(237538), 500000);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(46.13246), 50);
+ assert.strictEqual(tr.b.math.preferredNumberLargerThanMin(-823.34561),
+ -1000);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function.html b/chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function.html
new file mode 100644
index 00000000000..0a782adae01
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b.math', function() {
+ const PERCENTILE_PRECISION = 1e-7;
+ /**
+ * A function that consists of linear pieces.
+ * See https://en.wikipedia.org/wiki/Piecewise_linear_function.
+ * @constructor
+ */
+ function PiecewiseLinearFunction() {
+ this.pieces = [];
+ }
+
+ PiecewiseLinearFunction.prototype = {
+ /**
+ * Push a linear piece defined by linear interpolation between.
+ * (x1, y1) and (x2, y2).
+ * Pieces must be pushed in the order of increasing x coordinate.
+ */
+ push(x1, y1, x2, y2) {
+ if (x1 >= x2) {
+ throw new Error('Invalid segment');
+ }
+ if (this.pieces.length > 0 &&
+ this.pieces[this.pieces.length - 1].x2 > x1) {
+ throw new Error('Potentially overlapping segments');
+ }
+ if (x1 < x2) {
+ this.pieces.push(new Piece(x1, y1, x2, y2));
+ }
+ },
+
+ /**
+ * Returns the size of the set A such that for all x in A: f(x) < y.
+ */
+ partBelow(y) {
+ return this.pieces.reduce((acc, p) => (acc + p.partBelow(y)), 0);
+ },
+
+ get min() {
+ return this.pieces.reduce((acc, p) => Math.min(acc, p.min), Infinity);
+ },
+
+ get max() {
+ return this.pieces.reduce((acc, p) => Math.max(acc, p.max), -Infinity);
+ },
+
+ get average() {
+ let weightedSum = 0;
+ let totalWeight = 0;
+ this.pieces.forEach(function(piece) {
+ weightedSum += piece.width * piece.average;
+ totalWeight += piece.width;
+ });
+ if (totalWeight === 0) return 0;
+ return weightedSum / totalWeight;
+ },
+
+ /**
+ * Returns the minimum possible value y such that the percentage of x points
+ * that have f(x) <= y is approximately equal to the given |percent|.
+ */
+ percentile(percent) {
+ if (!(percent >= 0 && percent <= 1)) {
+ throw new Error('percent must be [0,1]');
+ }
+ let lower = this.min;
+ let upper = this.max;
+ const total = this.partBelow(upper);
+ if (total === 0) return 0;
+ while (upper - lower > PERCENTILE_PRECISION) {
+ const middle = (lower + upper) / 2;
+ const below = this.partBelow(middle);
+ if (below / total < percent) {
+ lower = middle;
+ } else {
+ upper = middle;
+ }
+ }
+ return (lower + upper) / 2;
+ }
+ };
+
+ /**
+ * A linear segment from (x1, y1) to (x2, y2).
+ * @constructor
+ */
+ function Piece(x1, y1, x2, y2) {
+ this.x1 = x1;
+ this.y1 = y1;
+ this.x2 = x2;
+ this.y2 = y2;
+ }
+
+ Piece.prototype = {
+ /**
+ * The total length of all x points such that f(x) < y.
+ * More formally:
+ * max(x2 - x1) such that for all x in [x1 .. x2]: f(x) < y.
+ */
+ partBelow(y) {
+ const width = this.width;
+ if (width === 0) return 0;
+ const minY = this.min;
+ const maxY = this.max;
+ if (y >= maxY) return width;
+ if (y < minY) return 0;
+ return (y - minY) / (maxY - minY) * width;
+ },
+
+ get min() {
+ return Math.min(this.y1, this.y2);
+ },
+
+ get max() {
+ return Math.max(this.y1, this.y2);
+ },
+
+ get average() {
+ return (this.y1 + this.y2) / 2;
+ },
+
+ get width() {
+ return this.x2 - this.x1;
+ }
+ };
+
+ return {
+ PiecewiseLinearFunction,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function_test.html
new file mode 100644
index 00000000000..fb33bb44a3d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/piecewise_linear_function_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/piecewise_linear_function.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('PiecewiseLinearFunctionEmpty', function() {
+ const f = new tr.b.math.PiecewiseLinearFunction();
+ assert.strictEqual(f.max, -Infinity);
+ assert.strictEqual(f.min, Infinity);
+ assert.strictEqual(f.average, 0);
+ assert.strictEqual(f.percentile(0.5), 0);
+ });
+
+ test('PiecewiseLinearFunction', function() {
+ const f = new tr.b.math.PiecewiseLinearFunction();
+ f.push(0, 0.0, 10, 1.0);
+ f.push(10, 1.0, 20, 0.0);
+ f.push(20, 0.0, 30, 0.0);
+ assert.strictEqual(f.max, 1.0);
+ assert.strictEqual(f.min, 0.0);
+ assert.closeTo(f.average, 20 * 1 / 2.0 / 30, 1e-6);
+ assert.closeTo(f.percentile(1.0 / 3.0), 0.0, 1e-6);
+ assert.closeTo(f.percentile(2.0 / 3.0), 0.5, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/quad.html b/chromium/third_party/catapult/tracing/tracing/base/math/quad.html
new file mode 100644
index 00000000000..b87f76a4f87
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/quad.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b.math', function() {
+ const tmpVec2s = [];
+ for (let i = 0; i < 8; i++) {
+ tmpVec2s[i] = vec2.create();
+ }
+
+ const tmpVec2a = vec4.create();
+ const tmpVec4a = vec4.create();
+ const tmpVec4b = vec4.create();
+ const tmpMat4 = mat4.create();
+ const tmpMat4b = mat4.create();
+
+ const p00 = vec2.createXY(0, 0);
+ const p10 = vec2.createXY(1, 0);
+ const p01 = vec2.createXY(0, 1);
+ const p11 = vec2.createXY(1, 1);
+
+ const lerpingVecA = vec2.create();
+ const lerpingVecB = vec2.create();
+ function lerpVec2(out, a, b, amt) {
+ vec2.scale(lerpingVecA, a, amt);
+ vec2.scale(lerpingVecB, b, 1 - amt);
+ vec2.add(out, lerpingVecA, lerpingVecB);
+ vec2.normalize(out, out);
+ return out;
+ }
+
+ /**
+ * @constructor
+ */
+ function Quad() {
+ this.p1 = vec2.create();
+ this.p2 = vec2.create();
+ this.p3 = vec2.create();
+ this.p4 = vec2.create();
+ }
+
+ Quad.fromXYWH = function(x, y, w, h) {
+ const q = new Quad();
+ vec2.set(q.p1, x, y);
+ vec2.set(q.p2, x + w, y);
+ vec2.set(q.p3, x + w, y + h);
+ vec2.set(q.p4, x, y + h);
+ return q;
+ };
+
+ Quad.fromRect = function(r) {
+ return new Quad.fromXYWH(
+ r.x, r.y,
+ r.width, r.height);
+ };
+
+ Quad.from4Vecs = function(p1, p2, p3, p4) {
+ const q = new Quad();
+ vec2.set(q.p1, p1[0], p1[1]);
+ vec2.set(q.p2, p2[0], p2[1]);
+ vec2.set(q.p3, p3[0], p3[1]);
+ vec2.set(q.p4, p4[0], p4[1]);
+ return q;
+ };
+
+ Quad.from8Array = function(arr) {
+ if (arr.length !== 8) {
+ throw new Error('Array must be 8 long');
+ }
+ const q = new Quad();
+ q.p1[0] = arr[0];
+ q.p1[1] = arr[1];
+ q.p2[0] = arr[2];
+ q.p2[1] = arr[3];
+ q.p3[0] = arr[4];
+ q.p3[1] = arr[5];
+ q.p4[0] = arr[6];
+ q.p4[1] = arr[7];
+ return q;
+ };
+
+ Quad.prototype = {
+ pointInside(point) {
+ return pointInImplicitQuad(point,
+ this.p1, this.p2, this.p3, this.p4);
+ },
+
+ boundingRect() {
+ const x0 = Math.min(this.p1[0], this.p2[0], this.p3[0], this.p4[0]);
+ const y0 = Math.min(this.p1[1], this.p2[1], this.p3[1], this.p4[1]);
+
+ const x1 = Math.max(this.p1[0], this.p2[0], this.p3[0], this.p4[0]);
+ const y1 = Math.max(this.p1[1], this.p2[1], this.p3[1], this.p4[1]);
+
+ return new tr.b.math.Rect.fromXYWH(x0, y0, x1 - x0, y1 - y0);
+ },
+
+ clone() {
+ const q = new Quad();
+ vec2.copy(q.p1, this.p1);
+ vec2.copy(q.p2, this.p2);
+ vec2.copy(q.p3, this.p3);
+ vec2.copy(q.p4, this.p4);
+ return q;
+ },
+
+ scale(s) {
+ const q = new Quad();
+ this.scaleFast(q, s);
+ return q;
+ },
+
+ scaleFast(dstQuad, s) {
+ vec2.copy(dstQuad.p1, this.p1, s);
+ vec2.copy(dstQuad.p2, this.p2, s);
+ vec2.copy(dstQuad.p3, this.p3, s);
+ vec2.copy(dstQuad.p3, this.p3, s);
+ },
+
+ isRectangle() {
+ // Simple rectangle check. Note: will not handle out-of-order components.
+ const bounds = this.boundingRect();
+ return (
+ bounds.x === this.p1[0] &&
+ bounds.y === this.p1[1] &&
+ bounds.width === this.p2[0] - this.p1[0] &&
+ bounds.y === this.p2[1] &&
+ bounds.width === this.p3[0] - this.p1[0] &&
+ bounds.height === this.p3[1] - this.p2[1] &&
+ bounds.x === this.p4[0] &&
+ bounds.height === this.p4[1] - this.p2[1]
+ );
+ },
+
+ projectUnitRect(rect) {
+ const q = new Quad();
+ this.projectUnitRectFast(q, rect);
+ return q;
+ },
+
+ projectUnitRectFast(dstQuad, rect) {
+ const v12 = tmpVec2s[0];
+ const v14 = tmpVec2s[1];
+ const v23 = tmpVec2s[2];
+ const v43 = tmpVec2s[3];
+
+ vec2.sub(v12, this.p2, this.p1);
+ const l12 = vec2.length(v12);
+ vec2.scale(v12, v12, 1 / l12);
+
+ vec2.sub(v14, this.p4, this.p1);
+ const l14 = vec2.length(v14);
+ vec2.scale(v14, v14, 1 / l14);
+
+ vec2.sub(v23, this.p3, this.p2);
+ const l23 = vec2.length(v23);
+ vec2.scale(v23, v23, 1 / l23);
+
+ vec2.sub(v43, this.p3, this.p4);
+ const l43 = vec2.length(v43);
+ vec2.scale(v43, v43, 1 / l43);
+
+ const b12 = tmpVec2s[0];
+ const b14 = tmpVec2s[1];
+ const b23 = tmpVec2s[2];
+ const b43 = tmpVec2s[3];
+ lerpVec2(b12, v12, v43, rect.y);
+ lerpVec2(b43, v12, v43, 1 - rect.bottom);
+ lerpVec2(b14, v14, v23, rect.x);
+ lerpVec2(b23, v14, v23, 1 - rect.right);
+
+ vec2.addTwoScaledUnitVectors(tmpVec2a,
+ b12, l12 * rect.x,
+ b14, l14 * rect.y);
+ vec2.add(dstQuad.p1, this.p1, tmpVec2a);
+
+ vec2.addTwoScaledUnitVectors(tmpVec2a,
+ b12, l12 * -(1.0 - rect.right),
+ b23, l23 * rect.y);
+ vec2.add(dstQuad.p2, this.p2, tmpVec2a);
+
+
+ vec2.addTwoScaledUnitVectors(tmpVec2a,
+ b43, l43 * -(1.0 - rect.right),
+ b23, l23 * -(1.0 - rect.bottom));
+ vec2.add(dstQuad.p3, this.p3, tmpVec2a);
+
+ vec2.addTwoScaledUnitVectors(tmpVec2a,
+ b43, l43 * rect.left,
+ b14, l14 * -(1.0 - rect.bottom));
+ vec2.add(dstQuad.p4, this.p4, tmpVec2a);
+ },
+
+ toString() {
+ return 'Quad(' +
+ vec2.toString(this.p1) + ', ' +
+ vec2.toString(this.p2) + ', ' +
+ vec2.toString(this.p3) + ', ' +
+ vec2.toString(this.p4) + ')';
+ }
+ };
+
+ function sign(p1, p2, p3) {
+ return (p1[0] - p3[0]) * (p2[1] - p3[1]) -
+ (p2[0] - p3[0]) * (p1[1] - p3[1]);
+ }
+
+ function pointInTriangle2(pt, p1, p2, p3) {
+ const b1 = sign(pt, p1, p2) < 0.0;
+ const b2 = sign(pt, p2, p3) < 0.0;
+ const b3 = sign(pt, p3, p1) < 0.0;
+ return ((b1 === b2) && (b2 === b3));
+ }
+
+ function pointInImplicitQuad(point, p1, p2, p3, p4) {
+ return pointInTriangle2(point, p1, p2, p3) ||
+ pointInTriangle2(point, p1, p3, p4);
+ }
+
+ return {
+ pointInTriangle2,
+ pointInImplicitQuad,
+ Quad,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/quad_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/quad_test.html
new file mode 100644
index 00000000000..9f31d46bf15
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/quad_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<script>
+'use strict';
+
+function assertQuadEquals(a, b, opt_message) {
+ let ok = true;
+ ok &= a.p1[0] === b.p1[0] && a.p1[1] === b.p1[1];
+ ok &= a.p2[0] === b.p2[0] && a.p2[1] === b.p2[1];
+ ok &= a.p3[0] === b.p3[0] && a.p3[1] === b.p3[1];
+ ok &= a.p4[0] === b.p4[0] && a.p4[1] === b.p4[1];
+ if (ok) return;
+ const message = opt_message || 'Expected "' + a.toString() +
+ '", got "' + b.toString() + '"';
+ assert.fail(a, b, message);
+}
+
+tr.b.unittest.testSuite(function() {
+ test('pointInTri', function() {
+ const res = tr.b.math.pointInTriangle2(
+ [0.25, 0.25],
+ [0, 0],
+ [1, 0],
+ [0, 1]);
+ assert.isTrue(res);
+ });
+
+ test('pointNotInTri', function() {
+ const res = tr.b.math.pointInTriangle2(
+ [0.75, 0.75],
+ [0, 0],
+ [1, 0],
+ [0, 1]);
+ assert.isFalse(res);
+ });
+
+ test('pointInside', function() {
+ const q = tr.b.math.Quad.from4Vecs([0, 0],
+ [1, 0],
+ [1, 1],
+ [0, 1]);
+ const res = q.pointInside([0.5, 0.5]);
+ assert.isTrue(res);
+ });
+
+ test('pointNotInQuad', function() {
+ const q = tr.b.math.Quad.from4Vecs([0, 0],
+ [1, 0],
+ [1, 1],
+ [0, 1]);
+ const res = q.pointInside([1.5, 0.5]);
+ assert.isFalse(res);
+ });
+
+ test('isRectangle', function() {
+ assert.isTrue(tr.b.math.Quad.fromXYWH(0, 0, 10, 10).isRectangle());
+ assert.isTrue(tr.b.math.Quad.fromXYWH(-10, -10, 5, 5).isRectangle());
+ assert.isTrue(tr.b.math.Quad.fromXYWH(-10, -10, 20, 20).isRectangle());
+ assert.isTrue(tr.b.math.Quad.fromXYWH(-10, 10, 5, 5).isRectangle());
+
+ assert.isFalse(tr.b.math.Quad.fromXYWH(0, 0, -10, -10).isRectangle());
+ assert.isFalse(
+ tr.b.math.Quad.from8Array([0, 1, 2, 3, 4, 5, 6, 7]).isRectangle());
+ assert.isFalse(
+ tr.b.math.Quad.from8Array([0, 0, 0, 5, 5, 5, 0, 0]).isRectangle());
+ });
+
+ test('projectUnitRect', function() {
+ const container = tr.b.math.Quad.fromXYWH(0, 0, 10, 10);
+ const srcRect = tr.b.math.Rect.fromXYWH(0.1, 0.8, 0.8, 0.1);
+ const expectedRect = srcRect.scale(10);
+
+ const q = new tr.b.math.Quad();
+ container.projectUnitRectFast(q, srcRect);
+
+ assertQuadEquals(tr.b.math.Quad.fromRect(expectedRect), q);
+ });
+
+ test('projectUnitRectOntoUnitQuad', function() {
+ const container = tr.b.math.Quad.fromXYWH(0, 0, 1, 1);
+ const srcRect = tr.b.math.Rect.fromXYWH(0.0, 0, 1, 1);
+ const expectedRect = srcRect;
+
+ const q = new tr.b.math.Quad();
+ container.projectUnitRectFast(q, srcRect);
+
+ assertQuadEquals(tr.b.math.Quad.fromRect(expectedRect), q);
+ });
+
+ test('projectUnitRectOntoSizeTwoQuad', function() {
+ const container = tr.b.math.Quad.fromXYWH(0, 0, 2, 2);
+ const srcRect = tr.b.math.Rect.fromXYWH(0.0, 0, 1, 1);
+ const expectedRect = srcRect.scale(2);
+
+ const q = new tr.b.math.Quad();
+ container.projectUnitRectFast(q, srcRect);
+
+ assertQuadEquals(tr.b.math.Quad.fromRect(expectedRect), q);
+ });
+
+ test('projectUnitRectOntoTranslatedQuad', function() {
+ const container = tr.b.math.Quad.fromXYWH(1, 1, 1, 1);
+ const srcRect = tr.b.math.Rect.fromXYWH(0.0, 0, 1, 1);
+ const expectedRect = srcRect.translate([1, 1]);
+
+ const q = new tr.b.math.Quad();
+ container.projectUnitRectFast(q, srcRect);
+
+ assertQuadEquals(tr.b.math.Quad.fromRect(expectedRect), q);
+ });
+
+ test('projectShrunkUnitRectOntoUnitQuad', function() {
+ const container = tr.b.math.Quad.fromXYWH(0, 0, 1, 1);
+ const srcRect = tr.b.math.Rect.fromXYWH(0.1, 0.1, 0.8, 0.8);
+ const expectedRect = srcRect;
+
+ const q = new tr.b.math.Quad();
+ container.projectUnitRectFast(q, srcRect);
+
+ assertQuadEquals(tr.b.math.Quad.fromRect(expectedRect), q);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/range.html b/chromium/third_party/catapult/tracing/tracing/base/math/range.html
new file mode 100644
index 00000000000..a225eed4d28
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/range.html
@@ -0,0 +1,308 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Quick range computations.
+ */
+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_;
+ },
+
+ /**
+ * Get a new Range spanning the powers (of opt_base || 10) that enclose
+ * |this| Range.
+ * If |this| is empty, returns a new empty Range.
+ *
+ * @param {number=} opt_base Defaults to 10.
+ * @return {!Range}
+ */
+ 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
+ };
+ },
+
+ /**
+ * Returns a slice of |sortedArray| that intersects with this range
+ * inclusively.
+ * If the range does not have a min, it is treated as unbounded from below.
+ * Similarly, if max is undefined, the range is unbounded from above.
+ *
+ * @param {Array} sortedArray The sorted array of elements to be filtered.
+ * @param {Funcation=} opt_keyFunc A function that extracts a numeric value,
+ * to be used in comparisons, from an element of the array. If not
+ * specified, array elements themselves will be used.
+ * @param {Object=} opt_this An optional this argument to be passed to
+ * opt_keyFunc.
+ */
+ 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;
+ }
+ throw new Error('Not a range');
+ };
+
+ Range.fromExplicitRange = function(min, max) {
+ const range = new Range();
+ range.min = min;
+ range.max = max;
+ return range;
+ };
+
+ Range.compareByMinTimes = function(a, b) {
+ if (!a.isEmpty && !b.isEmpty) return a.min_ - b.min_;
+
+ if (a.isEmpty && !b.isEmpty) return -1;
+
+ if (!a.isEmpty && b.isEmpty) return 1;
+
+ return 0;
+ };
+
+ /**
+ * Subtracts the intersection of |rangeA| and |rangeB| from |rangeA| and
+ * returns the remaining ranges as return. |rangeA| and |rangeB| are
+ * not changed during the subtraction.
+ *
+ * rangeA: |==========|
+ * rangeB: |===|
+ * result: |==| |===|
+ *
+ * @param {tr.b.math.Range} rangeA
+ * @param {tr.b.math.Range} rangeB
+ * @return {Array.<tr.b.math.Range>} An array of ranges which is the result of
+ * the subtraction.
+ */
+ Range.findDifference = function(rangeA, rangeB) {
+ if (!rangeA || rangeA.duration < 0 || !rangeB || rangeB.duration < 0) {
+ throw new Error(`Couldn't subtract ranges`);
+ }
+ const resultRanges = [];
+
+ if (rangeA.isEmpty) return resultRanges;
+ if (rangeB.isEmpty) return [rangeA.clone()];
+
+ const intersection = rangeA.findIntersection(rangeB);
+ if (intersection.isEmpty) {
+ return [rangeA.clone()];
+ }
+ if (rangeA.duration === 0 && rangeB.duration === 0) {
+ if (intersection.empty) return [rangeA.clone()];
+ else if (intersection.duration === 0) return resultRanges;
+ throw new Error(`Two points' intersection can only be a point or empty`);
+ }
+
+ // rangeA: |==========|
+ // rangeB: |===|
+ // result: |==| |===|
+ const leftRange = tr.b.math.Range.fromExplicitRange(
+ rangeA.min, intersection.min);
+ if (leftRange.duration > 0) {
+ resultRanges.push(leftRange);
+ }
+ const rightRange = tr.b.math.Range.fromExplicitRange(
+ intersection.max, rangeA.max);
+ if (rightRange.duration > 0) {
+ resultRanges.push(rightRange);
+ }
+ return resultRanges;
+ };
+
+ Range.PERCENT_RANGE = Range.fromExplicitRange(0, 1);
+ Object.freeze(Range.PERCENT_RANGE);
+
+ return {
+ Range,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/range_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/range_test.html
new file mode 100644
index 00000000000..595e4799a0b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/range_test.html
@@ -0,0 +1,591 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Range = tr.b.math.Range;
+
+ test('addValue', function() {
+ const range = new Range();
+ assert.isTrue(range.isEmpty);
+ range.addValue(1);
+ assert.isFalse(range.isEmpty);
+ assert.strictEqual(1, range.min);
+ assert.strictEqual(1, range.max);
+
+ range.addValue(2);
+ assert.isFalse(range.isEmpty);
+ assert.strictEqual(1, range.min);
+ assert.strictEqual(2, range.max);
+ });
+
+ test('addNonEmptyRange', function() {
+ const r1 = new Range();
+ r1.addValue(1);
+ r1.addValue(2);
+
+ const r = new Range();
+ r.addRange(r1);
+ assert.strictEqual(1, r.min);
+ assert.strictEqual(2, r.max);
+
+ const r2 = Range.fromDict(r.toJSON());
+ assert.strictEqual(r2.isEmpty, r.isEmpty);
+ assert.strictEqual(r2.max, r.max);
+ assert.strictEqual(r2.min, r.min);
+ });
+
+ test('addEmptyRange', function() {
+ const r1 = new Range();
+
+ const r = new Range();
+ r.addRange(r1);
+ assert.isTrue(r.isEmpty);
+ assert.isUndefined(r.min);
+ assert.isUndefined(r.max);
+
+ const r2 = Range.fromDict(r.toJSON());
+ assert.strictEqual(r2.isEmpty, r.isEmpty);
+ assert.isUndefined(r2.max);
+ assert.isUndefined(r2.min);
+ });
+
+ test('addRangeToRange', function() {
+ const r1 = new Range();
+ r1.addValue(1);
+ r1.addValue(2);
+
+ const r = new Range();
+ r.addValue(3);
+ r.addRange(r1);
+
+ assert.isFalse(r.isEmpty);
+ assert.strictEqual(1, r.min);
+ assert.strictEqual(3, r.max);
+ });
+
+ test('containsRange', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(1, 2);
+
+ assert.isTrue(r1.containsRangeInclusive(r2));
+ assert.isFalse(r2.containsRangeInclusive(r1));
+ assert.isTrue(r1.containsRangeExclusive(r2));
+ assert.isFalse(r2.containsRangeExclusive(r1));
+ });
+
+ test('containsRange_emptyRange', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = new Range();
+
+ assert.isFalse(r1.containsRangeInclusive(r2));
+ assert.isFalse(r2.containsRangeInclusive(r1));
+ assert.isFalse(r1.containsRangeExclusive(r2));
+ assert.isFalse(r2.containsRangeExclusive(r1));
+ });
+
+ test('containsRange_overlapping', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(2, 4);
+
+ assert.isFalse(r1.containsRangeInclusive(r2));
+ assert.isFalse(r2.containsRangeInclusive(r1));
+ assert.isFalse(r1.containsRangeExclusive(r2));
+ assert.isFalse(r2.containsRangeExclusive(r1));
+ });
+
+ test('containsRange_disjoint', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(3, 5);
+
+ assert.isFalse(r1.containsRangeInclusive(r2));
+ assert.isFalse(r2.containsRangeInclusive(r1));
+ assert.isFalse(r1.containsRangeExclusive(r2));
+ assert.isFalse(r2.containsRangeExclusive(r1));
+ });
+
+ test('containsRange_singlePointRange', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(1, 1);
+
+ assert.isTrue(r1.containsRangeInclusive(r2));
+ assert.isFalse(r2.containsRangeInclusive(r1));
+ assert.isTrue(r1.containsRangeExclusive(r2));
+ assert.isFalse(r2.containsRangeExclusive(r1));
+ });
+
+ test('containsRange_singlePointRangeAtBorder', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(2, 2);
+
+ assert.isTrue(r1.containsRangeInclusive(r2));
+ assert.isFalse(r2.containsRangeInclusive(r1));
+ assert.isFalse(r1.containsRangeExclusive(r2));
+ assert.isFalse(r2.containsRangeExclusive(r1));
+ });
+
+ test('containsExplicitRange', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(1, 2);
+
+ assert.isTrue(r1.containsExplicitRangeInclusive(1, 2));
+ assert.isFalse(r2.containsExplicitRangeInclusive(0, 3));
+ assert.isTrue(r1.containsExplicitRangeExclusive(1, 2));
+ assert.isFalse(r2.containsExplicitRangeExclusive(0, 3));
+ });
+
+ test('containsExplicitRange_overlapping', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(2, 4);
+
+ assert.isFalse(r1.containsExplicitRangeInclusive(2, 4));
+ assert.isFalse(r2.containsExplicitRangeInclusive(0, 3));
+ assert.isFalse(r1.containsExplicitRangeExclusive(2, 4));
+ assert.isFalse(r2.containsExplicitRangeExclusive(0, 3));
+ });
+
+ test('containsExplicitRange_disjoint', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(3, 5);
+
+ assert.isFalse(r1.containsExplicitRangeInclusive(3, 5));
+ assert.isFalse(r2.containsExplicitRangeInclusive(0, 2));
+ assert.isFalse(r1.containsExplicitRangeExclusive(3, 5));
+ assert.isFalse(r2.containsExplicitRangeExclusive(0, 2));
+ });
+
+ test('containsExplicitRange_singlePointRange', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(1, 1);
+
+ assert.isTrue(r1.containsExplicitRangeInclusive(1, 1));
+ assert.isFalse(r2.containsExplicitRangeInclusive(0, 2));
+ assert.isTrue(r1.containsExplicitRangeExclusive(1, 1));
+ assert.isFalse(r2.containsExplicitRangeExclusive(0, 2));
+ });
+
+ test('containsExplicitRange_singlePointRangeAtBorder', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(2, 2);
+
+ assert.isTrue(r1.containsExplicitRangeInclusive(2, 2));
+ assert.isFalse(r2.containsExplicitRangeInclusive(0, 2));
+ assert.isFalse(r1.containsExplicitRangeExclusive(2, 2));
+ assert.isFalse(r2.containsExplicitRangeExclusive(0, 2));
+ });
+
+ test('intersectsRange', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(1, 2);
+
+ assert.isTrue(r1.intersectsRangeInclusive(r2));
+ assert.isTrue(r2.intersectsRangeInclusive(r1));
+ assert.isTrue(r1.intersectsRangeExclusive(r2));
+ assert.isTrue(r2.intersectsRangeExclusive(r1));
+ });
+
+ test('intersectsRange_emptyRange', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = new Range();
+
+ assert.isFalse(r1.intersectsRangeInclusive(r2));
+ assert.isFalse(r2.intersectsRangeInclusive(r1));
+ assert.isFalse(r1.intersectsRangeExclusive(r2));
+ assert.isFalse(r2.intersectsRangeExclusive(r1));
+ });
+
+ test('intersectsRange_overlapping', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(2, 4);
+
+ assert.isTrue(r1.intersectsRangeInclusive(r2));
+ assert.isTrue(r2.intersectsRangeInclusive(r1));
+ assert.isTrue(r1.intersectsRangeExclusive(r2));
+ assert.isTrue(r2.intersectsRangeExclusive(r1));
+ });
+
+ test('intersectsRange_disjoint', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(3, 5);
+
+ assert.isFalse(r1.intersectsRangeInclusive(r2));
+ assert.isFalse(r2.intersectsRangeInclusive(r1));
+ assert.isFalse(r1.intersectsRangeExclusive(r2));
+ assert.isFalse(r2.intersectsRangeExclusive(r1));
+ });
+
+ test('intersectsRange_singlePointRange', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(1, 1);
+
+ assert.isTrue(r1.intersectsRangeInclusive(r2));
+ assert.isTrue(r2.intersectsRangeInclusive(r1));
+ assert.isTrue(r1.intersectsRangeExclusive(r2));
+ assert.isTrue(r2.intersectsRangeExclusive(r1));
+ });
+
+ test('intersectsRange_singlePointRangeAtBorder', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(2, 2);
+
+ assert.isTrue(r1.intersectsRangeInclusive(r2));
+ assert.isTrue(r2.intersectsRangeInclusive(r1));
+ assert.isFalse(r1.intersectsRangeExclusive(r2));
+ assert.isFalse(r2.intersectsRangeExclusive(r1));
+ });
+
+ test('intersectsExplicitRange', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(1, 2);
+
+ assert.isTrue(r1.intersectsExplicitRangeInclusive(1, 2));
+ assert.isTrue(r2.intersectsExplicitRangeInclusive(0, 3));
+ assert.isTrue(r1.intersectsExplicitRangeExclusive(1, 2));
+ assert.isTrue(r2.intersectsExplicitRangeExclusive(0, 3));
+ });
+
+ test('intersectsExplicitRange_overlapping', function() {
+ const r1 = Range.fromExplicitRange(0, 3);
+ const r2 = Range.fromExplicitRange(2, 4);
+
+ assert.isTrue(r1.intersectsExplicitRangeInclusive(2, 4));
+ assert.isTrue(r2.intersectsExplicitRangeInclusive(0, 3));
+ assert.isTrue(r1.intersectsExplicitRangeExclusive(2, 4));
+ assert.isTrue(r2.intersectsExplicitRangeExclusive(0, 3));
+ });
+
+ test('intersectsExplicitRange_disjoint', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(3, 5);
+
+ assert.isFalse(r1.intersectsExplicitRangeInclusive(3, 5));
+ assert.isFalse(r2.intersectsExplicitRangeInclusive(0, 2));
+ assert.isFalse(r1.intersectsExplicitRangeExclusive(3, 5));
+ assert.isFalse(r2.intersectsExplicitRangeExclusive(0, 2));
+ });
+
+ test('intersectsExplicitRange_singlePointRange', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(1, 1);
+
+ assert.isTrue(r1.intersectsExplicitRangeInclusive(1, 1));
+ assert.isTrue(r2.intersectsExplicitRangeInclusive(0, 2));
+ assert.isTrue(r1.intersectsExplicitRangeExclusive(1, 1));
+ assert.isTrue(r2.intersectsExplicitRangeExclusive(0, 2));
+ });
+
+ test('intersectsExplicitRange_singlePointRangeAtBorder', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(2, 2);
+
+ assert.isTrue(r1.intersectsExplicitRangeInclusive(2, 2));
+ assert.isTrue(r2.intersectsExplicitRangeInclusive(0, 2));
+ assert.isFalse(r1.intersectsExplicitRangeExclusive(2, 2));
+ assert.isFalse(r2.intersectsExplicitRangeExclusive(0, 2));
+ });
+
+ test('duration', function() {
+ assert.strictEqual(Range.fromExplicitRange(2, 4).duration, 2);
+ });
+
+ test('duration_singlePointRange', function() {
+ assert.strictEqual(Range.fromExplicitRange(2, 2).duration, 0);
+ });
+
+ test('duration_emptyRange', function() {
+ assert.strictEqual(new Range().duration, 0);
+ });
+
+ test('findIntersection_overlapping', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(1, 3);
+
+ const result = Range.fromExplicitRange(1, 2);
+ assert.deepEqual(r1.findIntersection(r2), result);
+ assert.deepEqual(r2.findIntersection(r1), result);
+ });
+
+ test('findIntersection_bordering', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(2, 4);
+
+ const result = Range.fromExplicitRange(2, 2);
+ assert.deepEqual(r1.findIntersection(r2), result);
+ assert.deepEqual(r2.findIntersection(r1), result);
+ });
+
+ test('findIntersection_singlePointRange', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(2, 2);
+
+ const result = Range.fromExplicitRange(2, 2);
+ assert.deepEqual(r1.findIntersection(r2), result);
+ assert.deepEqual(r2.findIntersection(r1), result);
+ });
+
+ test('findIntersection_disjoint', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = Range.fromExplicitRange(3, 5);
+
+ assert.isTrue(r1.findIntersection(r2).isEmpty);
+ assert.isTrue(r2.findIntersection(r1).isEmpty);
+ });
+
+ test('findIntersection_emptyRange', function() {
+ const r1 = Range.fromExplicitRange(0, 2);
+ const r2 = new Range();
+
+ assert.isTrue(r1.findIntersection(r2).isEmpty);
+ assert.isTrue(r2.findIntersection(r1).isEmpty);
+ });
+
+ test('findExplicitIntersectionDuration_overlapping', function() {
+ const r = Range.fromExplicitRange(2, 4);
+ assert.strictEqual(1, r.findExplicitIntersectionDuration(1, 3));
+ assert.strictEqual(1, r.findExplicitIntersectionDuration(3, 5));
+ assert.strictEqual(2, r.findExplicitIntersectionDuration(0, 5));
+ });
+
+ test('findExplicitIntersectionDuration_invalidOtherRange', function() {
+ const r = Range.fromExplicitRange(2, 4);
+ assert.strictEqual(0, r.findExplicitIntersectionDuration(3, 1));
+ assert.strictEqual(0, r.findExplicitIntersectionDuration(5, 3));
+ });
+
+ test('findExplicitIntersectionDuration_disjoint', function() {
+ const r = Range.fromExplicitRange(2, 4);
+ assert.strictEqual(0, r.findExplicitIntersectionDuration(0, 1));
+ assert.strictEqual(0, r.findExplicitIntersectionDuration(4.5, 5));
+ });
+
+ test('filter_numericField', function() {
+ const r = Range.fromExplicitRange(2, 3);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([{start: 2}, {start: 3}], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('filter_noNumericField', function() {
+ const r = Range.fromExplicitRange(2, 3);
+ assert.deepEqual([2, 3], r.filterArray([1, 2, 3, 4]));
+ });
+
+ test('filter_empty', function() {
+ const r = new Range();
+ assert.deepEqual([], r.filterArray([1, 2, 3, 4]));
+ });
+
+ test('filter_oneSided', function() {
+ const r = new Range();
+ r.min = 2;
+ assert.deepEqual([2, 3, 4], r.filterArray([1, 2, 3, 4]));
+ });
+
+ test('left_boundary_corner_case1', function() {
+ const r = Range.fromExplicitRange(1, 2);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([{start: 1}, {start: 2}], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('left_boundary_corner_case2', function() {
+ const r = Range.fromExplicitRange(0, 2);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([{start: 1}, {start: 2}], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('left_boundary_corner_case3', function() {
+ const r = Range.fromExplicitRange(1, 1);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([{start: 1}], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('left_boundary_corner_case4', function() {
+ const r = Range.fromExplicitRange(0, 0);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('right_boundary_corner_case1', function() {
+ const r = Range.fromExplicitRange(4, 5);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([{start: 4}], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('right_boundary_corner_case2', function() {
+ const r = Range.fromExplicitRange(4, 4);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([{start: 4}], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+ test('right_boundary_corner_case3', function() {
+ const r = Range.fromExplicitRange(5, 5);
+ const array = [{start: 1},
+ {start: 2},
+ {start: 3},
+ {start: 4}];
+ assert.deepEqual([], r.filterArray(
+ array, function(value) {return value.start;}));
+ });
+
+
+ test('clone_empty', function() {
+ const range = new Range();
+ const cloned = range.clone();
+ assert.deepEqual(cloned, range);
+ });
+
+ test('clone_normal', function() {
+ const range = Range.fromExplicitRange(1, 2);
+ const cloned = range.clone();
+ assert.deepEqual(range, cloned);
+ range.min = 0;
+ assert.strictEqual(1, cloned.min);
+ });
+
+ test('findDifference_normal', function() {
+ // [{a,min, a.max}, {b.min, b.max}, [{c1.min, c1.max}, ...]]
+ const truthTable = [
+ [[50, 100], [-Infinity, 0], [[50, 100]]],
+ [[50, 100], [-Infinity, 75], [[75, 100]]],
+ [[50, 100], [-Infinity, Infinity], []],
+
+ [[50, 100], [0, 0], [[50, 100]]],
+ [[50, 100], [0, 25], [[50, 100]]],
+ [[50, 100], [0, 50], [[50, 100]]],
+ [[50, 100], [0, 75], [[75, 100]]],
+ [[50, 100], [0, 100], []],
+ [[50, 100], [0, 150], []],
+ [[50, 100], [50, 50], [[50, 100]]],
+ [[50, 100], [50, 75], [[75, 100]]],
+ [[50, 100], [50, 100], []],
+ [[50, 100], [50, 150], []],
+ [[50, 100], [75, 75], [[50, 75], [75, 100]]],
+ [[50, 100], [75, 100], [[50, 75]]],
+ [[50, 100], [75, 150], [[50, 75]]],
+
+ [[50, 50], [0, 0], [[50, 50]]],
+ [[50, 50], [0, 25], [[50, 50]]],
+ [[50, 50], [0, 50], []],
+ [[50, 50], [50, 50], []],
+ [[50, 50], [0, 75], []],
+
+ [[50, Infinity], [0, 0], [[50, Infinity]]],
+ [[50, Infinity], [0, 25], [[50, Infinity]]],
+ [[50, Infinity], [0, 50], [[50, Infinity]]],
+ [[50, Infinity], [0, 75], [[75, Infinity]]],
+ [[50, Infinity], [0, 100], [[100, Infinity]]],
+ [[50, Infinity], [50, 50], [[50, Infinity]]],
+ [[50, Infinity], [50, 75], [[75, Infinity]]],
+ [[50, Infinity], [50, 100], [[100, Infinity]]],
+ [[50, Infinity], [75, 75], [[50, 75], [75, Infinity]]],
+ [[50, Infinity], [75, 100], [[50, 75], [100, Infinity]]],
+ ];
+
+ for (const row of truthTable) {
+ const ranges = Range.findDifference(
+ Range.fromExplicitRange(row[0][0], row[0][1]),
+ Range.fromExplicitRange(row[1][0], row[1][1]));
+ const simpleRanges = [];
+ for (const range of ranges) {
+ simpleRanges.push([range.min, range.max]);
+ }
+ assert.deepEqual(simpleRanges, row[2], 'range(' + row[0] +
+ ') subtracted by ' + 'range(' + row[1] + ') should be range(' +
+ row[2] + ').');
+ }
+ });
+
+ test('findDifference_AUndefined', function() {
+ const rangeA = undefined;
+ const rangeB = Range.fromExplicitRange(1, 2);
+ assert.throws(function() {Range.findDifference(rangeA, rangeB);});
+ });
+
+ test('findDifference_BUndefined', function() {
+ const rangeA = Range.fromExplicitRange(1, 2);
+ const rangeB = undefined;
+ assert.throws(function() {Range.findDifference(rangeA, rangeB);});
+ });
+
+ test('findDifference_EmptyMinusNormal', function() {
+ const rangeA = new Range();
+ const rangeB = Range.fromExplicitRange(1, 2);
+ const result = Range.findDifference(rangeA, rangeB);
+ assert.deepEqual(result, []);
+ });
+
+ test('findDifference_0MinusNormal', function() {
+ const rangeA = Range.fromExplicitRange(0, 0);
+ const rangeB = Range.fromExplicitRange(1, 2);
+ const result = Range.findDifference(rangeA, rangeB);
+ assert.deepEqual(result, [rangeA]);
+ });
+
+ test('findDifference_NormalMinus0', function() {
+ const rangeA = Range.fromExplicitRange(1, 2);
+ const rangeB = Range.fromExplicitRange(0, 0);
+ const result = Range.findDifference(rangeA, rangeB);
+ assert.deepEqual([rangeA], result);
+ result.min = 5;
+ assert.strictEqual(rangeA.min, 1);
+ });
+
+ test('findDifference_NormalMinusEmpty', function() {
+ const rangeA = Range.fromExplicitRange(1, 2);
+ const rangeB = new Range();
+ const result = Range.findDifference(rangeA, rangeB);
+ assert.deepEqual([rangeA], result);
+ result.min = 5;
+ assert.strictEqual(rangeA.min, 1);
+ });
+
+ test('findDifference_pointMinusEmpty', function() {
+ const rangeA = Range.fromExplicitRange(1, 1);
+ const rangeB = new Range();
+ const result = Range.findDifference(rangeA, rangeB);
+ assert.deepEqual(result, [rangeA]);
+ });
+
+ test('findDifference_emptyMinusPoint', function() {
+ const rangeA = new Range();
+ const rangeB = Range.fromExplicitRange(1, 1);
+ const result = Range.findDifference(rangeA, rangeB);
+ assert.deepEqual(result, []);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/range_utils.html b/chromium/third_party/catapult/tracing/tracing/base/math/range_utils.html
new file mode 100644
index 00000000000..60c5fdd2954
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/range_utils.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides event merging functionality for grouping/analysis.
+ */
+tr.exportTo('tr.b.math', function() {
+ function convertEventsToRanges(events) {
+ return events.map(function(event) {
+ return tr.b.math.Range.fromExplicitRange(event.start, event.end);
+ });
+ }
+
+ function mergeRanges(inRanges, mergeThreshold, mergeFunction) {
+ const remainingEvents = inRanges.slice();
+ remainingEvents.sort(function(x, y) {
+ return x.min - y.min;
+ });
+
+ if (remainingEvents.length <= 1) {
+ const merged = [];
+ if (remainingEvents.length === 1) {
+ merged.push(mergeFunction(remainingEvents));
+ }
+ return merged;
+ }
+
+ const mergedEvents = [];
+
+ let currentMergeBuffer = [];
+ let rightEdge;
+ function beginMerging() {
+ currentMergeBuffer.push(remainingEvents[0]);
+ remainingEvents.splice(0, 1);
+ rightEdge = currentMergeBuffer[0].max;
+ }
+
+ function flushCurrentMergeBuffer() {
+ if (currentMergeBuffer.length === 0) return;
+
+ mergedEvents.push(mergeFunction(currentMergeBuffer));
+ currentMergeBuffer = [];
+
+ // Refill merge buffer if needed.
+ if (remainingEvents.length !== 0) beginMerging();
+ }
+
+ beginMerging();
+
+ while (remainingEvents.length) {
+ const currentEvent = remainingEvents[0];
+
+ const distanceFromRightEdge = currentEvent.min - rightEdge;
+ if (distanceFromRightEdge < mergeThreshold) {
+ rightEdge = Math.max(rightEdge, currentEvent.max);
+ remainingEvents.splice(0, 1);
+ currentMergeBuffer.push(currentEvent);
+ continue;
+ }
+
+ // Too big a gap.
+ flushCurrentMergeBuffer();
+ }
+ flushCurrentMergeBuffer();
+
+ return mergedEvents;
+ }
+
+ // Pass in |opt_totalRange| in order to find empty ranges before the first of
+ // |inRanges| and after the last of |inRanges|.
+ function findEmptyRangesBetweenRanges(inRanges, opt_totalRange) {
+ if (opt_totalRange && opt_totalRange.isEmpty) opt_totalRange = undefined;
+
+ const emptyRanges = [];
+ if (!inRanges.length) {
+ if (opt_totalRange) emptyRanges.push(opt_totalRange);
+ return emptyRanges;
+ }
+
+ inRanges = inRanges.slice();
+ inRanges.sort(function(x, y) {
+ return x.min - y.min;
+ });
+ if (opt_totalRange &&
+ (opt_totalRange.min < inRanges[0].min)) {
+ emptyRanges.push(tr.b.math.Range.fromExplicitRange(
+ opt_totalRange.min, inRanges[0].min));
+ }
+
+ inRanges.forEach(function(range, index) {
+ for (let otherIndex = 0; otherIndex < inRanges.length; ++otherIndex) {
+ if (index === otherIndex) continue;
+ const other = inRanges[otherIndex];
+
+ if (other.min > range.max) {
+ // |inRanges| is sorted, so |other| is the first range after |range|,
+ // and there is an empty range between them.
+ emptyRanges.push(tr.b.math.Range.fromExplicitRange(
+ range.max, other.min));
+ return;
+ }
+ // Otherwise, |other| starts before |range| ends, so |other| might
+ // possibly contain the end of |range|.
+
+ if (other.max > range.max) {
+ // |other| does contain the end of |range|, so no empty range starts
+ // at the end of this |range|.
+ return;
+ }
+ }
+ if (opt_totalRange && (range.max < opt_totalRange.max)) {
+ emptyRanges.push(tr.b.math.Range.fromExplicitRange(
+ range.max, opt_totalRange.max));
+ }
+ });
+ return emptyRanges;
+ }
+
+ return {
+ convertEventsToRanges,
+ findEmptyRangesBetweenRanges,
+ mergeRanges,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/range_utils_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/range_utils_test.html
new file mode 100644
index 00000000000..183a90df6f0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/range_utils_test.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function simpleMerger(events) {
+ return {
+ min: events[0].min,
+ max: events[events.length - 1].max
+ };
+ }
+ test('simple', function() {
+ const inEvents = [
+ {min: 0, max: 100},
+ {min: 100, max: 120},
+ {min: 200, max: 220}
+ ];
+
+ const merged = tr.b.math.mergeRanges(inEvents, 50, simpleMerger);
+
+ assert.strictEqual(merged.length, 2);
+ assert.deepEqual(merged[0], {min: 0, max: 120});
+ assert.deepEqual(merged[1], {min: 200, max: 220});
+ });
+
+ test('overlapping', function() {
+ const inEvents = [
+ {min: 0, max: 100},
+ {min: 80, max: 120},
+ {min: 200, max: 220}
+ ];
+
+ const merged = tr.b.math.mergeRanges(inEvents, 50, simpleMerger);
+
+ assert.strictEqual(merged.length, 2);
+ assert.deepEqual(merged[0], {min: 0, max: 120});
+ assert.deepEqual(merged[1], {min: 200, max: 220});
+ });
+
+ test('middleOneIsSmall', function() {
+ const inEvents = [
+ {min: 0, max: 100},
+ {min: 40, max: 50},
+ {min: 100, max: 120}
+ ];
+
+ const merged = tr.b.math.mergeRanges(inEvents, 50, simpleMerger);
+
+ assert.strictEqual(merged.length, 1);
+ assert.deepEqual(merged[0], {min: 0, max: 120});
+ });
+
+ test('firstEventIsSplitPoint', function() {
+ const inEvents = [
+ {min: 0, max: 100},
+ {min: 150, max: 200}
+ ];
+
+ const merged = tr.b.math.mergeRanges(inEvents, 25, simpleMerger);
+
+ assert.strictEqual(merged.length, 2);
+ assert.deepEqual(merged[0], {min: 0, max: 100});
+ assert.deepEqual(merged[1], {min: 150, max: 200});
+ });
+
+ test('mergeSingleEvent', function() {
+ const inEvents = [
+ {min: 0, max: 100}
+ ];
+
+ let mergeCount = 0;
+ tr.b.math.mergeRanges(inEvents, 25, function(events) {
+ assert.deepEqual(events, inEvents);
+ mergeCount++;
+ });
+ assert.strictEqual(mergeCount, 1);
+ });
+
+ test('zeroDurationSplit', function() {
+ let inEvents = [0, 10, 20, 50, 60];
+ inEvents = inEvents.map(function(event) {
+ return tr.b.math.Range.fromExplicitRange(event, event);
+ });
+ const timestampMerger = function(timestamps) {
+ return {
+ min: timestamps[0].min,
+ max: timestamps[timestamps.length - 1].max
+ };
+ };
+ const merged = tr.b.math.mergeRanges(inEvents, 15, timestampMerger);
+ assert.strictEqual(merged.length, 2);
+ assert.deepEqual(merged[0], {min: 0, max: 20});
+ assert.deepEqual(merged[1], {min: 50, max: 60});
+ });
+
+ test('findEmptyRangesBetweenRanges', function() {
+ const events = [
+ {min: 2, max: 4},
+ {min: 1, max: 3},
+ {min: 6, max: 8}
+ ];
+ const ranges = tr.b.math.findEmptyRangesBetweenRanges(
+ events, {min: 0, max: 10});
+ assert.strictEqual(3, ranges.length);
+ assert.strictEqual(0, ranges[0].min);
+ assert.strictEqual(1, ranges[0].max);
+ assert.strictEqual(4, ranges[1].min);
+ assert.strictEqual(6, ranges[1].max);
+ assert.strictEqual(8, ranges[2].min);
+ assert.strictEqual(10, ranges[2].max);
+ });
+
+ test('findEmptyRangesWithEmptyInput', function() {
+ let ranges = tr.b.math.findEmptyRangesBetweenRanges(
+ [], {min: 0, max: 10});
+ assert.strictEqual(1, ranges.length);
+ assert.strictEqual(0, ranges[0].min);
+ assert.strictEqual(10, ranges[0].max);
+
+ ranges = tr.b.math.findEmptyRangesBetweenRanges([]);
+ assert.strictEqual(0, ranges.length);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/rect.html b/chromium/third_party/catapult/tracing/tracing/base/math/rect.html
new file mode 100644
index 00000000000..194b975a27b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/rect.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b.math', function() {
+ /**
+ * Tracks a 2D bounding box.
+ * @constructor
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/rect_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/rect_test.html
new file mode 100644
index 00000000000..640a7d9221f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/rect_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/rect.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('UVRectBasic', function() {
+ function assertRectEquals(a, b, opt_message) {
+ const ok = true;
+ if (a.x === b.x && a.y === b.y &&
+ a.width === b.width && a.height === b.height) {
+ return;
+ }
+ const message = opt_message || 'Expected "' + a.toString() +
+ '", got "' + b.toString() + '"';
+ assert.fail(a, b, message);
+ }
+ const container = tr.b.math.Rect.fromXYWH(0, 0, 10, 10);
+ const inner = tr.b.math.Rect.fromXYWH(1, 1, 8, 8);
+ const uv = inner.asUVRectInside(container);
+ assertRectEquals(uv, tr.b.math.Rect.fromXYWH(0.1, 0.1, .8, .8));
+ assert.strictEqual(10, container.size().width);
+ assert.strictEqual(10, container.size().height);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/running_statistics.html b/chromium/third_party/catapult/tracing/tracing/base/math/running_statistics.html
new file mode 100644
index 00000000000..d2dc5e5f6c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/running_statistics.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b.math', function() {
+ /**
+ * An object of this class computes basic statistics online in O(1).
+ * Usage:
+ * 1. Create an instance.
+ * 2. Add numbers using the |add| method.
+ * 3. Query statistics.
+ * 4. Repeat from step 2.
+ */
+ class RunningStatistics {
+ constructor() {
+ this.mean_ = 0;
+ this.count_ = 0;
+ this.max_ = -Infinity;
+ this.min_ = Infinity;
+ this.sum_ = 0;
+ this.variance_ = 0;
+
+ // Mean of logarithms of absolute values of samples, or undefined if any
+ // samples were <= 0.
+ this.meanlogs_ = 0;
+ }
+
+ get count() {
+ return this.count_;
+ }
+
+ get geometricMean() {
+ if (this.meanlogs_ === undefined) return 0;
+ return Math.exp(this.meanlogs_);
+ }
+
+ get mean() {
+ if (this.count_ === 0) return undefined;
+ return this.mean_;
+ }
+
+ get max() {
+ return this.max_;
+ }
+
+ get min() {
+ return this.min_;
+ }
+
+ get sum() {
+ return this.sum_;
+ }
+
+ get variance() {
+ if (this.count_ === 0) return undefined;
+ if (this.count_ === 1) return 0;
+ return this.variance_ / (this.count_ - 1);
+ }
+
+ get stddev() {
+ if (this.count_ === 0) return undefined;
+ return Math.sqrt(this.variance);
+ }
+
+ add(x) {
+ this.count_++;
+ this.max_ = Math.max(this.max_, x);
+ this.min_ = Math.min(this.min_, x);
+ this.sum_ += x;
+
+ // The geometric mean is computed using the arithmetic mean of logarithms.
+ if (x <= 0) {
+ this.meanlogs_ = undefined;
+ } else if (this.meanlogs_ !== undefined) {
+ this.meanlogs_ += (Math.log(Math.abs(x)) - this.meanlogs_) / this.count;
+ }
+
+ // The following uses Welford's algorithm for computing running mean
+ // and variance. See http://www.johndcook.com/blog/standard_deviation.
+ if (this.count_ === 1) {
+ this.mean_ = x;
+ this.variance_ = 0;
+ } else {
+ const oldMean = this.mean_;
+ const oldVariance = this.variance_;
+ // Using the 2nd formula for updating the mean yields better precision
+ // but it doesn't work for the case oldMean is Infinity. Hence we handle
+ // that case separately.
+ if (oldMean === Infinity || oldMean === -Infinity) {
+ this.mean_ = this.sum_ / this.count_;
+ } else {
+ this.mean_ = oldMean + (x - oldMean) / this.count_;
+ }
+ this.variance_ = oldVariance + (x - oldMean) * (x - this.mean_);
+ }
+ }
+
+ merge(other) {
+ const result = new RunningStatistics();
+ result.count_ = this.count_ + other.count_;
+ result.sum_ = this.sum_ + other.sum_;
+ result.min_ = Math.min(this.min_, other.min_);
+ result.max_ = Math.max(this.max_, other.max_);
+ if (result.count === 0) {
+ result.mean_ = 0;
+ result.variance_ = 0;
+ result.meanlogs_ = 0;
+ } else {
+ // Combine the mean and the variance using the formulas from
+ // https://goo.gl/ddcAep.
+ result.mean_ = result.sum / result.count;
+ const deltaMean = (this.mean || 0) - (other.mean || 0);
+ result.variance_ = this.variance_ + other.variance_ +
+ (this.count * other.count * deltaMean * deltaMean / result.count);
+
+ // Merge the arithmetic means of logarithms of absolute values of
+ // samples, weighted by counts.
+ 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) {
+ // geomean = exp(meanlogs)
+ // meanlogs is serialized, but geomean is displayed.
+ // truncate meanlogs such that geomean is unaffected.
+ 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 [];
+ }
+ // It's more efficient to serialize these fields in an array. If you
+ // add any other fields, you should re-evaluate whether it would be more
+ // efficient to serialize as a dict.
+ 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;
+ }
+ [
+ result.count_,
+ result.max_,
+ result.meanlogs_,
+ result.mean_,
+ result.min_,
+ result.sum_,
+ result.variance_,
+ ] = dict;
+ return result;
+ }
+ }
+
+ return {
+ RunningStatistics,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/running_statistics_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/running_statistics_test.html
new file mode 100644
index 00000000000..5a06ebe5cb8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/running_statistics_test.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/running_statistics.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const RunningStatistics = tr.b.math.RunningStatistics;
+ const Statistics = tr.b.math.Statistics;
+
+ function run(data) {
+ const running = new RunningStatistics();
+ data.forEach(x => running.add(x));
+ return running;
+ }
+
+ test('truncate', function() {
+ const running = run([50, 60, 70 + (1 / 3)]);
+ assert.closeTo(59.533, running.geometricMean, 1e-3);
+ assert.strictEqual(
+ '[3,70.33333333333333,4.0865378041527345,60.11111111111111,50,' +
+ '180.33333333333331,206.74074074074068]',
+ JSON.stringify(running.asDict()));
+
+ running.truncate(tr.b.Unit.byName.unitlessNumber);
+ assert.closeTo(59.533, running.geometricMean, 1e-3);
+ assert.strictEqual(70.3333, running.max);
+ assert.strictEqual(4.086538, running.meanlogs_);
+ assert.strictEqual(60.1111, running.mean);
+ assert.strictEqual(180.3333, running.sum);
+ assert.strictEqual(206.7407, running.variance_);
+ });
+
+ test('sum', function() {
+ let data;
+ data = [];
+ assert.closeTo(Statistics.sum(data), run(data).sum, 1e-6);
+ data = [1];
+ assert.closeTo(Statistics.sum(data), run(data).sum, 1e-6);
+ data = [1, 2, 3];
+ assert.closeTo(Statistics.sum(data), run(data).sum, 1e-6);
+ data = [2, 4, 4, 2];
+ assert.closeTo(Statistics.sum(data), run(data).sum, 1e-6);
+ data = [Infinity, Infinity, Infinity, 4, 4, Infinity, 1];
+ assert.strictEqual(Statistics.sum(data), run(data).sum, Infinity);
+ data = [-Infinity, -Infinity, 2, -Infinity, 5, -Infinity];
+ assert.strictEqual(Statistics.sum(data), run(data).sum, -Infinity);
+ });
+
+ test('min', function() {
+ let data;
+ data = [];
+ assert.strictEqual(Statistics.min(data), run(data).min);
+ data = [1];
+ assert.strictEqual(Statistics.min(data), run(data).min);
+ data = [1, 2, 3];
+ assert.strictEqual(Statistics.min(data), run(data).min);
+ data = [2, 4, 4, 2];
+ assert.strictEqual(Statistics.min(data), run(data).min);
+ });
+
+ test('max', function() {
+ let data;
+ data = [];
+ assert.strictEqual(Statistics.max(data), run(data).max);
+ data = [1];
+ assert.strictEqual(Statistics.max(data), run(data).max);
+ data = [1, 2, 3];
+ assert.strictEqual(Statistics.max(data), run(data).max);
+ data = [2, 4, 4, 2];
+ assert.strictEqual(Statistics.max(data), run(data).max);
+ });
+
+ test('mean', function() {
+ let data;
+ data = [];
+ assert.strictEqual(Statistics.mean(data), run(data).mean);
+ data = [1];
+ assert.strictEqual(Statistics.mean(data), run(data).mean);
+ data = [1, 2, 3];
+ assert.closeTo(Statistics.mean(data), run(data).mean, 1e-6);
+ data = [2, 4, 4, 2];
+ assert.closeTo(Statistics.mean(data), run(data).mean, 1e-6);
+ data = [Infinity, Infinity, Infinity, 4, 4, Infinity, 1];
+ assert.strictEqual(Statistics.mean(data), run(data).mean, Infinity);
+ data = [-Infinity, -Infinity, 2, -Infinity, 5, -Infinity];
+ assert.strictEqual(Statistics.mean(data), run(data).mean, -Infinity);
+ });
+
+ test('geometricMean', function() {
+ let data;
+ data = [];
+ assert.strictEqual(Statistics.geometricMean(data), run(data).geometricMean);
+ data = [-1];
+ assert.strictEqual(Statistics.geometricMean(data), run(data).geometricMean);
+ data = [1];
+ assert.strictEqual(Statistics.geometricMean(data), run(data).geometricMean);
+ data = [1, 2, 3];
+ assert.closeTo(Statistics.geometricMean(data),
+ run(data).geometricMean, 1e-6);
+ data = [2, 4, 4, 2];
+ assert.closeTo(Statistics.geometricMean(data),
+ run(data).geometricMean, 1e-6);
+ });
+
+ test('variance', function() {
+ let data;
+ data = [];
+ assert.strictEqual(Statistics.variance(data), run(data).variance);
+ data = [1];
+ assert.strictEqual(Statistics.variance(data), run(data).variance);
+ data = [1, 2, 3];
+ assert.closeTo(Statistics.variance(data), run(data).variance, 1e-6);
+ data = [2, 4, 4, 2];
+ assert.closeTo(Statistics.variance(data), run(data).variance, 1e-6);
+ });
+
+ test('stddev', function() {
+ let data;
+ data = [];
+ assert.strictEqual(Statistics.stddev(data), run(data).stddev);
+ data = [1];
+ assert.strictEqual(Statistics.stddev(data), run(data).stddev);
+ data = [1, 2, 3];
+ assert.closeTo(Statistics.stddev(data), run(data).stddev, 1e-6);
+ data = [2, 4, 4, 2];
+ assert.closeTo(Statistics.stddev(data), run(data).stddev, 1e-6);
+ });
+
+ test('merge', function() {
+ let data1 = [];
+ let data2 = [];
+ let data = data1.concat(data2);
+ let stats = run(data1).merge(run(data2));
+ assert.strictEqual(Statistics.sum(data), stats.sum);
+ assert.strictEqual(Statistics.min(data), stats.min);
+ assert.strictEqual(Statistics.max(data), stats.max);
+ assert.strictEqual(Statistics.mean(data), stats.mean);
+ assert.strictEqual(Statistics.variance(data), stats.variance);
+ assert.strictEqual(Statistics.stddev(data), stats.stddev);
+ assert.strictEqual(Statistics.geometricMean(data), stats.geometricMean);
+
+ data1 = [];
+ data2 = [1, 2, 3];
+ data = data1.concat(data2);
+ stats = run(data1).merge(run(data2));
+ assert.strictEqual(Statistics.sum(data), stats.sum);
+ assert.strictEqual(Statistics.min(data), stats.min);
+ assert.strictEqual(Statistics.max(data), stats.max);
+ assert.strictEqual(Statistics.mean(data), stats.mean);
+ assert.closeTo(Statistics.variance(data), stats.variance, 1e-6);
+ assert.closeTo(Statistics.stddev(data), stats.stddev, 1e-6);
+ assert.closeTo(Statistics.geometricMean(data), stats.geometricMean, 1e-6);
+
+ data1 = [1, 2, 3];
+ data2 = [];
+ data = data1.concat(data2);
+ stats = run(data1).merge(run(data2));
+ assert.strictEqual(Statistics.sum(data), stats.sum);
+ assert.strictEqual(Statistics.min(data), stats.min);
+ assert.strictEqual(Statistics.max(data), stats.max);
+ assert.strictEqual(Statistics.mean(data), stats.mean);
+ assert.closeTo(Statistics.variance(data), stats.variance, 1e-6);
+ assert.closeTo(Statistics.stddev(data), stats.stddev, 1e-6);
+ assert.closeTo(Statistics.geometricMean(data), stats.geometricMean, 1e-6);
+
+ data1 = [1, 2, 3];
+ data2 = [10, 20, 100];
+ data = data1.concat(data2);
+ stats = run(data1).merge(run(data2));
+ assert.strictEqual(Statistics.sum(data), stats.sum);
+ assert.strictEqual(Statistics.min(data), stats.min);
+ assert.strictEqual(Statistics.max(data), stats.max);
+ assert.strictEqual(Statistics.mean(data), stats.mean);
+ assert.closeTo(Statistics.variance(data), stats.variance, 1e-6);
+ assert.closeTo(Statistics.stddev(data), stats.stddev, 1e-6);
+ assert.closeTo(Statistics.geometricMean(data), stats.geometricMean, 1e-6);
+
+ data1 = [1, 1, 1, 1, 1];
+ data2 = [10, 20, 10, 40];
+ data = data1.concat(data2);
+ stats = run(data1).merge(run(data2));
+ assert.strictEqual(Statistics.sum(data), stats.sum);
+ assert.strictEqual(Statistics.min(data), stats.min);
+ assert.strictEqual(Statistics.max(data), stats.max);
+ assert.strictEqual(Statistics.mean(data), stats.mean);
+ assert.closeTo(Statistics.variance(data), stats.variance, 1e-6);
+ assert.closeTo(Statistics.stddev(data), stats.stddev, 1e-6);
+ assert.closeTo(Statistics.geometricMean(data), stats.geometricMean, 1e-6);
+ });
+
+ test('serialization', function() {
+ const data = [1, 2, 3];
+ const dict = run(data).asDict();
+ const cloneDict = RunningStatistics.fromDict(dict).asDict();
+ for (let field = 0; field < dict.length; ++field) {
+ assert.closeTo(dict[field], cloneDict[field], 1e-6);
+ }
+
+ // You can change this number, but when you do, please explain in your CL
+ // description why it changed.
+ assert.strictEqual(32, JSON.stringify(dict).length);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/statistics.html b/chromium/third_party/catapult/tracing/tracing/base/math/statistics.html
new file mode 100644
index 00000000000..daa49e522f3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/statistics.html
@@ -0,0 +1,857 @@
+<!DOCTYPE html>
+<!--
+Copyright 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<script src="/mannwhitneyu/mannwhitneyu.js"></script>
+
+<script>
+'use strict';
+
+// In node, the script-src for mannwhitneyu above brings in mannwhitneyui
+// into a module, instead of into the global scope. Whereas this file
+// assumes that mannwhitneyu is in the global scope. So, in Node only, we
+// require() it in, and then take all its exports and shove them into the
+// global scope by hand.
+(function(global) {
+ if (tr.isNode) {
+ const mwuAbsPath = HTMLImportsLoader.hrefToAbsolutePath(
+ '/mannwhitneyu.js');
+ const mwuModule = require(mwuAbsPath);
+ for (const exportName in mwuModule) {
+ global[exportName] = mwuModule[exportName];
+ }
+ }
+})(this);
+</script>
+
+<script>
+'use strict';
+
+// TODO(charliea): Remove:
+/* eslint-disable catapult-camelcase */
+
+tr.exportTo('tr.b.math', function() {
+ const Statistics = {};
+
+ /* Returns the quotient, or zero if the denominator is zero.*/
+ Statistics.divideIfPossibleOrZero = function(numerator, denominator) {
+ if (denominator === 0) return 0;
+ return numerator / denominator;
+ };
+
+ Statistics.sum = function(ary, opt_func, opt_this) {
+ const func = opt_func || (x => x);
+ let ret = 0;
+ let i = 0;
+ for (const elt of ary) {
+ ret += func.call(opt_this, elt, i++);
+ }
+ return ret;
+ };
+
+ Statistics.mean = function(ary, opt_func, opt_this) {
+ const func = opt_func || (x => x);
+ let sum = 0;
+ let i = 0;
+
+ for (const elt of ary) {
+ sum += func.call(opt_this, elt, i++);
+ }
+
+ if (i === 0) return undefined;
+
+ return sum / i;
+ };
+
+ Statistics.geometricMean = function(ary, opt_func, opt_this) {
+ const func = opt_func || (x => x);
+ let i = 0;
+ let logsum = 0;
+
+ // The geometric mean is expressed as the arithmetic mean of logarithms
+ // in order to prevent overflow.
+ for (const elt of ary) {
+ const x = func.call(opt_this, elt, i++);
+ if (x <= 0) return 0;
+ logsum += Math.log(Math.abs(x));
+ }
+
+ if (i === 0) return 1;
+
+ return Math.exp(logsum / i);
+ };
+
+ // Returns undefined if the sum of the weights is zero.
+ Statistics.weightedMean = function(
+ ary, weightCallback, opt_valueCallback, opt_this) {
+ const valueCallback = opt_valueCallback || (x => x);
+ let numerator = 0;
+ let denominator = 0;
+ let i = -1;
+
+ for (const elt of ary) {
+ i++;
+ const value = valueCallback.call(opt_this, elt, i);
+ if (value === undefined) continue;
+ const weight = weightCallback.call(opt_this, elt, i, value);
+ numerator += weight * value;
+ denominator += weight;
+ }
+
+ if (denominator === 0) return undefined;
+
+ return numerator / denominator;
+ };
+
+ Statistics.variance = function(ary, opt_func, opt_this) {
+ if (ary.length === 0) return undefined;
+ if (ary.length === 1) return 0;
+ const func = opt_func || (x => x);
+ const mean = Statistics.mean(ary, func, opt_this);
+ const sumOfSquaredDistances = Statistics.sum(
+ ary,
+ function(d, i) {
+ const v = func.call(this, d, i) - mean;
+ return v * v;
+ },
+ opt_this);
+ return sumOfSquaredDistances / (ary.length - 1);
+ };
+
+ Statistics.stddev = function(ary, opt_func, opt_this) {
+ if (ary.length === 0) return undefined;
+ return Math.sqrt(
+ Statistics.variance(ary, opt_func, opt_this));
+ };
+
+ Statistics.max = function(ary, opt_func, opt_this) {
+ const func = opt_func || (x => x);
+ let ret = -Infinity;
+ let i = 0;
+ for (const elt of ary) {
+ ret = Math.max(ret, func.call(opt_this, elt, i++));
+ }
+ return ret;
+ };
+
+ Statistics.min = function(ary, opt_func, opt_this) {
+ const func = opt_func || (x => x);
+ let ret = Infinity;
+ let i = 0;
+ for (const elt of ary) {
+ ret = Math.min(ret, func.call(opt_this, elt, i++));
+ }
+ return ret;
+ };
+
+ Statistics.range = function(ary, opt_func, opt_this) {
+ const func = opt_func || (x => x);
+ const ret = new tr.b.math.Range();
+ let i = 0;
+ for (const elt of ary) {
+ ret.addValue(func.call(opt_this, elt, i++));
+ }
+ return ret;
+ };
+
+ Statistics.percentile = function(ary, percent, opt_func, opt_this) {
+ if (!(percent >= 0 && percent <= 1)) {
+ throw new Error('percent must be [0,1]');
+ }
+
+ const func = opt_func || (x => x);
+ const tmp = new Array(ary.length);
+ let i = 0;
+ for (const elt of ary) {
+ tmp[i] = func.call(opt_this, elt, i++);
+ }
+ tmp.sort((a, b) => a - b);
+ const idx = Math.floor((ary.length - 1) * percent);
+ return tmp[idx];
+ };
+
+ /**
+ * Sorts the samples, and map them linearly to the range [0,1].
+ *
+ * They're mapped such that for the N samples, the first sample is 0.5/N and
+ * the last sample is (N-0.5)/N.
+ *
+ * Background: The discrepancy of the sample set i/(N-1); i=0, ..., N-1 is
+ * 2/N, twice the discrepancy of the sample set (i+1/2)/N; i=0, ..., N-1. In
+ * our case we don't want to distinguish between these two cases, as our
+ * original domain is not bounded (it is for Monte Carlo integration, where
+ * discrepancy was first used).
+ **/
+ Statistics.normalizeSamples = function(samples) {
+ if (samples.length === 0) {
+ return {
+ normalized_samples: samples,
+ scale: 1.0
+ };
+ }
+ // Create a copy to make sure that we don't mutate original |samples| input.
+ samples = samples.slice().sort(
+ function(a, b) {
+ return a - b;
+ }
+ );
+ const low = Math.min.apply(null, samples);
+ const high = Math.max.apply(null, samples);
+ const newLow = 0.5 / samples.length;
+ const newHigh = (samples.length - 0.5) / samples.length;
+ if (high - low === 0.0) {
+ // Samples is an array of 0.5 in this case.
+ samples = Array.apply(null, new Array(samples.length)).map(
+ function() { return 0.5;});
+ return {
+ normalized_samples: samples,
+ scale: 1.0
+ };
+ }
+ const scale = (newHigh - newLow) / (high - low);
+ for (let i = 0; i < samples.length; i++) {
+ samples[i] = (samples[i] - low) * scale + newLow;
+ }
+ return {
+ normalized_samples: samples,
+ scale
+ };
+ };
+
+ /**
+ * Computes the discrepancy of a set of 1D samples from the interval [0,1].
+ *
+ * The samples must be sorted. We define the discrepancy of an empty set
+ * of samples to be zero.
+ *
+ * http://en.wikipedia.org/wiki/Low-discrepancy_sequence
+ * http://mathworld.wolfram.com/Discrepancy.html
+ */
+ Statistics.discrepancy = function(samples, opt_locationCount) {
+ if (samples.length === 0) return 0.0;
+
+ let maxLocalDiscrepancy = 0;
+ const invSampleCount = 1.0 / samples.length;
+ const locations = [];
+ // For each location, stores the number of samples less than that location.
+ const countLess = [];
+ // For each location, stores the number of samples less than or equal to
+ // that location.
+ const countLessEqual = [];
+
+ if (opt_locationCount !== undefined) {
+ // Generate list of equally spaced locations.
+ let sampleIndex = 0;
+ for (let i = 0; i < opt_locationCount; i++) {
+ const location = i / (opt_locationCount - 1);
+ locations.push(location);
+ while (sampleIndex < samples.length &&
+ samples[sampleIndex] < location) {
+ sampleIndex += 1;
+ }
+ countLess.push(sampleIndex);
+ while (sampleIndex < samples.length &&
+ samples[sampleIndex] <= location) {
+ sampleIndex += 1;
+ }
+ countLessEqual.push(sampleIndex);
+ }
+ } else {
+ // Populate locations with sample positions. Append 0 and 1 if necessary.
+ if (samples[0] > 0.0) {
+ locations.push(0.0);
+ countLess.push(0);
+ countLessEqual.push(0);
+ }
+ for (let i = 0; i < samples.length; i++) {
+ locations.push(samples[i]);
+ countLess.push(i);
+ countLessEqual.push(i + 1);
+ }
+ if (samples[-1] < 1.0) {
+ locations.push(1.0);
+ countLess.push(samples.length);
+ countLessEqual.push(samples.length);
+ }
+ }
+
+ // Compute discrepancy as max(overshoot, -undershoot), where
+ // overshoot = max(countClosed(i, j)/N - length(i, j)) for all i < j,
+ // undershoot = min(countOpen(i, j)/N - length(i, j)) for all i < j,
+ // N = len(samples),
+ // countClosed(i, j) is the number of points between i and j
+ // including ends,
+ // countOpen(i, j) is the number of points between i and j excluding ends,
+ // length(i, j) is locations[i] - locations[j].
+
+ // The following algorithm is modification of Kadane's algorithm,
+ // see https://en.wikipedia.org/wiki/Maximum_subarray_problem.
+
+ // The maximum of (countClosed(k, i-1)/N - length(k, i-1)) for any k < i-1.
+ let maxDiff = 0;
+ // The minimum of (countOpen(k, i-1)/N - length(k, i-1)) for any k < i-1.
+ let minDiff = 0;
+ for (let i = 1; i < locations.length; i++) {
+ const length = locations[i] - locations[i - 1];
+ const countClosed = countLessEqual[i] - countLess[i - 1];
+ const countOpen = countLess[i] - countLessEqual[i - 1];
+ // Number of points that are added if we extend a closed range that
+ // ends at location (i-1).
+ const countClosedIncrement =
+ countLessEqual[i] - countLessEqual[i - 1];
+ // Number of points that are added if we extend an open range that
+ // ends at location (i-1).
+ const countOpenIncrement = countLess[i] - countLess[i - 1];
+
+ // Either extend the previous optimal range or start a new one.
+ maxDiff = Math.max(
+ countClosedIncrement * invSampleCount - length + maxDiff,
+ countClosed * invSampleCount - length);
+ minDiff = Math.min(
+ countOpenIncrement * invSampleCount - length + minDiff,
+ countOpen * invSampleCount - length);
+
+ maxLocalDiscrepancy = Math.max(
+ maxDiff, -minDiff, maxLocalDiscrepancy);
+ }
+ return maxLocalDiscrepancy;
+ };
+
+ /**
+ * A discrepancy based metric for measuring timestamp jank.
+ *
+ * timestampsDiscrepancy quantifies the largest area of jank observed in a
+ * series of timestamps. Note that this is different from metrics based on
+ * the max_time_interval. For example, the time stamp series A = [0,1,2,3,5,6]
+ * and B = [0,1,2,3,5,7] have the same max_time_interval = 2, but
+ * Discrepancy(B) > Discrepancy(A).
+ *
+ * Two variants of discrepancy can be computed:
+ *
+ * Relative discrepancy is following the original definition of
+ * discrepancy. It characterized the largest area of jank, relative to the
+ * duration of the entire time stamp series. We normalize the raw results,
+ * because the best case discrepancy for a set of N samples is 1/N (for
+ * equally spaced samples), and we want our metric to report 0.0 in that
+ * case.
+ *
+ * Absolute discrepancy also characterizes the largest area of jank, but its
+ * value wouldn't change (except for imprecisions due to a low
+ * |interval_multiplier|) if additional 'good' intervals were added to an
+ * exisiting list of time stamps. Its range is [0,inf] and the unit is
+ * milliseconds.
+ *
+ * The time stamp series C = [0,2,3,4] and D = [0,2,3,4,5] have the same
+ * absolute discrepancy, but D has lower relative discrepancy than C.
+ *
+ * |timestamps| may be a list of lists S = [S_1, S_2, ..., S_N], where each
+ * S_i is a time stamp series. In that case, the discrepancy D(S) is:
+ * D(S) = max(D(S_1), D(S_2), ..., D(S_N))
+ **/
+ Statistics.timestampsDiscrepancy = function(timestamps, opt_absolute,
+ opt_locationCount) {
+ if (timestamps.length === 0) return 0.0;
+
+ if (opt_absolute === undefined) opt_absolute = true;
+
+ if (Array.isArray(timestamps[0])) {
+ const rangeDiscrepancies = timestamps.map(function(r) {
+ return Statistics.timestampsDiscrepancy(r);
+ });
+ return Math.max.apply(null, rangeDiscrepancies);
+ }
+
+ const s = Statistics.normalizeSamples(timestamps);
+ const samples = s.normalized_samples;
+ const sampleScale = s.scale;
+ let discrepancy = Statistics.discrepancy(samples, opt_locationCount);
+ const invSampleCount = 1.0 / samples.length;
+ if (opt_absolute === true) {
+ // Compute absolute discrepancy
+ discrepancy /= sampleScale;
+ } else {
+ // Compute relative discrepancy
+ discrepancy = tr.b.math.clamp(
+ (discrepancy - invSampleCount) / (1.0 - invSampleCount), 0.0, 1.0);
+ }
+ return discrepancy;
+ };
+
+ /**
+ * Modifies |samples| in-place to reduce its length down to |count|.
+ *
+ * @param {!Array} samples
+ * @param {number} count
+ * @return {!Array}
+ */
+ Statistics.uniformlySampleArray = function(samples, count) {
+ if (samples.length <= count) {
+ return samples;
+ }
+ while (samples.length > count) {
+ const i = parseInt(Math.random() * samples.length);
+ samples.splice(i, 1);
+ }
+ return samples;
+ };
+
+ /**
+ * A mechanism to uniformly sample elements from an arbitrary long stream.
+ *
+ * Call this method every time a new element is obtained from the stream,
+ * passing always the same |samples| array and the |numSamples| you desire.
+ * Also pass in the current |streamLength|, which is the same as the index of
+ * |newElement| within that stream.
+ *
+ * The |samples| array will possibly be updated, replacing one of its element
+ * with |newElements|. The length of |samples| will not be more than
+ * |numSamples|.
+ *
+ * This method guarantees that after |streamLength| elements have been
+ * processed each one has equal probability of being in |samples|. The order
+ * of samples is not preserved though.
+ *
+ * Args:
+ * samples: Array of elements that have already been selected. Start with [].
+ * streamLength: The current length of the stream, up to |newElement|.
+ * newElement: The element that was just extracted from the stream.
+ * numSamples: The total number of samples desired.
+ **/
+ Statistics.uniformlySampleStream = function(samples, streamLength, newElement,
+ numSamples) {
+ if (streamLength <= numSamples) {
+ if (samples.length >= streamLength) {
+ samples[streamLength - 1] = newElement;
+ } else {
+ samples.push(newElement);
+ }
+ return;
+ }
+
+ const probToKeep = numSamples / streamLength;
+ if (Math.random() > probToKeep) return; // New sample was rejected.
+
+ // Keeping it, replace an alement randomly.
+ const index = Math.floor(Math.random() * numSamples);
+ samples[index] = newElement;
+ };
+
+ /**
+ * A mechanism to merge two arrays of uniformly sampled elements in a way that
+ * ensures elements in the final array are still sampled uniformly.
+ *
+ * This works similarly to sampleStreamUniform. The |samplesA| array will be
+ * updated, some of its elements replaced by elements from |samplesB| in a
+ * way that ensure that elements will be sampled uniformly.
+ *
+ * Args:
+ * samplesA: Array of uniformly sampled elements, will be updated.
+ * streamLengthA: The length of the stream from which |samplesA| was sampled.
+ * samplesB: Other array of uniformly sampled elements, will NOT be updated.
+ * streamLengthB: The length of the stream from which |samplesB| was sampled.
+ * numSamples: The total number of samples desired, both in |samplesA| and
+ * |samplesB|.
+ **/
+ Statistics.mergeSampledStreams = function(
+ samplesA, streamLengthA,
+ samplesB, streamLengthB, numSamples) {
+ if (streamLengthB < numSamples) {
+ // samplesB has not reached max capacity so every sample of stream B were
+ // chosen with certainty. Add them one by one into samplesA.
+ const nbElements = Math.min(streamLengthB, samplesB.length);
+ for (let i = 0; i < nbElements; ++i) {
+ Statistics.uniformlySampleStream(samplesA, streamLengthA + i + 1,
+ samplesB[i], numSamples);
+ }
+ return;
+ }
+ if (streamLengthA < numSamples) {
+ // samplesA has not reached max capacity so every sample of stream A were
+ // chosen with certainty. Add them one by one into samplesB.
+ const nbElements = Math.min(streamLengthA, samplesA.length);
+ const tempSamples = samplesB.slice();
+ for (let i = 0; i < nbElements; ++i) {
+ Statistics.uniformlySampleStream(tempSamples, streamLengthB + i + 1,
+ samplesA[i], numSamples);
+ }
+ // Copy that back into the first vector.
+ for (let i = 0; i < tempSamples.length; ++i) {
+ samplesA[i] = tempSamples[i];
+ }
+ return;
+ }
+
+ // Both sample arrays are at max capacity, use the power of maths!
+ // Elements in samplesA have been selected with probability
+ // numSamples / streamLengthA. Same for samplesB. For each index of the
+ // array we keep samplesA[i] with probability
+ // P = streamLengthA / (streamLengthA + streamLengthB)
+ // and replace it with samplesB[i] with probability 1-P.
+ // The total probability of keeping it is therefore
+ // numSamples / streamLengthA *
+ // streamLengthA / (streamLengthA + streamLengthB)
+ // = numSamples / (streamLengthA + streamLengthB)
+ // A similar computation shows we have the same probability of keeping any
+ // element in samplesB. Magic!
+ const nbElements = Math.min(numSamples, samplesB.length);
+ const probOfSwapping = streamLengthB / (streamLengthA + streamLengthB);
+ for (let i = 0; i < nbElements; ++i) {
+ if (Math.random() < probOfSwapping) {
+ samplesA[i] = samplesB[i];
+ }
+ }
+ };
+
+ /* Continuous distributions are defined by probability density functions.
+ *
+ * Random variables are referred to by capital letters: X, Y, Z.
+ * Particular values from these distributions are referred to by lowercase
+ * letters like |x|.
+ * The probability that |X| ever exactly equals |x| is P(X==x) = 0.
+ *
+ * For a discrete probability distribution, see tr.v.Histogram.
+ */
+ function Distribution() {
+ }
+
+ Distribution.prototype = {
+ /* The probability density of the random variable at value |x| is the
+ * relative likelihood for this random variable to take on the given value
+ * |x|.
+ *
+ * @param {number} x A value from the random distribution.
+ * @return {number} probability density at x.
+ */
+ computeDensity(x) {
+ throw Error('Not implemented');
+ },
+
+ /* A percentile is the probability that a sample from the distribution is
+ * less than the given value |x|. This function is monotonically increasing.
+ *
+ * @param {number} x A value from the random distribution.
+ * @return {number} P(X<x).
+ */
+ computePercentile(x) {
+ throw Error('Not implemented');
+ },
+
+ /* A complementary percentile is the probability that a sample from the
+ * distribution is greater than the given value |x|. This function is
+ * monotonically decreasing.
+ *
+ * @param {number} x A value from the random distribution.
+ * @return {number} P(X>x).
+ */
+ computeComplementaryPercentile(x) {
+ return 1 - this.computePercentile(x);
+ },
+
+ /* Compute the mean of the probability distribution.
+ *
+ * @return {number} mean.
+ */
+ get mean() {
+ throw Error('Not implemented');
+ },
+
+ /* The mode of a distribution is the most likely value.
+ * The maximum of the computeDensity() function is at this mode.
+ * @return {number} mode.
+ */
+ get mode() {
+ throw Error('Not implemented');
+ },
+
+ /* The median is the center value of the distribution.
+ * computePercentile(median) = computeComplementaryPercentile(median) = 0.5
+ *
+ * @return {number} median.
+ */
+ get median() {
+ throw Error('Not implemented');
+ },
+
+ /* The standard deviation is a measure of how dispersed or spread out the
+ * distribution is (this statistic has the same units as the values).
+ *
+ * @return {number} standard deviation.
+ */
+ get standardDeviation() {
+ throw Error('Not implemented');
+ },
+
+ /* An alternative measure of how spread out the distribution is,
+ * the variance is the square of the standard deviation.
+ * @return {number} variance.
+ */
+ get variance() {
+ throw Error('Not implemented');
+ }
+ };
+
+ Statistics.UniformDistribution = function(opt_range) {
+ if (!opt_range) opt_range = tr.b.math.Range.fromExplicitRange(0, 1);
+ this.range = opt_range;
+ };
+
+ Statistics.UniformDistribution.prototype = {
+ __proto__: Distribution.prototype,
+
+ computeDensity(x) {
+ return 1 / this.range.range;
+ },
+
+ computePercentile(x) {
+ return tr.b.math.normalize(x, this.range.min, this.range.max);
+ },
+
+ get mean() {
+ return this.range.center;
+ },
+
+ get mode() {
+ return undefined;
+ },
+
+ get median() {
+ return this.mean;
+ },
+
+ get standardDeviation() {
+ return Math.sqrt(this.variance);
+ },
+
+ get variance() {
+ return Math.pow(this.range.range, 2) / 12;
+ }
+ };
+
+ /* The Normal or Gaussian distribution, or bell curve, is common in complex
+ * processes such as are found in many of the natural sciences. If Z is the
+ * standard normal distribution with mean = 0 and variance = 1, then the
+ * general normal distribution is Y = mean + Z*sqrt(variance).
+ * https://www.desmos.com/calculator/tqtbjm4s3z
+ */
+ Statistics.NormalDistribution = function(opt_mean, opt_variance) {
+ this.mean_ = opt_mean || 0;
+ this.variance_ = opt_variance || 1;
+ this.standardDeviation_ = Math.sqrt(this.variance_);
+ };
+
+ Statistics.NormalDistribution.prototype = {
+ __proto__: Distribution.prototype,
+
+ computeDensity(x) {
+ const scale = (1.0 / (this.standardDeviation * Math.sqrt(2.0 * Math.PI)));
+ const exponent = -Math.pow(x - this.mean, 2) / (2.0 * this.variance);
+ return scale * Math.exp(exponent);
+ },
+
+ computePercentile(x) {
+ const standardizedX = ((x - this.mean) /
+ Math.sqrt(2.0 * this.variance));
+ return (1.0 + tr.b.math.erf(standardizedX)) / 2.0;
+ },
+
+ get mean() {
+ return this.mean_;
+ },
+
+ get median() {
+ return this.mean;
+ },
+
+ get mode() {
+ return this.mean;
+ },
+
+ get standardDeviation() {
+ return this.standardDeviation_;
+ },
+
+ get variance() {
+ return this.variance_;
+ }
+ };
+
+ /* The log-normal distribution is a continuous probability distribution of a
+ * random variable whose logarithm is normally distributed.
+ * If Y is the general normal distribution, then X = exp(Y) is the general
+ * log-normal distribution.
+ * X will have different parameters from Y,
+ * so the mean of Y is called the "location" of X,
+ * and the standard deviation of Y is called the "shape" of X.
+ * The standard lognormal distribution exp(Z) has location = 0 and shape = 1.
+ * https://www.desmos.com/calculator/tqtbjm4s3z
+ */
+ Statistics.LogNormalDistribution = function(opt_location, opt_shape) {
+ this.normalDistribution_ = new Statistics.NormalDistribution(
+ opt_location, Math.pow(opt_shape || 1, 2));
+ };
+
+ Statistics.LogNormalDistribution.prototype = {
+ __proto__: Statistics.NormalDistribution.prototype,
+
+ computeDensity(x) {
+ return this.normalDistribution_.computeDensity(Math.log(x)) / x;
+ },
+
+ computePercentile(x) {
+ return this.normalDistribution_.computePercentile(Math.log(x));
+ },
+
+ get mean() {
+ return Math.exp(this.normalDistribution_.mean +
+ (this.normalDistribution_.variance / 2));
+ },
+
+ get variance() {
+ const nm = this.normalDistribution_.mean;
+ const nv = this.normalDistribution_.variance;
+ return (Math.exp(2 * (nm + nv)) -
+ Math.exp(2 * nm + nv));
+ },
+
+ get standardDeviation() {
+ return Math.sqrt(this.variance);
+ },
+
+ get median() {
+ return Math.exp(this.normalDistribution_.mean);
+ },
+
+ get mode() {
+ return Math.exp(this.normalDistribution_.mean -
+ this.normalDistribution_.variance);
+ }
+ };
+
+ /**
+ * Instead of describing a LogNormalDistribution in terms of its "location"
+ * and "shape", it can also be described in terms of its median
+ * and the point at which its complementary cumulative distribution
+ * function bends between the linear-ish region in the middle and the
+ * exponential-ish region. When the distribution is used to compute
+ * percentiles for log-normal random processes such as latency, as the latency
+ * improves, it hits a point of diminishing returns, when it becomes
+ * relatively difficult to improve the score further. This point of
+ * diminishing returns is the first x-intercept of the third derivative of the
+ * CDF, which is the second derivative of the PDF.
+ *
+ * https://www.desmos.com/calculator/cg5rnftabn
+ *
+ * @param {number} median The median of the distribution.
+ * @param {number} diminishingReturns The point of diminishing returns.
+ * @return {LogNormalDistribution}
+ */
+ Statistics.LogNormalDistribution.fromMedianAndDiminishingReturns =
+ function(median, diminishingReturns) {
+ diminishingReturns = Math.log(diminishingReturns / median);
+ const shape = Math.sqrt(1 - 3 * diminishingReturns -
+ Math.sqrt(Math.pow(diminishingReturns - 3, 2) - 8)) / 2;
+ const location = Math.log(median);
+ return new Statistics.LogNormalDistribution(location, shape);
+ };
+
+ // p-values less than this indicate statistical significance.
+ Statistics.DEFAULT_ALPHA = 0.01;
+
+ // If a statistical significant difference has not been established with
+ // this many observations per sample, we'll assume none exists.
+ Statistics.MAX_SUGGESTED_SAMPLE_SIZE = 20;
+
+ /** @enum */
+ Statistics.Significance = {
+ SIGNIFICANT: 'REJECT',
+ INSIGNIFICANT: 'FAIL_TO_REJECT',
+ NEED_MORE_DATA: 'NEED_MORE_DATA',
+ DONT_CARE: 'DONT_CARE',
+ };
+
+
+ class HypothesisTestResult {
+ constructor(p, u, needMoreData, opt_alpha) {
+ this.p_ = p;
+ this.u_ = u;
+ this.needMoreData_ = needMoreData;
+ this.compare(opt_alpha);
+ }
+
+ /**
+ * The probability under the null hypothesis (i.e. the two samples are
+ * indistinguishable) of obtaining a result more extreme than observed.
+ * @return number in (0,1)
+ */
+ get p() {
+ return this.p_;
+ }
+
+ /**
+ * The U statistic from MWU.
+ * @return number
+ */
+ get U() {
+ return this.u_;
+ }
+
+ /**
+ * @return {!tr.b.math.Statistics.Significance}
+ */
+ get significance() {
+ return this.significance_;
+ }
+
+ /**
+ * Recompute |significance| with the same p-value but a new alpha threshold.
+ * Faster than recomputing MWU.
+ *
+ * @param {number=}
+ * @return {!tr.b.math.Statistics.Significance}
+ */
+ compare(opt_alpha) {
+ const alpha = opt_alpha || Statistics.DEFAULT_ALPHA;
+ if (this.p < alpha) {
+ this.significance_ = Statistics.Significance.SIGNIFICANT;
+ } else if (this.needMoreData_) {
+ this.significance_ = Statistics.Significance.NEED_MORE_DATA;
+ } else {
+ this.significance_ = Statistics.Significance.INSIGNIFICANT;
+ }
+ return this.significance_;
+ }
+
+ asDict() {
+ return {
+ p: this.p,
+ U: this.U,
+ significance: this.significance,
+ };
+ }
+ }
+
+ /**
+ * @param {!Array.<number>} a
+ * @param {!Array.<number>} b
+ * @param {number=} opt_alpha
+ * @param {number=} opt_reqSampleSize
+ * @return {!HypothesisTestResult}
+ */
+ Statistics.mwu = function(a, b, opt_alpha, opt_reqSampleSize) {
+ const result = mannwhitneyu.test(a, b);
+ const needMoreData = opt_reqSampleSize &&
+ Math.min(a.length, b.length) < opt_reqSampleSize;
+ return new HypothesisTestResult(
+ result.p, result.U, needMoreData, opt_alpha);
+ };
+
+ return {
+ Statistics,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/math/statistics_test.html b/chromium/third_party/catapult/tracing/tracing/base/math/statistics_test.html
new file mode 100644
index 00000000000..e0b081a3af1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/math/statistics_test.html
@@ -0,0 +1,579 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/statistics.html">
+<script>
+'use strict';
+
+// TODO(charliea): Remove:
+/* eslint-disable catapult-camelcase */
+
+tr.b.unittest.testSuite(function() {
+ const Statistics = tr.b.math.Statistics;
+
+ /**
+ * Lloyd relaxation in 1D.
+ *
+ * Keeps the position of the first and last sample.
+ **/
+ function relax(samples, opt_iterations) {
+ opt_iterations = opt_iterations || 10;
+ for (let i = 0; i < opt_iterations; i++) {
+ const voronoiBoundaries = [];
+ for (let j = 1; j < samples.length; j++) {
+ voronoiBoundaries.push((samples[j] + samples[j - 1]) * 0.5);
+ }
+
+ const relaxedSamples = [];
+ relaxedSamples.push(samples[0]);
+ for (let j = 1; j < samples.length - 1; j++) {
+ relaxedSamples.push(
+ (voronoiBoundaries[j - 1] + voronoiBoundaries[j]) * 0.5);
+ }
+ relaxedSamples.push(samples[samples.length - 1]);
+ samples = relaxedSamples;
+ }
+ return samples;
+ }
+
+ function createRandomSamples(numSamples) {
+ const samples = [];
+ let position = 0.0;
+ samples.push(position);
+ for (let i = 1; i < numSamples; i++) {
+ position += Math.random();
+ samples.push(position);
+ }
+ return samples;
+ }
+
+ test('normalDistribution', function() {
+ for (let mean = -100; mean <= 100; mean += 25) {
+ for (let stddev = 0.1; stddev < 2; stddev += 0.2) {
+ const dist = new Statistics.NormalDistribution(mean, stddev * stddev);
+ assert.closeTo(mean, dist.mean, 1e-6);
+ assert.closeTo(stddev, dist.standardDeviation, 1e-6);
+ assert.closeTo(0, dist.standardDeviation * dist.computeDensity(
+ -1e10), 1e-5);
+ assert.closeTo(0.05399, dist.standardDeviation * dist.computeDensity(
+ dist.mean - 2 * dist.standardDeviation), 1e-5);
+ assert.closeTo(0.24197, dist.standardDeviation * dist.computeDensity(
+ dist.mean - dist.standardDeviation), 1e-5);
+ assert.closeTo(0.39894, dist.standardDeviation * dist.computeDensity(
+ dist.mean), 1e-5);
+ assert.closeTo(0.24197, dist.standardDeviation * dist.computeDensity(
+ dist.mean + dist.standardDeviation), 1e-5);
+ assert.closeTo(0.054, dist.standardDeviation * dist.computeDensity(
+ dist.mean + 2 * dist.standardDeviation), 1e-5);
+ assert.closeTo(0, dist.standardDeviation * dist.computeDensity(
+ 1e10), 1e-5);
+
+ assert.closeTo(0, dist.computePercentile(-1e10), 1e-5);
+ assert.closeTo(0.02275, dist.computePercentile(
+ dist.mean - 2 * dist.standardDeviation), 1e-5);
+ assert.closeTo(0.15866, dist.computePercentile(
+ dist.mean - dist.standardDeviation), 1e-5);
+ assert.closeTo(0.5, dist.computePercentile(dist.mean), 1e-5);
+ assert.closeTo(0.841344, dist.computePercentile(
+ dist.mean + dist.standardDeviation), 1e-5);
+ assert.closeTo(0.97725, dist.computePercentile(
+ dist.mean + 2 * dist.standardDeviation), 1e-5);
+ assert.closeTo(1, dist.computePercentile(1e10), 1e-5);
+ }
+ }
+ });
+
+ test('logNormalDistribution', function() {
+ // Unlike the Normal distribution, the LogNormal distribution can look very
+ // different depending on its parameters, and it's defined in terms of the
+ // Normal distribution anyway, so only test the standard LogNormal
+ // distribution.
+ const dist = new Statistics.LogNormalDistribution(0, 1);
+ assert.closeTo(0.3678, dist.mode, 1e-4);
+ assert.closeTo(1, dist.median, 1e-6);
+ assert.closeTo(1.6487, dist.mean, 1e-4);
+ assert.closeTo(0.65774, dist.computeDensity(dist.mode), 1e-5);
+ assert.closeTo(0.39894, dist.computeDensity(dist.median), 1e-5);
+ assert.closeTo(0.21354, dist.computeDensity(dist.mean), 1e-5);
+ assert.closeTo(0, dist.computePercentile(1e-10), 1e-6);
+ assert.closeTo(0.15865, dist.computePercentile(dist.mode), 1e-5);
+ assert.closeTo(0.5, dist.computePercentile(dist.median), 1e-6);
+ assert.closeTo(0.69146, dist.computePercentile(dist.mean), 1e-5);
+ assert.closeTo(1, dist.computePercentile(1e100), 1e-5);
+ });
+
+ test('divideIfPossibleOrZero', function() {
+ assert.strictEqual(Statistics.divideIfPossibleOrZero(1, 2), 0.5);
+ assert.strictEqual(Statistics.divideIfPossibleOrZero(0, 2), 0);
+ assert.strictEqual(Statistics.divideIfPossibleOrZero(1, 0), 0);
+ assert.strictEqual(Statistics.divideIfPossibleOrZero(0, 0), 0);
+ });
+
+ test('sumBasic', function() {
+ assert.strictEqual(Statistics.sum([1, 2, 3]), 6);
+ });
+
+ test('sumWithFunctor', function() {
+ const ctx = {};
+ const ary = [1, 2, 3];
+ assert.strictEqual(12, Statistics.sum(ary, function(x, i) {
+ assert.strictEqual(this, ctx);
+ assert.strictEqual(ary[i], x);
+ return x * 2;
+ }, ctx));
+ });
+
+ test('minMaxWithFunctor', function() {
+ const ctx = {};
+ const ary = [1, 2, 3];
+ function func(x, i) {
+ assert.strictEqual(this, ctx);
+ assert.strictEqual(ary[i], x);
+ return x;
+ }
+ assert.strictEqual(Statistics.max(ary, func, ctx), 3);
+ assert.strictEqual(Statistics.min(ary, func, ctx), 1);
+
+ const range = Statistics.range(ary, func, ctx);
+ assert.isFalse(range.isEmpty);
+ assert.strictEqual(range.min, 1);
+ assert.strictEqual(range.max, 3);
+ });
+
+ test('maxExtrema', function() {
+ assert.strictEqual(Statistics.max([]), -Infinity);
+ assert.strictEqual(Statistics.min([]), Infinity);
+ });
+
+ test('meanBasic', function() {
+ assert.closeTo(Statistics.mean([1, 2, 3]), 2, 1e-6);
+ assert.closeTo(Statistics.mean(new Set([1, 2, 3])), 2, 1e-6);
+ });
+
+ test('geometricMean', function() {
+ assert.strictEqual(1, Statistics.geometricMean([]));
+ assert.strictEqual(1, Statistics.geometricMean([1]));
+ assert.strictEqual(0, Statistics.geometricMean([-1]));
+ assert.strictEqual(0, Statistics.geometricMean([0]));
+ assert.strictEqual(0, Statistics.geometricMean([1, 2, 3, 0]));
+ assert.strictEqual(0, Statistics.geometricMean([1, 2, 3, -1]));
+ assert.strictEqual(1, Statistics.geometricMean([1, 1, 1]));
+ assert.strictEqual(2, Statistics.geometricMean([2]));
+ assert.closeTo(Math.sqrt(6), Statistics.geometricMean([2, 3]), 1e-6);
+ assert.closeTo(6, Statistics.geometricMean(new Set([4, 9])), 1e-6);
+
+ let samples = [];
+ for (let i = 0; i < 1e3; ++i) {
+ samples.push(Number.MAX_SAFE_INTEGER);
+ }
+ assert.closeTo(Number.MAX_SAFE_INTEGER, Statistics.geometricMean(samples),
+ Number.MAX_SAFE_INTEGER * 1e-13);
+
+ samples = [];
+ for (let i = 0; i < 1e3; ++i) {
+ samples.push(Number.MAX_VALUE / 1e3);
+ }
+ assert.closeTo(Number.MAX_VALUE / 1e3, Statistics.geometricMean(samples),
+ Number.MAX_VALUE * 1e-13);
+ });
+
+ test('weightedMean', function() {
+ function getWeight(element) {
+ return element.weight;
+ }
+ function getValue(element) {
+ return element.value;
+ }
+
+ let data = [
+ {value: 10, weight: 3},
+ {value: 20, weight: 1},
+ {value: 30, weight: 6}
+ ];
+ assert.strictEqual(23, Statistics.weightedMean(data, getWeight, getValue));
+
+ data = [
+ {value: 10, weight: 0},
+ {value: 20, weight: 0},
+ {value: 30, weight: 0}
+ ];
+ assert.strictEqual(
+ undefined, Statistics.weightedMean(data, getWeight, getValue));
+
+ data = [
+ {value: 10, weight: -10},
+ {value: 20, weight: 5},
+ {value: 30, weight: 5}
+ ];
+ assert.strictEqual(
+ undefined, Statistics.weightedMean(data, getWeight, getValue));
+ });
+
+ test('weightedMean_positionDependent', function() {
+ function getWeight(element, idx) {
+ return idx;
+ }
+ // 3 has weight of 0, 6 has weight of 1, 9 has weight of 2
+ assert.strictEqual(8, Statistics.weightedMean([3, 6, 9], getWeight));
+ });
+
+ test('max_positionDependent', function() {
+ function getValue(element, idx) {
+ return element * idx;
+ }
+ assert.strictEqual(6, Statistics.max([1, 2, 3], getValue));
+ });
+
+ test('min_positionDependent', function() {
+ function getValue(element, idx) {
+ return element * idx;
+ }
+ assert.strictEqual(-6, Statistics.min([1, 2, -3], getValue));
+ });
+
+ test('varianceBasic', function() {
+ // In [2, 4, 4, 2], all items have a deviation of 1.0 from the mean so the
+ // population variance is 4.0 / 4 = 1.0, but the sample variance is 4.0 / 3.
+ assert.strictEqual(Statistics.variance([2, 4, 4, 2]), 4.0 / 3);
+
+ // In [1, 2, 3], the squared deviations are 1.0, 0.0 and 1.0 respectively;
+ // population variance 2.0 / 3 but sample variance is 2.0 / 2 = 1.0.
+ assert.strictEqual(Statistics.variance([1, 2, 3]), 1.0);
+ });
+
+ test('varianceWithFunctor', function() {
+ const ctx = {};
+ const ary = [{x: 2},
+ {x: 4},
+ {x: 4},
+ {x: 2}];
+ assert.strictEqual(4.0 / 3, Statistics.variance(ary, function(d) {
+ assert.strictEqual(ctx, this);
+ return d.x;
+ }, ctx));
+ });
+
+ test('stddevBasic', function() {
+ assert.strictEqual(Statistics.stddev([2, 4, 4, 2]), Math.sqrt(4.0 / 3));
+ });
+
+ test('stddevWithFunctor', function() {
+ const ctx = {};
+ const ary = [{x: 2},
+ {x: 4},
+ {x: 4},
+ {x: 2}];
+ assert.strictEqual(Math.sqrt(4.0 / 3), Statistics.stddev(ary, function(d) {
+ assert.strictEqual(ctx, this);
+ return d.x;
+ }, ctx));
+ });
+
+ test('percentile', function() {
+ const ctx = {};
+ const ary = [{x: 0},
+ {x: 1},
+ {x: 2},
+ {x: 3},
+ {x: 4},
+ {x: 5},
+ {x: 6},
+ {x: 7},
+ {x: 8},
+ {x: 9}];
+ function func(d, i) {
+ assert.strictEqual(ctx, this);
+ return d.x;
+ }
+ assert.strictEqual(Statistics.percentile(ary, 0, func, ctx), 0);
+ assert.strictEqual(Statistics.percentile(ary, .5, func, ctx), 4);
+ assert.strictEqual(Statistics.percentile(ary, .75, func, ctx), 6);
+ assert.strictEqual(Statistics.percentile(ary, 1, func, ctx), 9);
+ });
+
+ test('percentile_positionDependent', function() {
+ const ctx = {};
+ const ary = [{x: 0},
+ {x: 1},
+ {x: 2},
+ {x: 3},
+ {x: 4},
+ {x: 5},
+ {x: 6},
+ {x: 7},
+ {x: 8},
+ {x: 9}];
+ function func(d, i) {
+ assert.strictEqual(ctx, this);
+ assert.strictEqual(d.x, i);
+ return d.x * i;
+ }
+ assert.strictEqual(Statistics.percentile(ary, 0, func, ctx), 0);
+ assert.strictEqual(Statistics.percentile(ary, .5, func, ctx), 16);
+ assert.strictEqual(Statistics.percentile(ary, .75, func, ctx), 36);
+ assert.strictEqual(Statistics.percentile(ary, 1, func, ctx), 81);
+ });
+
+ test('normalizeSamples', function() {
+ let samples = [];
+ let results = Statistics.normalizeSamples(samples);
+ assert.deepEqual(results.normalized_samples, []);
+ assert.deepEqual(results.scale, 1.0);
+
+ samples = [0.0, 0.0];
+ results = Statistics.normalizeSamples(samples);
+ assert.deepEqual(results.normalized_samples, [0.5, 0.5]);
+ assert.deepEqual(results.scale, 1.0);
+
+ samples = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
+ results = Statistics.normalizeSamples(samples);
+ assert.deepEqual(results.normalized_samples,
+ [1.0 / 8.0, 3.0 / 8.0, 5.0 / 8.0, 7.0 / 8.0]);
+ assert.deepEqual(results.scale, 0.75);
+
+ samples = [1.0 / 8.0, 3.0 / 8.0, 5.0 / 8.0, 7.0 / 8.0];
+ results = Statistics.normalizeSamples(samples);
+ assert.deepEqual(results.normalized_samples, samples);
+ assert.deepEqual(results.scale, 1.0);
+ });
+
+ /**
+ *Tests NormalizeSamples and Discrepancy with random samples.
+ *
+ * Generates 10 sets of 10 random samples, computes the discrepancy,
+ * relaxes the samples using Llloyd's algorithm in 1D, and computes the
+ * discrepancy of the relaxed samples. Discrepancy of the relaxed samples
+ * must be less than or equal to the discrepancy of the original samples.
+ **/
+ test('discrepancy_Random', function() {
+ for (let i = 0; i < 10; i++) {
+ const samples = Statistics.normalizeSamples(
+ createRandomSamples(10)).normalized_samples;
+ const d = Statistics.discrepancy(samples);
+ const relaxedSamples = relax(samples);
+ const dRelaxed = Statistics.discrepancy(relaxedSamples);
+ assert.isBelow(dRelaxed, d);
+ }
+ });
+
+
+ /* Computes discrepancy for sample sets with known statistics. */
+ test('discrepancy_Analytic', function() {
+ let samples = [];
+ let d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.0);
+
+ samples = [0.5];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.5);
+
+ samples = [0.0, 1.0];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 1.0);
+
+ samples = [0.5, 0.5, 0.5];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 1.0);
+
+ samples = [1.0 / 8.0, 3.0 / 8.0, 5.0 / 8.0, 7.0 / 8.0];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.25);
+
+ samples = [1.0 / 8.0, 5.0 / 8.0, 5.0 / 8.0, 7.0 / 8.0];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.5);
+
+ samples = [1.0 / 8.0, 3.0 / 8.0, 5.0 / 8.0, 5.0 / 8.0, 7.0 / 8.0];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.4);
+
+ samples = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.5);
+
+ samples = Statistics.normalizeSamples(samples).normalized_samples;
+ d = Statistics.discrepancy(samples);
+ assert.strictEqual(d, 0.25);
+ });
+
+ test('timestampsDiscrepancy', function() {
+ let timestamps = [];
+ let dAbs = Statistics.timestampsDiscrepancy(timestamps, true);
+ assert.strictEqual(dAbs, 0.0);
+
+ timestamps = [4];
+ dAbs = Statistics.timestampsDiscrepancy(timestamps, true);
+ assert.strictEqual(dAbs, 0.5);
+
+ const timestampsA = [0, 1, 2, 3, 5, 6];
+ const timestampsB = [0, 1, 2, 3, 5, 7];
+ const timestampsC = [0, 2, 3, 4];
+ const timestampsD = [0, 2, 3, 4, 5];
+
+
+ const dAbsA = Statistics.timestampsDiscrepancy(timestampsA, true);
+ const dAbsB = Statistics.timestampsDiscrepancy(timestampsB, true);
+ const dAbsC = Statistics.timestampsDiscrepancy(timestampsC, true);
+ const dAbsD = Statistics.timestampsDiscrepancy(timestampsD, true);
+ const dRelA = Statistics.timestampsDiscrepancy(timestampsA, false);
+ const dRelB = Statistics.timestampsDiscrepancy(timestampsB, false);
+ const dRelC = Statistics.timestampsDiscrepancy(timestampsC, false);
+ const dRelD = Statistics.timestampsDiscrepancy(timestampsD, false);
+
+
+ assert.isBelow(dAbsA, dAbsB);
+ assert.isBelow(dRelA, dRelB);
+ assert.isBelow(dRelD, dRelC);
+ assert.closeTo(dAbsD, dAbsC, 0.0001);
+ });
+
+ test('discrepancyMultipleRanges', function() {
+ const samples = [[0.0, 1.2, 2.3, 3.3], [6.3, 7.5, 8.4], [4.2, 5.4, 5.9]];
+ const d0 = Statistics.timestampsDiscrepancy(samples[0]);
+ const d1 = Statistics.timestampsDiscrepancy(samples[1]);
+ const d2 = Statistics.timestampsDiscrepancy(samples[2]);
+ const d = Statistics.timestampsDiscrepancy(samples);
+ assert.strictEqual(d, Math.max(d0, d1, d2));
+ });
+
+ /**
+ * Tests approimate discrepancy implementation by comparing to exact
+ * solution.
+ **/
+ test('approximateDiscrepancy', function() {
+ for (let i = 0; i < 5; i++) {
+ let samples = createRandomSamples(10);
+ samples = Statistics.normalizeSamples(samples).normalized_samples;
+ const d = Statistics.discrepancy(samples);
+ const dApprox = Statistics.discrepancy(samples, 500);
+ assert.closeTo(d, dApprox, 0.01);
+ }
+ });
+
+ test('uniformlySampleArray', function() {
+ const samples = ['A', 'B', 'C', 'D', 'E'];
+ for (let i = samples.length; i >= 0; --i) {
+ Statistics.uniformlySampleArray(samples, i);
+ assert.lengthOf(samples, i);
+ }
+ });
+
+ test('uniformlySampleStream', function() {
+ let samples = [];
+ Statistics.uniformlySampleStream(samples, 1, 'A', 5);
+ assert.deepEqual(['A'], samples);
+ Statistics.uniformlySampleStream(samples, 2, 'B', 5);
+ Statistics.uniformlySampleStream(samples, 3, 'C', 5);
+ Statistics.uniformlySampleStream(samples, 4, 'D', 5);
+ Statistics.uniformlySampleStream(samples, 5, 'E', 5);
+ assert.deepEqual(['A', 'B', 'C', 'D', 'E'], samples);
+
+ Statistics.uniformlySampleStream(samples, 6, 'F', 5);
+ // Can't really assert anything more than the length since the elements are
+ // drawn at random.
+ assert.strictEqual(samples.length, 5);
+
+ // Try starting with a non-empty array.
+ samples = [0, 0, 0];
+ Statistics.uniformlySampleStream(samples, 1, 'G', 5);
+ assert.deepEqual(['G', 0, 0], samples);
+ });
+
+ test('mergeSampledStreams', function() {
+ let samples = [];
+ Statistics.mergeSampledStreams(samples, 0, ['A'], 1, 5);
+ assert.deepEqual(['A'], samples);
+ Statistics.mergeSampledStreams(samples, 1, ['B', 'C', 'D', 'E'], 4, 5);
+ assert.deepEqual(['A', 'B', 'C', 'D', 'E'], samples);
+
+ Statistics.mergeSampledStreams(samples, 9, ['F', 'G', 'H', 'I', 'J'], 7, 5);
+ // Can't really assert anything more than the length since the elements are
+ // drawn at random.
+ assert.strictEqual(samples.length, 5);
+
+ samples = ['A', 'B'];
+ Statistics.mergeSampledStreams(samples, 2, ['F', 'G', 'H', 'I', 'J'], 7, 5);
+ assert.strictEqual(samples.length, 5);
+ });
+
+ test('mannWhitneyUTestSmokeTest', function() {
+ // x < 0.01
+ let sampleA = [1, 2, 2.1, 2.2, 2, 1];
+ let sampleB = [12, 13, 13.1, 13.2, 13, 12];
+ let results = Statistics.mwu(sampleA, sampleB);
+ assert.isBelow(results.p, Statistics.DEFAULT_ALPHA);
+
+ assert.strictEqual(results.significance,
+ Statistics.Significance.SIGNIFICANT);
+ assert.strictEqual(results.compare(Statistics.DEFAULT_ALPHA / 10),
+ Statistics.Significance.INSIGNIFICANT);
+ assert.strictEqual(results.significance,
+ Statistics.Significance.INSIGNIFICANT);
+
+ // 0.01 < x < 0.1
+ sampleA = [1, 2, 2.1, 2.2, 2, 1];
+ sampleB = [2, 3, 3.1, 3.2, 3, 2];
+ results = Statistics.mwu(sampleA, sampleB);
+ assert.isBelow(results.p, 0.1);
+ assert.isAbove(results.p, 0.01);
+
+ // 0.1 < x
+ sampleA = [1, 2, 2.1, 2.2, 2, 1];
+ sampleB = [1, 2, 2.1, 2.2, 2, 1];
+ results = Statistics.mwu(sampleA, sampleB);
+ assert.isAbove(results.p, 0.1);
+ });
+
+ test('mannWhitneyUEdgeCases', function() {
+ const longRepeatingSample = [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ ];
+ const emptySample = [];
+ const singleLargeValue = [1000000];
+ // mean 10, std 2
+ const normallyDistributedSample = [
+ 8.341540e+0, 7.216640e+0, 8.844310e+0, 9.801980e+0, 1.048760e+1,
+ 6.915150e+0, 7.881740e+0, 1.131160e+1, 9.959400e+0, 9.030880e+0
+ ];
+ // Identical samples should not cause the null to be rejected.
+ let results = Statistics.mwu(longRepeatingSample, longRepeatingSample);
+ assert.isAbove(results.p, 0.05);
+ results = Statistics.mwu(normallyDistributedSample,
+ normallyDistributedSample);
+ assert.isAbove(results.p, 0.05);
+ results = Statistics.mwu(singleLargeValue, singleLargeValue);
+
+ // A single value is generally not sufficient to reject the null, no matter
+ // how far off it is.
+ results = Statistics.mwu(normallyDistributedSample, singleLargeValue);
+ assert.isAbove(results.p, 0.05);
+
+ // A single value way outside the first sample may be enough to reject,
+ // if the first sample is large enough.
+ results = Statistics.mwu(longRepeatingSample, singleLargeValue);
+ assert.isBelow(results.p, 0.005);
+
+ // Empty samples should not be comparable.
+ results = Statistics.mwu(emptySample, emptySample);
+ assert(isNaN(results.p));
+
+ results = Statistics.mwu(
+ emptySample, emptySample, Statistics.DEFAULT_ALPHA, 1);
+ assert.strictEqual(results.significance,
+ Statistics.Significance.NEED_MORE_DATA);
+ assert.strictEqual(results.compare(0.9),
+ Statistics.Significance.NEED_MORE_DATA);
+ assert.strictEqual(results.significance,
+ Statistics.Significance.NEED_MORE_DATA);
+
+ // The result of comparing a sample against an empty sample should not be a
+ // valid p value. NOTE: The current implementation returns 0, it is up to
+ // the caller to interpret this.
+ results = Statistics.mwu(normallyDistributedSample, emptySample);
+ assert(!results.p);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view.html b/chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view.html
new file mode 100644
index 00000000000..03468c6c2aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view.html
@@ -0,0 +1,1217 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Multi-dimensional view data structure.
+ *
+ * A multi-dimensional view provides a hierarchical representation of a
+ * collection of multi-dimensional paths with associated scalar values. Unlike
+ * separate single-dimensional views (e.g. one tree for each dimension),
+ * multi-dimensional views facilitate aggregation over combinations of
+ * substrings of the path dimensions (rather than just substrings of a single
+ * path dimension).
+ *
+ * Every view consists of multi-dimensional nodes (see MultiDimensionalViewNode
+ * for more details). This file also provides a builder class for constructing
+ * top-down and bottom-up representations of arbitrary collections of
+ * multi-dimensional paths (see MultiDimensionalViewBuilder for more details).
+ *
+ * Example: Given the following collection of two dimensional paths:
+ *
+ * <===================== Path =====================> <== Total values ===>
+ * <------- dimension 0 -------> <- dimension 1 -> <- v 0 -> <- v 1 ->
+ * [['Run()', 'Exec()', 'Call()'], ['Obj', 'View'] ]: [1 , 3
+ * [['Run()', 'Exec()', 'Call()'], ['Obj', 'Widget']]: [2 , 5
+ * [['Run()', 'Exec()', 'Load()'], ['Obj'] ]: [4 , 11
+ * [['Run()', 'Exec()'] , ['int'] ]: [8 , 7
+ * [['Run()'] , ['Obj', 'Window']]: [16 , 0
+ * [['Stop()'] , ['Obj'] ]: [32 , 13
+ *
+ * a multi-dimensional view provides a recursive breakdown of the aggregated
+ * values, e.g. (total values shown in square brackets):
+ *
+ * (root): [63, 39]
+ * |
+ * | break down by 0th dimension
+ * v
+ * Run(): [31, 26]
+ * |
+ * | break down by 0th dimension
+ * v
+ * Exec(): [15, 26]
+ * |
+ * | break down by 1st dimension
+ * v
+ * Obj: [7, 19]
+ * |
+ * | break down by 0th dimension again
+ * v
+ * Call(): [3, 8]
+ * |
+ * | break down by 1st dimension again
+ * v
+ * View: [1, 3]
+ *
+ * Observe that the recursive breakdown above is over both dimensions.
+ * Furthermore, the underlying single-dimension paths (Run() -> Exec() -> Call()
+ * and Obj -> View) can be arbitrarily interleaved in the breakdown.
+ */
+tr.exportTo('tr.b', function() {
+ /**
+ * Node of a multi-dimensional view.
+ *
+ * The structure of a view is encoded in the nodes using links to their
+ * children wrt each dimension. The diagram below shows how the nodes
+ * corresponding to the following four two-dimensional view paths:
+ *
+ * 1. [['A', 'B'], ['1', '2']]
+ * 2. [['A', 'C'], ['1', '2']]
+ * 3. [['A', 'B'], ['1', '3']]
+ * 4. [['A', 'C'], ['1', '3']]
+ *
+ * can be reached from the root of a two-dimensional view using these links
+ * ('*' stands for undefined):
+ *
+ * +---------------------+
+ * | title: [*,*] (root) |
+ * +---------------------+
+ * children wrt children wrt
+ * 0th dimension 1st dimension
+ * | :
+ * _______A________| :........1.........
+ * | :
+ * v v
+ * +--------------+ +--------------+
+ * | title: [A,*] | | title: [*,1] |
+ * +--------------+ +--------------+
+ * children wrt children wrt children wrt children wrt
+ * 0th dimension 1st dimension 0th dimension 1st dimension
+ * | | :.....1...... _____A_____| : :
+ * _B_| |__C__ : | ...2..: :.3..
+ * | | : | : :
+ * v v v v v v
+ * +-------+ +-------+ +-------+ +-------+ +-------+
+ * | [B,*] | | [C,*] | | [A,1] | | [*,2] | | [*,3] |
+ * +-------+ +-------+ +-------+ +-------+ +-------+
+ * : ___:_____B______| | : :......3.....|.... |
+ * :.1.. | :.1.. __C___| :...2... _A_| : _A_|
+ * : | : | : | : |
+ * v v v v v v v v
+ * +-------+ +-------+ +-------+ +-------+
+ * | [B,1] | | [C,1] | | [A,2] | | [A,3] |
+ * +-------+ +-------+ +-------+ +-------+
+ * : : : :.......3.......||.......... ||
+ * : :..3....:................ BC : BC
+ * : ______:_______________:___|| : ||
+ * 2 | 2 _______:____| ______:___||
+ * : | : | : | : |
+ * v v v v v v v v
+ * +----------+ +----------+ +----------+ +----------+
+ * | [B,2] | | [C,2] | | [B,3] | | [C,3] |
+ * | (node 1) | | (node 2) | | (node 3) | | (node 4) |
+ * +----------+ +----------+ +----------+ +----------+
+ *
+ * The self/total values of a node represents the aggregated values of all
+ * paths (in the collection from which the view was built) matching the node
+ * excluding/including the node's descendants.
+ *
+ * Terminology examples:
+ *
+ * - Children of [A,*] wrt 0th dimension: [B,*], [C,*]
+ * - Children of [A,*] (wrt all dimensions): [B,*], [C,*], [A,1]
+ * - Descendants of [A,*] wrt 1st dimension: [A,1], [A,2], [A,3]
+ * - Single-dimensional descendants of [A,*]: [A,1], [A,2], [A,3], [B,*],
+ * [C,*]
+ * - Descendants of [A,*] (wrt all dimensions): [A,1], [A,2], [A,3], [B,*],
+ * [C,*], [B,1], [C,1], [B,2], [C,2], [B,3], [C,3]
+ *
+ * @{constructor}
+ */
+ function MultiDimensionalViewNode(title, valueCount) {
+ // List of titles of this node wrt each dimension.
+ this.title = title;
+
+ // Map from child name to child node for each dimension.
+ const dimensions = title.length;
+ this.children = new Array(dimensions);
+ for (let i = 0; i < dimensions; i++) {
+ this.children[i] = new Map();
+ }
+
+ // For each value index (from 0 to |valueCount| - 1), we store the self and
+ // total values together with a Boolean flag whether the value is only a
+ // lower bound (i.e. aggregated from children rather than provided
+ // directly).
+ this.values = new Array(valueCount);
+ for (let v = 0; v < valueCount; v++) {
+ this.values[v] = { self: 0, total: 0, totalState: NOT_PROVIDED };
+ }
+ }
+
+ /**
+ * States of total values stored in multi-dimensional view nodes.
+ *
+ * @enum
+ */
+ MultiDimensionalViewNode.TotalState = {
+ // Neither total nor self value was provided for either the node or any of
+ // its descendants.
+ NOT_PROVIDED: 0,
+
+ // The total value was NOT provided for the node, but the self value was
+ // provided for the node or the total or self value was provided for at
+ // least one of its descendants.
+ LOWER_BOUND: 1,
+
+ // The total value was provided for the node.
+ EXACT: 2
+ };
+ // Cache the total value states to avoid repeated object field lookups.
+ const NOT_PROVIDED = MultiDimensionalViewNode.TotalState.NOT_PROVIDED;
+ const LOWER_BOUND = MultiDimensionalViewNode.TotalState.LOWER_BOUND;
+ const EXACT = MultiDimensionalViewNode.TotalState.EXACT;
+
+ MultiDimensionalViewNode.prototype = {
+ /** Duck type <tr-ui-b-table> rows. */
+ get subRows() {
+ return Array.from(this.children[0].values());
+ }
+ };
+
+ /**
+ * Builder for multi-dimensional views.
+ *
+ * Given a collection of multi-dimensional paths, a builder can be used to
+ * construct the following three representations of the paths:
+ *
+ * 1. Top-down tree view
+ * A multi-dimensional path in the view corresponds to all paths in the
+ * collection that have it as their prefix.
+ *
+ * 2. Top-down heavy view
+ * A multi-dimensional path in the view corresponds to all paths in the
+ * collection that have it as their substring
+ *
+ * 3. Bottom-up heavy view
+ * A multi-dimensional path in the view corresponds to all paths in the
+ * collection that have it as their substring reversed.
+ *
+ * For example, the following collection of 2-dimensional paths (with single
+ * values):
+ *
+ * 2-dimensional path | self
+ * Time (0th dimension) | Activity (1st dimension) | value
+ * ========================+========================+=======
+ * Saturday | Cooking | 1 h
+ * Saturday | Sports -> Football | 2 h
+ * Sunday | Sports -> Basketball | 3 h
+ *
+ * gives rise to the following top-down tree view, which aggregates the
+ * scalar values over prefixes of the given paths:
+ *
+ * +---------+
+ * | * |
+ * | * |
+ * | self=0 |
+ * | total=6 |
+ * +---------+
+ * | : | :
+ * _________Cooking_______| : | :............Sunday............
+ * | : | :
+ * | ...Saturday..: |_Sports_ :
+ * | : | :
+ * v v v v
+ * +---------+ +---------+ +---------+ +---------+
+ * | * | | Sat | | * | | Sun |
+ * | Cooking | | * | | Sports | | * |
+ * | self=0 | | self=0 | | self=0 | | self=0 |
+ * | total=1 | | total=3 | | total=5 | | total=3 |
+ * +---------+ +---------+ +---------+ +---------+
+ * : | | : | | : |
+ * Saturday | Sports : | | : Sports
+ * : | | .....Saturday....: | | :.....Sunday....... |
+ * : _Cook_| | : _Foot_| |_Bask_ : |
+ * : | | : | | : |
+ * v v v v v v v v
+ * +---------+ +---------+ +------------+ +--------------+ +---------+
+ * | Sat | | Sat | | * | | * | | Sun |
+ * | Cooking | | Sports | | S/Football | | S/Basketball | | Sports |
+ * | self=1 | | self=0 | | self=0 | | self=0 | | self=0 |
+ * | total=1 | | total=2 | | total=2 | | total=3 | | total=3 |
+ * +---------+ +---------+ +------------+ +--------------+ +---------+
+ * | : : |
+ * |_Foot_ ..Sat.: :.Sun.. _Bask_|
+ * | : : |
+ * v v v v
+ * +------------+ +--------------+
+ * | Sat | | Sun |
+ * | S/Football | | S/Basketball |
+ * | self=2 | | self=3 |
+ * | total=2 | | total=3 |
+ * +------------+ +--------------+
+ *
+ * To build a multi-dimensional view of a collection of multi-dimensional
+ * paths, you create a builder, add the paths to it and then use it to
+ * construct the view. For example, the following code generates the
+ * 2-dimensional top-down tree view shown above:
+ *
+ * const builder = new MultiDimensionalViewBuilder(2);
+ * builder.addPath([['Saturday'], ['Cooking']], [1], SELF);
+ * builder.addPath([['Saturday'], ['Sports', 'Football']], [2], SELF);
+ * builder.addPath([['Sunday'], ['Sports', 'Basketball']], [3], SELF);
+ * const treeViewRoot = builder.buildTopDownTreeView();
+ *
+ * The heavy views can be constructed analogously (by calling
+ * buildTopDownHeavyView() or buildBottomUpHeavyView() at the end instead).
+ *
+ * Note that the same builder can be used to construct both the tree and
+ * heavy views (for the same collection of paths). However, no more paths can
+ * be added once either view has been built.
+ *
+ * @{constructor}
+ */
+ function MultiDimensionalViewBuilder(dimensions, valueCount) {
+ if (typeof(dimensions) !== 'number' || dimensions < 0) {
+ throw new Error('Dimensions must be a non-negative number');
+ }
+ this.dimensions_ = dimensions;
+
+ if (typeof(valueCount) !== 'number' || valueCount < 0) {
+ throw new Error('Number of values must be a non-negative number');
+ }
+ this.valueCount_ = valueCount;
+
+ this.buildRoot_ = this.createRootNode_();
+ this.topDownTreeViewRoot_ = undefined;
+ this.topDownHeavyViewRoot_ = undefined;
+ this.bottomUpHeavyViewNode_ = undefined;
+ this.complete_ = false;
+
+ this.maxDimensionDepths_ = new Array(dimensions);
+ for (let d = 0; d < dimensions; d++) {
+ this.maxDimensionDepths_[d] = 0;
+ }
+ }
+
+ /** @{enum} */
+ MultiDimensionalViewBuilder.ValueKind = {
+ SELF: 0,
+ TOTAL: 1
+ };
+
+ /**
+ * Types of multi-dimensional views provided by MultiDimensionalViewBuilder.
+ *
+ * @enum
+ */
+ MultiDimensionalViewBuilder.ViewType = {
+ TOP_DOWN_TREE_VIEW: 0,
+ TOP_DOWN_HEAVY_VIEW: 1,
+ BOTTOM_UP_HEAVY_VIEW: 2
+ };
+
+ MultiDimensionalViewBuilder.prototype = {
+ /**
+ * Add values associated with a multi-dimensional path to the tree.
+ *
+ * The path must have the same number of dimensions as the builder. Its
+ * elements must be single-dimension paths (lists of strings) of arbitrary
+ * length (empty for the root of the given dimension). Starting from the
+ * root of the tree, each single-dimension path is traversed from left to
+ * right to reach the node corresponding to the whole path.
+ *
+ * The length of the provided list of values must be equal to the builder's
+ * value count. The builder supports adding both kinds of values
+ * (self/total) wrt all value indices for an arbitrary multi-dimensional
+ * path. The rationale for adding total values (in addition to/instead of
+ * self values) is to cater for missing sub-paths. Example: Consider the
+ * following collection of single-dimensional paths (with single values):
+ *
+ * [['Loop::Run()', 'Execute()', 'FunctionBig']]: self=99000
+ * [['Loop::Run()', 'Execute()', 'FunctionSmall1']]: self=1
+ * [['Loop::Run()', 'Execute()', 'FunctionSmall2']]: self=1
+ * ...
+ * [['Loop::Run()', 'Execute()', 'FunctionSmall1000']]: self=1
+ *
+ * If we required that only self values could be added to the builder, then
+ * all of the 1001 paths would need to be provided (most likely in a trace)
+ * to obtain the correct total of [['Loop::Run()', 'Execute()']]. However,
+ * since we allow adding total values as well, only the following 2 paths
+ * need to be provided to get the correct numbers explaining 99% of the
+ * aggregated total value:
+ *
+ * [['Loop::Run()', 'Execute()']]: total=100000
+ * [['Loop::Run()', 'Execute()', 'FunctionBig']]: self=99000
+ *
+ * In other words, the long tail containing 1000 small paths need not be
+ * dumped (greatly reducing the size of a trace where applicable).
+ *
+ * Important: No paths can be added to a builder once either view has been
+ * built!
+ */
+ addPath(path, values, valueKind) {
+ if (this.buildRoot_ === undefined) {
+ throw new Error(
+ 'Paths cannot be added after either view has been built');
+ }
+ if (path.length !== this.dimensions_) {
+ throw new Error('Path must be ' + this.dimensions_ + '-dimensional');
+ }
+ if (values.length !== this.valueCount_) {
+ throw new Error('Must provide ' + this.valueCount_ + ' values');
+ }
+
+ let isTotal;
+ switch (valueKind) {
+ case MultiDimensionalViewBuilder.ValueKind.SELF:
+ isTotal = false;
+ break;
+ case MultiDimensionalViewBuilder.ValueKind.TOTAL:
+ isTotal = true;
+ break;
+ default:
+ throw new Error('Invalid value kind: ' + valueKind);
+ }
+
+ let node = this.buildRoot_;
+ for (let d = 0; d < path.length; d++) {
+ const singleDimensionPath = path[d];
+ const singleDimensionPathLength = singleDimensionPath.length;
+ this.maxDimensionDepths_[d] =
+ Math.max(this.maxDimensionDepths_[d], singleDimensionPathLength);
+ for (let i = 0; i < singleDimensionPathLength; i++) {
+ node = this.getOrCreateChildNode_(node, d, singleDimensionPath[i]);
+ }
+ }
+
+ for (let v = 0; v < this.valueCount_; v++) {
+ const addedValue = values[v];
+ if (addedValue === undefined) continue;
+ const nodeValue = node.values[v];
+ if (isTotal) {
+ nodeValue.total += addedValue;
+ nodeValue.totalState = EXACT;
+ } else {
+ nodeValue.self += addedValue;
+ nodeValue.totalState = Math.max(nodeValue.totalState, LOWER_BOUND);
+ }
+ }
+ },
+
+
+ get complete() {
+ return this.complete_;
+ },
+
+ /**
+ * Force all MultiDimensionalViewNode's to have totalState EXACT.
+ * Set to true only if all SELF paths for the tree have been provided.
+ * Setting unnecessary if providing TOTAL values.
+ *
+ * MultiDimensionalViewBuilder allows both 'self' and 'total' values to be
+ * entered for paths then later when the veiws are constructed it
+ * determines the total and whether that total is exact or a lower bound
+ * for each node. When total values are provided we know that that total
+ * is exact however when self values are provided the computed totals
+ * *could* be complete ...if the user has provided the all the self
+ * values for the whole tree. We can't know this within the
+ * MultiDimensionalViewBuilder so this flag allows the user to specify
+ * that this is the case.
+ *
+ * Important: Can't be set once any view has been built.
+ */
+ set complete(isComplete) {
+ if (this.buildRoot_ === undefined) {
+ throw new Error('Can\'t set complete after any view has been built.');
+ }
+ this.complete_ = isComplete;
+ },
+
+ buildView(viewType) {
+ switch (viewType) {
+ case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW:
+ return this.buildTopDownTreeView();
+ case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW:
+ return this.buildTopDownHeavyView();
+ case MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW:
+ return this.buildBottomUpHeavyView();
+ default:
+ throw new Error('Unknown multi-dimensional view type: ' + viewType);
+ }
+ },
+
+ /**
+ * Build the top-down tree view of the multi-dimensional view.
+ *
+ * Note that no more paths can be added to the builder once either view has
+ * been built.
+ */
+ buildTopDownTreeView() {
+ if (this.topDownTreeViewRoot_ === undefined) {
+ const treeViewRoot = this.buildRoot_;
+ this.buildRoot_ = undefined;
+
+ this.setUpMissingChildRelationships_(treeViewRoot,
+ 0 /* firstDimensionToSetUp */);
+ this.finalizeTotalValues_(treeViewRoot,
+ 0 /* firstDimensionToFinalize */,
+ new WeakMap() /* dimensionalSelfSumsMap */);
+
+ this.topDownTreeViewRoot_ = treeViewRoot;
+ }
+
+ return this.topDownTreeViewRoot_;
+ },
+
+ /**
+ * Build the top-down heavy view of the multi-dimensional view.
+ *
+ * Note that no more paths can be added to the builder once either view has
+ * been built.
+ */
+ buildTopDownHeavyView() {
+ if (this.topDownHeavyViewRoot_ === undefined) {
+ this.topDownHeavyViewRoot_ = this.buildGenericHeavyView_(
+ this.addDimensionToTopDownHeavyViewNode_.bind(this));
+ }
+ return this.topDownHeavyViewRoot_;
+ },
+
+ /**
+ * Build the bottom-up heavy view of the multi-dimensional view.
+ *
+ * Note that no more paths can be added to the builder once either view has
+ * been built.
+ */
+ buildBottomUpHeavyView() {
+ if (this.bottomUpHeavyViewNode_ === undefined) {
+ this.bottomUpHeavyViewNode_ = this.buildGenericHeavyView_(
+ this.addDimensionToBottomUpHeavyViewNode_.bind(this));
+ }
+ return this.bottomUpHeavyViewNode_;
+ },
+
+ createRootNode_() {
+ return new MultiDimensionalViewNode(
+ new Array(this.dimensions_) /* title */, this.valueCount_);
+ },
+
+ getOrCreateChildNode_(
+ parentNode, dimension, childDimensionTitle) {
+ if (dimension < 0 || dimension >= this.dimensions_) {
+ throw new Error('Invalid dimension');
+ }
+
+ const dimensionChildren = parentNode.children[dimension];
+
+ let childNode = dimensionChildren.get(childDimensionTitle);
+ if (childNode !== undefined) {
+ return childNode;
+ }
+
+ const childTitle = parentNode.title.slice();
+ childTitle[dimension] = childDimensionTitle;
+ childNode = new MultiDimensionalViewNode(childTitle, this.valueCount_);
+ dimensionChildren.set(childDimensionTitle, childNode);
+
+ return childNode;
+ },
+
+ /**
+ * Set up missing child relationships.
+ *
+ * When an arbitrary multi-dimensional path [path1, path2, ..., pathN] is
+ * added to the build tree (see addPath), only the nodes on the path1 ->
+ * path2 -> ... -> pathN chain are created (i.e. no interleavings of the
+ * single-dimensional paths are added to the tree). This method recursively
+ * adds all the missing paths.
+ *
+ * Two-dimensional example:
+ *
+ * Initial build tree . After path . After missing child
+ * (root only) . [[A, B], [1, 2]] . relationships were
+ * . was added . set up
+ * . .
+ * +---+ . +---+ . +---+
+ * |*,*| . |*,*| . |*,*|
+ * +---+ . +---+ . +---+
+ * . A . A 1
+ * . | . | :
+ * . v . v V
+ * . +---+ . +---+ +---+
+ * . |A,*| . |A,*| |*,1|
+ * . +---+ . +---+ +---+
+ * . B . B 1 A 2
+ * . | . | : | :
+ * . v . v v v v
+ * . +---+ . +---+ +---+ +---+
+ * . |B,*| . |B,*| |A,1| |*,2|
+ * . +---+ . +---+ +---+ +---+
+ * . 1 . 1 B 2 A
+ * . : . : | : |
+ * . v . v v v v
+ * . +---+ . +---+ +---+
+ * . |B,1| . |B,1| |A,2|
+ * . +---+ . +---+ +---+
+ * . 2 . 2 B
+ * . : . : |
+ * . v . v V
+ * . +---+ . +---+
+ * . |B,2| . |B,2|
+ * . +---+ . +---+
+ */
+ setUpMissingChildRelationships_(node, firstDimensionToSetUp) {
+ // Missing child relationships of this node wrt dimensions 0, ...,
+ // (firstDimensionToSetUp - 1) and all descendants of the associated
+ // children have already been set up. Now we do the same for dimensions
+ // firstDimensionToSetUp, ..., (this.dimensions_ - 1).
+ for (let d = firstDimensionToSetUp; d < this.dimensions_; d++) {
+ // Step 1. Gather the names of all children wrt the current dimension.
+ const currentDimensionChildTitles = new Set(node.children[d].keys());
+ for (let i = 0; i < d; i++) {
+ for (const previousDimensionChildNode of node.children[i].values()) {
+ for (const previousDimensionGrandChildTitle of
+ previousDimensionChildNode.children[d].keys()) {
+ currentDimensionChildTitles.add(previousDimensionGrandChildTitle);
+ }
+ }
+ }
+
+ // Step 2. Add missing children wrt the current dimension and
+ // recursively set up its missing child relationships.
+ for (const currentDimensionChildTitle of currentDimensionChildTitles) {
+ // Add a missing child (if it doesn't exist).
+ const currentDimensionChildNode =
+ this.getOrCreateChildNode_(node, d, currentDimensionChildTitle);
+
+ // Set-up child relationships (of the child node) wrt dimensions 0,
+ // ..., d - 1.
+ for (let i = 0; i < d; i++) {
+ for (const previousDimensionChildNode of
+ node.children[i].values()) {
+ const previousDimensionGrandChildNode =
+ previousDimensionChildNode.children[d].get(
+ currentDimensionChildTitle);
+ if (previousDimensionGrandChildNode !== undefined) {
+ currentDimensionChildNode.children[i].set(
+ previousDimensionChildNode.title[i],
+ previousDimensionGrandChildNode);
+ }
+ }
+ }
+
+ // Set-up child relationships (of the child node) wrt dimensions d,
+ // ..., (this.dimensions_ - 1).
+ this.setUpMissingChildRelationships_(currentDimensionChildNode, d);
+ }
+ }
+ },
+
+ /**
+ * Finalize the total values of a multi-dimensional tree.
+ *
+ * The intermediate builder tree, a node of which we want to finalize
+ * recursively, already has the right shape. The only thing that needs to
+ * be done is to propagate self and total values from subsumed child nodes
+ * in each dimension and update total value states appropriately.
+ *
+ * To derive the expression for the lower bound on the total value wrt
+ * value index V (from 1 to |this.valueCount_| - 1), we rely on the
+ * following assumptions:
+ *
+ * 1. Self/total values associated with different value indices are
+ * independent. From this point onwards, "self/total value" refers to
+ * self/total value wrt the fixed value index V.
+ *
+ * 2. Each node's self value does NOT overlap with the self or total value
+ * of any other node.
+ *
+ * 3. The total values of a node's children wrt a single dimension (e.g.
+ * [path1/A, path2] and [path1/B, path2]) do NOT overlap.
+ *
+ * 4. The total values of a node's children wrt different dimensions
+ * (e.g. [path1/A, path2] and [path1, path2/1]) MIGHT overlap.
+ *
+ * As a consequence of assumptions 1 and 3, the total value of a node can
+ * be split into the part that cannot overlap (so-called "self-sum") and
+ * the part that can overlap (so-called "residual"):
+ *
+ * total(N, V) = selfSum(N, V) + residual(N, V) (A)
+ *
+ * where the self-sum is calculated as the sum of the node's self value
+ * plus the sum of its descendants' self values (summed over all
+ * dimensions):
+ *
+ * selfSum(N, V) = self(N, V) + sum over all descendants C of N {
+ * self(C, V) (B)
+ * }
+ *
+ * Observe that the residual of a node does not include any self value (of
+ * any node in the view). Furthermore, by assumption 2, we derive that the
+ * residuals of a node's children wrt a single dimension don't overlap. On
+ * the other hand, the residuals of a node's children wrt different
+ * dimensions might overlap. This gives us the following lower bound on the
+ * residual of a node:
+ *
+ * residual(N, V) >= minResidual(N, V) = max over dimensions D {
+ * sum over children C of N at dimension D {
+ * residual(C, V) (C)
+ * }
+ * })
+ *
+ * By combining equation (A) and inequality (C), we get a lower bound on
+ * the total value of a node:
+ *
+ * total(N, V) >= selfSum(N, V) + minResidual(N, V)
+ *
+ * For example, given a two-dimensional node [path1, path2] with self value
+ * 10 and four children (2 wrt each dimension):
+ *
+ * Child | Self value | Total value
+ * ==================+============+=============
+ * [path1/A, path2] | 21 | 30
+ * [path1/B, path2] | 25 | 32
+ * [path1, path2/1] | 3 | 15
+ * [path1, path2/2] | 40 | 41
+ *
+ * and assuming that the children have no further descendants (i.e. their
+ * residual values are equal to the differences between their total and
+ * self values), the lower bound on the total value of [path1, path2] is:
+ *
+ * total([path1, path2], 0)
+ * >= selfSum([path1, path2], 0) +
+ * minResidual([path1, path2], 0)
+ * = self([path1, path2], 0) +
+ * sum over all descendants C of [path1, path2] {
+ * self (C, 0)
+ * } +
+ * max over dimensions D {
+ * sum over children C of [path1, path2] at dimension D {
+ * residual(C, 0)
+ * }
+ * }
+ * = self([path1, path2], 0) +
+ * ((self([path1/A, path2], 0) + self([path1/B, path2], 0)) +
+ * (self([path1, path2/1], 0) + self([path1, path2/2], 0))) +
+ * max(residual([path1/A, path2], 0) +
+ * residual([path1/B, path2], 0),
+ * residual([path1, path2/1], 0) +
+ * residual([path1, path2/2], 0))
+ * = 10 +
+ * ((21 + 25) + (3 + 40)) +
+ * max((30 - 21) + (32 - 25), (15 - 3) + (41 - 40))
+ * = 115
+ *
+ * To reduce the complexity of the calculation, we keep a temporary list of
+ * dimensional self-sums for each node that we have already visited. For a
+ * given node, the Kth element in the list is equal to the self size of the
+ * node plus the sum of self sizes of all its descendants wrt dimensions 0
+ * to K (inclusive). The list has two important properties:
+ *
+ * 1. The last element in the list is equal to the self-sum of the
+ * associated node (equation (B)).
+ *
+ * 2. The calculation of the list can be performed recursively using the
+ * lists of the associated node's children (avoids square complexity
+ * in the size of the graph):
+ *
+ * dimensionalSelfSum(N, V)[D] =
+ * self(N, V) +
+ * sum I = 0 to D {
+ * sum over children C of N at dimension I {
+ * dimensionalSelfSum(C, V)[I]
+ * }
+ * }
+ *
+ * This method also (recursively) ensures that, for each value index V, if
+ * at least one of the descendants C of node N has at least a LOWER_BOUND
+ * on total(C, V), then the N will also be marked as having a LOWER_BOUND
+ * on total(N, V) (unless N contains the EXACT value of total(N, V), in
+ * which case its relevant totalState won't be modified).
+ */
+ finalizeTotalValues_(
+ node, firstDimensionToFinalize, dimensionalSelfSumsMap) {
+ // Dimension D -> Value index V -> dimensionalSelfSum(|node|, V)[D].
+ const dimensionalSelfSums = new Array(this.dimensions_);
+
+ // Value index V -> minResidual(|node|, V).
+ const minResidual = new Array(this.valueCount_);
+ for (let v = 0; v < this.valueCount_; v++) minResidual[v] = 0;
+
+ // Value index V -> |node| value V.
+ const nodeValues = node.values;
+
+ // Value index V -> dimensionalSelfSum(|node|, V)[|d|].
+ const nodeSelfSums = new Array(this.valueCount_);
+ for (let v = 0; v < this.valueCount_; v++) {
+ nodeSelfSums[v] = nodeValues[v].self;
+ }
+
+ for (let d = 0; d < this.dimensions_; d++) {
+ // Value index V -> sum over children C of |node| at dimension |d| {
+ // residual(C, V) }.
+ const childResidualSums = new Array(this.valueCount_);
+ for (let v = 0; v < this.valueCount_; v++) {
+ childResidualSums[v] = 0;
+ }
+
+ for (const childNode of node.children[d].values()) {
+ if (d >= firstDimensionToFinalize) {
+ this.finalizeTotalValues_(childNode, d, dimensionalSelfSumsMap);
+ }
+ // Dimension D -> Value index V ->
+ // dimensionalSelfSum(|childNode|, V)[D].
+ const childNodeSelfSums = dimensionalSelfSumsMap.get(childNode);
+ const childNodeValues = childNode.values;
+ for (let v = 0; v < this.valueCount_; v++) {
+ nodeSelfSums[v] += childNodeSelfSums[d][v];
+ const residual = childNodeValues[v].total -
+ childNodeSelfSums[this.dimensions_ - 1][v];
+ childResidualSums[v] += residual;
+ if (this.complete) {
+ nodeValues[v].totalState = EXACT;
+ } else if (childNodeValues[v].totalState > NOT_PROVIDED) {
+ nodeValues[v].totalState = Math.max(
+ nodeValues[v].totalState, LOWER_BOUND);
+ }
+ }
+ }
+
+ dimensionalSelfSums[d] = nodeSelfSums.slice();
+ for (let v = 0; v < this.valueCount_; v++) {
+ minResidual[v] = Math.max(minResidual[v], childResidualSums[v]);
+ }
+ }
+
+ for (let v = 0; v < this.valueCount_; v++) {
+ nodeValues[v].total = Math.max(
+ nodeValues[v].total, nodeSelfSums[v] + minResidual[v]);
+ }
+
+ if (dimensionalSelfSumsMap.has(node)) {
+ throw new Error('Internal error: Node finalized more than once');
+ }
+ dimensionalSelfSumsMap.set(node, dimensionalSelfSums);
+ },
+
+ /**
+ * Build a generic heavy view of the multi-dimensional view.
+ */
+ buildGenericHeavyView_(treeViewNodeHandler) {
+ // 1. Clone the root node of the top-down tree view node (except
+ // children).
+ const treeViewRoot = this.buildTopDownTreeView();
+ const heavyViewRoot = this.createRootNode_();
+ heavyViewRoot.values = treeViewRoot.values;
+
+ // 2. Create recursion depth trackers (to avoid total value
+ // double-counting).
+ const recursionDepthTrackers = new Array(this.dimensions_);
+ for (let d = 0; d < this.dimensions_; d++) {
+ recursionDepthTrackers[d] =
+ new RecursionDepthTracker(this.maxDimensionDepths_[d], d);
+ }
+
+ // 3. Add all paths associated with the single-dimensional descendants of
+ // the top-down tree view root node to the heavy view root node
+ // (depending on the type of the target heavy view).
+ this.addDimensionsToGenericHeavyViewNode_(treeViewRoot, heavyViewRoot,
+ 0 /* startDimension */, recursionDepthTrackers,
+ false /* previousDimensionsRecursive */, treeViewNodeHandler);
+
+ // 4. Set up missing child relationships.
+ this.setUpMissingChildRelationships_(heavyViewRoot,
+ 0 /* firstDimensionToSetUp */);
+
+ return heavyViewRoot;
+ },
+
+ /**
+ * Add all paths associated with the single-dimensional descendants of a
+ * top-down tree-view node wrt multiple dimensions to a generic heavy-view
+ * node (depending on the type of the target heavy view).
+ */
+ addDimensionsToGenericHeavyViewNode_(treeViewParentNode,
+ heavyViewParentNode, startDimension, recursionDepthTrackers,
+ previousDimensionsRecursive, treeViewNodeHandler) {
+ for (let d = startDimension; d < this.dimensions_; d++) {
+ this.addDimensionDescendantsToGenericHeavyViewNode_(treeViewParentNode,
+ heavyViewParentNode, d, recursionDepthTrackers,
+ previousDimensionsRecursive, treeViewNodeHandler);
+ }
+ },
+
+ /**
+ * Add all paths associated with the descendants of a top-down tree-view
+ * node wrt a single dimension to a generic heavy-view node (depending on
+ * the type of the target heavy view).
+ */
+ addDimensionDescendantsToGenericHeavyViewNode_(treeViewParentNode,
+ heavyViewParentNode, currentDimension, recursionDepthTrackers,
+ previousDimensionsRecursive, treeViewNodeHandler) {
+ const treeViewChildren = treeViewParentNode.children[currentDimension];
+ const recursionDepthTracker = recursionDepthTrackers[currentDimension];
+ for (const treeViewChildNode of treeViewChildren.values()) {
+ recursionDepthTracker.push(treeViewChildNode);
+
+ // Add all paths associated with the child node to the heavy view-node
+ // parent node.
+ treeViewNodeHandler(
+ treeViewChildNode, heavyViewParentNode, currentDimension,
+ recursionDepthTrackers, previousDimensionsRecursive);
+
+ // Recursively add all paths associated with the descendants of the
+ // tree view child node wrt the current dimension to the heavy-view
+ // parent node.
+ this.addDimensionDescendantsToGenericHeavyViewNode_(treeViewChildNode,
+ heavyViewParentNode, currentDimension, recursionDepthTrackers,
+ previousDimensionsRecursive, treeViewNodeHandler);
+
+ recursionDepthTracker.pop();
+ }
+ },
+
+ /**
+ * Add a top-down tree-view child node together with its single-dimensional
+ * subtree to a top-down heavy-view parent node (tree-view node handler for
+ * top-down heavy view).
+ *
+ * Sample resulting top-down heavy view:
+ *
+ * +----------------+ +-----------------+
+ * | source | | destination |
+ * | tree-view root | ===============> | heavy-view root |
+ * | self=0 | | self=0 |
+ * | total=48 | | total=48 |
+ * +----------------+ +-----------------+
+ * | | ______| | |______
+ * v v v v v
+ * +----------+ +----------+ +----------+ +----------+ +----------+
+ * | A* | | B | | A*** | | B | | C |
+ * | self=10 | | self=12 | | self=13 | | self=13 | | self=2 |
+ * | total=30 | | total=18 | | total=30 | | total=34 | | total=7 |
+ * +----------+ +----------+ +----------+ +----------+ +----------+
+ * | : : :.........
+ * v v v v
+ * +----------+ ............ ............ ............
+ * | B | : B : : A : : C :
+ * | self=1 | : self=1 : : self=3 : : self=2 :
+ * | total=16 | : total=16 : : total=8 : : total=7 :
+ * +----------+ ............ ............ ............
+ * | |________ : :.........
+ * v v v v
+ * +----------+ +----------+ ............ ............
+ * | A** | | C | : A : : C :
+ * | self=3 | | self=2 | : self=3 : : self=2 :
+ * | total=8 | | total=7 | : total=8 : : total=7 :
+ * +----------+ +----------+ ............ ............
+ *
+ * Observe that care needs to be taken when dealing with recursion to avoid
+ * double-counting, e.g. the total value of A** (8) was not added to the
+ * total value of A*** (30) because it is already included in the total
+ * value of A* (30) (which was also added to A***). That is why we need to
+ * keep track of the path we traversed along the current dimension (to
+ * determine whether total value should be added or not).
+ */
+ addDimensionToTopDownHeavyViewNode_(
+ treeViewChildNode, heavyViewParentNode, currentDimension,
+ recursionDepthTrackers, previousDimensionsRecursive) {
+ this.addDimensionToTopDownHeavyViewNodeRecursively_(treeViewChildNode,
+ heavyViewParentNode, currentDimension, recursionDepthTrackers,
+ previousDimensionsRecursive, 1 /* subTreeDepth */);
+ },
+
+ addDimensionToTopDownHeavyViewNodeRecursively_(
+ treeViewChildNode, heavyViewParentNode, currentDimension,
+ recursionDepthTrackers, previousDimensionsRecursive, subTreeDepth) {
+ const recursionDepthTracker = recursionDepthTrackers[currentDimension];
+ const currentDimensionRecursive =
+ subTreeDepth <= recursionDepthTracker.recursionDepth;
+ const currentOrPreviousDimensionsRecursive =
+ currentDimensionRecursive || previousDimensionsRecursive;
+
+ const dimensionTitle = treeViewChildNode.title[currentDimension];
+ const heavyViewChildNode = this.getOrCreateChildNode_(
+ heavyViewParentNode, currentDimension, dimensionTitle);
+
+ this.addNodeValues_(treeViewChildNode, heavyViewChildNode,
+ !currentOrPreviousDimensionsRecursive /* addTotal */);
+
+ // Add the descendants of the tree-view child node wrt the next
+ // dimensions as children of the heavy-view child node.
+ this.addDimensionsToGenericHeavyViewNode_(treeViewChildNode,
+ heavyViewChildNode, currentDimension + 1, recursionDepthTrackers,
+ currentOrPreviousDimensionsRecursive,
+ this.addDimensionToTopDownHeavyViewNode_.bind(this));
+
+ for (const treeViewGrandChildNode of
+ treeViewChildNode.children[currentDimension].values()) {
+ recursionDepthTracker.push(treeViewGrandChildNode);
+
+ // Recursively add the tree-view grandchild node to the heavy-view
+ // child node.
+ this.addDimensionToTopDownHeavyViewNodeRecursively_(
+ treeViewGrandChildNode, heavyViewChildNode, currentDimension,
+ recursionDepthTrackers, previousDimensionsRecursive,
+ subTreeDepth + 1);
+
+ recursionDepthTracker.pop();
+ }
+ },
+
+ /**
+ * Add a top-down tree-view child node together with all its ancestors wrt
+ * the given dimension as descendants of a bottom-up heavy-view parent node
+ * in the reverse order (tree-view node handler for bottom-up heavy view).
+ *
+ * Sample resulting bottom-up heavy view:
+ *
+ * +----------------+ +-----------------+
+ * | source | | destination |
+ * | tree-view root | ===============> | heavy-view root |
+ * | self=0 | | self=0 |
+ * | total=48 | | total=48 |
+ * +----------------+ +-----------------+
+ * | | ______| | |______
+ * v v v v v
+ * +----------+ +----------+ +----------+ +----------+ +----------+
+ * | A* | | B | | A*** | | B | | C |
+ * | self=10 | | self=12 | | self=13 | | self=13 | | self=2 |
+ * | total=30 | | total=18 | | total=30 | | total=34 | | total=7 |
+ * +----------+ +----------+ +----------+ +----------+ +----------+
+ * | : : :
+ * v v v v
+ * +----------+ ............ ............ ............
+ * | B# | : B : : A : : B## :
+ * | self=1 | : self=3 : : self=1 : : self=2 :
+ * | total=16 | : total=8 : : total=16 : : total=7 :
+ * +----------+ ............ ............ ............
+ * | |________ : :
+ * v v v v
+ * +----------+ +----------+ ............ ............
+ * | A** | | C | : A : : A :
+ * | self=3 | | self=2 | : self=3 : : self=2 :
+ * | total=8 | | total=7 | : total=8 : : total=7 :
+ * +----------+ +----------+ ............ ............
+ *
+ * Similarly to the construction of the top-down heavy view, care needs to
+ * be taken when dealing with recursion to avoid double-counting, e.g. the
+ * total value of A** (8) was not added to the total value of A*** (30)
+ * because it is already included in the total value of A* (30) (which was
+ * also added to A***). That is why we need to keep track of the path we
+ * traversed along the current dimension (to determine whether total value
+ * should be added or not).
+ *
+ * Note that when we add an ancestor (B#) of a top-down tree-view node (C)
+ * to the bottom-up heavy view, the values of the original tree-view node
+ * (C) (rather than the ancestor's values) are added to the corresponding
+ * heavy-view node (B##).
+ */
+ addDimensionToBottomUpHeavyViewNode_(
+ treeViewChildNode, heavyViewParentNode, currentDimension,
+ recursionDepthTrackers, previousDimensionsRecursive) {
+ const recursionDepthTracker = recursionDepthTrackers[currentDimension];
+ const bottomIndex = recursionDepthTracker.bottomIndex;
+ const topIndex = recursionDepthTracker.topIndex;
+ const firstNonRecursiveIndex =
+ bottomIndex + recursionDepthTracker.recursionDepth;
+ const viewNodePath = recursionDepthTracker.viewNodePath;
+
+ const trackerAncestorNode = recursionDepthTracker.trackerAncestorNode;
+ let heavyViewDescendantNode = heavyViewParentNode;
+ for (let i = bottomIndex; i < topIndex; i++) {
+ const treeViewAncestorNode = viewNodePath[i];
+ const dimensionTitle = treeViewAncestorNode.title[currentDimension];
+ heavyViewDescendantNode = this.getOrCreateChildNode_(
+ heavyViewDescendantNode, currentDimension, dimensionTitle);
+
+ const currentDimensionRecursive = i < firstNonRecursiveIndex;
+ const currentOrPreviousDimensionsRecursive =
+ currentDimensionRecursive || previousDimensionsRecursive;
+
+ // The self and total values are taken from the original top-down tree
+ // view child node (rather than the ancestor node).
+ this.addNodeValues_(treeViewChildNode, heavyViewDescendantNode,
+ !currentOrPreviousDimensionsRecursive);
+
+ // Add the descendants of the tree-view child node wrt the next
+ // dimensions as children of the heavy-view child node.
+ this.addDimensionsToGenericHeavyViewNode_(treeViewChildNode,
+ heavyViewDescendantNode, currentDimension + 1,
+ recursionDepthTrackers, currentOrPreviousDimensionsRecursive,
+ this.addDimensionToBottomUpHeavyViewNode_.bind(this));
+ }
+ },
+
+ addNodeValues_(sourceNode, targetNode, addTotal) {
+ const targetNodeValues = targetNode.values;
+ const sourceNodeValues = sourceNode.values;
+ for (let v = 0; v < this.valueCount_; v++) {
+ const targetNodeValue = targetNodeValues[v];
+ const sourceNodeValue = sourceNodeValues[v];
+ targetNodeValue.self += sourceNodeValue.self;
+ if (addTotal) {
+ targetNodeValue.total += sourceNodeValue.total;
+ if (this.complete) {
+ targetNodeValue.totalState = EXACT;
+ } else if (sourceNodeValue.totalState > NOT_PROVIDED) {
+ targetNodeValue.totalState = Math.max(
+ targetNodeValue.totalState, LOWER_BOUND);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Recursion depth tracker.
+ *
+ * This class tracks the recursion depth of the current stack (updated via
+ * the push and pop methods). The recursion depth of a stack is the lengh of
+ * its longest leaf suffix that is repeated within the stack itself.
+ *
+ * For example, the recursion depth of the stack A -> B -> C -> A -> B -> B
+ * -> C (where C is the leaf node) is 2 because the suffix B -> C is repeated
+ * within it.
+ *
+ * @{constructor}
+ */
+ function RecursionDepthTracker(maxDepth, dimension) {
+ this.titlePath = new Array(maxDepth);
+ this.viewNodePath = new Array(maxDepth);
+ this.bottomIndex = this.topIndex = maxDepth;
+
+ this.dimension_ = dimension;
+ this.currentTrackerNode_ =
+ this.createNode_(0 /* recursionDepth */, undefined /* parent */);
+ }
+
+ RecursionDepthTracker.prototype = {
+ push(viewNode) {
+ if (this.bottomIndex === 0) {
+ throw new Error('Cannot push to a full tracker');
+ }
+ const title = viewNode.title[this.dimension_];
+ this.bottomIndex--;
+ this.titlePath[this.bottomIndex] = title;
+ this.viewNodePath[this.bottomIndex] = viewNode;
+
+ let childTrackerNode = this.currentTrackerNode_.children.get(title);
+ if (childTrackerNode !== undefined) {
+ // Child node already exists, so we don't need to calculate anything.
+ this.currentTrackerNode_ = childTrackerNode;
+ return;
+ }
+
+ // Child node doesn't exist yet, so we need to calculate its recursion
+ // depth.
+ const maxLengths = zFunction(this.titlePath, this.bottomIndex);
+ let recursionDepth = 0;
+ for (let i = 0; i < maxLengths.length; i++) {
+ recursionDepth = Math.max(recursionDepth, maxLengths[i]);
+ }
+
+ childTrackerNode =
+ this.createNode_(recursionDepth, this.currentTrackerNode_);
+ this.currentTrackerNode_.children.set(title, childTrackerNode);
+ this.currentTrackerNode_ = childTrackerNode;
+ },
+
+ pop() {
+ if (this.bottomIndex === this.topIndex) {
+ throw new Error('Cannot pop from an empty tracker');
+ }
+
+ this.titlePath[this.bottomIndex] = undefined;
+ this.viewNodePath[this.bottomIndex] = undefined;
+ this.bottomIndex++;
+
+ this.currentTrackerNode_ = this.currentTrackerNode_.parent;
+ },
+
+ get recursionDepth() {
+ return this.currentTrackerNode_.recursionDepth;
+ },
+
+ createNode_(recursionDepth, parent) {
+ return {
+ recursionDepth,
+ parent,
+ children: new Map()
+ };
+ }
+ };
+
+ /**
+ * Calculate the Z-function of (a suffix of) a list.
+ *
+ * Z-function: Given a list (or a string) of length n, for each index i from
+ * 1 to n - 1, find the length z[i] of the longest substring starting at
+ * position i which is also a prefix of the list. This function returns the
+ * list of maximum lengths z.
+ *
+ * Mathematically, for each i from 1 to n - 1, z[i] is the maximum value such
+ * that [list[0], ..., list[i - 1]] = [list[i], ..., list[i + z[i] - 1]].
+ * z[0] is defined to be zero for convenience.
+ *
+ * Example:
+ *
+ * Input (list): ['A', 'B', 'A', 'C', 'A', 'B', 'A']
+ * Output (z): [ 0 , 0 , 1 , 0 , 3 , 0 , 1 ]
+ *
+ * Unlike the brute-force approach (which is O(n^2) in the worst case), the
+ * complexity of this implementation is linear in the size of the list, i.e.
+ * O(n).
+ *
+ * Source: http://e-maxx-eng.github.io/string/z-function.html
+ */
+ function zFunction(list, startIndex) {
+ const n = list.length - startIndex;
+ if (n === 0) return [];
+
+ const z = new Array(n);
+ z[0] = 0;
+
+ for (let i = 1, left = 0, right = 0; i < n; ++i) {
+ let maxLength;
+ if (i <= right) {
+ maxLength = Math.min(right - i + 1, z[i - left]);
+ } else {
+ maxLength = 0;
+ }
+
+ while (i + maxLength < n && list[startIndex + maxLength] ===
+ list[startIndex + i + maxLength]) {
+ ++maxLength;
+ }
+
+ if (i + maxLength - 1 > right) {
+ left = i;
+ right = i + maxLength - 1;
+ }
+
+ z[i] = maxLength;
+ }
+
+ return z;
+ }
+
+ return {
+ MultiDimensionalViewBuilder,
+ MultiDimensionalViewNode,
+
+ // Exports below are for testing only.
+ RecursionDepthTracker,
+ zFunction,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view_test.html b/chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view_test.html
new file mode 100644
index 00000000000..b51d738617e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/multi_dimensional_view_test.html
@@ -0,0 +1,13382 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
+ const MultiDimensionalViewNode = tr.b.MultiDimensionalViewNode;
+ const ViewType = MultiDimensionalViewBuilder.ViewType;
+ const SELF = MultiDimensionalViewBuilder.ValueKind.SELF;
+ const TOTAL = MultiDimensionalViewBuilder.ValueKind.TOTAL;
+ const NOT_PROVIDED = MultiDimensionalViewNode.TotalState.NOT_PROVIDED;
+ const LOWER_BOUND = MultiDimensionalViewNode.TotalState.LOWER_BOUND;
+ const EXACT = MultiDimensionalViewNode.TotalState.EXACT;
+ const RecursionDepthTracker = tr.b.RecursionDepthTracker;
+ const zFunction = tr.b.zFunction;
+
+ function assertListStrictEqual(a, b) {
+ assert.lengthOf(a, b.length);
+ for (let i = 0; i < a.length; i++) {
+ assert.strictEqual(a[i], b[i]);
+ }
+ }
+
+ function checkTree(actualTreeRootNode, expectedStructureRootNode) {
+ // Build map from expected structure node IDs to expected structure nodes.
+ const expectedStructureNodesById = new Map();
+ addExpectedStructureToMap(
+ expectedStructureRootNode, expectedStructureNodesById);
+
+ // Recursively check the structure of the actual tree.
+ const actualTreeNodesById = new Map();
+ checkTreeStructure(actualTreeRootNode, expectedStructureRootNode,
+ actualTreeNodesById, expectedStructureNodesById);
+
+ // Test sanity check.
+ assert.strictEqual(
+ actualTreeNodesById.size, expectedStructureNodesById.size);
+ }
+
+ function addExpectedStructureToMap(expectedStructureNode, map) {
+ if (typeof expectedStructureNode === 'string') {
+ return; // Reference to another expected structure node.
+ }
+
+ const expectedStructureNodeId = expectedStructureNode.id;
+ if (expectedStructureNodeId !== undefined) {
+ assert.isFalse(map.has(expectedStructureNodeId));
+ map.set(expectedStructureNodeId, expectedStructureNode);
+ }
+
+ const expectedStructureChildren = expectedStructureNode.children;
+ for (let d = 0; d < expectedStructureChildren.length; d++) {
+ const expectedStructureDimensionChildren = expectedStructureChildren[d];
+ for (let i = 0; i < expectedStructureDimensionChildren.length; i++) {
+ addExpectedStructureToMap(expectedStructureDimensionChildren[i], map);
+ }
+ }
+ }
+
+ function checkTreeStructure(actualTreeNode, expectedStructureNode,
+ actualTreeNodesById, expectedStructureNodesById) {
+ // Check the multi-dimensional title.
+ assert.deepEqual(
+ Array.from(actualTreeNode.title), expectedStructureNode.title);
+
+ // Check the values.
+ assert.deepEqual(actualTreeNode.values, expectedStructureNode.values);
+
+ // Check the children.
+ const expectedStructureChildNodes = expectedStructureNode.children;
+ const actualTreeChildNodes = actualTreeNode.children;
+ assert.lengthOf(actualTreeChildNodes, expectedStructureChildNodes.length);
+
+ for (let d = 0; d < expectedStructureChildNodes.length; d++) {
+ const expectedStructureDimensionChildNodes =
+ expectedStructureChildNodes[d];
+ const actualTreeDimensionChildNodes = actualTreeChildNodes[d];
+ assert.strictEqual(actualTreeDimensionChildNodes.size,
+ expectedStructureDimensionChildNodes.length);
+
+ const expectedStructureDimensionChildNodeTitles = new Set();
+
+ for (let i = 0; i < expectedStructureDimensionChildNodes.length; i++) {
+ let expectedStructureDimensionChildNode =
+ expectedStructureDimensionChildNodes[i];
+ let isReference = false;
+
+ // If the expected structure child node is a reference to another
+ // expected structure node, resolve it.
+ if (typeof expectedStructureDimensionChildNode === 'string') {
+ expectedStructureDimensionChildNode = expectedStructureNodesById.get(
+ expectedStructureDimensionChildNode);
+ assert.isDefined(expectedStructureDimensionChildNode);
+ isReference = true;
+ }
+
+ // Check that the expected structure doesn't contain two children with
+ // the same title.
+ const childTitle = expectedStructureDimensionChildNode.title[d];
+ assert.isFalse(
+ expectedStructureDimensionChildNodeTitles.has(childTitle));
+ expectedStructureDimensionChildNodeTitles.add(childTitle);
+
+ // Get the associated child node of the actual tree.
+ const actualTreeDimensionChildNode =
+ actualTreeDimensionChildNodes.get(childTitle);
+ assert.isDefined(actualTreeDimensionChildNode);
+
+ // Check that all expected structure nodes with the same ID correspond
+ // to the same actual tree node.
+ const childId = expectedStructureDimensionChildNode.id;
+ if (childId !== undefined) {
+ if (actualTreeNodesById.has(childId)) {
+ assert.strictEqual(actualTreeDimensionChildNode,
+ actualTreeNodesById.get(childId));
+ } else {
+ actualTreeNodesById.set(childId, actualTreeDimensionChildNode);
+ }
+ }
+
+ // Recursively check the structure of the actual tree child node
+ // (unless the expected structure child node was a reference).
+ if (!isReference) {
+ checkTreeStructure(actualTreeDimensionChildNode,
+ expectedStructureDimensionChildNode, actualTreeNodesById,
+ expectedStructureNodesById);
+ }
+ }
+
+ // Test sanity check (all child titles should be unique).
+ assert.strictEqual(expectedStructureDimensionChildNodeTitles.size,
+ expectedStructureDimensionChildNodes.length);
+ }
+ }
+
+ function createBuilderWithEntries(dimensions, valueCount, pathEntries) {
+ const builder = new MultiDimensionalViewBuilder(dimensions, valueCount);
+ pathEntries.forEach(function(pathEntry) {
+ builder.addPath(pathEntry.path, pathEntry.values, pathEntry.kind);
+ });
+ return builder;
+ }
+
+ function builderTest(testName, dimensions, valueCount, setComplete,
+ pathEntries, expectedTopDownTreeViewStructure,
+ expectedTopDownHeavyViewStructure, expectedBottomUpHeavyViewStructure) {
+ test('builder_' + testName, function() {
+ // Create a multi-dimensional tree builder and add all paths to it.
+ const builder =
+ createBuilderWithEntries(dimensions, valueCount, pathEntries);
+ builder.complete = setComplete;
+
+ // Build and check the views.
+ checkTree(
+ builder.buildView(ViewType.TOP_DOWN_TREE_VIEW),
+ expectedTopDownTreeViewStructure);
+ checkTree(
+ builder.buildView(ViewType.TOP_DOWN_HEAVY_VIEW),
+ expectedTopDownHeavyViewStructure);
+ checkTree(
+ builder.buildView(ViewType.BOTTOM_UP_HEAVY_VIEW),
+ expectedBottomUpHeavyViewStructure);
+ });
+ }
+
+ /**
+ * Calculate the sum of binary powers.
+ *
+ * Each exponent can either be (1) a single number corresponding to a single
+ * power of two (2**exponent), or (2) a two-element list for a sum over a
+ * range of exponents (2**exponent[0] + 2**(exponent[0] + 1) + ... +
+ * 2**exponent[1]).
+ */
+ function b(/* exponent1, ..., exponentN */) {
+ let sum = 0;
+ for (let i = 0; i < arguments.length; i++) {
+ const exponent = arguments[i];
+ if (typeof exponent === 'number') {
+ sum += 1 << arguments[i];
+ } else {
+ assert.lengthOf(exponent, 2); // Test sanity check.
+ // We use the fact that 2**A + 2**(A + 1) ... + 2**B =
+ // (2**0 + 2**1 + ... 2**B) - (2**0 + 2**1 + ... + 2**(A - 1)) =
+ // (2**(B + 1) - 1) - (2**A - 1) = 2**(B + 1) - 2**A.
+ sum += (1 << (exponent[1] + 1)) - (1 << exponent[0]);
+ }
+ }
+ return sum;
+ }
+
+ function checkZFunction(list, expectedResult) {
+ if (typeof list === 'string') {
+ assert.deepEqual(zFunction(list, 0), expectedResult);
+ assert.deepEqual(zFunction(list[0] + list, 1), expectedResult);
+ assert.deepEqual(zFunction(list + list, list.length), expectedResult);
+ } else {
+ assert.deepEqual(zFunction([].concat(list), 0), expectedResult);
+ assert.deepEqual(zFunction([list[0]].concat(list), 1), expectedResult);
+ assert.deepEqual(
+ zFunction(list.concat(list), list.length), expectedResult);
+ }
+ }
+
+ /**
+ * Helper function for generating builder tests. Given a number of dimensions
+ * and a list of path entries, this function generates the source code of
+ * the corresponding builder test with expected top-down tree view, top-down
+ * heavy view and bottom-up heavy view structures.
+ *
+ * This avoids the need to write such tests manually, which is very tedious.
+ * However, the correctness of the generated structures needs to be verified
+ * by the developer! Maximum line length must also be enforced manually.
+ */
+ function generateBuilderTest(
+ targetTestName, dimensions, valueCount, pathEntries) {
+ test('builderTestGenerator_' + targetTestName, function() {
+ // Create the builder.
+ const builder =
+ createBuilderWithEntries(dimensions, valueCount, pathEntries);
+
+ // Generate the test case source code.
+ const generator = new tr.c.TestUtils.SourceGenerator();
+ generator.indentBlock(2, false /* don't break line */, function() {
+ // Test name and number of dimensions (first line).
+ generator.push('builderTest(\'', targetTestName, '\', ',
+ String(dimensions), ' /* dimensions */, ',
+ String(valueCount), ' /* valueCount */,');
+
+ generator.indentBlock(4, true /* break line */, function() {
+ // Path entries.
+ generator.formatMultiLineList(pathEntries, function(pathEntry) {
+ generator.push('{ path: ');
+ generator.formatSingleLineList(
+ pathEntry.path,
+ function(singleDimensionPath) {
+ generator.formatSingleLineList(
+ singleDimensionPath, generator.formatString, generator);
+ });
+ generator.push(', values: ');
+ generator.formatSingleLineList(
+ pathEntry.values,
+ function(value) {
+ generator.push(String(value));
+ });
+ const kind = pathEntry.kind === SELF ? 'SELF' : 'TOTAL';
+ generator.push(', kind: ', kind, ' }');
+ });
+ generator.push(',');
+ generator.breakLine();
+
+ function formatExpectedTreeStructure(root, label) {
+ let nextNodeId = 0;
+ const nodeInfos = new WeakMap();
+
+ function assignNodeIdsToRepeatedNodes(node) {
+ if (nodeInfos.has(node)) {
+ // We have already visited the node (one or more times), so
+ // there is no need to visit its children.
+ if (nodeInfos.get(node) === undefined) {
+ // This is the second time we visited the node: Change the
+ // undefined entry to a defined node info entry.
+ nodeInfos.set(node, { id: undefined });
+ }
+ return;
+ }
+
+ // This is the first time we visited the node: Add an undefined
+ // entry to the node info map and recursively visit all its
+ // children.
+ nodeInfos.set(node, undefined);
+ node.children.forEach(function(singleDimensionChildren) {
+ for (const child of singleDimensionChildren.values()) {
+ assignNodeIdsToRepeatedNodes(child);
+ }
+ });
+ }
+ assignNodeIdsToRepeatedNodes(root);
+
+ // Track the multi-dimensional path to the current node to generate
+ // comments.
+ const paths = new Array(dimensions);
+ for (let i = 0; i < paths.length; i++) {
+ paths[i] = [];
+ }
+ function withChild(childNode, dimension, callback) {
+ paths[dimension].push(childNode.title[dimension]);
+ callback();
+ paths[dimension].pop();
+ }
+ function appendPathComment(opt_label) {
+ if (opt_label) {
+ generator.pushComment(opt_label);
+ return;
+ }
+
+ paths.forEach(function(dimensionPath, dimensionIndex) {
+ if (dimensionIndex > 0) {
+ generator.pushComment(', ');
+ }
+ if (dimensionPath.length === 0) {
+ generator.pushComment('*');
+ return;
+ }
+ dimensionPath.forEach(function(ancestorTitle, ancestorIndex) {
+ if (ancestorIndex > 0) {
+ generator.pushComment(' -> ');
+ }
+ generator.pushComment(ancestorTitle);
+ });
+ });
+ }
+
+ function formatExpectedTreeStructureRecursively(node, opt_label) {
+ let nodeId = undefined;
+ const nodeInfo = nodeInfos.get(node);
+ if (nodeInfo !== undefined) {
+ // This node is referenced multiple times in the expected tree
+ // structure.
+ nodeId = nodeInfo.id;
+ if (nodeId === undefined) {
+ // This is the first time we visited the node: Assign it a
+ // unique node id and then format it and its descendants
+ // recursively.
+ nodeId = '#' + (nextNodeId++);
+ nodeInfo.id = nodeId;
+ } else {
+ // We have already visited this node: Just insert the node's
+ // id (instead of formatting it and its descendants
+ // recursively again).
+ generator.push('\'', nodeId, '\'');
+ appendPathComment();
+ return;
+ }
+ }
+
+ generator.push('{');
+ appendPathComment(opt_label);
+
+ generator.indentBlock(2, true /* break line */, function() {
+ // Node id (if defined).
+ if (nodeId !== undefined) {
+ generator.push('id: \'', nodeId, '\',');
+ generator.breakLine();
+ }
+
+ // Node title.
+ generator.push('title: ');
+ generator.formatSingleLineList(
+ node.title, generator.formatString, generator);
+ generator.push(',');
+ generator.breakLine();
+
+ // Node values.
+ generator.push('values: ');
+ generator.formatMultiLineList(
+ node.values,
+ function(value) {
+ generator.push('{');
+ generator.indentBlock(2, true /* break line */,
+ function() {
+ generator.push('total: ', String(value.total), ',');
+ generator.breakLine();
+ generator.push('self: ', String(value.self), ',');
+ generator.breakLine();
+ generator.push('totalState: ');
+
+ let totalStateName;
+ for (const [name, state] of Object.entries(
+ MultiDimensionalViewNode.TotalState)) {
+ if (state === value.totalState) {
+ totalStateName = name;
+ break;
+ }
+ }
+ if (totalStateName === undefined) {
+ throw new Error(
+ 'Unknown total state: ' + value.totalState);
+ }
+ generator.push(totalStateName);
+ generator.breakLine();
+ });
+ generator.push('}');
+ });
+ generator.push(',');
+ generator.breakLine();
+
+ // Node children.
+ const children = node.children;
+ generator.push('children: ');
+ generator.formatMultiLineList(
+ children,
+ function(singleDimensionChildren, dimension) {
+ generator.formatMultiLineList(
+ Array.from(singleDimensionChildren.values()),
+ function(child, childIndex) {
+ withChild(child, dimension, function() {
+ formatExpectedTreeStructureRecursively(child);
+ });
+ });
+ });
+ });
+ generator.breakLine();
+ generator.push('}');
+ }
+
+ formatExpectedTreeStructureRecursively(root, label);
+ }
+
+ // Build and format the three multi-dimensional views.
+ formatExpectedTreeStructure(
+ builder.buildTopDownTreeView(), 'Top-down tree view');
+ generator.push(',');
+ generator.breakLine();
+ formatExpectedTreeStructure(
+ builder.buildTopDownHeavyView(), 'Top-down heavy view');
+ generator.push(',');
+ generator.breakLine();
+ formatExpectedTreeStructure(
+ builder.buildBottomUpHeavyView(), 'Bottom-up heavy view');
+ generator.push(');');
+ });
+ });
+
+ tr.c.TestUtils.addSourceListing(this, generator.build());
+
+ throw new Error('This error is thrown to prevent accidentally ' +
+ 'checking in a test generator instead of an actual test.');
+ });
+ }
+
+ builderTest('zeroDimensions_noPaths', 0 /* dimensions */, 1 /* valueCount */,
+ false /* setComplete */,
+ [],
+ { // Top-down tree view.
+ title: [],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: []
+ },
+ { // Top-down heavy view.
+ title: [],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: []
+ },
+ { // Bottom-up heavy view.
+ title: [],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: []
+ });
+
+ builderTest('zeroDimensions_withPaths', 0 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [], values: [2], kind: SELF },
+ { path: [], values: [3], kind: TOTAL },
+ { path: [], values: [4], kind: SELF },
+ { path: [], values: [5], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [],
+ values: [
+ {
+ total: 8,
+ self: 6,
+ totalState: EXACT
+ }
+ ],
+ children: []
+ },
+ { // Top-down heavy view.
+ title: [],
+ values: [
+ {
+ total: 8,
+ self: 6,
+ totalState: EXACT
+ }
+ ],
+ children: []
+ },
+ { // Bottom-up heavy view.
+ title: [],
+ values: [
+ {
+ total: 8,
+ self: 6,
+ totalState: EXACT
+ }
+ ],
+ children: []
+ });
+
+ builderTest('oneDimension_noPaths', 1 /* dimensions */, 1 /* valueCount */,
+ false /* setComplete */,
+ [],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ []
+ ]
+ });
+
+ builderTest('oneDimension_zeroLengthPath', 1 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [[]], values: [42], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 42,
+ self: 42,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 42,
+ self: 42,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 42,
+ self: 42,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ });
+
+ builderTest('oneDimension_noRecursion', 1 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A', 'B', 'C']], values: [10], kind: SELF },
+ { path: [['A', 'B']], values: [20], kind: SELF },
+ { path: [['B', 'D']], values: [30], kind: SELF },
+ { path: [['A', 'B', 'D']], values: [40], kind: SELF },
+ { path: [['A', 'C']], values: [50], kind: SELF },
+ { path: [[]], values: [60], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 210,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 120,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 70,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 30,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 210,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 120,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 70,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 100,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 70,
+ self: 70,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 60,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // D.
+ title: ['D'],
+ values: [
+ {
+ total: 70,
+ self: 70,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 210,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 120,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 100,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 70,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 60,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // D.
+ title: ['D'],
+ values: [
+ {
+ total: 70,
+ self: 70,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 70,
+ self: 70,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('oneDimension_simpleRecursion', 1 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A']], values: [10], kind: SELF },
+ { path: [['A', 'A', 'A']], values: [20], kind: SELF },
+ { path: [['A', 'A']], values: [30], kind: SELF },
+ { path: [['A', 'A', 'A', 'A']], values: [40], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 100,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 90,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 100,
+ self: 100,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 90,
+ self: 90,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 100,
+ self: 100,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 90,
+ self: 90,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 60,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> A -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('oneDimension_complexRecursion', 1 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A', 'B', 'C']], values: [10], kind: SELF },
+ { path: [['A', 'D', 'B', 'C', 'A', 'B', 'C']], values: [20],
+ kind: SELF },
+ { path: [['A', 'D', 'B', 'C', 'A', 'B', 'D']], values: [30],
+ kind: SELF },
+ { path: [['C', 'B', 'C']], values: [40], kind: SELF },
+ { path: [['C', 'B', 'C', 'B', 'C']], values: [50], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 150,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C -> A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C -> A ->
+ // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> D -> B -> C -> A ->
+ // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 90,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 90,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 90,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 150,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C -> A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> D -> B -> C -> A ->
+ // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> D -> B -> C -> A ->
+ // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 150,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 150,
+ self: 120,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // B -> C -> A -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 150,
+ self: 120,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // C -> A -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 90,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 90,
+ self: 90,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> C -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> C -> A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> C -> A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // D -> B -> C -> A -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 150,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> C -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> C -> B -> D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 150,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A -> C -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A -> C -> B -> D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 90,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 150,
+ self: 120,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 150,
+ self: 120,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A -> C -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A -> C -> B ->
+ // D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 20,
+ self: 20,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // C -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 90,
+ self: 90,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> C -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 50,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // D.
+ title: ['D'],
+ values: [
+ {
+ total: 50,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // D -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> A -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> A -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> A -> C -> B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> B -> A -> C -> B ->
+ // D -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 30,
+ self: 30,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('oneDimension_withTotalSizes', 1 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['B', 'C']], values: [10], kind: TOTAL },
+ { path: [['B', 'C', 'D']], values: [5], kind: TOTAL },
+ { path: [['B']], values: [15], kind: SELF },
+ { path: [['B']], values: [20], kind: TOTAL },
+ { path: [['B', 'D']], values: [40], kind: SELF },
+ { path: [['C']], values: [50], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 115,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 65,
+ self: 15,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 50,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 115,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 65,
+ self: 15,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> C -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> D.
+ title: ['D'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // D.
+ title: ['D'],
+ values: [
+ {
+ total: 45,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 115,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 65,
+ self: 15,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // D.
+ title: ['D'],
+ values: [
+ {
+ total: 45,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // D -> C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // D -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 40,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('oneDimension_protoTitle', 1 /* dimensions */, 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['__proto__']], values: [45], kind: SELF },
+ { path: [['A']], values: [18], kind: SELF },
+ { path: [['A', '__proto__']], values: [89], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 152,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // __proto__.
+ title: ['__proto__'],
+ values: [
+ {
+ total: 45,
+ self: 45,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 107,
+ self: 18,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> __proto__.
+ title: ['__proto__'],
+ values: [
+ {
+ total: 89,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 152,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // __proto__.
+ title: ['__proto__'],
+ values: [
+ {
+ total: 134,
+ self: 45,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 107,
+ self: 18,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> __proto__.
+ title: ['__proto__'],
+ values: [
+ {
+ total: 89,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 152,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // __proto__.
+ title: ['__proto__'],
+ values: [
+ {
+ total: 134,
+ self: 45,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // __proto__ -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 89,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 107,
+ self: 18,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ });
+
+ // See tracing/tracing/base/multi_dimensional_view.html
+ // (MultiDimensionalViewBuilder.addDimensionToTopDownHeavyViewNode_ and
+ // MultiDimensionalViewBuilder.addDimensionToBottomUpHeavyViewNode_
+ // documentation).
+ builderTest('oneDimension_documentationExample', 1 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A']], values: [10], kind: SELF },
+ { path: [['A']], values: [30], kind: TOTAL },
+ { path: [['A', 'B']], values: [1], kind: SELF },
+ { path: [['A', 'B', 'A']], values: [3], kind: SELF },
+ { path: [['A', 'B', 'A']], values: [8], kind: TOTAL },
+ { path: [['A', 'B', 'C']], values: [2], kind: SELF },
+ { path: [['A', 'B', 'C']], values: [7], kind: TOTAL },
+ { path: [['B']], values: [12], kind: SELF },
+ { path: [['B']], values: [18], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined],
+ values: [
+ {
+ total: 48,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 30,
+ self: 10,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 16,
+ self: 1,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 8,
+ self: 3,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 18,
+ self: 12,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 48,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 30,
+ self: 13,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 16,
+ self: 1,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 8,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // A -> B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 34,
+ self: 13,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 8,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ },
+ { // B -> C.
+ title: ['C'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined],
+ values: [
+ {
+ total: 48,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A.
+ title: ['A'],
+ values: [
+ {
+ total: 30,
+ self: 13,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 8,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 8,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B.
+ title: ['B'],
+ values: [
+ {
+ total: 34,
+ self: 13,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 16,
+ self: 1,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C.
+ title: ['C'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B.
+ title: ['B'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A.
+ title: ['A'],
+ values: [
+ {
+ total: 7,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('twoDimensions_noPaths', 2 /* dimensions */, 1 /* valueCount */,
+ false /* setComplete */,
+ [],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ });
+
+ // See tracing/tracing/base/multi_dimensional_view.html
+ // (MultiDimensionalViewNode.finalizeTotalValues_ documentation).
+ builderTest('twoDimensions_totalCalculation', 2 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [[], []], values: [10], kind: SELF },
+ { path: [['A'], []], values: [21], kind: SELF },
+ { path: [['A'], []], values: [30], kind: TOTAL },
+ { path: [['B'], []], values: [25], kind: SELF },
+ { path: [['B'], []], values: [32], kind: TOTAL },
+ { path: [[], ['1']], values: [3], kind: SELF },
+ { path: [[], ['1']], values: [15], kind: TOTAL },
+ { path: [[], ['2']], values: [40], kind: SELF },
+ { path: [[], ['2']], values: [41], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 115,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 30,
+ self: 21,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 32,
+ self: 25,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 15,
+ self: 3,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 41,
+ self: 40,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 115,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 30,
+ self: 21,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 32,
+ self: 25,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 15,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 41,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 115,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 30,
+ self: 21,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 32,
+ self: 25,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 15,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 41,
+ self: 40,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ });
+
+ // See tracing/tracing/base/multi_dimensional_view.html
+ // (MultiDimensionalViewNode documentation).
+ builderTest('twoDimensions_documentationExample1', 2 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A', 'B'], ['T1', 'T2']], values: [1], kind: TOTAL },
+ { path: [['A', 'B'], ['T1']], values: [2], kind: TOTAL },
+ { path: [['A', 'B'], []], values: [4], kind: TOTAL },
+ { path: [['A'], ['T1', 'T2']], values: [10], kind: TOTAL },
+ { path: [['A'], ['T1']], values: [20], kind: TOTAL },
+ { path: [['A'], []], values: [40], kind: TOTAL },
+ { path: [[], ['T1', 'T2']], values: [100], kind: TOTAL },
+ { path: [[], ['T1']], values: [200], kind: TOTAL },
+ { path: [[], []], values: [400], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 400,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 4,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, T1.
+ id: '#0',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, T1 -> T2.
+ id: '#1',
+ title: ['B', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, T1.
+ id: '#2',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> B, T1.
+ ],
+ [
+ { // A, T1 -> T2.
+ id: '#3',
+ title: ['A', 'T2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> B, T1 -> T2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 200,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#2' // A, T1.
+ ],
+ [
+ { // *, T1 -> T2.
+ title: [undefined, 'T2'],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#3' // A, T1 -> T2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 400,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 4,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, T1.
+ id: '#0',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, T1 -> T2.
+ id: '#1',
+ title: ['B', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, T2.
+ id: '#2',
+ title: ['B', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, T1.
+ id: '#3',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> B, T1.
+ ],
+ [
+ { // A, T1 -> T2.
+ id: '#4',
+ title: ['A', 'T2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> B, T1 -> T2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, T2.
+ id: '#5',
+ title: ['A', 'T2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // A -> B, T2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 4,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B, T1.
+ id: '#6',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B, T1 -> T2.
+ id: '#7',
+ title: ['B', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, T2.
+ id: '#8',
+ title: ['B', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 200,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3', // A, T1.
+ '#6' // B, T1.
+ ],
+ [
+ { // *, T1 -> T2.
+ title: [undefined, 'T2'],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4', // A, T1 -> T2.
+ '#7' // B, T1 -> T2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, T2.
+ title: [undefined, 'T2'],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5', // A, T2.
+ '#8' // B, T2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 400,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A, T1.
+ id: '#0',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // A, T2.
+ id: '#1',
+ title: ['A', 'T2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A, T2 -> T1.
+ id: '#2',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 4,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 4,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, T1.
+ id: '#3',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B -> A, T2.
+ id: '#4',
+ title: ['A', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, T2 -> T1.
+ id: '#5',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, T1.
+ id: '#6',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // B -> A, T1.
+ ],
+ []
+ ]
+ },
+ { // B, T2.
+ id: '#7',
+ title: ['B', 'T2'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // B -> A, T2.
+ ],
+ [
+ { // B, T2 -> T1.
+ id: '#8',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // B -> A, T2 -> T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 200,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0', // A, T1.
+ '#6' // B, T1.
+ ],
+ []
+ ]
+ },
+ { // *, T2.
+ title: [undefined, 'T2'],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1', // A, T2.
+ '#7' // B, T2.
+ ],
+ [
+ { // *, T2 -> T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 100,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2', // A, T2 -> T1.
+ '#8' // B, T2 -> T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ // See tracing/tracing/base/multi_dimensional_view.html
+ // (MultiDimensionalViewBuilder documentation).
+ builderTest('twoDimensions_documentationExample2', 2 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['Saturday'], ['Cooking']], values: [1], kind: SELF },
+ { path: [['Saturday'], ['Sports', 'Football']], values: [2],
+ kind: SELF },
+ { path: [['Sunday'], ['Sports', 'Basketball']], values: [3],
+ kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 6,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // Saturday, *.
+ title: ['Saturday', undefined],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Saturday, Cooking.
+ id: '#0',
+ title: ['Saturday', 'Cooking'],
+ values: [
+ {
+ total: 1,
+ self: 1,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Saturday, Sports.
+ id: '#1',
+ title: ['Saturday', 'Sports'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Saturday, Sports -> Football.
+ id: '#2',
+ title: ['Saturday', 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Sunday, *.
+ title: ['Sunday', undefined],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Sunday, Sports.
+ id: '#3',
+ title: ['Sunday', 'Sports'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Sunday, Sports -> Basketball.
+ id: '#4',
+ title: ['Sunday', 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, Cooking.
+ title: [undefined, 'Cooking'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // Saturday, Cooking.
+ ],
+ []
+ ]
+ },
+ { // *, Sports.
+ title: [undefined, 'Sports'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1', // Saturday, Sports.
+ '#3' // Sunday, Sports.
+ ],
+ [
+ { // *, Sports -> Football.
+ title: [undefined, 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // Saturday, Sports -> Football.
+ ],
+ []
+ ]
+ },
+ { // *, Sports -> Basketball.
+ title: [undefined, 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // Sunday, Sports -> Basketball.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 6,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // Saturday, *.
+ title: ['Saturday', undefined],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Saturday, Cooking.
+ id: '#0',
+ title: ['Saturday', 'Cooking'],
+ values: [
+ {
+ total: 1,
+ self: 1,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Saturday, Sports.
+ id: '#1',
+ title: ['Saturday', 'Sports'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Saturday, Sports -> Football.
+ id: '#2',
+ title: ['Saturday', 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Saturday, Football.
+ id: '#3',
+ title: ['Saturday', 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Sunday, *.
+ title: ['Sunday', undefined],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Sunday, Sports.
+ id: '#4',
+ title: ['Sunday', 'Sports'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Sunday, Sports -> Basketball.
+ id: '#5',
+ title: ['Sunday', 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Sunday, Basketball.
+ id: '#6',
+ title: ['Sunday', 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, Cooking.
+ title: [undefined, 'Cooking'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // Saturday, Cooking.
+ ],
+ []
+ ]
+ },
+ { // *, Sports.
+ title: [undefined, 'Sports'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1', // Saturday, Sports.
+ '#4' // Sunday, Sports.
+ ],
+ [
+ { // *, Sports -> Football.
+ title: [undefined, 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // Saturday, Sports -> Football.
+ ],
+ []
+ ]
+ },
+ { // *, Sports -> Basketball.
+ title: [undefined, 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // Sunday, Sports -> Basketball.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, Football.
+ title: [undefined, 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // Saturday, Football.
+ ],
+ []
+ ]
+ },
+ { // *, Basketball.
+ title: [undefined, 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6' // Sunday, Basketball.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 6,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // Saturday, *.
+ title: ['Saturday', undefined],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Saturday, Cooking.
+ id: '#0',
+ title: ['Saturday', 'Cooking'],
+ values: [
+ {
+ total: 1,
+ self: 1,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Saturday, Sports.
+ id: '#1',
+ title: ['Saturday', 'Sports'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Saturday, Football.
+ id: '#2',
+ title: ['Saturday', 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Saturday, Football -> Sports.
+ id: '#3',
+ title: ['Saturday', 'Sports'],
+ values: [
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Sunday, *.
+ title: ['Sunday', undefined],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Sunday, Sports.
+ id: '#4',
+ title: ['Sunday', 'Sports'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Sunday, Basketball.
+ id: '#5',
+ title: ['Sunday', 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Sunday, Basketball -> Sports.
+ id: '#6',
+ title: ['Sunday', 'Sports'],
+ values: [
+ {
+ total: 3,
+ self: 3,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, Cooking.
+ title: [undefined, 'Cooking'],
+ values: [
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // Saturday, Cooking.
+ ],
+ []
+ ]
+ },
+ { // *, Sports.
+ title: [undefined, 'Sports'],
+ values: [
+ {
+ total: 5,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1', // Saturday, Sports.
+ '#4' // Sunday, Sports.
+ ],
+ []
+ ]
+ },
+ { // *, Football.
+ title: [undefined, 'Football'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // Saturday, Football.
+ ],
+ [
+ { // *, Football -> Sports.
+ title: [undefined, 'Sports'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // Saturday, Football -> Sports.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, Basketball.
+ title: [undefined, 'Basketball'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // Sunday, Basketball.
+ ],
+ [
+ { // *, Basketball -> Sports.
+ title: [undefined, 'Sports'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6' // Sunday, Basketball -> Sports.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ // See https://goo.gl/KY7zVE.
+ builderTest('twoDimensions_heapDumpExample', 2 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['BrMain', 'Init'], ['T']], values: [151], kind: TOTAL },
+ { path: [['BrMain', 'Init'], ['W']], values: [83], kind: TOTAL },
+ { path: [['BrMain', 'Init'], []], values: [242], kind: TOTAL },
+ { path: [['BrMain', 'MsgLp'], ['T']], values: [307], kind: TOTAL },
+ { path: [['BrMain', 'MsgLp'], ['V']], values: [281], kind: TOTAL },
+ { path: [['BrMain', 'MsgLp'], []], values: [601], kind: TOTAL },
+ { path: [['RdMain', 'RTask'], ['T']], values: [211], kind: TOTAL },
+ { path: [['RdMain', 'RTask'], ['W']], values: [337], kind: TOTAL },
+ { path: [['RdMain', 'RTask'], []], values: [556], kind: TOTAL },
+ { path: [[], ['T']], values: [698], kind: TOTAL },
+ { path: [[], ['V']], values: [340], kind: TOTAL },
+ { path: [[], ['W']], values: [461], kind: TOTAL },
+ { path: [[], []], values: [1538], kind: TOTAL },
+ { path: [['BrMain'], ['T']], values: [465], kind: TOTAL },
+ { path: [['BrMain'], ['V']], values: [297], kind: TOTAL },
+ { path: [['BrMain'], ['W']], values: [96], kind: TOTAL },
+ { path: [['BrMain'], []], values: [876], kind: TOTAL },
+ { path: [['RdMain'], ['T']], values: [229], kind: TOTAL },
+ { path: [['RdMain'], ['W']], values: [355], kind: TOTAL },
+ { path: [['RdMain'], []], values: [628], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 1538,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // BrMain, *.
+ title: ['BrMain', undefined],
+ values: [
+ {
+ total: 876,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // BrMain -> Init, *.
+ title: ['Init', undefined],
+ values: [
+ {
+ total: 242,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // BrMain -> Init, T.
+ id: '#0',
+ title: ['Init', 'T'],
+ values: [
+ {
+ total: 151,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // BrMain -> Init, W.
+ id: '#1',
+ title: ['Init', 'W'],
+ values: [
+ {
+ total: 83,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // BrMain -> MsgLp, *.
+ title: ['MsgLp', undefined],
+ values: [
+ {
+ total: 601,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // BrMain -> MsgLp, T.
+ id: '#2',
+ title: ['MsgLp', 'T'],
+ values: [
+ {
+ total: 307,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // BrMain -> MsgLp, V.
+ id: '#3',
+ title: ['MsgLp', 'V'],
+ values: [
+ {
+ total: 281,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // BrMain, T.
+ id: '#4',
+ title: ['BrMain', 'T'],
+ values: [
+ {
+ total: 465,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#0', // BrMain -> Init, T.
+ '#2' // BrMain -> MsgLp, T.
+ ],
+ []
+ ]
+ },
+ { // BrMain, V.
+ id: '#5',
+ title: ['BrMain', 'V'],
+ values: [
+ {
+ total: 297,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#3' // BrMain -> MsgLp, V.
+ ],
+ []
+ ]
+ },
+ { // BrMain, W.
+ id: '#6',
+ title: ['BrMain', 'W'],
+ values: [
+ {
+ total: 96,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#1' // BrMain -> Init, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // RdMain, *.
+ title: ['RdMain', undefined],
+ values: [
+ {
+ total: 628,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // RdMain -> RTask, *.
+ title: ['RTask', undefined],
+ values: [
+ {
+ total: 556,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // RdMain -> RTask, T.
+ id: '#7',
+ title: ['RTask', 'T'],
+ values: [
+ {
+ total: 211,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // RdMain -> RTask, W.
+ id: '#8',
+ title: ['RTask', 'W'],
+ values: [
+ {
+ total: 337,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // RdMain, T.
+ id: '#9',
+ title: ['RdMain', 'T'],
+ values: [
+ {
+ total: 229,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#7' // RdMain -> RTask, T.
+ ],
+ []
+ ]
+ },
+ { // RdMain, W.
+ id: '#10',
+ title: ['RdMain', 'W'],
+ values: [
+ {
+ total: 355,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#8' // RdMain -> RTask, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T.
+ title: [undefined, 'T'],
+ values: [
+ {
+ total: 698,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#4', // BrMain, T.
+ '#9' // RdMain, T.
+ ],
+ []
+ ]
+ },
+ { // *, V.
+ title: [undefined, 'V'],
+ values: [
+ {
+ total: 340,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#5' // BrMain, V.
+ ],
+ []
+ ]
+ },
+ { // *, W.
+ title: [undefined, 'W'],
+ values: [
+ {
+ total: 461,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#6', // BrMain, W.
+ '#10' // RdMain, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 1538,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // BrMain, *.
+ title: ['BrMain', undefined],
+ values: [
+ {
+ total: 876,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // BrMain -> Init, *.
+ title: ['Init', undefined],
+ values: [
+ {
+ total: 242,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // BrMain -> Init, T.
+ id: '#0',
+ title: ['Init', 'T'],
+ values: [
+ {
+ total: 151,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // BrMain -> Init, W.
+ id: '#1',
+ title: ['Init', 'W'],
+ values: [
+ {
+ total: 83,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // BrMain -> MsgLp, *.
+ title: ['MsgLp', undefined],
+ values: [
+ {
+ total: 601,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // BrMain -> MsgLp, T.
+ id: '#2',
+ title: ['MsgLp', 'T'],
+ values: [
+ {
+ total: 307,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // BrMain -> MsgLp, V.
+ id: '#3',
+ title: ['MsgLp', 'V'],
+ values: [
+ {
+ total: 281,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // BrMain, T.
+ id: '#4',
+ title: ['BrMain', 'T'],
+ values: [
+ {
+ total: 465,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0', // BrMain -> Init, T.
+ '#2' // BrMain -> MsgLp, T.
+ ],
+ []
+ ]
+ },
+ { // BrMain, V.
+ id: '#5',
+ title: ['BrMain', 'V'],
+ values: [
+ {
+ total: 297,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // BrMain -> MsgLp, V.
+ ],
+ []
+ ]
+ },
+ { // BrMain, W.
+ id: '#6',
+ title: ['BrMain', 'W'],
+ values: [
+ {
+ total: 96,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // BrMain -> Init, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Init, *.
+ title: ['Init', undefined],
+ values: [
+ {
+ total: 242,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Init, T.
+ id: '#7',
+ title: ['Init', 'T'],
+ values: [
+ {
+ total: 151,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Init, W.
+ id: '#8',
+ title: ['Init', 'W'],
+ values: [
+ {
+ total: 83,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // MsgLp, *.
+ title: ['MsgLp', undefined],
+ values: [
+ {
+ total: 601,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // MsgLp, T.
+ id: '#9',
+ title: ['MsgLp', 'T'],
+ values: [
+ {
+ total: 307,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // MsgLp, V.
+ id: '#10',
+ title: ['MsgLp', 'V'],
+ values: [
+ {
+ total: 281,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // RdMain, *.
+ title: ['RdMain', undefined],
+ values: [
+ {
+ total: 628,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // RdMain -> RTask, *.
+ title: ['RTask', undefined],
+ values: [
+ {
+ total: 556,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // RdMain -> RTask, T.
+ id: '#11',
+ title: ['RTask', 'T'],
+ values: [
+ {
+ total: 211,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // RdMain -> RTask, W.
+ id: '#12',
+ title: ['RTask', 'W'],
+ values: [
+ {
+ total: 337,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // RdMain, T.
+ id: '#13',
+ title: ['RdMain', 'T'],
+ values: [
+ {
+ total: 229,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11' // RdMain -> RTask, T.
+ ],
+ []
+ ]
+ },
+ { // RdMain, W.
+ id: '#14',
+ title: ['RdMain', 'W'],
+ values: [
+ {
+ total: 355,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12' // RdMain -> RTask, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // RTask, *.
+ title: ['RTask', undefined],
+ values: [
+ {
+ total: 556,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // RTask, T.
+ id: '#15',
+ title: ['RTask', 'T'],
+ values: [
+ {
+ total: 211,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // RTask, W.
+ id: '#16',
+ title: ['RTask', 'W'],
+ values: [
+ {
+ total: 337,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T.
+ title: [undefined, 'T'],
+ values: [
+ {
+ total: 698,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4', // BrMain, T.
+ '#7', // Init, T.
+ '#9', // MsgLp, T.
+ '#13', // RdMain, T.
+ '#15' // RTask, T.
+ ],
+ []
+ ]
+ },
+ { // *, V.
+ title: [undefined, 'V'],
+ values: [
+ {
+ total: 340,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5', // BrMain, V.
+ '#10' // MsgLp, V.
+ ],
+ []
+ ]
+ },
+ { // *, W.
+ title: [undefined, 'W'],
+ values: [
+ {
+ total: 461,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6', // BrMain, W.
+ '#8', // Init, W.
+ '#14', // RdMain, W.
+ '#16' // RTask, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 1538,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // BrMain, *.
+ title: ['BrMain', undefined],
+ values: [
+ {
+ total: 876,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // BrMain, T.
+ id: '#0',
+ title: ['BrMain', 'T'],
+ values: [
+ {
+ total: 465,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // BrMain, V.
+ id: '#1',
+ title: ['BrMain', 'V'],
+ values: [
+ {
+ total: 297,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // BrMain, W.
+ id: '#2',
+ title: ['BrMain', 'W'],
+ values: [
+ {
+ total: 96,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Init, *.
+ title: ['Init', undefined],
+ values: [
+ {
+ total: 242,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // Init -> BrMain, *.
+ title: ['BrMain', undefined],
+ values: [
+ {
+ total: 242,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // Init -> BrMain, T.
+ id: '#3',
+ title: ['BrMain', 'T'],
+ values: [
+ {
+ total: 151,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // Init -> BrMain, W.
+ id: '#4',
+ title: ['BrMain', 'W'],
+ values: [
+ {
+ total: 83,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // Init, T.
+ id: '#5',
+ title: ['Init', 'T'],
+ values: [
+ {
+ total: 151,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // Init -> BrMain, T.
+ ],
+ []
+ ]
+ },
+ { // Init, W.
+ id: '#6',
+ title: ['Init', 'W'],
+ values: [
+ {
+ total: 83,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // Init -> BrMain, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // MsgLp, *.
+ title: ['MsgLp', undefined],
+ values: [
+ {
+ total: 601,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // MsgLp -> BrMain, *.
+ title: ['BrMain', undefined],
+ values: [
+ {
+ total: 601,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // MsgLp -> BrMain, T.
+ id: '#7',
+ title: ['BrMain', 'T'],
+ values: [
+ {
+ total: 307,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // MsgLp -> BrMain, V.
+ id: '#8',
+ title: ['BrMain', 'V'],
+ values: [
+ {
+ total: 281,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // MsgLp, T.
+ id: '#9',
+ title: ['MsgLp', 'T'],
+ values: [
+ {
+ total: 307,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7' // MsgLp -> BrMain, T.
+ ],
+ []
+ ]
+ },
+ { // MsgLp, V.
+ id: '#10',
+ title: ['MsgLp', 'V'],
+ values: [
+ {
+ total: 281,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8' // MsgLp -> BrMain, V.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // RdMain, *.
+ title: ['RdMain', undefined],
+ values: [
+ {
+ total: 628,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // RdMain, T.
+ id: '#11',
+ title: ['RdMain', 'T'],
+ values: [
+ {
+ total: 229,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // RdMain, W.
+ id: '#12',
+ title: ['RdMain', 'W'],
+ values: [
+ {
+ total: 355,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // RTask, *.
+ title: ['RTask', undefined],
+ values: [
+ {
+ total: 556,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // RTask -> RdMain, *.
+ title: ['RdMain', undefined],
+ values: [
+ {
+ total: 556,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // RTask -> RdMain, T.
+ id: '#13',
+ title: ['RdMain', 'T'],
+ values: [
+ {
+ total: 211,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // RTask -> RdMain, W.
+ id: '#14',
+ title: ['RdMain', 'W'],
+ values: [
+ {
+ total: 337,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // RTask, T.
+ id: '#15',
+ title: ['RTask', 'T'],
+ values: [
+ {
+ total: 211,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13' // RTask -> RdMain, T.
+ ],
+ []
+ ]
+ },
+ { // RTask, W.
+ id: '#16',
+ title: ['RTask', 'W'],
+ values: [
+ {
+ total: 337,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14' // RTask -> RdMain, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T.
+ title: [undefined, 'T'],
+ values: [
+ {
+ total: 698,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0', // BrMain, T.
+ '#5', // Init, T.
+ '#9', // MsgLp, T.
+ '#11', // RdMain, T.
+ '#15' // RTask, T.
+ ],
+ []
+ ]
+ },
+ { // *, V.
+ title: [undefined, 'V'],
+ values: [
+ {
+ total: 340,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1', // BrMain, V.
+ '#10' // MsgLp, V.
+ ],
+ []
+ ]
+ },
+ { // *, W.
+ title: [undefined, 'W'],
+ values: [
+ {
+ total: 461,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2', // BrMain, W.
+ '#6', // Init, W.
+ '#12', // RdMain, W.
+ '#16' // RTask, W.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('twoDimensions_oneRecursiveDimension', 2 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A', 'B'], []], values: [1500], kind: TOTAL },
+ { path: [['A', 'B', 'A'], []], values: [200], kind: TOTAL },
+ { path: [['A', 'B', 'B'], []], values: [300], kind: TOTAL },
+ { path: [['A', 'B', 'C'], []], values: [700], kind: TOTAL },
+ { path: [['A', 'B'], ['T1']], values: [15], kind: TOTAL },
+ { path: [['A', 'B', 'A'], ['T1']], values: [2], kind: TOTAL },
+ { path: [['A', 'B', 'B'], ['T1']], values: [3], kind: TOTAL },
+ { path: [['A', 'B', 'C'], ['T1']], values: [7], kind: TOTAL },
+ { path: [['B', 'A'], ['T1']], values: [30000], kind: TOTAL },
+ { path: [['B', 'A'], []], values: [40000], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 1500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 1500,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 200,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> A, T1.
+ id: '#0',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 300,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, T1.
+ id: '#1',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B -> C, *.
+ title: ['C', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> C, T1.
+ id: '#2',
+ title: ['C', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, T1.
+ id: '#3',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 15,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#0', // A -> B -> A, T1.
+ '#1', // A -> B -> B, T1.
+ '#2' // A -> B -> C, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, T1.
+ id: '#4',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 15,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // A -> B, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 40000,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40000,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, T1.
+ id: '#5',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 30000,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, T1.
+ id: '#6',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 30000,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // B -> A, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4', // A, T1.
+ '#6' // B, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 1500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 200,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> A, T1.
+ id: '#0',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 300,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, T1.
+ id: '#1',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B -> C, *.
+ title: ['C', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> C, T1.
+ id: '#2',
+ title: ['C', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, T1.
+ id: '#3',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 15,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0', // A -> B -> A, T1.
+ '#1', // A -> B -> B, T1.
+ '#2' // A -> B -> C, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, T1.
+ id: '#4',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // A -> B, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40200,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, T1.
+ id: '#5',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 30002,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 300,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B, T1.
+ id: '#6',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> C, *.
+ title: ['C', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> C, T1.
+ id: '#7',
+ title: ['C', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, T1.
+ id: '#8',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5', // B -> A, T1.
+ '#6', // B -> B, T1.
+ '#7' // B -> C, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C, *.
+ title: ['C', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // C, T1.
+ id: '#9',
+ title: ['C', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4', // A, T1.
+ '#8', // B, T1.
+ '#9' // C, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 40200,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 200,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> A, T1.
+ id: '#0',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 2,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, T1.
+ id: '#1',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 30002,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> B -> A, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, T1.
+ id: '#2',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> B, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 41500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 1500,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, T1.
+ id: '#3',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 15,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 300,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 300,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B -> A, T1.
+ id: '#4',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B -> B, T1.
+ id: '#5',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 3,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // B -> B -> A, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, T1.
+ id: '#6',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3', // B -> A, T1.
+ '#5' // B -> B, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // C, *.
+ title: ['C', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // C -> B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 700,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // C -> B -> A, T1.
+ id: '#7',
+ title: ['A', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // C -> B, T1.
+ id: '#8',
+ title: ['B', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7' // C -> B -> A, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // C, T1.
+ id: '#9',
+ title: ['C', 'T1'],
+ values: [
+ {
+ total: 7,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8' // C -> B, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, T1.
+ title: [undefined, 'T1'],
+ values: [
+ {
+ total: 30015,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2', // A, T1.
+ '#6', // B, T1.
+ '#9' // C, T1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('twoDimensions_twoRecursiveDimensions', 2 /* dimensions */,
+ 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A', 'A', 'B'], ['1', '2', '2']], values: [10], kind: SELF },
+ { path: [['A', 'A'], ['1', '2']], values: [40], kind: TOTAL },
+ { path: [['A', 'B', 'B'], ['1', '1', '2']], values: [20], kind: TOTAL },
+ { path: [['A', 'B'], ['1', '1']], values: [5], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 1.
+ id: '#0',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 1 -> 2.
+ id: '#1',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 1 -> 2 -> 2.
+ id: '#2',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> A, 1.
+ id: '#3',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> A -> B, 1.
+ ],
+ [
+ { // A -> A, 1 -> 2.
+ id: '#4',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> A -> B, 1 -> 2.
+ ],
+ [
+ { // A -> A, 1 -> 2 -> 2.
+ id: '#5',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // A -> A -> B, 1 -> 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, 1.
+ id: '#6',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, 1 -> 1.
+ id: '#7',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, 1 -> 1 -> 2.
+ id: '#8',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, 1.
+ id: '#9',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6' // A -> B -> B, 1.
+ ],
+ [
+ { // A -> B, 1 -> 1.
+ id: '#10',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 25,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7' // A -> B -> B, 1 -> 1.
+ ],
+ [
+ { // A -> B, 1 -> 1 -> 2.
+ id: '#11',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8' // A -> B -> B, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, 1.
+ id: '#12',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3', // A -> A, 1.
+ '#9' // A -> B, 1.
+ ],
+ [
+ { // A, 1 -> 2.
+ id: '#13',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // A -> A, 1 -> 2.
+ ],
+ [
+ { // A, 1 -> 2 -> 2.
+ id: '#14',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // A -> A, 1 -> 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, 1 -> 1.
+ id: '#15',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#10' // A -> B, 1 -> 1.
+ ],
+ [
+ { // A, 1 -> 1 -> 2.
+ id: '#16',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11' // A -> B, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12' // A, 1.
+ ],
+ [
+ { // *, 1 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13' // A, 1 -> 2.
+ ],
+ [
+ { // *, 1 -> 2 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14' // A, 1 -> 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, 1 -> 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#15' // A, 1 -> 1.
+ ],
+ [
+ { // *, 1 -> 1 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#16' // A, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 1.
+ id: '#0',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 1 -> 2.
+ id: '#1',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 1 -> 2 -> 2.
+ id: '#2',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> A -> B, 2.
+ id: '#3',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A -> B, 2 -> 2.
+ id: '#4',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> A, 1.
+ id: '#5',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> A -> B, 1.
+ ],
+ [
+ { // A -> A, 1 -> 2.
+ id: '#6',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> A -> B, 1 -> 2.
+ ],
+ [
+ { // A -> A, 1 -> 2 -> 2.
+ id: '#7',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // A -> A -> B, 1 -> 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> A, 2.
+ id: '#8',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // A -> A -> B, 2.
+ ],
+ [
+ { // A -> A, 2 -> 2.
+ id: '#9',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // A -> A -> B, 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 35,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, 1.
+ id: '#10',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, 1 -> 1.
+ id: '#11',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B -> B, 1 -> 1 -> 2.
+ id: '#12',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B -> B, 1 -> 2.
+ id: '#13',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B -> B, 2.
+ id: '#14',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, 1.
+ id: '#15',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 35,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#10' // A -> B -> B, 1.
+ ],
+ [
+ { // A -> B, 1 -> 1.
+ id: '#16',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 25,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11' // A -> B -> B, 1 -> 1.
+ ],
+ [
+ { // A -> B, 1 -> 1 -> 2.
+ id: '#17',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12' // A -> B -> B, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, 1 -> 2.
+ id: '#18',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 30,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13' // A -> B -> B, 1 -> 2.
+ ],
+ [
+ { // A -> B, 1 -> 2 -> 2.
+ id: '#19',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, 2.
+ id: '#20',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 30,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14' // A -> B -> B, 2.
+ ],
+ [
+ { // A -> B, 2 -> 2.
+ id: '#21',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, 1.
+ id: '#22',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5', // A -> A, 1.
+ '#15' // A -> B, 1.
+ ],
+ [
+ { // A, 1 -> 2.
+ id: '#23',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6', // A -> A, 1 -> 2.
+ '#18' // A -> B, 1 -> 2.
+ ],
+ [
+ { // A, 1 -> 2 -> 2.
+ id: '#24',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7', // A -> A, 1 -> 2 -> 2.
+ '#19' // A -> B, 1 -> 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, 1 -> 1.
+ id: '#25',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#16' // A -> B, 1 -> 1.
+ ],
+ [
+ { // A, 1 -> 1 -> 2.
+ id: '#26',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#17' // A -> B, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, 2.
+ id: '#27',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8', // A -> A, 2.
+ '#20' // A -> B, 2.
+ ],
+ [
+ { // A, 2 -> 2.
+ id: '#28',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#9', // A -> A, 2 -> 2.
+ '#21' // A -> B, 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 35,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B, 1.
+ id: '#29',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B, 1 -> 1.
+ id: '#30',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B, 1 -> 1 -> 2.
+ id: '#31',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B, 1 -> 2.
+ id: '#32',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B, 2.
+ id: '#33',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, 1.
+ id: '#34',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 35,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#29' // B -> B, 1.
+ ],
+ [
+ { // B, 1 -> 2.
+ id: '#35',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 30,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#32' // B -> B, 1 -> 2.
+ ],
+ [
+ { // B, 1 -> 2 -> 2.
+ id: '#36',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, 1 -> 1.
+ id: '#37',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 25,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#30' // B -> B, 1 -> 1.
+ ],
+ [
+ { // B, 1 -> 1 -> 2.
+ id: '#38',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#31' // B -> B, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, 2.
+ id: '#39',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 30,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#33' // B -> B, 2.
+ ],
+ [
+ { // B, 2 -> 2.
+ id: '#40',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#22', // A, 1.
+ '#34' // B, 1.
+ ],
+ [
+ { // *, 1 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#23', // A, 1 -> 2.
+ '#35' // B, 1 -> 2.
+ ],
+ [
+ { // *, 1 -> 2 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#24', // A, 1 -> 2 -> 2.
+ '#36' // B, 1 -> 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, 1 -> 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#25', // A, 1 -> 1.
+ '#37' // B, 1 -> 1.
+ ],
+ [
+ { // *, 1 -> 1 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#26', // A, 1 -> 1 -> 2.
+ '#38' // B, 1 -> 1 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#27', // A, 2.
+ '#39' // B, 2.
+ ],
+ [
+ { // *, 2 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#28', // A, 2 -> 2.
+ '#40' // B, 2 -> 2.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A, 1.
+ id: '#0',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // A -> A, 2.
+ id: '#1',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A, 2 -> 1.
+ id: '#2',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 40,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // A -> A, 2 -> 2.
+ id: '#3',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> A, 2 -> 2 -> 1.
+ id: '#4',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, 1.
+ id: '#5',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> A, 1.
+ ],
+ [
+ { // A, 1 -> 1.
+ id: '#6',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, 2.
+ id: '#7',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> A, 2.
+ ],
+ [
+ { // A, 2 -> 1.
+ id: '#8',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // A -> A, 2 -> 1.
+ ],
+ [
+ { // A, 2 -> 1 -> 1.
+ id: '#9',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, 2 -> 2.
+ id: '#10',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // A -> A, 2 -> 2.
+ ],
+ [
+ { // A, 2 -> 2 -> 1.
+ id: '#11',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // A -> A, 2 -> 2 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 35,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 35,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A -> A, 1.
+ id: '#12',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B -> A -> A, 2.
+ id: '#13',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A -> A, 2 -> 1.
+ id: '#14',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B -> A -> A, 2 -> 2.
+ id: '#15',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A -> A, 2 -> 2 -> 1.
+ id: '#16',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B -> A, 1.
+ id: '#17',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 35,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12' // B -> A -> A, 1.
+ ],
+ [
+ { // B -> A, 1 -> 1.
+ id: '#18',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 25,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> A, 2.
+ id: '#19',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 30,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13' // B -> A -> A, 2.
+ ],
+ [
+ { // B -> A, 2 -> 1.
+ id: '#20',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 30,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14' // B -> A -> A, 2 -> 1.
+ ],
+ [
+ { // B -> A, 2 -> 1 -> 1.
+ id: '#21',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> A, 2 -> 2.
+ id: '#22',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#15' // B -> A -> A, 2 -> 2.
+ ],
+ [
+ { // B -> A, 2 -> 2 -> 1.
+ id: '#23',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#16' // B -> A -> A, 2 -> 2 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> B -> A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B -> A, 1.
+ id: '#24',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B -> A, 1 -> 1.
+ id: '#25',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B -> A, 2.
+ id: '#26',
+ title: ['A', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B -> A, 2 -> 1.
+ id: '#27',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> B -> A, 2 -> 1 -> 1.
+ id: '#28',
+ title: ['A', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B -> B, 1.
+ id: '#29',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#24' // B -> B -> A, 1.
+ ],
+ [
+ { // B -> B, 1 -> 1.
+ id: '#30',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#25' // B -> B -> A, 1 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> B, 2.
+ id: '#31',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#26' // B -> B -> A, 2.
+ ],
+ [
+ { // B -> B, 2 -> 1.
+ id: '#32',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#27' // B -> B -> A, 2 -> 1.
+ ],
+ [
+ { // B -> B, 2 -> 1 -> 1.
+ id: '#33',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#28' // B -> B -> A, 2 -> 1 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, 1.
+ id: '#34',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 35,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#17', // B -> A, 1.
+ '#29' // B -> B, 1.
+ ],
+ [
+ { // B, 1 -> 1.
+ id: '#35',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 25,
+ self: 5,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#18', // B -> A, 1 -> 1.
+ '#30' // B -> B, 1 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, 2.
+ id: '#36',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 30,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#19', // B -> A, 2.
+ '#31' // B -> B, 2.
+ ],
+ [
+ { // B, 2 -> 1.
+ id: '#37',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 30,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#20', // B -> A, 2 -> 1.
+ '#32' // B -> B, 2 -> 1.
+ ],
+ [
+ { // B, 2 -> 1 -> 1.
+ id: '#38',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#21', // B -> A, 2 -> 1 -> 1.
+ '#33' // B -> B, 2 -> 1 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, 2 -> 2.
+ id: '#39',
+ title: ['B', '2'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#22' // B -> A, 2 -> 2.
+ ],
+ [
+ { // B, 2 -> 2 -> 1.
+ id: '#40',
+ title: ['B', '1'],
+ values: [
+ {
+ total: 10,
+ self: 10,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#23' // B -> A, 2 -> 2 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 65,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5', // A, 1.
+ '#34' // B, 1.
+ ],
+ [
+ { // *, 1 -> 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 25,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6', // A, 1 -> 1.
+ '#35' // B, 1 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7', // A, 2.
+ '#36' // B, 2.
+ ],
+ [
+ { // *, 2 -> 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 60,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8', // A, 2 -> 1.
+ '#37' // B, 2 -> 1.
+ ],
+ [
+ { // *, 2 -> 1 -> 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 20,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#9', // A, 2 -> 1 -> 1.
+ '#38' // B, 2 -> 1 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, 2 -> 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#10', // A, 2 -> 2.
+ '#39' // B, 2 -> 2.
+ ],
+ [
+ { // *, 2 -> 2 -> 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 10,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11', // A, 2 -> 2 -> 1.
+ '#40' // B, 2 -> 2 -> 1.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('threeDimensions', 3 /* dimensions */, 1 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [['A', 'B'], ['C', 'D'], ['E', 'F']], values: [b(0)],
+ kind: SELF },
+ { path: [['A', 'B'], ['C', 'D'], ['E']], values: [b(1)], kind: SELF },
+ { path: [['A', 'B'], ['C', 'D'], []], values: [b(2)], kind: SELF },
+ { path: [['A', 'B'], ['C'], ['E', 'F']], values: [b(3)], kind: SELF },
+ { path: [['A', 'B'], ['C'], ['E']], values: [b(4)], kind: SELF },
+ { path: [['A', 'B'], ['C'], []], values: [b(5)], kind: SELF },
+ { path: [['A', 'B'], [], ['E', 'F']], values: [b(6)], kind: SELF },
+ { path: [['A', 'B'], [], ['E']], values: [b(7)], kind: SELF },
+ { path: [['A', 'B'], [], []], values: [b(8)], kind: SELF },
+
+ { path: [['A'], ['C', 'D'], ['E', 'F']], values: [b(9)], kind: SELF },
+ { path: [['A'], ['C', 'D'], ['E']], values: [b(10)], kind: SELF },
+ { path: [['A'], ['C', 'D'], []], values: [b(11)], kind: SELF },
+ { path: [['A'], ['C'], ['E', 'F']], values: [b(12)], kind: SELF },
+ { path: [['A'], ['C'], ['E']], values: [b(13)], kind: SELF },
+ { path: [['A'], ['C'], []], values: [b(14)], kind: SELF },
+ { path: [['A'], [], ['E', 'F']], values: [b(15)], kind: SELF },
+ { path: [['A'], [], ['E']], values: [b(16)], kind: SELF },
+ { path: [['A'], [], []], values: [b(17)], kind: SELF },
+
+ { path: [[], ['C', 'D'], ['E', 'F']], values: [b(18)], kind: SELF },
+ { path: [[], ['C', 'D'], ['E']], values: [b(19)], kind: SELF },
+ { path: [[], ['C', 'D'], []], values: [b(20)], kind: SELF },
+ { path: [[], ['C'], ['E', 'F']], values: [b(21)], kind: SELF },
+ { path: [[], ['C'], ['E']], values: [b(22)], kind: SELF },
+ { path: [[], ['C'], []], values: [b(23)], kind: SELF },
+ { path: [[], [], ['E', 'F']], values: [b(24)], kind: SELF },
+ { path: [[], [], ['E']], values: [b(25)], kind: SELF },
+ { path: [[], [], []], values: [b(26)], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined, undefined],
+ values: [
+ {
+ total: b([0, 26]),
+ self: b(26),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: b([0, 17]),
+ self: b(17),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: b([0, 8]),
+ self: b(8),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, C, *.
+ id: '#0',
+ title: ['B', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5]),
+ self: b(5),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, C -> D, *.
+ id: '#1',
+ title: ['B', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A -> B, C -> D, E.
+ id: '#2',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A -> B, C -> D, E -> F.
+ id: '#3',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, C, E.
+ id: '#4',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4),
+ self: b(4),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#2' // A -> B, C -> D, E.
+ ],
+ [
+ { // A -> B, C, E -> F.
+ id: '#5',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#3' // A -> B, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, *, E.
+ id: '#6',
+ title: ['B', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7),
+ self: b(7),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#4' // A -> B, C, E.
+ ],
+ [
+ { // A -> B, *, E -> F.
+ id: '#7',
+ title: ['B', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#5' // A -> B, C, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, C, *.
+ id: '#8',
+ title: ['A', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5], [9, 14]),
+ self: b(14),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> B, C, *.
+ ],
+ [
+ { // A, C -> D, *.
+ id: '#9',
+ title: ['A', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11]),
+ self: b(11),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> B, C -> D, *.
+ ],
+ [],
+ [
+ { // A, C -> D, E.
+ id: '#10',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10),
+ self: b(10),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // A -> B, C -> D, E.
+ ],
+ [],
+ [
+ { // A, C -> D, E -> F.
+ id: '#11',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // A -> B, C -> D, E -> F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, C, E.
+ id: '#12',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 9, 10, 12, 13),
+ self: b(13),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // A -> B, C, E.
+ ],
+ [
+ '#10' // A, C -> D, E.
+ ],
+ [
+ { // A, C, E -> F.
+ id: '#13',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12),
+ self: b(12),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // A -> B, C, E -> F.
+ ],
+ [
+ '#11' // A, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, *, E.
+ id: '#14',
+ title: ['A', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16),
+ self: b(16),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6' // A -> B, *, E.
+ ],
+ [
+ '#12' // A, C, E.
+ ],
+ [
+ { // A, *, E -> F.
+ id: '#15',
+ title: ['A', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15),
+ self: b(15),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7' // A -> B, *, E -> F.
+ ],
+ [
+ '#13' // A, C, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, C, *.
+ title: [undefined, 'C', undefined],
+ values: [
+ {
+ total: b([0, 5], [9, 14], [18, 23]),
+ self: b(23),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8' // A, C, *.
+ ],
+ [
+ { // *, C -> D, *.
+ title: [undefined, 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11], [18, 20]),
+ self: b(20),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#9' // A, C -> D, *.
+ ],
+ [],
+ [
+ { // *, C -> D, E.
+ id: '#16',
+ title: [undefined, 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10, 18, 19),
+ self: b(19),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#10' // A, C -> D, E.
+ ],
+ [],
+ [
+ { // *, C -> D, E -> F.
+ id: '#17',
+ title: [undefined, 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11' // A, C -> D, E -> F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, C, E.
+ id: '#18',
+ title: [undefined, 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22),
+ self: b(22),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12' // A, C, E.
+ ],
+ [
+ '#16' // *, C -> D, E.
+ ],
+ [
+ { // *, C, E -> F.
+ id: '#19',
+ title: [undefined, 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12, 18, 21),
+ self: b(21),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13' // A, C, E -> F.
+ ],
+ [
+ '#17' // *, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, *, E.
+ title: [undefined, undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21,
+ 22, 24, 25),
+ self: b(25),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14' // A, *, E.
+ ],
+ [
+ '#18' // *, C, E.
+ ],
+ [
+ { // *, *, E -> F.
+ title: [undefined, undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15, 18, 21, 24),
+ self: b(24),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#15' // A, *, E -> F.
+ ],
+ [
+ '#19' // *, C, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined, undefined],
+ values: [
+ {
+ total: b([0, 26]),
+ self: b(26),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: b([0, 17]),
+ self: b(17),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A -> B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: b([0, 8]),
+ self: b(8),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, C, *.
+ id: '#0',
+ title: ['B', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5]),
+ self: b(5),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A -> B, C -> D, *.
+ id: '#1',
+ title: ['B', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A -> B, C -> D, E.
+ id: '#2',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A -> B, C -> D, E -> F.
+ id: '#3',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, C -> D, F.
+ id: '#4',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, C, E.
+ id: '#5',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4),
+ self: b(4),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#2' // A -> B, C -> D, E.
+ ],
+ [
+ { // A -> B, C, E -> F.
+ id: '#6',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#3' // A -> B, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, C, F.
+ id: '#7',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#4' // A -> B, C -> D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, D, *.
+ id: '#8',
+ title: ['B', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A -> B, D, E.
+ id: '#9',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A -> B, D, E -> F.
+ id: '#10',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, D, F.
+ id: '#11',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A -> B, *, E.
+ id: '#12',
+ title: ['B', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7),
+ self: b(7),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#5', // A -> B, C, E.
+ '#9' // A -> B, D, E.
+ ],
+ [
+ { // A -> B, *, E -> F.
+ id: '#13',
+ title: ['B', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#6', // A -> B, C, E -> F.
+ '#10' // A -> B, D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A -> B, *, F.
+ id: '#14',
+ title: ['B', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#7', // A -> B, C, F.
+ '#11' // A -> B, D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, C, *.
+ id: '#15',
+ title: ['A', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5], [9, 14]),
+ self: b(14),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0' // A -> B, C, *.
+ ],
+ [
+ { // A, C -> D, *.
+ id: '#16',
+ title: ['A', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11]),
+ self: b(11),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1' // A -> B, C -> D, *.
+ ],
+ [],
+ [
+ { // A, C -> D, E.
+ id: '#17',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10),
+ self: b(10),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2' // A -> B, C -> D, E.
+ ],
+ [],
+ [
+ { // A, C -> D, E -> F.
+ id: '#18',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3' // A -> B, C -> D, E -> F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, C -> D, F.
+ id: '#19',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4' // A -> B, C -> D, F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, C, E.
+ id: '#20',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 9, 10, 12, 13),
+ self: b(13),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5' // A -> B, C, E.
+ ],
+ [
+ '#17' // A, C -> D, E.
+ ],
+ [
+ { // A, C, E -> F.
+ id: '#21',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12),
+ self: b(12),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6' // A -> B, C, E -> F.
+ ],
+ [
+ '#18' // A, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, C, F.
+ id: '#22',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12),
+ self: b(12),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7' // A -> B, C, F.
+ ],
+ [
+ '#19' // A, C -> D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, D, *.
+ id: '#23',
+ title: ['A', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11]),
+ self: b(11),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8' // A -> B, D, *.
+ ],
+ [],
+ [
+ { // A, D, E.
+ id: '#24',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10),
+ self: b(10),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#9' // A -> B, D, E.
+ ],
+ [],
+ [
+ { // A, D, E -> F.
+ id: '#25',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#10' // A -> B, D, E -> F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, D, F.
+ id: '#26',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11' // A -> B, D, F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, *, E.
+ id: '#27',
+ title: ['A', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16),
+ self: b(16),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12' // A -> B, *, E.
+ ],
+ [
+ '#20', // A, C, E.
+ '#24' // A, D, E.
+ ],
+ [
+ { // A, *, E -> F.
+ id: '#28',
+ title: ['A', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15),
+ self: b(15),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13' // A -> B, *, E -> F.
+ ],
+ [
+ '#21', // A, C, E -> F.
+ '#25' // A, D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, *, F.
+ id: '#29',
+ title: ['A', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15),
+ self: b(15),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14' // A -> B, *, F.
+ ],
+ [
+ '#22', // A, C, F.
+ '#26' // A, D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: b([0, 8]),
+ self: b(8),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B, C, *.
+ id: '#30',
+ title: ['B', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5]),
+ self: b(5),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B, C -> D, *.
+ id: '#31',
+ title: ['B', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B, C -> D, E.
+ id: '#32',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B, C -> D, E -> F.
+ id: '#33',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, C -> D, F.
+ id: '#34',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, C, E.
+ id: '#35',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4),
+ self: b(4),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#32' // B, C -> D, E.
+ ],
+ [
+ { // B, C, E -> F.
+ id: '#36',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#33' // B, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, C, F.
+ id: '#37',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#34' // B, C -> D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, D, *.
+ id: '#38',
+ title: ['B', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B, D, E.
+ id: '#39',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B, D, E -> F.
+ id: '#40',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, D, F.
+ id: '#41',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, *, E.
+ id: '#42',
+ title: ['B', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7),
+ self: b(7),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#35', // B, C, E.
+ '#39' // B, D, E.
+ ],
+ [
+ { // B, *, E -> F.
+ id: '#43',
+ title: ['B', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#36', // B, C, E -> F.
+ '#40' // B, D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *, F.
+ id: '#44',
+ title: ['B', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#37', // B, C, F.
+ '#41' // B, D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, C, *.
+ title: [undefined, 'C', undefined],
+ values: [
+ {
+ total: b([0, 5], [9, 14], [18, 23]),
+ self: b(23),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#15', // A, C, *.
+ '#30' // B, C, *.
+ ],
+ [
+ { // *, C -> D, *.
+ title: [undefined, 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11], [18, 20]),
+ self: b(20),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#16', // A, C -> D, *.
+ '#31' // B, C -> D, *.
+ ],
+ [],
+ [
+ { // *, C -> D, E.
+ id: '#45',
+ title: [undefined, 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10, 18, 19),
+ self: b(19),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#17', // A, C -> D, E.
+ '#32' // B, C -> D, E.
+ ],
+ [],
+ [
+ { // *, C -> D, E -> F.
+ id: '#46',
+ title: [undefined, 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#18', // A, C -> D, E -> F.
+ '#33' // B, C -> D, E -> F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, C -> D, F.
+ id: '#47',
+ title: [undefined, 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#19', // A, C -> D, F.
+ '#34' // B, C -> D, F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, C, E.
+ id: '#48',
+ title: [undefined, 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22),
+ self: b(22),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#20', // A, C, E.
+ '#35' // B, C, E.
+ ],
+ [
+ '#45' // *, C -> D, E.
+ ],
+ [
+ { // *, C, E -> F.
+ id: '#49',
+ title: [undefined, 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12, 18, 21),
+ self: b(21),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#21', // A, C, E -> F.
+ '#36' // B, C, E -> F.
+ ],
+ [
+ '#46' // *, C -> D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, C, F.
+ id: '#50',
+ title: [undefined, 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12, 18, 21),
+ self: b(21),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#22', // A, C, F.
+ '#37' // B, C, F.
+ ],
+ [
+ '#47' // *, C -> D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, D, *.
+ title: [undefined, 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11], [18, 20]),
+ self: b(20),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#23', // A, D, *.
+ '#38' // B, D, *.
+ ],
+ [],
+ [
+ { // *, D, E.
+ id: '#51',
+ title: [undefined, 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10, 18, 19),
+ self: b(19),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#24', // A, D, E.
+ '#39' // B, D, E.
+ ],
+ [],
+ [
+ { // *, D, E -> F.
+ id: '#52',
+ title: [undefined, 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#25', // A, D, E -> F.
+ '#40' // B, D, E -> F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, D, F.
+ id: '#53',
+ title: [undefined, 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#26', // A, D, F.
+ '#41' // B, D, F.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, *, E.
+ title: [undefined, undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21,
+ 22, 24, 25),
+ self: b(25),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#27', // A, *, E.
+ '#42' // B, *, E.
+ ],
+ [
+ '#48', // *, C, E.
+ '#51' // *, D, E.
+ ],
+ [
+ { // *, *, E -> F.
+ title: [undefined, undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15, 18, 21, 24),
+ self: b(24),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#28', // A, *, E -> F.
+ '#43' // B, *, E -> F.
+ ],
+ [
+ '#49', // *, C, E -> F.
+ '#52' // *, D, E -> F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, *, F.
+ title: [undefined, undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15, 18, 21, 24),
+ self: b(24),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#29', // A, *, F.
+ '#44' // B, *, F.
+ ],
+ [
+ '#50', // *, C, F.
+ '#53' // *, D, F.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined, undefined],
+ values: [
+ {
+ total: b([0, 26]),
+ self: b(26),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: b([0, 17]),
+ self: b(17),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A, C, *.
+ id: '#0',
+ title: ['A', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5], [9, 14]),
+ self: b(14),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A, C, E.
+ id: '#1',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 9, 10, 12, 13),
+ self: b(13),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // A, C, F.
+ id: '#2',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12),
+ self: b(12),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A, C, F -> E.
+ id: '#3',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 3, 9, 12),
+ self: b(12),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // A, D, *.
+ id: '#4',
+ title: ['A', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11]),
+ self: b(11),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // A, D -> C, *.
+ id: '#5',
+ title: ['A', 'C', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11]),
+ self: b(11),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A, D -> C, E.
+ id: '#6',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10),
+ self: b(10),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // A, D -> C, F.
+ id: '#7',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // A, D -> C, F -> E.
+ id: '#8',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, D, E.
+ id: '#9',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10),
+ self: b(10),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#6' // A, D -> C, E.
+ ],
+ []
+ ]
+ },
+ { // A, D, F.
+ id: '#10',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#7' // A, D -> C, F.
+ ],
+ [
+ { // A, D, F -> E.
+ id: '#11',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 9),
+ self: b(9),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#8' // A, D -> C, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // A, *, E.
+ id: '#12',
+ title: ['A', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16),
+ self: b(16),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#1', // A, C, E.
+ '#9' // A, D, E.
+ ],
+ []
+ ]
+ },
+ { // A, *, F.
+ id: '#13',
+ title: ['A', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15),
+ self: b(15),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#2', // A, C, F.
+ '#10' // A, D, F.
+ ],
+ [
+ { // A, *, F -> E.
+ id: '#14',
+ title: ['A', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15),
+ self: b(15),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#3', // A, C, F -> E.
+ '#11' // A, D, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: b([0, 8]),
+ self: b(8),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // B -> A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: b([0, 8]),
+ self: b(8),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, C, *.
+ id: '#15',
+ title: ['A', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5]),
+ self: b(5),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B -> A, C, E.
+ id: '#16',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4),
+ self: b(4),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // B -> A, C, F.
+ id: '#17',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B -> A, C, F -> E.
+ id: '#18',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B -> A, D, *.
+ id: '#19',
+ title: ['A', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ { // B -> A, D -> C, *.
+ id: '#20',
+ title: ['A', 'C', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B -> A, D -> C, E.
+ id: '#21',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // B -> A, D -> C, F.
+ id: '#22',
+ title: ['A', 'C', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ [
+ { // B -> A, D -> C, F -> E.
+ id: '#23',
+ title: ['A', 'C', 'E'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B -> A, D, E.
+ id: '#24',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#21' // B -> A, D -> C, E.
+ ],
+ []
+ ]
+ },
+ { // B -> A, D, F.
+ id: '#25',
+ title: ['A', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#22' // B -> A, D -> C, F.
+ ],
+ [
+ { // B -> A, D, F -> E.
+ id: '#26',
+ title: ['A', 'D', 'E'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#23' // B -> A, D -> C, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B -> A, *, E.
+ id: '#27',
+ title: ['A', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7),
+ self: b(7),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#16', // B -> A, C, E.
+ '#24' // B -> A, D, E.
+ ],
+ []
+ ]
+ },
+ { // B -> A, *, F.
+ id: '#28',
+ title: ['A', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#17', // B -> A, C, F.
+ '#25' // B -> A, D, F.
+ ],
+ [
+ { // B -> A, *, F -> E.
+ id: '#29',
+ title: ['A', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [
+ '#18', // B -> A, C, F -> E.
+ '#26' // B -> A, D, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, C, *.
+ id: '#30',
+ title: ['B', 'C', undefined],
+ values: [
+ {
+ total: b([0, 5]),
+ self: b(5),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#15' // B -> A, C, *.
+ ],
+ [],
+ [
+ { // B, C, E.
+ id: '#31',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4),
+ self: b(4),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#16' // B -> A, C, E.
+ ],
+ [],
+ []
+ ]
+ },
+ { // B, C, F.
+ id: '#32',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#17' // B -> A, C, F.
+ ],
+ [],
+ [
+ { // B, C, F -> E.
+ id: '#33',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 3),
+ self: b(3),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#18' // B -> A, C, F -> E.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // B, D, *.
+ id: '#34',
+ title: ['B', 'D', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#19' // B -> A, D, *.
+ ],
+ [
+ { // B, D -> C, *.
+ id: '#35',
+ title: ['B', 'C', undefined],
+ values: [
+ {
+ total: b([0, 2]),
+ self: b(2),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#20' // B -> A, D -> C, *.
+ ],
+ [],
+ [
+ { // B, D -> C, E.
+ id: '#36',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#21' // B -> A, D -> C, E.
+ ],
+ [],
+ []
+ ]
+ },
+ { // B, D -> C, F.
+ id: '#37',
+ title: ['B', 'C', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#22' // B -> A, D -> C, F.
+ ],
+ [],
+ [
+ { // B, D -> C, F -> E.
+ id: '#38',
+ title: ['B', 'C', 'E'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#23' // B -> A, D -> C, F -> E.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, D, E.
+ id: '#39',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1),
+ self: b(1),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#24' // B -> A, D, E.
+ ],
+ [
+ '#36' // B, D -> C, E.
+ ],
+ []
+ ]
+ },
+ { // B, D, F.
+ id: '#40',
+ title: ['B', 'D', 'F'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#25' // B -> A, D, F.
+ ],
+ [
+ '#37' // B, D -> C, F.
+ ],
+ [
+ { // B, D, F -> E.
+ id: '#41',
+ title: ['B', 'D', 'E'],
+ values: [
+ {
+ total: b(0),
+ self: b(0),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#26' // B -> A, D, F -> E.
+ ],
+ [
+ '#38' // B, D -> C, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // B, *, E.
+ id: '#42',
+ title: ['B', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7),
+ self: b(7),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#27' // B -> A, *, E.
+ ],
+ [
+ '#31', // B, C, E.
+ '#39' // B, D, E.
+ ],
+ []
+ ]
+ },
+ { // B, *, F.
+ id: '#43',
+ title: ['B', undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#28' // B -> A, *, F.
+ ],
+ [
+ '#32', // B, C, F.
+ '#40' // B, D, F.
+ ],
+ [
+ { // B, *, F -> E.
+ id: '#44',
+ title: ['B', undefined, 'E'],
+ values: [
+ {
+ total: b(0, 3, 6),
+ self: b(6),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#29' // B -> A, *, F -> E.
+ ],
+ [
+ '#33', // B, C, F -> E.
+ '#41' // B, D, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, C, *.
+ title: [undefined, 'C', undefined],
+ values: [
+ {
+ total: b([0, 5], [9, 14], [18, 23]),
+ self: b(23),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#0', // A, C, *.
+ '#30' // B, C, *.
+ ],
+ [],
+ [
+ { // *, C, E.
+ id: '#45',
+ title: [undefined, 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22),
+ self: b(22),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#1', // A, C, E.
+ '#31' // B, C, E.
+ ],
+ [],
+ []
+ ]
+ },
+ { // *, C, F.
+ id: '#46',
+ title: [undefined, 'C', 'F'],
+ values: [
+ {
+ total: b(0, 3, 9, 12, 18, 21),
+ self: b(21),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#2', // A, C, F.
+ '#32' // B, C, F.
+ ],
+ [],
+ [
+ { // *, C, F -> E.
+ id: '#47',
+ title: [undefined, 'C', 'E'],
+ values: [
+ {
+ total: b(0, 3, 9, 12, 18, 21),
+ self: b(21),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#3', // A, C, F -> E.
+ '#33' // B, C, F -> E.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ },
+ { // *, D, *.
+ title: [undefined, 'D', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11], [18, 20]),
+ self: b(20),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#4', // A, D, *.
+ '#34' // B, D, *.
+ ],
+ [
+ { // *, D -> C, *.
+ title: [undefined, 'C', undefined],
+ values: [
+ {
+ total: b([0, 2], [9, 11], [18, 20]),
+ self: b(20),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#5', // A, D -> C, *.
+ '#35' // B, D -> C, *.
+ ],
+ [],
+ [
+ { // *, D -> C, E.
+ id: '#48',
+ title: [undefined, 'C', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10, 18, 19),
+ self: b(19),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#6', // A, D -> C, E.
+ '#36' // B, D -> C, E.
+ ],
+ [],
+ []
+ ]
+ },
+ { // *, D -> C, F.
+ id: '#49',
+ title: [undefined, 'C', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#7', // A, D -> C, F.
+ '#37' // B, D -> C, F.
+ ],
+ [],
+ [
+ { // *, D -> C, F -> E.
+ id: '#50',
+ title: [undefined, 'C', 'E'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#8', // A, D -> C, F -> E.
+ '#38' // B, D -> C, F -> E.
+ ],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, D, E.
+ id: '#51',
+ title: [undefined, 'D', 'E'],
+ values: [
+ {
+ total: b(0, 1, 9, 10, 18, 19),
+ self: b(19),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#9', // A, D, E.
+ '#39' // B, D, E.
+ ],
+ [
+ '#48' // *, D -> C, E.
+ ],
+ []
+ ]
+ },
+ { // *, D, F.
+ id: '#52',
+ title: [undefined, 'D', 'F'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#10', // A, D, F.
+ '#40' // B, D, F.
+ ],
+ [
+ '#49' // *, D -> C, F.
+ ],
+ [
+ { // *, D, F -> E.
+ id: '#53',
+ title: [undefined, 'D', 'E'],
+ values: [
+ {
+ total: b(0, 9, 18),
+ self: b(18),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#11', // A, D, F -> E.
+ '#41' // B, D, F -> E.
+ ],
+ [
+ '#50' // *, D -> C, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ],
+ [
+ { // *, *, E.
+ title: [undefined, undefined, 'E'],
+ values: [
+ {
+ total: b(0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21,
+ 22, 24, 25),
+ self: b(25),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#12', // A, *, E.
+ '#42' // B, *, E.
+ ],
+ [
+ '#45', // *, C, E.
+ '#51' // *, D, E.
+ ],
+ []
+ ]
+ },
+ { // *, *, F.
+ title: [undefined, undefined, 'F'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15, 18, 21, 24),
+ self: b(24),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#13', // A, *, F.
+ '#43' // B, *, F.
+ ],
+ [
+ '#46', // *, C, F.
+ '#52' // *, D, F.
+ ],
+ [
+ { // *, *, F -> E.
+ title: [undefined, undefined, 'E'],
+ values: [
+ {
+ total: b(0, 3, 6, 9, 12, 15, 18, 21, 24),
+ self: b(24),
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ '#14', // A, *, F -> E.
+ '#44' // B, *, F -> E.
+ ],
+ [
+ '#47', // *, C, F -> E.
+ '#53' // *, D, F -> E.
+ ],
+ []
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ });
+
+ builderTest('twoDimensionsComplete_totalCalculation', 2 /* dimensions */,
+ 1 /* valueCount */,
+ true /* setComplete */,
+ [
+ { path: [[], []], values: [10], kind: SELF },
+ { path: [['A'], []], values: [21], kind: SELF },
+ { path: [['A'], []], values: [30], kind: TOTAL },
+ { path: [['B'], []], values: [25], kind: SELF },
+ { path: [['B'], []], values: [32], kind: TOTAL },
+ { path: [[], ['1']], values: [3], kind: SELF },
+ { path: [[], ['1']], values: [15], kind: TOTAL },
+ { path: [[], ['2']], values: [40], kind: SELF },
+ { path: [[], ['2']], values: [41], kind: TOTAL }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 115,
+ self: 10,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 30,
+ self: 21,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 32,
+ self: 25,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 15,
+ self: 3,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 41,
+ self: 40,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 115,
+ self: 10,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 30,
+ self: 21,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 32,
+ self: 25,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 15,
+ self: 3,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 41,
+ self: 40,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined],
+ values: [
+ {
+ total: 115,
+ self: 10,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [
+ { // A, *.
+ title: ['A', undefined],
+ values: [
+ {
+ total: 30,
+ self: 21,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // B, *.
+ title: ['B', undefined],
+ values: [
+ {
+ total: 32,
+ self: 25,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1.
+ title: [undefined, '1'],
+ values: [
+ {
+ total: 15,
+ self: 3,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ },
+ { // *, 2.
+ title: [undefined, '2'],
+ values: [
+ {
+ total: 41,
+ self: 40,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ });
+
+
+ builderTest('threeDimensions_twoValues', 3 /* dimensions */,
+ 2 /* valueCount */,
+ false /* setComplete */,
+ [
+ { path: [[], [], []], values: [1, 89], kind: SELF },
+ { path: [['A'], [], []], values: [2, 34], kind: SELF },
+ { path: [['A'], [], []], values: [4, 55], kind: TOTAL },
+ { path: [['B'], [], []], values: [undefined, 13], kind: SELF },
+ { path: [['B'], [], []], values: [16, 21], kind: TOTAL },
+ { path: [[], ['1'], []], values: [32, undefined], kind: SELF },
+ { path: [[], ['1'], []], values: [64, 8], kind: TOTAL },
+ { path: [[], ['2'], []], values: [128, 2], kind: SELF },
+ { path: [[], ['2'], []], values: [undefined, undefined], kind: TOTAL },
+ { path: [[], [], ['%']], values: [undefined, 1], kind: TOTAL },
+ { path: [[], [], ['$']], values: [1024, undefined], kind: TOTAL },
+ { path: [[], [], ['@']], values: [undefined, undefined], kind: SELF }
+ ],
+ { // Top-down tree view.
+ title: [undefined, undefined, undefined],
+ values: [
+ {
+ total: (1 + 2 + 32 + 128) /* selfSum([[], [], []], 0) */ +
+ 1024 /* minResidual([[], [], []], 0) */,
+ self: 1,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: (89 + 34 + 13 + 2) /* selfSum([[], [], []], 1) */ +
+ ((55 - 34) + (21 - 13)) /* maxResidual([[], [], []], 1) */,
+ self: 89,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: 4,
+ self: 2,
+ totalState: EXACT
+ },
+ {
+ total: 55,
+ self: 34,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: 16,
+ self: 0,
+ totalState: EXACT
+ },
+ {
+ total: 21,
+ self: 13,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1, *.
+ title: [undefined, '1', undefined],
+ values: [
+ {
+ total: 64,
+ self: 32,
+ totalState: EXACT
+ },
+ {
+ total: 8,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, 2, *.
+ title: [undefined, '2', undefined],
+ values: [
+ {
+ total: 128,
+ self: 128,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, *, %.
+ title: [undefined, undefined, '%'],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ },
+ {
+ total: 1,
+ self: 0,
+ totalState: EXACT
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, *, $.
+ title: [undefined, undefined, '$'],
+ values: [
+ {
+ total: 1024,
+ self: 0,
+ totalState: EXACT
+ },
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, *, @.
+ title: [undefined, undefined, '@'],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ },
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Top-down heavy view.
+ title: [undefined, undefined, undefined],
+ values: [
+ {
+ total: 1187,
+ self: 1,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 167,
+ self: 89,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: 4,
+ self: 2,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 55,
+ self: 34,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: 16,
+ self: 0,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 21,
+ self: 13,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1, *.
+ title: [undefined, '1', undefined],
+ values: [
+ {
+ total: 64,
+ self: 32,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 8,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, 2, *.
+ title: [undefined, '2', undefined],
+ values: [
+ {
+ total: 128,
+ self: 128,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, *, %.
+ title: [undefined, undefined, '%'],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ },
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, *, $.
+ title: [undefined, undefined, '$'],
+ values: [
+ {
+ total: 1024,
+ self: 0,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, *, @.
+ title: [undefined, undefined, '@'],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ },
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ },
+ { // Bottom-up heavy view.
+ title: [undefined, undefined, undefined],
+ values: [
+ {
+ total: 1187,
+ self: 1,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 167,
+ self: 89,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [
+ { // A, *, *.
+ title: ['A', undefined, undefined],
+ values: [
+ {
+ total: 4,
+ self: 2,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 55,
+ self: 34,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // B, *, *.
+ title: ['B', undefined, undefined],
+ values: [
+ {
+ total: 16,
+ self: 0,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 21,
+ self: 13,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, 1, *.
+ title: [undefined, '1', undefined],
+ values: [
+ {
+ total: 64,
+ self: 32,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 8,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, 2, *.
+ title: [undefined, '2', undefined],
+ values: [
+ {
+ total: 128,
+ self: 128,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 2,
+ self: 2,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ],
+ [
+ { // *, *, %.
+ title: [undefined, undefined, '%'],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ },
+ {
+ total: 1,
+ self: 0,
+ totalState: LOWER_BOUND
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, *, $.
+ title: [undefined, undefined, '$'],
+ values: [
+ {
+ total: 1024,
+ self: 0,
+ totalState: LOWER_BOUND
+ },
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ },
+ { // *, *, @.
+ title: [undefined, undefined, '@'],
+ values: [
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ },
+ {
+ total: 0,
+ self: 0,
+ totalState: NOT_PROVIDED
+ }
+ ],
+ children: [
+ [],
+ [],
+ []
+ ]
+ }
+ ]
+ ]
+ });
+
+ test('recursionDepthTracker', function() {
+ const MAX_DEPTH = 5;
+ const tracker = new RecursionDepthTracker(MAX_DEPTH, 2 /* dimension */);
+
+ function pushNewNode(title) {
+ const node = new MultiDimensionalViewNode(
+ [undefined, 'ignored dimension', title, 'also ignored'],
+ 1 /* valueCount (not relevant for this test) */);
+ tracker.push(node);
+ return node;
+ }
+
+ function checkTracker(expectedDefinedViewNodePath, expectedRecursionDepth) {
+ const expectedBottomIndex = MAX_DEPTH -
+ expectedDefinedViewNodePath.length;
+ assert.strictEqual(tracker.bottomIndex, expectedBottomIndex);
+ assert.strictEqual(tracker.topIndex, MAX_DEPTH);
+
+ const undefinedPadding = new Array(expectedBottomIndex);
+ const expectedViewNodePath =
+ undefinedPadding.concat(expectedDefinedViewNodePath);
+ const expectedTitlePath =
+ undefinedPadding.concat(expectedDefinedViewNodePath.map(
+ function(node) { return node.title[2]; }));
+ assertListStrictEqual(tracker.viewNodePath, expectedViewNodePath);
+ assertListStrictEqual(tracker.titlePath, expectedTitlePath);
+
+ assert.strictEqual(tracker.recursionDepth, expectedRecursionDepth);
+ }
+
+ checkTracker([] /* empty stack */, 0);
+ const a1 = pushNewNode('A');
+ checkTracker([a1], 0);
+ const b1 = pushNewNode('B');
+ checkTracker([b1, a1], 0);
+ const c1 = pushNewNode('C');
+ checkTracker([c1, b1, a1], 0);
+ const d1 = pushNewNode('D');
+ checkTracker([d1, c1, b1, a1], 0);
+ tracker.pop();
+ checkTracker([c1, b1, a1], 0);
+ const a2 = pushNewNode('A');
+ checkTracker([a2, c1, b1, a1], 1);
+ const b2 = pushNewNode('B');
+ checkTracker([b2, a2, c1, b1, a1], 2);
+ tracker.pop();
+ checkTracker([a2, c1, b1, a1], 1);
+ tracker.pop();
+ checkTracker([c1, b1, a1], 0);
+ tracker.push(b2);
+ checkTracker([b2, c1, b1, a1], 1);
+ tracker.pop();
+ checkTracker([c1, b1, a1], 0);
+ tracker.pop();
+ checkTracker([b1, a1], 0);
+ tracker.pop();
+ checkTracker([a1], 0);
+ const a3 = pushNewNode('A');
+ checkTracker([a3, a1], 1);
+ tracker.push(a2);
+ checkTracker([a2, a3, a1], 2);
+ const a4 = pushNewNode('A');
+ checkTracker([a4, a2, a3, a1], 3);
+ tracker.pop();
+ checkTracker([a2, a3, a1], 2);
+ const b3 = pushNewNode('B');
+ checkTracker([b3, a2, a3, a1], 0);
+ tracker.push(a4);
+ checkTracker([a4, b3, a2, a3, a1], 1);
+ tracker.pop();
+ checkTracker([b3, a2, a3, a1], 0);
+ tracker.pop();
+ checkTracker([a2, a3, a1], 2);
+ tracker.pop();
+ checkTracker([a3, a1], 1);
+ tracker.pop();
+ checkTracker([a1], 0);
+ tracker.pop();
+ checkTracker([], 0);
+ tracker.push(a4);
+ checkTracker([a4], 0);
+ tracker.push(b1);
+ checkTracker([b1, a4], 0);
+ tracker.push(a1);
+ checkTracker([a1, b1, a4], 1);
+ tracker.pop();
+ checkTracker([b1, a4], 0);
+ const c2 = pushNewNode('C');
+ checkTracker([c2, b1, a4], 0);
+ tracker.push(a3);
+ checkTracker([a3, c2, b1, a4], 1);
+ tracker.pop();
+ checkTracker([c2, b1, a4], 0);
+ tracker.pop();
+ checkTracker([b1, a4], 0);
+ tracker.pop();
+ checkTracker([a4], 0);
+ tracker.pop();
+ checkTracker([], 0);
+
+ assert.throws(function() {
+ // Try popping from an empty tracker.
+ tracker.pop();
+ });
+
+ pushNewNode('F');
+ pushNewNode('U');
+ pushNewNode('L');
+ pushNewNode('L');
+ pushNewNode('!');
+ assert.throws(function() {
+ // Try pushing to a full tracker.
+ pushNewNode(':-(');
+ });
+ });
+
+ test('zFunction', function() {
+ // Empty list/string (suffix).
+ assert.deepEqual(zFunction([], 0), []);
+ assert.deepEqual(zFunction(['A'], 1), []);
+ assert.deepEqual(zFunction(['A', 'B', 'C'], 3), []);
+ assert.deepEqual(zFunction('', 0), []);
+ assert.deepEqual(zFunction('A', 1), []);
+ assert.deepEqual(zFunction('ABC', 3), []);
+
+ // Singleton list/string.
+ checkZFunction([1], [0]);
+ checkZFunction('T', [0]);
+
+ // No duplicate elements.
+ checkZFunction([1, 2, 3, 4, 5], [0, 0, 0, 0, 0]);
+ checkZFunction('ABCDEF', [0, 0, 0, 0, 0, 0]);
+
+ // No substring is a suffix.
+ checkZFunction([1, 2, 3, 2], [0, 0, 0, 0]);
+ checkZFunction('ABBB', [0, 0, 0, 0]);
+
+ // Pure repetition.
+ checkZFunction([1, 1, 1, 1, 1], [0, 4, 3, 2, 1]);
+ checkZFunction('AAAAA', [0, 4, 3, 2, 1]);
+
+ // Interleaved repetition.
+ checkZFunction([1, 2, 1, 3, 1, 2, 1], [0, 0, 1, 0, 3, 0, 1]);
+ checkZFunction('AAABAAB', [0, 2, 1, 0, 2, 1, 0]);
+
+ // Complex patterns.
+ checkZFunction([7, 9, 7, 9, 7, 9, 7, 9], [0, 0, 6, 0, 4, 0, 2, 0]);
+ checkZFunction('CCGTCCCGTACC', [0, 1, 0, 0, 2, 4, 1, 0, 0, 0, 2, 1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/raf.html b/chromium/third_party/catapult/tracing/tracing/base/raf.html
new file mode 100644
index 00000000000..726a211be1f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/raf.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+tr.exportTo('tr.b', function() {
+ const ESTIMATED_IDLE_PERIOD_LENGTH_MILLISECONDS = 10;
+ // The maximum amount of time that we allow for a task to get scheduled
+ // in idle time before forcing the task to run.
+ const REQUEST_IDLE_CALLBACK_TIMEOUT_MILLISECONDS = 100;
+
+ // Setting this to true will cause stack traces to get dumped into the
+ // tasks. When an exception happens the original stack will be printed.
+ //
+ // NOTE: This should never be set committed as true.
+ const recordRAFStacks = false;
+
+ let pendingPreAFs = [];
+ let pendingRAFs = [];
+ const pendingIdleCallbacks = [];
+ let currentRAFDispatchList = undefined;
+
+ let rafScheduled = false;
+ let idleWorkScheduled = false;
+
+ function scheduleRAF() {
+ if (rafScheduled) return;
+ rafScheduled = true;
+ if (tr.isHeadless) {
+ Promise.resolve().then(function() {
+ processRequests(false, 0);
+ }, function(e) {
+ throw e;
+ });
+ } else {
+ if (window.requestAnimationFrame) {
+ window.requestAnimationFrame(processRequests.bind(this, false));
+ } else {
+ const delta = Date.now() - window.performance.now();
+ window.webkitRequestAnimationFrame(function(domTimeStamp) {
+ processRequests(false, domTimeStamp - delta);
+ });
+ }
+ }
+ }
+
+ function nativeRequestIdleCallbackSupported() {
+ return !tr.isHeadless && window.requestIdleCallback;
+ }
+
+ function scheduleIdleWork() {
+ if (idleWorkScheduled) return;
+ if (!nativeRequestIdleCallbackSupported()) {
+ scheduleRAF();
+ return;
+ }
+ idleWorkScheduled = true;
+ window.requestIdleCallback(function(deadline, didTimeout) {
+ processIdleWork(false /* forceAllTasksToRun */, deadline);
+ }, { timeout: REQUEST_IDLE_CALLBACK_TIMEOUT_MILLISECONDS });
+ }
+
+ function onAnimationFrameError(e, opt_stack) {
+ console.log(e.stack);
+ if (tr.isHeadless) throw e;
+
+ if (opt_stack) console.log(opt_stack);
+
+ if (e.message) {
+ console.error(e.message, e.stack);
+ } else {
+ console.error(e);
+ }
+ }
+
+ function runTask(task, frameBeginTime) {
+ try {
+ task.callback.call(task.context, frameBeginTime);
+ } catch (e) {
+ tr.b.onAnimationFrameError(e, task.stack);
+ }
+ }
+
+ function processRequests(forceAllTasksToRun, frameBeginTime) {
+ rafScheduled = false;
+
+ const currentPreAFs = pendingPreAFs;
+ currentRAFDispatchList = pendingRAFs;
+ pendingPreAFs = [];
+ pendingRAFs = [];
+ const hasRAFTasks = currentPreAFs.length || currentRAFDispatchList.length;
+
+ for (let i = 0; i < currentPreAFs.length; i++) {
+ runTask(currentPreAFs[i], frameBeginTime);
+ }
+
+ while (currentRAFDispatchList.length > 0) {
+ runTask(currentRAFDispatchList.shift(), frameBeginTime);
+ }
+ currentRAFDispatchList = undefined;
+
+ if ((!hasRAFTasks && !nativeRequestIdleCallbackSupported()) ||
+ forceAllTasksToRun) {
+ // We assume that we want to do a fixed maximum amount of optional work
+ // per frame. Hopefully rAF will eventually pass this in for us.
+ const rafCompletionDeadline =
+ frameBeginTime + ESTIMATED_IDLE_PERIOD_LENGTH_MILLISECONDS;
+ processIdleWork(
+ forceAllTasksToRun, {
+ timeRemaining() {
+ return rafCompletionDeadline - window.performance.now();
+ }
+ }
+ );
+ }
+
+ if (pendingIdleCallbacks.length > 0) scheduleIdleWork();
+ }
+
+ function processIdleWork(forceAllTasksToRun, deadline) {
+ idleWorkScheduled = false;
+ while (pendingIdleCallbacks.length > 0) {
+ runTask(pendingIdleCallbacks.shift());
+ // Check timer after running at least one idle task to avoid buggy
+ // window.performance.now() on some platforms from blocking the idle
+ // task queue.
+ if (!forceAllTasksToRun &&
+ (tr.isHeadless || deadline.timeRemaining() <= 0)) {
+ break;
+ }
+ }
+
+ if (pendingIdleCallbacks.length > 0) scheduleIdleWork();
+ }
+
+ function getStack_() {
+ if (!recordRAFStacks) return '';
+
+ const stackLines = tr.b.stackTrace();
+ // Strip off getStack_.
+ stackLines.shift();
+ return stackLines.join('\n');
+ }
+
+ function requestPreAnimationFrame(callback, opt_this) {
+ pendingPreAFs.push({
+ callback,
+ context: opt_this || global,
+ stack: getStack_()});
+ scheduleRAF();
+ }
+
+ function requestAnimationFrameInThisFrameIfPossible(callback, opt_this) {
+ if (!currentRAFDispatchList) {
+ requestAnimationFrame(callback, opt_this);
+ return;
+ }
+ currentRAFDispatchList.push({
+ callback,
+ context: opt_this || global,
+ stack: getStack_()});
+ return;
+ }
+
+ function requestAnimationFrame(callback, opt_this) {
+ pendingRAFs.push({
+ callback,
+ context: opt_this || global,
+ stack: getStack_()});
+ scheduleRAF();
+ }
+
+ function animationFrame() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+ }
+
+ function requestIdleCallback(callback, opt_this) {
+ pendingIdleCallbacks.push({
+ callback,
+ context: opt_this || global,
+ stack: getStack_()});
+ scheduleIdleWork();
+ }
+
+ function forcePendingRAFTasksToRun(frameBeginTime) {
+ if (!rafScheduled) return;
+ processRequests(false, frameBeginTime);
+ }
+
+ function forceAllPendingTasksToRunForTest() {
+ if (!rafScheduled && !idleWorkScheduled) return;
+ processRequests(true, 0);
+ }
+
+ function timeout(ms) {
+ return new Promise(resolve => window.setTimeout(resolve, ms));
+ }
+
+ function idle() {
+ return new Promise(resolve => requestIdleCallback(resolve));
+ }
+
+ return {
+ animationFrame,
+ forceAllPendingTasksToRunForTest,
+ forcePendingRAFTasksToRun,
+ idle,
+ onAnimationFrameError,
+ requestAnimationFrame,
+ requestAnimationFrameInThisFrameIfPossible,
+ requestIdleCallback,
+ requestPreAnimationFrame,
+ timeout,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/raf_test.html b/chromium/third_party/catapult/tracing/tracing/base/raf_test.html
new file mode 100644
index 00000000000..d3f2b0fc5ee
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/raf_test.html
@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/raf.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ if (tr.isHeadless) return;
+
+ test('idleCallbackWorks', function() {
+ return new Promise(function(resolve, reject) {
+ tr.b.requestIdleCallback(resolve);
+ });
+ });
+
+ test('forceAllPendingTasksToRunForTest', function() {
+ let rafRan = false;
+ tr.b.requestAnimationFrame(function() {
+ rafRan = true;
+ });
+ let idleRan = false;
+ tr.b.requestIdleCallback(function() {
+ idleRan = true;
+ });
+ assert.isFalse(rafRan);
+ assert.isFalse(idleRan);
+ tr.b.forceAllPendingTasksToRunForTest();
+ assert.isTrue(rafRan);
+ assert.isTrue(idleRan);
+ });
+
+ test('forcePendingRAFTasksToRun', function() {
+ let rafRan = false;
+ tr.b.requestAnimationFrame(function() {
+ rafRan = true;
+ });
+ let idleRan = false;
+ tr.b.requestIdleCallback(function() {
+ idleRan = true;
+ });
+ tr.b.forcePendingRAFTasksToRun();
+ assert.isTrue(rafRan);
+ assert.isFalse(idleRan);
+ });
+
+ let fakeNow = undefined;
+ function withFakeWindowPerformanceNow(func) {
+ const oldNow = window.performance.now;
+ try {
+ window.performance.now = function() { return fakeNow; };
+ func();
+ } finally {
+ window.performance.now = oldNow;
+ }
+ }
+
+ // None of the following tests are relevant if the browser supports idle
+ // callbacks natively. Nevertheless, run them without native idle support to
+ // make sure the fallback keeps working.
+ function withoutNativeIdleCallbacks(func) {
+ const oldRIC = window.requestIdleCallback;
+ try {
+ window.requestIdleCallback = undefined;
+ func();
+ } finally {
+ window.requestIdleCallback = oldRIC;
+ }
+ }
+
+ function withMockedScheduling(func) {
+ withFakeWindowPerformanceNow(function() {
+ withoutNativeIdleCallbacks(func);
+ });
+ }
+
+ test('runIdleTaskWhileIdle', function() {
+ withMockedScheduling(function() {
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let rafRan = false;
+ tr.b.requestAnimationFrame(function() {
+ rafRan = true;
+ });
+ let idleRan = false;
+ tr.b.requestIdleCallback(function() {
+ idleRan = true;
+ });
+ fakeNow = 0;
+ tr.b.forcePendingRAFTasksToRun(fakeNow);
+ assert.isFalse(idleRan);
+ assert.isTrue(rafRan);
+ tr.b.forcePendingRAFTasksToRun(fakeNow);
+ assert.isTrue(idleRan);
+ });
+ });
+
+ test('twoShortIdleCallbacks', function() {
+ withMockedScheduling(function() {
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let idle1Ran = false;
+ let idle2Ran = false;
+ tr.b.requestIdleCallback(function() {
+ fakeNow += 1;
+ idle1Ran = true;
+ });
+ tr.b.requestIdleCallback(function() {
+ fakeNow += 1;
+ idle2Ran = true;
+ });
+ fakeNow = 0;
+ tr.b.forcePendingRAFTasksToRun(fakeNow);
+ assert.isTrue(idle1Ran);
+ assert.isTrue(idle2Ran);
+ });
+ });
+
+
+ test('oneLongOneShortIdleCallback', function() {
+ withMockedScheduling(function() {
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let idle1Ran = false;
+ let idle2Ran = false;
+ tr.b.requestIdleCallback(function() {
+ fakeNow += 100;
+ idle1Ran = true;
+ });
+ tr.b.requestIdleCallback(function() {
+ fakeNow += 1;
+ idle2Ran = true;
+ });
+ fakeNow = 0;
+ tr.b.forcePendingRAFTasksToRun(fakeNow);
+ assert.isTrue(idle1Ran);
+ assert.isFalse(idle2Ran);
+
+ // Reset idle1Ran to verify that it dosn't run again.
+ idle1Ran = false;
+
+ // Now run. idle2 should now run.
+ tr.b.forcePendingRAFTasksToRun(fakeNow);
+ assert.isFalse(idle1Ran);
+ assert.isTrue(idle2Ran);
+ });
+ });
+
+ test('buggyPerformanceNowDoesNotBlockIdleTasks', function() {
+ withMockedScheduling(function() {
+ tr.b.forcePendingRAFTasksToRun(); // Clear current RAF task queue.
+
+ let idle1Ran = false;
+ let idle2Ran = false;
+ tr.b.requestIdleCallback(function() {
+ fakeNow += 100;
+ idle1Ran = true;
+ });
+ tr.b.requestIdleCallback(function() {
+ fakeNow += 1;
+ idle2Ran = true;
+ });
+ fakeNow = 10000;
+ tr.b.forcePendingRAFTasksToRun(0);
+ assert.isTrue(idle1Ran);
+ assert.isFalse(idle2Ran);
+
+ // Reset idle1Ran to verify that it dosn't run again.
+ idle1Ran = false;
+
+ // Now run. idle2 should now run.
+ tr.b.forcePendingRAFTasksToRun(0);
+ assert.isFalse(idle1Ran);
+ assert.isTrue(idle2Ran);
+ });
+ });
+
+ function withFixedIdleTimeRemaining(idleTime, func) {
+ const oldRIC = window.requestIdleCallback;
+ try {
+ const pendingIdleCallbacks = [];
+ window.requestIdleCallback = function(callback) {
+ const deadline = {
+ timeRemaining() {
+ return idleTime;
+ }
+ };
+ pendingIdleCallbacks.push(function() {
+ callback(deadline, false /* didTimeout */);
+ });
+ };
+ func(pendingIdleCallbacks);
+ } finally {
+ window.requestIdleCallback = oldRIC;
+ }
+ }
+
+ test('idleCallbackWithIdletime', function() {
+ withFixedIdleTimeRemaining(1000, function(pendingIdleCallbacks) {
+ let idle1Ran = false;
+ let idle2Ran = false;
+ tr.b.requestIdleCallback(function() {
+ idle1Ran = true;
+ });
+ tr.b.requestIdleCallback(function() {
+ idle2Ran = true;
+ });
+ assert.lengthOf(pendingIdleCallbacks, 1);
+ pendingIdleCallbacks.shift()();
+
+ // Both callbacks should have run since there was idle time.
+ assert.isTrue(idle1Ran);
+ assert.isTrue(idle2Ran);
+ });
+ });
+
+ test('idleCallbackWithoutIdletime', function() {
+ withFixedIdleTimeRemaining(0, function(pendingIdleCallbacks) {
+ let idle1Ran = false;
+ let idle2Ran = false;
+ tr.b.requestIdleCallback(function() {
+ idle1Ran = true;
+ });
+ tr.b.requestIdleCallback(function() {
+ idle2Ran = true;
+ });
+ assert.lengthOf(pendingIdleCallbacks, 1);
+ pendingIdleCallbacks.shift()();
+
+ // Only the first idle callback should have run since there was no idle
+ // time left.
+ assert.isTrue(idle1Ran);
+ assert.isFalse(idle2Ran);
+
+ // Run the remaining idle task.
+ assert.lengthOf(pendingIdleCallbacks, 1);
+ pendingIdleCallbacks.shift()();
+ assert.isTrue(idle2Ran);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/scalar.html b/chromium/third_party/catapult/tracing/tracing/base/scalar.html
new file mode 100644
index 00000000000..4c91eb69dc2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/scalar.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ class Scalar {
+ constructor(unit, value) {
+ if (!(unit instanceof tr.b.Unit)) {
+ throw new Error('Expected Unit');
+ }
+
+ if (!(typeof(value) === 'number')) {
+ throw new Error('Expected value to be number');
+ }
+
+ this.unit = unit;
+ this.value = value;
+ }
+
+ asDict() {
+ return {
+ unit: this.unit.asJSON(),
+ value: tr.b.numberToJson(this.value),
+ };
+ }
+
+ toString() {
+ return this.unit.format(this.value);
+ }
+
+ static fromDict(d) {
+ return new Scalar(tr.b.Unit.fromJSON(d.unit),
+ tr.b.numberFromJson(d.value));
+ }
+ }
+
+ return {
+ Scalar,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/scalar_test.html b/chromium/third_party/catapult/tracing/tracing/base/scalar_test.html
new file mode 100644
index 00000000000..3bfd772732d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/scalar_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('nonUnitThrows', function() {
+ assert.throws(function() { new tr.b.Scalar('foo', -273.15); });
+ });
+
+ test('nonNumberScalarThrows', function() {
+ const unit = tr.b.Unit.byName.sizeInBytes;
+ assert.throws(function() { new tr.b.Scalar(unit, 'foo'); });
+ });
+
+ test('scalarBasic', function() {
+ const unit = tr.b.Unit.byName.sizeInBytes;
+
+ const d = {
+ unit: unit.asJSON(),
+ value: 42
+ };
+
+ assert.deepEqual(d, tr.b.Scalar.fromDict(d).asDict());
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/serializable.html b/chromium/third_party/catapult/tracing/tracing/base/serializable.html
new file mode 100644
index 00000000000..de8efe78c48
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/serializable.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ class Serializable {
+ constructor() {
+ Object.defineProperty(this, 'properties_', {
+ configurable: false,
+ enumerable: false,
+ value: new Map(),
+ });
+ }
+
+ /**
+ * @param {string} name
+ * @param {!Object} initialValue
+ */
+ define(name, initialValue) {
+ if (this[name] !== undefined) {
+ throw new Error(`"${name}" is already defined.`);
+ }
+ if (name[name.length - 1] === '_') {
+ throw new Error(`"${name}" cannot end with an underscore.`);
+ }
+
+ this.properties_.set(name, initialValue);
+
+ Object.defineProperty(this, name, {
+ configurable: false,
+ enumerable: true,
+ get: () => this.properties_.get(name),
+ set: value => this.setProperty_(name, value),
+ });
+ }
+
+ setProperty_(name, value) {
+ this.properties_.set(name, value);
+ }
+
+ clone() {
+ return Serializable.fromDict(this.asDict());
+ }
+
+ asDict() {
+ function visit(obj) {
+ if (obj instanceof Serializable) return obj.asDict();
+ if (obj instanceof Set) return Array.from(obj);
+ if (obj instanceof Array) return obj.map(visit);
+ if (!(obj instanceof Map)) return obj;
+
+ const result = {};
+ for (const [name, value] of obj) {
+ result[name] = visit(value);
+ }
+ return result;
+ }
+
+ const dict = {type: this.constructor.name};
+ for (const [name, value] of this.properties_) {
+ dict[name.replace(/_$/, '')] = visit(value);
+ }
+ return dict;
+ }
+
+ static fromDict(dict) {
+ function visit(d) {
+ if (d instanceof Array) return d.map(visit);
+ if (!(d instanceof Object)) return d;
+ if (typeof d.type === 'string') return Serializable.fromDict(d);
+
+ const result = new Map();
+ for (const [name, value] of Object.entries(d)) {
+ result.set(name, visit(value));
+ }
+ return result;
+ }
+
+ const typeInfo = Serializable.findTypeInfoWithName(dict.type);
+ const result = new typeInfo.constructor();
+ for (const [name, value] of Object.entries(dict)) {
+ result[name] = visit(value);
+ }
+ return result;
+ }
+ }
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.defaultMetadata = {};
+ options.mandatoryBaseClass = Serializable;
+ tr.b.decorateExtensionRegistry(Serializable, options);
+
+ return {
+ Serializable,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/serializable_test.html b/chromium/third_party/catapult/tracing/tracing/base/serializable_test.html
new file mode 100644
index 00000000000..502c1afcc11
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/serializable_test.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/serializable.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ class Stuff extends tr.b.Serializable {
+ constructor() {
+ super();
+ this.define('boole', false);
+ this.define('answer', 0);
+ this.define('hipparchus', new Map());
+ this.define('ary', []);
+ this.define('cantor', new Set());
+ }
+ }
+
+ tr.b.Serializable.register(Stuff);
+
+ test('serializable', function() {
+ const stuff = new Stuff();
+ stuff.boole = true;
+ stuff.answer = 42;
+ stuff.hipparchus.set('thing', new Stuff());
+ stuff.hipparchus.get('thing').answer = -1;
+ stuff.ary.push('holy');
+ stuff.cantor.add('handgrenade');
+
+ const clone = stuff.clone();
+ assert.isTrue(clone.boole);
+ assert.strictEqual(42, clone.answer);
+ assert.strictEqual(-1, clone.hipparchus.get('thing').answer);
+ assert.strictEqual('holy', tr.b.getOnlyElement(clone.ary));
+ assert.strictEqual('handgrenade', tr.b.getOnlyElement(clone.cantor));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/settings.html b/chromium/third_party/catapult/tracing/tracing/base/settings.html
new file mode 100644
index 00000000000..4a5d786edd2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/settings.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Settings object.
+ */
+tr.exportTo('tr.b', function() {
+ /**
+ * Settings is a simple wrapper around local storage, to make it easier
+ * to test classes that have settings.
+ *
+ * May be called as new tr.b.Settings() or simply tr.b.Settings()
+ * @constructor
+ */
+ function Settings() {
+ return Settings;
+ }
+
+ if (tr.b.unittest && tr.b.unittest.TestRunner) {
+ tr.b.unittest.TestRunner.addEventListener(
+ 'tr-unittest-will-run',
+ function() {
+ if (tr.isHeadless) {
+ Settings.setAlternativeStorageInstance(new HeadlessStorage());
+ } else {
+ Settings.setAlternativeStorageInstance(global.sessionStorage);
+ global.sessionStorage.clear();
+ }
+ });
+ }
+
+ function SessionSettings() {
+ return SessionSettings;
+ }
+
+ function AddStaticStorageFunctionsToClass_(inputClass, storage) {
+ inputClass.storage_ = storage;
+
+ /**
+ * Get the setting with the given name.
+ *
+ * @param {string} key The name of the setting.
+ * @param {string=} opt_default The default value to return if not set.
+ * @param {string=} opt_namespace If set, the setting name will be prefixed
+ * with this namespace, e.g. "categories.settingName". This is useful for
+ * a set of related settings.
+ */
+ inputClass.get = function(key, opt_default, opt_namespace) {
+ key = inputClass.namespace_(key, opt_namespace);
+ const rawVal = inputClass.storage_.getItem(key);
+ if (rawVal === null || rawVal === undefined) {
+ return opt_default;
+ }
+
+ // Old settings versions used to stringify objects instead of putting them
+ // into JSON. If those are encountered, parse will fail. In that case,
+ // "upgrade" the setting to the default value.
+ try {
+ return JSON.parse(rawVal).value;
+ } catch (e) {
+ inputClass.storage_.removeItem(key);
+ return opt_default;
+ }
+ };
+
+ /**
+ * Set the setting with the given name to the given value.
+ *
+ * @param {string} key The name of the setting.
+ * @param {string} value The value of the setting.
+ * @param {string=} opt_namespace If set, the setting name will be prefixed
+ * with this namespace, e.g. "categories.settingName". This is useful for
+ * a set of related settings.
+ */
+ inputClass.set = function(key, value, opt_namespace) {
+ if (value === undefined) {
+ throw new Error('Settings.set: value must not be undefined');
+ }
+ const v = JSON.stringify({value});
+ inputClass.storage_.setItem(
+ inputClass.namespace_(key, opt_namespace), v);
+ };
+
+ /**
+ * Return a list of all the keys, or all the keys in the given namespace
+ * if one is provided.
+ *
+ * @param {string=} opt_namespace If set, only return settings which
+ * begin with this prefix.
+ */
+ inputClass.keys = function(opt_namespace) {
+ const result = [];
+ opt_namespace = opt_namespace || '';
+ for (let i = 0; i < inputClass.storage_.length; i++) {
+ const key = inputClass.storage_.key(i);
+ if (inputClass.isnamespaced_(key, opt_namespace)) {
+ result.push(inputClass.unnamespace_(key, opt_namespace));
+ }
+ }
+ return result;
+ };
+
+ inputClass.isnamespaced_ = function(key, opt_namespace) {
+ return key.indexOf(inputClass.normalize_(opt_namespace)) === 0;
+ };
+
+ inputClass.namespace_ = function(key, opt_namespace) {
+ return inputClass.normalize_(opt_namespace) + key;
+ };
+
+ inputClass.unnamespace_ = function(key, opt_namespace) {
+ return key.replace(inputClass.normalize_(opt_namespace), '');
+ };
+
+ /**
+ * All settings are prefixed with a global namespace to avoid collisions.
+ * inputClass may also be namespaced with an additional prefix passed into
+ * the get, set, and keys methods in order to group related settings.
+ * This method makes sure the two namespaces are always set properly.
+ */
+ inputClass.normalize_ = function(opt_namespace) {
+ return inputClass.NAMESPACE + (opt_namespace ? opt_namespace + '.' : '');
+ };
+
+ inputClass.setAlternativeStorageInstance = function(instance) {
+ inputClass.storage_ = instance;
+ };
+
+ inputClass.getAlternativeStorageInstance = function() {
+ if (!tr.isHeadless && inputClass.storage_ === localStorage) {
+ return undefined;
+ }
+ return inputClass.storage_;
+ };
+
+ inputClass.NAMESPACE = 'trace-viewer';
+ }
+
+ function HeadlessStorage() {
+ this.length = 0;
+ this.hasItem_ = {};
+ this.items_ = {};
+ this.itemsAsArray_ = undefined;
+ }
+ HeadlessStorage.prototype = {
+ key(index) {
+ return this.itemsAsArray[index];
+ },
+
+ get itemsAsArray() {
+ if (this.itemsAsArray_ !== undefined) {
+ return this.itemsAsArray_;
+ }
+ const itemsAsArray = [];
+ for (const k in this.items_) {
+ itemsAsArray.push(k);
+ }
+ this.itemsAsArray_ = itemsAsArray;
+ return this.itemsAsArray_;
+ },
+
+ getItem(key) {
+ if (!this.hasItem_[key]) {
+ return null;
+ }
+ return this.items_[key];
+ },
+
+ removeItem(key) {
+ if (!this.hasItem_[key]) {
+ return;
+ }
+ const value = this.items_[key];
+ delete this.hasItem_[key];
+ delete this.items_[key];
+ this.length--;
+ this.itemsAsArray_ = undefined;
+ return value;
+ },
+
+ setItem(key, value) {
+ if (this.hasItem_[key]) {
+ this.items_[key] = value;
+ return;
+ }
+ this.items_[key] = value;
+ this.hasItem_[key] = true;
+ this.length++;
+ this.itemsAsArray_ = undefined;
+ return value;
+ }
+ };
+
+ if (tr.isHeadless) {
+ AddStaticStorageFunctionsToClass_(Settings, new HeadlessStorage());
+ AddStaticStorageFunctionsToClass_(SessionSettings, new HeadlessStorage());
+ } else {
+ AddStaticStorageFunctionsToClass_(Settings, localStorage);
+ AddStaticStorageFunctionsToClass_(SessionSettings, sessionStorage);
+ }
+
+ return {
+ Settings,
+ SessionSettings,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/settings_test.html b/chromium/third_party/catapult/tracing/tracing/base/settings_test.html
new file mode 100644
index 00000000000..5fb452f1816
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/settings_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/settings.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function assertSettingIs(expectedValue, key) {
+ assert.strictEqual(tr.b.Settings.get(key), expectedValue);
+ }
+
+ // Old settings versions used to stringify objects instead of putting them
+ // into JSON. This test makes sure that these old settings yield the default
+ // value instead of strings.
+ test('oldStyleSettingYieldsDefaultValue', function() {
+ const storage = tr.b.Settings.getAlternativeStorageInstance();
+ storage.setItem(tr.b.Settings.namespace_('key'), 'hello world');
+
+ assert.strictEqual(tr.b.Settings.get('key', 'value'), 'value');
+ });
+
+ test('setGetString', function() {
+ const settings = new tr.b.Settings();
+ settings.set('my_key', 'my_val');
+ assert.strictEqual(settings.get('my_key'), 'my_val');
+ // tr.b.Settings() is a singleton
+ assert.strictEqual(tr.b.Settings().get('my_key'), 'my_val');
+ });
+
+ test('setGetNumber', function() {
+ const settings = new tr.b.Settings();
+ settings.set('my_key', 5);
+ assertSettingIs(5, 'my_key');
+ });
+
+ test('setGetBool', function() {
+ const settings = new tr.b.Settings();
+ settings.set('my_key', false);
+ assertSettingIs(false, 'my_key');
+ });
+
+ test('setGetObject', function() {
+ const settings = new tr.b.Settings();
+ settings.set('my_key', {'hello': 5});
+ assert.deepEqual(settings.get('my_key'), {'hello': 5});
+ });
+
+ test('setInvalidObject', function() {
+ const settings = new tr.b.Settings();
+ const obj = {'hello': undefined};
+ obj.hello = obj;
+ assert.throws(function() {
+ settings.set('my_key', obj);
+ });
+ });
+
+ test('setUndefined', function() {
+ const settings = new tr.b.Settings();
+ assert.throws(function() {
+ settings.set('my_key', undefined);
+ });
+ });
+
+ test('getUnset', function() {
+ const settings = new tr.b.Settings();
+ // Undefined should be returned if value isn't set.
+ assertSettingIs(undefined, 'my_key');
+ });
+
+ test('getDefault', function() {
+ const settings = new tr.b.Settings();
+ // default_val should be returned if value isn't set.
+ assert.strictEqual(settings.get('my_key', 'default_val'), 'default_val');
+ });
+
+ test('setGetPrefix', function() {
+ const settings = new tr.b.Settings();
+ settings.set('key_a', 'foo', 'my_prefix');
+ assert.strictEqual(settings.get('key_a', undefined, 'my_prefix'), 'foo');
+ assert.strictEqual(settings.get('key_a', 'bar', 'my_prefix'), 'foo');
+ assert.isUndefined(settings.get('key_a'));
+ assert.strictEqual(settings.get('key_a', 'bar'), 'bar');
+ });
+
+ test('keys', function() {
+ const settings = new tr.b.Settings();
+ settings.set('key_a', 'foo');
+ settings.set('key_b', 'bar');
+ settings.set('key_c', 'baz');
+ assert.sameMembers(settings.keys(), ['key_a', 'key_b', 'key_c']);
+ });
+
+ test('keysPrefix', function() {
+ const settings = new tr.b.Settings();
+ settings.set('key_a', 'foo', 'prefix1');
+ settings.set('key_b', 'bar', 'prefix1');
+ settings.set('key_c', 'baz', 'prefix1');
+ settings.set('key_a', 'foo', 'prefix2');
+ settings.set('key_b', 'bar', 'prefix2');
+ settings.set('key_C', 'baz', 'prefix2');
+ assert.sameMembers(settings.keys('prefix1'), ['key_a', 'key_b', 'key_c']);
+ assert.sameMembers(settings.keys('prefix2'), ['key_C', 'key_a', 'key_b']);
+ assert.sameMembers(
+ settings.keys(),
+ ['prefix1.key_a', 'prefix1.key_b', 'prefix1.key_c',
+ 'prefix2.key_C', 'prefix2.key_a', 'prefix2.key_b']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator.html b/chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator.html
new file mode 100644
index 00000000000..dac1471f205
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ /**
+ * Generate pretty colors!
+ * http://basecase.org/env/on-rainbows
+ * https://mycarta.wordpress.com/2012/10/06/the-rainbow-is-deadlong-live-the-rainbow-part-3/
+ *
+ * Set brightness = 0 to always generate black.
+ * Set brightness = 2 to always generate white.
+ * Set brightness = 1 to generate saturated colors.
+ *
+ * @constructor
+ * @param {number=} opt_a alpha opacity in [0,1]
+ * @param {number=} opt_brightness in [0,2]
+ */
+ 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;
+ // Roughly correct for human perception.
+ // https://en.wikipedia.org/wiki/Luma_%28video%29
+ // Multiply by 2 to normalize all values to 0.5.
+ // (Halfway between black and white.)
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator_test.html b/chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator_test.html
new file mode 100644
index 00000000000..db3ed984994
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/sinebow_color_generator_test.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/sinebow_color_generator.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ test('SinebowColorGenerator', function() {
+ let generator = new tr.b.SinebowColorGenerator();
+ assert.strictEqual(
+ 'rgba(270,67,67,1)', generator.colorForKey('z').toString());
+ assert.strictEqual(
+ 'rgba(44,132,329,1)', generator.colorForKey('y').toString());
+ assert.strictEqual(
+ 'rgba(99,166,9,1)', generator.colorForKey('x').toString());
+ assert.strictEqual(
+ 'rgba(270,67,67,1)', generator.colorForKey('z').toString());
+
+ generator = new tr.b.SinebowColorGenerator(0.5);
+ assert.strictEqual(
+ 'rgba(270,67,67,0.5)', generator.colorForKey('z').toString());
+ assert.strictEqual(
+ 'rgba(44,132,329,0.5)', generator.colorForKey('y').toString());
+ assert.strictEqual(
+ 'rgba(99,166,9,0.5)', generator.colorForKey('x').toString());
+ assert.strictEqual(
+ 'rgba(270,67,67,0.5)', generator.colorForKey('z').toString());
+
+ generator = new tr.b.SinebowColorGenerator(1, 0);
+ assert.strictEqual(
+ 'rgba(0,0,0,1)', generator.colorForKey('z').toString());
+ assert.strictEqual(
+ 'rgba(0,0,0,1)', generator.colorForKey('y').toString());
+ assert.strictEqual(
+ 'rgba(0,0,0,1)', generator.colorForKey('x').toString());
+ assert.strictEqual(
+ 'rgba(0,0,0,1)', generator.colorForKey('z').toString());
+
+ generator = new tr.b.SinebowColorGenerator(1, 2);
+ assert.strictEqual(
+ 'rgba(255,255,255,1)', generator.colorForKey('z').toString());
+ assert.strictEqual(
+ 'rgba(255,255,255,1)', generator.colorForKey('y').toString());
+ assert.strictEqual(
+ 'rgba(255,255,255,1)', generator.colorForKey('x').toString());
+ assert.strictEqual(
+ 'rgba(255,255,255,1)', generator.colorForKey('z').toString());
+ });
+
+ test('SinebowColorGeneratorVisual', function() {
+ if (tr.isHeadless) return;
+
+ const generator = new tr.b.SinebowColorGenerator();
+ const parentDiv = document.createElement('div');
+ this.addHTMLOutput(parentDiv);
+
+ for (let i = 0; i < 350; i++) {
+ const spanElem = document.createElement('span');
+ spanElem.style.width = '50px';
+ spanElem.style.display = 'inline-block';
+ spanElem.style.backgroundColor = generator.colorForKey(i).toString();
+ const rgb = generator.colorForKey(i).toString().split('(')[1].split(',');
+ const r = rgb[0];
+ const g = rgb[1];
+ const b = rgb[2].split(')')[0];
+ const hsl = new tr.b.Color(r, g, b, 1).toHSL();
+ spanElem.appendChild(document.createTextNode(hsl.h.toFixed(3)));
+
+ parentDiv.appendChild(spanElem);
+ if (i % 21 === 20) {
+ parentDiv.appendChild(document.createElement('br'));
+ }
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/task.html b/chromium/third_party/catapult/tracing/tracing/base/task.html
new file mode 100644
index 00000000000..60ff686e898
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/task.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/timing.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const Timing = tr.b.Timing;
+ /**
+ * A task is a combination of a run callback, a set of subtasks, and an after
+ * task.
+ *
+ * When executed, a task does the following things:
+ * 1. Runs its callback
+ * 2. Runs its subtasks
+ * 3. Runs its after callback.
+ *
+ * The list of subtasks and after task can be mutated inside step #1 but as
+ * soon as the task's callback returns, the subtask list and after task is
+ * fixed and cannot be changed again.
+ *
+ * Use task.after().after().after() to describe the toplevel passes that make
+ * up your computation. Then, use subTasks to add detail to each subtask as it
+ * runs. For example:
+ * var pieces = [];
+ * taskA = new Task(function() { pieces = getPieces(); });
+ * taskA.after(function(taskA) {
+ * pieces.forEach(function(piece) {
+ * taskA.subTask(function(taskB) { piece.process(); }, this);
+ * });
+ * });
+ *
+ * @constructor
+ */
+ function Task(runCb, thisArg) {
+ if (runCb !== undefined && thisArg === undefined &&
+ runCb.prototype !== undefined) {
+ throw new Error('Almost certainly you meant to pass a bound callback ' +
+ 'or thisArg.');
+ }
+ this.runCb_ = runCb;
+ this.thisArg_ = thisArg;
+ this.afterTask_ = undefined;
+ this.subTasks_ = [];
+ this.updatesUi_ = false;
+ }
+
+ Task.prototype = {
+ get name() {
+ return this.runCb_.name;
+ },
+
+ /** Sets a hint for whether or not this task updates the UI. */
+ set updatesUi(value) {
+ this.updatesUi_ = value;
+ },
+
+ /*
+ * See constructor documentation on semantics of subtasks.
+ */
+ subTask(cb, thisArg) {
+ if (cb instanceof Task) {
+ this.subTasks_.push(cb);
+ } else {
+ this.subTasks_.push(new Task(cb, thisArg));
+ }
+ return this.subTasks_[this.subTasks_.length - 1];
+ },
+
+ /**
+ * Runs the current task and returns the task that should be executed next.
+ */
+ run() {
+ if (this.runCb_ !== undefined) this.runCb_.call(this.thisArg_, this);
+ const subTasks = this.subTasks_;
+ this.subTasks_ = undefined; // Prevent more subTasks from being posted.
+
+ if (!subTasks.length) return this.afterTask_;
+
+ // If there are subtasks, then we want to execute all the subtasks and
+ // then this task's afterTask. To make this happen, we update the
+ // afterTask of all the subtasks so the point upward to each other, e.g.
+ // subTask[0].afterTask to subTask[1] and so on. Then, the last subTask's
+ // afterTask points at this task's afterTask.
+ for (let i = 1; i < subTasks.length; i++) {
+ subTasks[i - 1].afterTask_ = subTasks[i];
+ }
+ subTasks[subTasks.length - 1].afterTask_ = this.afterTask_;
+ return subTasks[0];
+ },
+
+ /*
+ * See constructor documentation on semantics of after tasks.
+ */
+ after(cb, thisArg) {
+ if (this.afterTask_) {
+ throw new Error('Has an after task already');
+ }
+ if (cb instanceof Task) {
+ this.afterTask_ = cb;
+ } else {
+ this.afterTask_ = new Task(cb, thisArg);
+ }
+ return this.afterTask_;
+ },
+
+ /*
+ * Adds a task after the chain of tasks.
+ */
+ enqueue(cb, thisArg) {
+ if (!this.afterTask_) return this.after(cb, thisArg);
+ return this.afterTask_.enqueue(cb, thisArg);
+ }
+ };
+
+ Task.RunSynchronously = function(task) {
+ let curTask = task;
+ while (curTask) {
+ curTask = curTask.run();
+ }
+ };
+
+ /**
+ * Runs a task using raf.requestIdleCallback, returning
+ * a promise for its completion.
+ */
+ Task.RunWhenIdle = function(task) {
+ return new Promise(function(resolve, reject) {
+ let curTask = task;
+ function runAnother() {
+ try {
+ curTask = curTask.run();
+ } catch (e) {
+ reject(e);
+ return;
+ }
+
+ if (curTask) {
+ if (curTask.updatesUi_) {
+ tr.b.requestAnimationFrameInThisFrameIfPossible(runAnother);
+ } else {
+ tr.b.requestIdleCallback(runAnother);
+ }
+ return;
+ }
+
+ resolve();
+ }
+ tr.b.requestIdleCallback(runAnother);
+ });
+ };
+
+ return {
+ Task,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/task_test.html b/chromium/third_party/catapult/tracing/tracing/base/task_test.html
new file mode 100644
index 00000000000..7f32ccf30dc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/task_test.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/task.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Task = tr.b.Task;
+
+ test('basicAllStepsPass', function() {
+ const results = [];
+
+ const startingTask = new Task(function(task) {
+ results.push('a');
+ task.subTask(function() {
+ results.push('a/1');
+ }, this);
+ task.subTask(function() {
+ results.push('a/2');
+ }, this);
+ }, this);
+ startingTask.after(function() {
+ results.push('b');
+ }, this).after(function() {
+ results.push('c');
+ }, this);
+
+ Task.RunSynchronously(startingTask);
+ assert.deepEqual(results, ['a', 'a/1', 'a/2', 'b', 'c']);
+ });
+
+ test('basicAllStepsPassAsync', function() {
+ const results = [];
+
+ const startingTask = new Task(function(task) {
+ results.push('a');
+ task.subTask(function() {
+ results.push('a/1');
+ }, this);
+ task.subTask(function() {
+ results.push('a/2');
+ }, this);
+ }, this);
+ startingTask.after(function() {
+ results.push('b');
+ }, this).after(function() {
+ results.push('c');
+ }, this);
+
+ const promise = Task.RunWhenIdle(startingTask);
+ promise.then(function() {
+ assert.deepEqual(results, ['a', 'a/1', 'a/2', 'b', 'c']);
+ });
+ return promise;
+ });
+
+ test('taskThatThrowsShouldRejectItsPromise', function() {
+ const startingTask = new Task(function(task) {
+ throw new Error(
+ 'IGNORE. This is an expected error to test error handling.');
+ }, this);
+
+ const taskPromise = Task.RunWhenIdle(startingTask);
+
+ return new Promise(function(resolve, reject) {
+ taskPromise.then(function() {
+ reject(new Error('Should have thrown'));
+ }, function(err) {
+ resolve();
+ });
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/time_display_modes.html b/chromium/third_party/catapult/tracing/tracing/base/time_display_modes.html
new file mode 100644
index 00000000000..09b34030745
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/time_display_modes.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit_scale.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Time currentDisplayUnit
+ */
+tr.exportTo('tr.b', function() {
+ const msDisplayMode = {
+ scale: 1e-3,
+ suffix: 'ms',
+ // Compares a < b with adjustments to precision errors.
+ roundedLess(a, b) {
+ return Math.round(a * 1000) < Math.round(b * 1000);
+ },
+ formatSpec: {
+ unitScale: [tr.b.UnitScale.TIME.MILLI_SEC],
+ minimumFractionDigits: 3,
+ }
+ };
+
+ const nsDisplayMode = {
+ scale: 1e-9,
+ suffix: 'ns',
+ // Compares a < b with adjustments to precision errors.
+ roundedLess(a, b) {
+ return Math.round(a * 1000000) < Math.round(b * 1000000);
+ },
+ formatSpec: {
+ unitScale: [tr.b.UnitScale.TIME.NANO_SEC],
+ maximumFractionDigits: 0
+ }
+ };
+
+ const TimeDisplayModes = {
+ ns: nsDisplayMode,
+ ms: msDisplayMode
+ };
+
+ return {
+ TimeDisplayModes,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/timing.html b/chromium/third_party/catapult/tracing/tracing/base/timing.html
new file mode 100644
index 00000000000..501abf96856
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/timing.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ class Mark {
+ constructor(groupName, functionName, opt_timestamp) {
+ if (tr.isHeadless) return;
+
+ this.groupName_ = groupName;
+ this.functionName_ = functionName;
+ const guid = tr.b.GUID.allocateSimple();
+ this.measureName_ = `${groupName} ${functionName}`;
+ if (opt_timestamp) {
+ this.startMark_ = {startTime: opt_timestamp};
+ } else {
+ this.startMarkName_ = `${this.measureName} ${guid} start`;
+ }
+ this.endMark_ = undefined;
+ this.endMarkName_ = `${this.measureName} ${guid} end`;
+
+ window.performance.mark(this.startMarkName_);
+ }
+
+ get groupName() {
+ return this.groupName_;
+ }
+
+ get functionName() {
+ return this.functionName_;
+ }
+
+ get measureName() {
+ return this.measureName_;
+ }
+
+ get startMark() {
+ return this.startMark_ || tr.b.getOnlyElement(
+ window.performance.getEntriesByName(this.startMarkName_));
+ }
+
+ get endMark() {
+ return this.endMark_ || tr.b.getOnlyElement(
+ window.performance.getEntriesByName(this.endMarkName_));
+ }
+
+ get durationMs() {
+ // There may be many measures named `this.measureName`, but the start and
+ // end mark names contain a GUID so they are unique.
+ return this.endMark.startTime - this.startMark.startTime;
+ }
+
+ end(opt_timestamp) {
+ if (tr.isHeadless) return;
+
+ if (opt_timestamp) {
+ this.endMark_ = {startTime: opt_timestamp};
+ } else {
+ window.performance.mark(this.endMarkName_);
+ }
+
+ if (!this.startMark_ && !this.endMark_) {
+ window.performance.measure(
+ this.measureName_, this.startMarkName_, this.endMarkName_);
+ } else if (Timing.logVoidMarks && !(window.ga instanceof Function)) {
+ // eslint-disable-next-line no-console
+ console.log('void mark',
+ this.groupName, this.functionName, this.durationMs);
+ }
+
+ if (!(window.ga instanceof Function)) return;
+ // Google Analytics
+ ga('send', {
+ hitType: 'event',
+ eventCategory: this.groupName,
+ eventAction: this.functionName,
+ eventValue: this.durationMs,
+ });
+ }
+ }
+
+ class Timing {
+ static mark(groupName, functionName, opt_timestamp) {
+ return new Mark(groupName, functionName, opt_timestamp);
+ }
+
+ static instant(groupName, functionName, opt_value) {
+ const valueString = opt_value === undefined ? '' : ' ' + opt_value;
+
+ /* eslint-disable no-console */
+ if (console && console.timeStamp) {
+ console.timeStamp(`${groupName} ${functionName}${valueString}`);
+ }
+ /* eslint-enable no-console */
+
+ // Google Analytics
+ if (window && window.ga instanceof Function) {
+ ga('send', {
+ hitType: 'event',
+ eventCategory: groupName,
+ eventAction: functionName,
+ eventValue: opt_value,
+ });
+ }
+ }
+
+ static getCurrentTimeMs() {
+ try {
+ return performance.now();
+ } catch (error) {}
+ return 0;
+ }
+ }
+
+ Timing.logVoidMarks = false;
+
+ return {
+ Timing,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/timing_test.html b/chromium/third_party/catapult/tracing/tracing/base/timing_test.html
new file mode 100644
index 00000000000..82cb798af66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/timing_test.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ test('mark', function() {
+ if (tr.isHeadless) return;
+
+ const markedTime = tr.b.Timing.mark('timing_test', 'mark');
+ markedTime.end();
+ const result = window.performance.getEntriesByName('timing_test mark');
+ const duration = parseFloat(result[0].duration);
+ assert.isTrue(duration >= 0.0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/trace_stream.html b/chromium/third_party/catapult/tracing/tracing/base/trace_stream.html
new file mode 100644
index 00000000000..3d4452f1b10
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/trace_stream.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ /**
+ * A TraceStream is a data structure holding trace data that supports
+ * sequentially reading data, efficiently. It also supports rewinding to the
+ * beginning of the trace data and building a sub-stream; but, the latter two
+ * operations do not have to be very efficient. For example, it is OK that an
+ * implementation sends a new XHR to get the trace data again when rewinding
+ * or creating a sub-stream from a large stream that does not fit in memory.
+ */
+ class TraceStream {
+ static get HEADER_SIZE() {
+ return Math.pow(2, 10);
+ }
+
+ static get CHUNK_SIZE() {
+ return Math.pow(2, 20);
+ }
+
+ get isBinary() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * @returns {boolean} true if there is data remaining in the stream to read,
+ * without rewinding.
+ */
+ get hasData() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * @returns {string} The first chunk of the stream. The header size is
+ * either given in the constructor or the default 1KB is used.
+ */
+ get header() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * Reads until a given character, including the given character.
+ *
+ * @param {!string} delim A string of size exactly one.
+ *
+ * @returns {!string} The data until the delimiter as a string. If the
+ * delimiter is not found, all of the remaining data is returned.
+ */
+ readUntilDelimiter(delim) {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * Reads a specific number of bytes from the stream.
+ *
+ * @param {number=} opt_size The maximum number of bytes to be read from the
+ * stream. If unspecified, at most 1MB will be read.
+ *
+ * @returns {!string} A string of size opt_size, unless there is not enough
+ * data in the stream in which the string will be smaller.
+ */
+ readNumBytes(opt_size) {
+ throw new Error('Not implemented');
+ }
+
+ rewind() {
+ throw new Error('Not implemented');
+ }
+
+ /**
+ * Returns a new stream, created from a subset of this stream. This is
+ * needed to support importing subtraces. For example, when the importer
+ * encounters a trace event stream that has a BattOr subtrace in it, it will
+ * create a new stream for the BattOr subtrace and use the proper importer.
+ *
+ * @param {!number} startOffset The start offset of the new stream.
+ * @param {number=} opt_endOffset The end offset of the new stream
+ * (exclusive). If unspecified, the end offset of the current stream is
+ * used.
+ * @param {number=} opt_headerSize The header size of the new stream. If
+ * unspecified, the header size is assumed to be 1KB.
+ *
+ * @returns {!TraceStream} The new stream. The cursor of the new stream will
+ * be at the beginning.
+ */
+ substream(offset, opt_length, opt_headerSize) {
+ throw new Error('Not implemented');
+ }
+ }
+
+ return {
+ TraceStream,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unit.html b/chromium/third_party/catapult/tracing/tracing/base/unit.html
new file mode 100644
index 00000000000..90a5eba869b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unit.html
@@ -0,0 +1,531 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const TimeDisplayModes = tr.b.TimeDisplayModes;
+
+ const PLUS_MINUS_SIGN = String.fromCharCode(177);
+
+ const CACHED_FORMATTERS = {};
+ function getNumberFormatter(minSpec, maxSpec, minCtx, maxCtx) {
+ const key = minSpec + '-' + maxSpec + '-' + minCtx + '-' + maxCtx;
+ let formatter = CACHED_FORMATTERS[key];
+ if (formatter === undefined) {
+ let minimumFractionDigits = minCtx !== undefined ? minCtx : minSpec;
+ let maximumFractionDigits = maxCtx !== undefined ? maxCtx : maxSpec;
+
+ // If the context overrides only one of the two |*FractionDigits|
+ // properties and the other one is provided by the unit, we might need to
+ // shift the other property so that
+ // |minimumFractionDigits| <= |maximumFractionDigits|.
+ if (minimumFractionDigits > maximumFractionDigits) {
+ if (minCtx !== undefined && maxCtx === undefined) {
+ // Only minimumFractionDigits was overriden by context.
+ maximumFractionDigits = minimumFractionDigits;
+ } else if (minCtx === undefined && maxCtx !== undefined) {
+ // Only maximumFractionDigits was overriden by context.
+ minimumFractionDigits = maximumFractionDigits;
+ }
+ }
+
+ formatter = new Intl.NumberFormat(undefined, {
+ minimumFractionDigits,
+ maximumFractionDigits,
+ });
+
+ CACHED_FORMATTERS[key] = formatter;
+ }
+ return formatter;
+ }
+
+ function max(a, b) {
+ if (a === undefined) return b;
+ if (b === undefined) return a;
+ return a.scale > b.scale ? a : b;
+ }
+
+ /** @enum */
+ const ImprovementDirection = {
+ DONT_CARE: 0,
+ BIGGER_IS_BETTER: 1,
+ SMALLER_IS_BETTER: 2
+ };
+
+ /** @constructor */
+ 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;
+
+ // Example: powerInWattsDelta_biggerIsBetter -> powerInWatts.
+ this.baseUnit = undefined;
+
+ // Example: energyInJoules_smallerIsBetter ->
+ // energyInJoulesDelta_smallerIsBetter.
+ this.correspondingDeltaUnit = undefined;
+ }
+
+ Unit.prototype = {
+ asJSON() {
+ return this.jsonName;
+ },
+
+ /**
+ * Remove insignificant digits from a number so that it consumes less disk
+ * space when serialized.
+ *
+ * @param {number} value
+ * @return {number}
+ */
+ 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);
+ }
+
+ // If formatSpec is a function or uses a unitScale, then its formatting is
+ // unpredictable.
+ // Binary search to find the smallest number of decimal digits that
+ // preserves the correct formatted value.
+
+ 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) {
+ // TODO(aiolos): Switch all calls to format to use UnitScales instead
+ // of UnitPrefixScales. UnitPrefixeScales use in Unit is deprecated.
+ 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) {
+ // Unit has no conversion value(s). Ex: Watts, count.
+ 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);
+ }
+ return scale[0].symbol;
+ },
+
+ /**
+ * Returns a human readable string representation of the value passed.
+ *
+ * Example: .00023 formatted using the timeInMsAutoFormat Unit would return
+ * '230 ns' since the base unit scale is ms.
+ *
+ * @param {number} value - The value to be formatted.
+ * @param {Object} [opt_context] - Optional formatting parameters.
+ * @param {!tr.b.UnitScale=} [opt_context.unitScale] - A UnitScale to use
+ * while formatting the value instead of this Unit's UnitScale.
+ * @param {!tr.b.UnitPrefix=} [opt_context.unitPrefix] - A UnitPrefix that
+ * the value should be formatted into.
+ * @param {number} [opt_context.deltaValue] - Format the value based on
+ * this delta between it and another number instead of the actual value.
+ */
+ format(value, opt_context) {
+ let signString = '';
+ if (value < 0) {
+ signString = '-';
+ value = -value; // Treat positive and negative values symmetrically.
+ } else if (this.isDelta) {
+ signString = value === 0 ? PLUS_MINUS_SIGN : '+';
+ }
+
+ 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) {
+ // In this special case we need to format to unit the same, if the value
+ // was 1. It is required for example to prevent 0 nJ instead of 0 J.
+ 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_;
+ },
+ // Use tr-v-ui-preferred-display-unit element instead of directly setting.
+ 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;
+ // TODO(aiolos): base should not depend on ui. Move the functionality of
+ // searching for preferred-display-unit out of Unit.
+ // https://github.com/catapult-project/catapult/issues/3092
+ 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;
+ }
+ throw new Error(`Unrecognized unit "${object}"`);
+ };
+
+ /**
+ * Define all combinations of a unit with isDelta and improvementDirection
+ * flags. For example, the following code:
+ *
+ * Unit.define({
+ * baseUnitName: 'powerInWatts'
+ * baseJsonName: 'W'
+ * formatSpec: {
+ * // Specification of how the unit should be formatted (unit symbol,
+ * // unit prefix, fraction digits, etc), or a function returning such
+ * // a specification.
+ * }
+ * });
+ *
+ * generates the following six units (JSON names shown in parentheses):
+ *
+ * Unit.byName.powerInWatts (W)
+ * Unit.byName.powerInWatts_smallerIsBetter (W_smallerIsBetter)
+ * Unit.byName.powerInWatts_biggerIsBetter (W_biggerIsBetter)
+ * Unit.byName.powerInWattsDelta (WDelta)
+ * Unit.byName.powerInWattsDelta_smallerIsBetter (WDelta_smallerIsBetter)
+ * Unit.byName.powerInWattsDelta_biggerIsBetter (WDelta_biggerIsBetter)
+ *
+ * with the appropriate flags and formatting code (including +/- prefixes
+ * for deltas).
+ */
+ 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');
+ }
+
+ let scaleBaseUnit = params.scaleBaseUnit;
+ if (!scaleBaseUnit) {
+ let formatSpec = params.formatSpec;
+ if (typeof formatSpec === 'function') formatSpec = formatSpec();
+ const baseSymbol = formatSpec.unitScale ?
+ formatSpec.unitScale[0].baseSymbol : (formatSpec.baseSymbol || '');
+ scaleBaseUnit = { value: 1, symbol: baseSymbol, baseSymbol };
+ }
+ const unit = new Unit(unitName, jsonName, scaleBaseUnit,
+ isDelta, improvementDirection, params.formatSpec);
+ Unit.byName[unitName] = unit;
+ Unit.byJSONName[jsonName] = unit;
+
+ return unit;
+ };
+
+ tr.b.EventTarget.decorate(Unit);
+ Unit.reset();
+
+ // Known display units follow.
+ //////////////////////////////////////////////////////////////////////////////
+
+ Unit.define({
+ baseUnitName: 'timeInMsAutoFormat',
+ baseJsonName: 'msBestFitFormat',
+ scaleBaseUnit: tr.b.UnitScale.TIME.MILLI_SEC,
+ formatSpec: {
+ unitScale: tr.b.UnitScale.TIME.AUTO,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'timeDurationInMs',
+ baseJsonName: 'ms',
+ scaleBaseUnit: tr.b.UnitScale.TIME.MILLI_SEC,
+ formatSpec() {
+ return Unit.currentTimeDisplayMode_.formatSpec;
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'timeStampInMs',
+ baseJsonName: 'tsMs',
+ scaleBaseUnit: tr.b.UnitScale.TIME.MILLI_SEC,
+ formatSpec() {
+ return Unit.currentTimeDisplayMode_.formatSpec;
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'normalizedPercentage',
+ baseJsonName: 'n%',
+ formatSpec: {
+ unitScale: [{value: 0.01, symbol: '%'}],
+ avoidSpacePrecedingUnit: true,
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'sizeInBytes',
+ baseJsonName: 'sizeInBytes',
+ formatSpec: {
+ unitScale: tr.b.UnitScale.MEMORY.AUTO,
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'bandwidthInBytesPerSecond',
+ baseJsonName: 'bytesPerSecond',
+ formatSpec: {
+ unitScale: tr.b.UnitScale.BANDWIDTH_BYTES.AUTO,
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'energyInJoules',
+ baseJsonName: 'J',
+ formatSpec: {
+ unitScale: tr.b.UnitScale.defineUnitScaleFromPrefixScale(
+ 'J', 'JOULE', tr.b.UnitPrefixScale.METRIC, 'JOULE').AUTO,
+ minimumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'powerInWatts',
+ baseJsonName: 'W',
+ formatSpec: {
+ unitScale: tr.b.UnitScale.defineUnitScaleFromPrefixScale(
+ 'W', 'WATT', tr.b.UnitPrefixScale.METRIC, 'WATT').AUTO,
+ minimumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'electricCurrentInAmperes',
+ baseJsonName: 'A',
+ formatSpec: {
+ baseSymbol: 'A',
+ unitScale: tr.b.UnitScale.defineUnitScaleFromPrefixScale(
+ 'A', 'AMPERE', tr.b.UnitPrefixScale.METRIC, 'AMPERE').AUTO,
+ minimumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'electricPotentialInVolts',
+ baseJsonName: 'V',
+ formatSpec: {
+ baseSymbol: 'V',
+ unitScale: tr.b.UnitScale.defineUnitScaleFromPrefixScale(
+ 'V', 'VOLT', tr.b.UnitPrefixScale.METRIC, 'VOLT').AUTO,
+ minimumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'frequencyInHertz',
+ baseJsonName: 'Hz',
+ formatSpec: {
+ baseSymbol: 'Hz',
+ unitScale: tr.b.UnitScale.defineUnitScaleFromPrefixScale(
+ 'Hz', 'HERTZ', tr.b.UnitPrefixScale.METRIC, 'HERTZ').AUTO,
+ minimumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'unitlessNumber',
+ baseJsonName: 'unitless',
+ formatSpec: {
+ minimumFractionDigits: 3,
+ maximumFractionDigits: 3
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'count',
+ baseJsonName: 'count',
+ formatSpec: {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0
+ }
+ });
+
+ Unit.define({
+ baseUnitName: 'sigma',
+ baseJsonName: 'sigma',
+ formatSpec: {
+ baseSymbol: String.fromCharCode(963),
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1
+ }
+ });
+
+ return {
+ ImprovementDirection,
+ Unit,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unit_scale.html b/chromium/third_party/catapult/tracing/tracing/base/unit_scale.html
new file mode 100644
index 00000000000..4350a5575bf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unit_scale.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const GREEK_SMALL_LETTER_MU = String.fromCharCode(956);
+
+ const SECONDS_IN_A_MINUTE = 60;
+ const SECONDS_IN_AN_HOUR = SECONDS_IN_A_MINUTE * 60;
+ const SECONDS_IN_A_DAY = SECONDS_IN_AN_HOUR * 24;
+ const SECONDS_IN_A_WEEK = SECONDS_IN_A_DAY * 7;
+ const SECONDS_IN_A_YEAR = SECONDS_IN_A_DAY * 365.2422;
+ const SECONDS_IN_A_MONTH = SECONDS_IN_A_YEAR / 12;
+
+ const UnitPrefixScale = {};
+ const UnitScale = {};
+
+ function defineUnitPrefixScale(name, prefixes) {
+ if (UnitPrefixScale[name] !== undefined) {
+ throw new Error('Unit prefix scale \'' + name + '\' already exists');
+ }
+ if (prefixes.AUTO !== undefined) {
+ throw new Error('The \'AUTO\' unit prefix is not supported for unit' +
+ 'prefix scales and cannot be added to scale \'' + name + '\'');
+ }
+
+ UnitPrefixScale[name] = prefixes;
+ }
+
+ UnitScale.defineUnitScale = function(name, unitScale) {
+ if (UnitScale[name] !== undefined) {
+ throw new Error('Unit scale \'' + name + '\' already exists');
+ }
+ if (unitScale.AUTO !== undefined) {
+ throw new Error('\'AUTO\' unit scale will be added automatically ' +
+ 'for unit scale \'' + name + '\'');
+ }
+
+ // The 'AUTO' unit scale is used in auto formatting Units. In units using
+ // the 'BINARY' UnitScale the absolute formatted value closest to the
+ // [1, 1024) interval as possible is used. So 1023 and 1024 bytes are
+ // displayed as "1,023.0 B" and "1.0 KiB", respectively.
+ unitScale.AUTO = Object.values(unitScale);
+ unitScale.AUTO.sort((a, b) => a.value - b.value);
+
+ if (name) UnitScale[name] = unitScale;
+ return unitScale;
+ };
+
+ function definePrefixScaleFromUnitScale(prefixName, unitScale) {
+ if (!unitScale) {
+ throw new Error('Cannot create PrefixScale without a unit scale.');
+ }
+ const prefixScale = {};
+ for (const [curPrefix, curScale] of Object.entries(unitScale)) {
+ if (curPrefix === 'AUTO') {
+ continue;
+ }
+ if (curScale.symbol === undefined || !curScale.value) {
+ throw new Error(
+ `Cannot create PrefixScale from malformed unit ${curScale}.`);
+ }
+ prefixScale[curPrefix] = {
+ value: curScale.value,
+ symbol: curScale.symbol
+ };
+ }
+ return defineUnitPrefixScale(prefixName, prefixScale);
+ }
+
+ UnitScale.defineUnitScaleFromPrefixScale = function(
+ baseSymbol, baseName, prefixScale, opt_scaleName) {
+ if (baseSymbol === undefined) {
+ throw new Error('Cannot create UnitScale with undefined baseSymbol.');
+ }
+ if (!baseName) {
+ throw new Error('Cannot create UnitScale without a baseName.');
+ }
+ if (!prefixScale) {
+ throw new Error('Cannot create UnitScale without a prefix scale.');
+ }
+ const unitScale = {};
+ for (const curPrefix of Object.keys(prefixScale)) {
+ const curScale = prefixScale[curPrefix];
+ if (curScale.symbol === undefined || !curScale.value) {
+ throw new Error(
+ `Cannot convert PrefixScale with malformed prefix ${curScale}.`);
+ }
+ const name = curPrefix === 'NONE' ? baseName : `${curPrefix}_${baseName}`;
+ unitScale[name] = {
+ value: curScale.value,
+ symbol: curScale.symbol + baseSymbol,
+ baseSymbol
+ };
+ }
+ return UnitScale.defineUnitScale(opt_scaleName, unitScale);
+ };
+
+ /**
+ * Converts |value| from |fromScale| (e.g. kilo) to |toScale| (e.g. mega).
+ *
+ * Returns undefined if |value| is undefined.
+ * |fromScale| and |toScale| need not come from the same UnitScale or
+ * UnitPrefixScale. But if they are both UnitScales they must have matching
+ * or undefined baseSymbol's.
+ *
+ * @param {(undefined|number)} value
+ * @param {!object} fromScale
+ * @param {!object} toScale
+ * @return {(undefined|number)}
+ */
+ function convertUnit(value, fromScale, toScale) {
+ if (value === undefined) return undefined;
+ const fromScaleBase = fromScale.baseSymbol;
+ const toScaleBase = toScale.baseSymbol;
+ if (fromScaleBase !== undefined && toScaleBase !== undefined &&
+ fromScaleBase !== toScaleBase) {
+ throw new Error(
+ 'Cannot convert between units with different base symbols.');
+ }
+ return value * (fromScale.value / toScale.value);
+ }
+
+ // See https://en.wikipedia.org/wiki/Binary_prefix.
+ defineUnitPrefixScale('BINARY', {
+ NONE: { value: Math.pow(1024, 0), symbol: '' },
+ KIBI: { value: Math.pow(1024, 1), symbol: 'Ki' },
+ MEBI: { value: Math.pow(1024, 2), symbol: 'Mi' },
+ GIBI: { value: Math.pow(1024, 3), symbol: 'Gi' },
+ TEBI: { value: Math.pow(1024, 4), symbol: 'Ti' }
+ });
+
+ // See https://en.wikipedia.org/wiki/Metric_prefix.
+ defineUnitPrefixScale('METRIC', {
+ NANO: { value: 1e-9, symbol: 'n' },
+ MICRO: { value: 1e-6, symbol: GREEK_SMALL_LETTER_MU },
+ MILLI: { value: 1e-3, symbol: 'm' },
+ NONE: { value: 1, symbol: ''},
+ KILO: { value: 1e3, symbol: 'k'},
+ MEGA: { value: 1e6, symbol: 'M'},
+ GIGA: { value: 1e9, symbol: 'G'}
+ });
+
+ UnitScale.defineUnitScale('TIME', {
+ NANO_SEC: { value: 1e-9, symbol: 'ns', baseSymbol: 's'},
+ MICRO_SEC: { value: 1e-6, symbol: GREEK_SMALL_LETTER_MU + 's',
+ baseSymbol: 's'},
+ MILLI_SEC: { value: 1e-3, symbol: 'ms', baseSymbol: 's'},
+ SEC: { value: 1, symbol: 's', baseSymbol: 's'},
+ MINUTE: { value: SECONDS_IN_A_MINUTE, symbol: 'min', baseSymbol: 's'},
+ HOUR: { value: SECONDS_IN_AN_HOUR, symbol: 'hr', baseSymbol: 's'},
+ DAY: { value: SECONDS_IN_A_DAY, symbol: 'days', baseSymbol: 's'},
+ WEEK: { value: SECONDS_IN_A_WEEK, symbol: 'weeks', baseSymbol: 's'},
+ MONTH: { value: SECONDS_IN_A_MONTH, symbol: 'months', baseSymbol: 's'},
+ YEAR: { value: SECONDS_IN_A_YEAR, symbol: 'years', baseSymbol: 's'}
+ });
+
+ UnitScale.defineUnitScaleFromPrefixScale(
+ 'B', 'BYTE', UnitPrefixScale.BINARY, 'MEMORY');
+
+ definePrefixScaleFromUnitScale('DATA_SIZE', UnitScale.MEMORY);
+
+ UnitScale.defineUnitScaleFromPrefixScale(
+ '/s', 'SECONDS', UnitPrefixScale.DATA_SIZE, 'BANDWIDTH_BYTES');
+
+ return {
+ UnitPrefixScale,
+ UnitScale,
+ convertUnit,
+ GREEK_SMALL_LETTER_MU,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unit_scale_test.html b/chromium/third_party/catapult/tracing/tracing/base/unit_scale_test.html
new file mode 100644
index 00000000000..cfd20f52811
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unit_scale_test.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const UnitPrefixScale = tr.b.UnitPrefixScale;
+ const UnitScale = tr.b.UnitScale;
+
+ test('convertUnit', function() {
+ // UnitPrefixScale to UnitScale
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitPrefixScale.BINARY.MEBI, UnitScale.MEMORY.MEBI_BYTE),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 43, UnitPrefixScale.BINARY.MEBI, UnitScale.MEMORY.BYTE), 45088768);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitPrefixScale.BINARY.NONE, UnitScale.MEMORY.KIBI_BYTE),
+ -0.390625);
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitPrefixScale.METRIC.MILLI, UnitScale.TIME.MILLI_SEC),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 50, UnitPrefixScale.METRIC.MILLI, UnitScale.TIME.SEC), 0.05);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitPrefixScale.METRIC.NONE, UnitScale.TIME.MICRO_SEC),
+ -400000000);
+
+ // UnitScale to UnitPrefixScale
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitScale.MEMORY.MEBI_BYTE, UnitPrefixScale.BINARY.MEBI),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 43, UnitScale.MEMORY.MEBI_BYTE, UnitPrefixScale.BINARY.NONE),
+ 45088768);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitScale.MEMORY.BYTE, UnitPrefixScale.BINARY.KIBI),
+ -0.390625);
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitScale.TIME.MILLI_SEC, UnitPrefixScale.METRIC.MILLI),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 50, UnitPrefixScale.METRIC.MILLI, UnitScale.TIME.SEC), 0.05);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitPrefixScale.METRIC.NONE, UnitScale.TIME.MICRO_SEC),
+ -400000000);
+
+ // UnitScale to UnitScale
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitScale.MEMORY.MEBI_BYTE, UnitScale.MEMORY.MEBI_BYTE),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 43, UnitScale.MEMORY.MEBI_BYTE, UnitScale.MEMORY.BYTE), 45088768);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitScale.MEMORY.BYTE, UnitScale.MEMORY.KIBI_BYTE),
+ -0.390625);
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitScale.TIME.MILLI_SEC, UnitScale.TIME.MILLI_SEC), 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 50, UnitScale.TIME.MILLI_SEC, UnitScale.TIME.SEC), 0.05);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitScale.TIME.SEC, UnitScale.TIME.MICRO_SEC), -400000000);
+ assert.closeTo(tr.b.convertUnit(
+ 12, UnitScale.TIME.YEAR, UnitScale.TIME.MONTH), 144, 1.e-8);
+
+ // UnitPrefixScale to UnitPrefixScale
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitPrefixScale.BINARY.MEBI, UnitPrefixScale.BINARY.MEBI),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 43, UnitPrefixScale.BINARY.MEBI, UnitPrefixScale.BINARY.NONE),
+ 45088768);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitPrefixScale.BINARY.NONE, UnitPrefixScale.BINARY.KIBI),
+ -0.390625);
+ assert.strictEqual(tr.b.convertUnit(
+ 43.53, UnitPrefixScale.METRIC.MILLI, UnitPrefixScale.METRIC.MILLI),
+ 43.53);
+ assert.strictEqual(tr.b.convertUnit(
+ 50, UnitPrefixScale.METRIC.MILLI, UnitPrefixScale.METRIC.NONE),
+ 0.05);
+ assert.strictEqual(tr.b.convertUnit(
+ -400, UnitPrefixScale.METRIC.NONE, UnitPrefixScale.METRIC.MICRO),
+ -400000000);
+ });
+
+ test('defineUnitScale', function() {
+ assert.strictEqual(UnitPrefixScale.PONY, undefined);
+ const ponyScale = {
+ SHORT_JUMPS: { value: 1e-3, symbol: 'SJ', baseSymbol: 'J'},
+ REGULAR_JUMPS: { value: 1, symbol: 'J', baseSymbol: 'J'},
+ LONG_JUMPS: {value: 1e3, symbol: 'LJ', baseSymbol: 'J'},
+ EPIC_JUMPS: {value: 1e6, symbol: 'EJ', baseSymbol: 'J'}
+ };
+ UnitScale.defineUnitScale('PONY', ponyScale);
+ assert.strictEqual(tr.b.convertUnit(
+ 32.1, UnitScale.PONY.REGULAR_JUMPS, UnitScale.PONY.SHORT_JUMPS),
+ 32100);
+ assert.strictEqual(tr.b.convertUnit(
+ -32, UnitScale.PONY.SHORT_JUMPS, UnitScale.PONY.REGULAR_JUMPS),
+ -0.032);
+ assert.strictEqual(tr.b.convertUnit(
+ 32.1, UnitScale.PONY.EPIC_JUMPS, UnitScale.PONY.LONG_JUMPS),
+ 32100);
+ assert.strictEqual(tr.b.convertUnit(
+ -34600000, UnitScale.PONY.SHORT_JUMPS, UnitScale.PONY.EPIC_JUMPS),
+ -0.0346);
+ });
+
+ test('defineUnitScaleFromPrefixScale', function() {
+ assert.strictEqual(UnitPrefixScale.BUNNY, undefined);
+ UnitScale.defineUnitScaleFromPrefixScale(
+ 'H', 'HOPS', UnitPrefixScale.METRIC, 'BUNNY');
+ assert.strictEqual(tr.b.convertUnit(
+ 32.1, UnitScale.BUNNY.HOPS, UnitScale.BUNNY.MILLI_HOPS),
+ 32100);
+ assert.strictEqual(tr.b.convertUnit(
+ -32, UnitScale.BUNNY.MILLI_HOPS, UnitScale.BUNNY.HOPS),
+ -0.032);
+ assert.strictEqual(tr.b.convertUnit(
+ 32.1, UnitScale.BUNNY.GIGA_HOPS, UnitScale.BUNNY.KILO_HOPS),
+ 32100000);
+ assert.strictEqual(tr.b.convertUnit(
+ -32, UnitScale.BUNNY.KILO_HOPS, UnitScale.BUNNY.MEGA_HOPS),
+ -0.032);
+ });
+
+ test('timeScale', function() {
+ assert.strictEqual(tr.b.convertUnit(
+ 123.45, UnitScale.TIME.NANO_SEC, UnitScale.TIME.MICRO_SEC), 0.12345);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.45, UnitScale.TIME.MICRO_SEC, UnitScale.TIME.MILLI_SEC), 0.12345);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.45, UnitScale.TIME.MILLI_SEC, UnitScale.TIME.SEC), 0.12345);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.45, UnitScale.TIME.SEC, UnitScale.TIME.MINUTE), 2.0575);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.45, UnitScale.TIME.MINUTE, UnitScale.TIME.HOUR), 2.0575);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.48, UnitScale.TIME.HOUR, UnitScale.TIME.DAY), 5.145);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.48, UnitScale.TIME.DAY, UnitScale.TIME.WEEK), 17.64);
+ assert.strictEqual(tr.b.convertUnit(
+ 123.48, UnitScale.TIME.WEEK, UnitScale.TIME.MONTH),
+ 28.3984709324388);
+ assert.closeTo(tr.b.convertUnit(
+ 123.48, UnitScale.TIME.MONTH, UnitScale.TIME.YEAR), 10.29,
+ 0.000001);
+ });
+
+ test('memoryScale', function() {
+ assert.strictEqual(tr.b.convertUnit(
+ 4608, UnitScale.MEMORY.BYTE, UnitScale.MEMORY.KIBI_BYTE), 4.5);
+ assert.strictEqual(tr.b.convertUnit(
+ 4608, UnitScale.MEMORY.KIBI_BYTE, UnitScale.MEMORY.MEBI_BYTE), 4.5);
+ assert.strictEqual(tr.b.convertUnit(
+ 4608, UnitScale.MEMORY.MEBI_BYTE, UnitScale.MEMORY.GIBI_BYTE), 4.5);
+ assert.strictEqual(tr.b.convertUnit(
+ 4608, UnitScale.MEMORY.GIBI_BYTE, UnitScale.MEMORY.TEBI_BYTE), 4.5);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unit_test.html b/chromium/third_party/catapult/tracing/tracing/base/unit_test.html
new file mode 100644
index 00000000000..6639e1ae208
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unit_test.html
@@ -0,0 +1,444 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ImprovementDirection = tr.b.ImprovementDirection;
+ const Unit = tr.b.Unit;
+ const UnitPrefixScale = tr.b.UnitPrefixScale;
+
+ test('truncate', function() {
+ assert.isTrue(isNaN(Unit.byName.count.truncate(NaN)));
+ assert.isUndefined(Unit.byName.count.truncate(undefined));
+ assert.isNull(Unit.byName.count.truncate(null));
+
+ assert.strictEqual(0, Unit.byName.unitlessNumber.truncate(0));
+ assert.strictEqual(1, Unit.byName.unitlessNumber.truncate(1));
+ assert.strictEqual(10, Unit.byName.unitlessNumber.truncate(10));
+
+ assert.strictEqual(0.3, Unit.byName.count.truncate(1 / 3));
+ assert.strictEqual(0.33, Unit.byName.sigma.truncate(1 / 3));
+ assert.strictEqual(0.3333, Unit.byName.unitlessNumber.truncate(1 / 3));
+ assert.strictEqual(0.333333, Unit.byName.frequencyInHertz.truncate(1 / 3));
+ assert.strictEqual(0.333333, Unit.byName.electricPotentialInVolts.truncate(
+ 1 / 3));
+ assert.strictEqual(0.333333, Unit.byName.electricCurrentInAmperes.truncate(
+ 1 / 3));
+ assert.strictEqual(0.333333, Unit.byName.powerInWatts.truncate(1 / 3));
+ assert.strictEqual(0.333333, Unit.byName.energyInJoules.truncate(1 / 3));
+ assert.strictEqual(0.3, Unit.byName.bandwidthInBytesPerSecond.truncate(
+ 1 / 3));
+ assert.strictEqual(0.3, Unit.byName.sizeInBytes.truncate(1 / 3));
+ assert.strictEqual(0.333, Unit.byName.normalizedPercentage.truncate(1 / 3));
+ assert.strictEqual(0.333, Unit.byName.timeStampInMs.truncate(1 / 3));
+ assert.strictEqual(0.333, Unit.byName.timeDurationInMs.truncate(1 / 3));
+ assert.strictEqual(0.333333, Unit.byName.timeInMsAutoFormat.truncate(
+ 1 / 3));
+
+ assert.strictEqual(333.333, Unit.byName.energyInJoules.truncate(1e3 / 3));
+ assert.strictEqual(3333, Unit.byName.energyInJoules.truncate(1e4 / 3));
+
+ assert.strictEqual(0.7, Unit.byName.count.truncate(2 / 3));
+ assert.strictEqual(0.67, Unit.byName.sigma.truncate(2 / 3));
+ assert.strictEqual(0.6667, Unit.byName.unitlessNumber.truncate(2 / 3));
+ assert.strictEqual(0.666667, Unit.byName.frequencyInHertz.truncate(2 / 3));
+ assert.strictEqual(0.666667, Unit.byName.electricPotentialInVolts.truncate(
+ 2 / 3));
+ assert.strictEqual(0.666667, Unit.byName.electricCurrentInAmperes.truncate(
+ 2 / 3));
+ assert.strictEqual(0.666667, Unit.byName.powerInWatts.truncate(2 / 3));
+ assert.strictEqual(0.666667, Unit.byName.energyInJoules.truncate(2 / 3));
+ assert.strictEqual(0.7, Unit.byName.bandwidthInBytesPerSecond.truncate(
+ 2 / 3));
+ assert.strictEqual(0.7, Unit.byName.sizeInBytes.truncate(2 / 3));
+ assert.strictEqual(0.667, Unit.byName.normalizedPercentage.truncate(2 / 3));
+ assert.strictEqual(0.667, Unit.byName.timeStampInMs.truncate(2 / 3));
+ assert.strictEqual(0.667, Unit.byName.timeDurationInMs.truncate(2 / 3));
+ assert.strictEqual(0.666667, Unit.byName.timeInMsAutoFormat.truncate(
+ 2 / 3));
+
+ assert.strictEqual(666.667, Unit.byName.energyInJoules.truncate(2e3 / 3));
+ assert.strictEqual(6667, Unit.byName.energyInJoules.truncate(2e4 / 3));
+ });
+
+ test('Unit.display-mode-changed', function() {
+ const TimeDisplayModes = tr.b.TimeDisplayModes;
+
+ let listenerWasCalled = false;
+ function listener(e) {
+ listenerWasCalled = true;
+ }
+
+ try {
+ Unit.currentTimeDisplayMode = TimeDisplayModes.ms;
+ Unit.addEventListener('display-mode-changed', listener);
+
+ listenerWasCalled = false;
+ Unit.currentTimeDisplayMode = TimeDisplayModes.ns;
+ assert.isTrue(listenerWasCalled);
+ assert.strictEqual(Unit.currentTimeDisplayMode, TimeDisplayModes.ns);
+ } finally {
+ Unit.removeEventListener('display-mode-changed', listener);
+ Unit.reset();
+ }
+ });
+
+ test('Unit.didPreferredTimeDisplayUnitChange', function() {
+ if (tr.isHeadless) return;
+ const Unit = tr.b.Unit;
+ const TimeDisplayModes = tr.b.TimeDisplayModes;
+ assert.strictEqual(Unit.currentTimeDisplayMode, TimeDisplayModes.ms);
+
+ const displayUnit = document.createElement(
+ 'tr-v-ui-preferred-display-unit');
+ displayUnit.preferredTimeDisplayMode = TimeDisplayModes.ns;
+ this.addHTMLOutput(displayUnit);
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ assert.strictEqual(Unit.currentTimeDisplayMode, TimeDisplayModes.ns);
+ });
+
+ function checkTimeUnit(unit) {
+ try {
+ // Use milliseconds to display time (default behavior).
+ Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ms;
+
+ assert.strictEqual(unit.format(0), '0.000 ms');
+ assert.strictEqual(unit.format(0.02), '0.020 ms');
+ assert.strictEqual(unit.format(0.001), '0.001 ms');
+ assert.strictEqual(unit.format(0.0005), '0.001 ms');
+ assert.strictEqual(unit.format(0.00049), '0.000 ms');
+ assert.strictEqual(unit.format(999.999), '999.999 ms');
+ assert.strictEqual(unit.format(1000.001), '1,000.001 ms');
+ assert.strictEqual(unit.format(123456789), '123,456,789.000 ms');
+ assert.strictEqual(unit.format(-0.00051), '-0.001 ms');
+ assert.strictEqual(unit.format(-123456789), '-123,456,789.000 ms');
+
+ // Change the unit to nanoseconds.
+ Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ns;
+
+ assert.strictEqual(unit.format(0), '0 ns');
+ assert.strictEqual(unit.format(1), '1,000,000 ns');
+ assert.strictEqual(unit.format(0.000042), '42 ns');
+ assert.strictEqual(unit.format(0.000001), '1 ns');
+ assert.strictEqual(unit.format(0.0000005), '1 ns');
+ assert.strictEqual(unit.format(0.00000049), '0 ns');
+ assert.strictEqual(unit.format(123.456), '123,456,000 ns');
+ assert.strictEqual(unit.format(-0.07), '-70,000 ns');
+ } finally {
+ Unit.reset();
+ }
+ }
+
+ test('timeStampInMs', function() {
+ assert.strictEqual(Unit.byName.timeStampInMs.unitName, 'timeStampInMs');
+ assert.strictEqual(Unit.byName.timeStampInMs.asJSON(), 'tsMs');
+ checkTimeUnit(Unit.byName.timeStampInMs);
+ });
+
+ test('timeDurationInMs', function() {
+ assert.strictEqual(Unit.byName.timeDurationInMs.unitName,
+ 'timeDurationInMs');
+ assert.strictEqual(Unit.byName.timeDurationInMs.asJSON(), 'ms');
+ checkTimeUnit(Unit.byName.timeDurationInMs);
+ });
+
+ test('sizeInBytes', function() {
+ const SOURCE_VALUES = [0, 1, 1536, 424.5 * 1024 * 1024,
+ 1025 * 1024 * 1024 * 1024 * 1024, -2.5 * 1024 * 1024];
+ const EXPECTED_REGULAR_FORMATTED_VALUES = ['0.0 B', '1.0 B', '1.5 KiB',
+ '424.5 MiB', '1,025.0 TiB', '-2.5 MiB'];
+ const EXPECTED_DELTA_FORMATTED_VALUES = [
+ '\u00B10.0 B', '+1.0 B', '+1.5 KiB',
+ '+424.5 MiB', '+1,025.0 TiB', '-2.5 MiB',
+ ];
+
+ function checkSizeUnit(unit, expectation) {
+ assert.strictEqual(unit.unitName, expectation.unitName);
+ assert.strictEqual(unit.asJSON(), expectation.asJSON);
+ assert.strictEqual(unit.isDelta, expectation.isDelta);
+ assert.strictEqual(unit.baseUnit, expectation.baseUnit);
+ assert.strictEqual(unit.correspondingDeltaUnit,
+ expectation.correspondingDeltaUnit);
+ assert.strictEqual(unit.improvementDirection,
+ expectation.improvementDirection);
+ assert.deepEqual(SOURCE_VALUES.map(v => unit.format(v)),
+ expectation.formattedValues);
+ }
+
+ // Regular (non-delta).
+ checkSizeUnit(Unit.byName.sizeInBytes, {
+ unitName: 'sizeInBytes',
+ asJSON: 'sizeInBytes',
+ isDelta: false,
+ baseUnit: Unit.byName.sizeInBytes,
+ correspondingDeltaUnit: Unit.byName.sizeInBytesDelta,
+ improvementDirection: ImprovementDirection.DONT_CARE,
+ formattedValues: EXPECTED_REGULAR_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.sizeInBytes_smallerIsBetter, {
+ unitName: 'sizeInBytes_smallerIsBetter',
+ asJSON: 'sizeInBytes_smallerIsBetter',
+ isDelta: false,
+ baseUnit: Unit.byName.sizeInBytes,
+ correspondingDeltaUnit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ improvementDirection: ImprovementDirection.SMALLER_IS_BETTER,
+ formattedValues: EXPECTED_REGULAR_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.sizeInBytes_biggerIsBetter, {
+ unitName: 'sizeInBytes_biggerIsBetter',
+ asJSON: 'sizeInBytes_biggerIsBetter',
+ isDelta: false,
+ baseUnit: Unit.byName.sizeInBytes,
+ correspondingDeltaUnit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ improvementDirection: ImprovementDirection.BIGGER_IS_BETTER,
+ formattedValues: EXPECTED_REGULAR_FORMATTED_VALUES
+ });
+
+ // Delta.
+ checkSizeUnit(Unit.byName.sizeInBytesDelta, {
+ unitName: 'sizeInBytesDelta',
+ asJSON: 'sizeInBytesDelta',
+ isDelta: true,
+ baseUnit: Unit.byName.sizeInBytes,
+ correspondingDeltaUnit: Unit.byName.sizeInBytesDelta,
+ improvementDirection: ImprovementDirection.DONT_CARE,
+ formattedValues: EXPECTED_DELTA_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.sizeInBytesDelta_smallerIsBetter, {
+ unitName: 'sizeInBytesDelta_smallerIsBetter',
+ asJSON: 'sizeInBytesDelta_smallerIsBetter',
+ isDelta: true,
+ baseUnit: Unit.byName.sizeInBytes,
+ correspondingDeltaUnit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ improvementDirection: ImprovementDirection.SMALLER_IS_BETTER,
+ formattedValues: EXPECTED_DELTA_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.sizeInBytesDelta_biggerIsBetter, {
+ unitName: 'sizeInBytesDelta_biggerIsBetter',
+ asJSON: 'sizeInBytesDelta_biggerIsBetter',
+ isDelta: true,
+ baseUnit: Unit.byName.sizeInBytes,
+ correspondingDeltaUnit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ improvementDirection: ImprovementDirection.BIGGER_IS_BETTER,
+ formattedValues: EXPECTED_DELTA_FORMATTED_VALUES
+ });
+ });
+
+ test('bytesPerSecond', function() {
+ const SOURCE_VALUES = [0, 1, 1536, 424.5 * 1024 * 1024,
+ 1025 * 1024 * 1024 * 1024 * 1024, -2.5 * 1024 * 1024];
+ const EXPECTED_REGULAR_FORMATTED_VALUES = ['0.0 B/s', '1.0 B/s',
+ '1.5 KiB/s', '424.5 MiB/s', '1,025.0 TiB/s', '-2.5 MiB/s'];
+ const EXPECTED_DELTA_FORMATTED_VALUES = [
+ '\u00B10.0 B/s', '+1.0 B/s', '+1.5 KiB/s',
+ '+424.5 MiB/s', '+1,025.0 TiB/s', '-2.5 MiB/s',
+ ];
+
+ function checkSizeUnit(unit, expectation) {
+ assert.strictEqual(unit.unitName, expectation.unitName);
+ assert.strictEqual(unit.asJSON(), expectation.asJSON);
+ assert.strictEqual(unit.isDelta, expectation.isDelta);
+ assert.strictEqual(unit.baseUnit, expectation.baseUnit);
+ assert.strictEqual(unit.correspondingDeltaUnit,
+ expectation.correspondingDeltaUnit);
+ assert.strictEqual(unit.improvementDirection,
+ expectation.improvementDirection);
+ assert.deepEqual(SOURCE_VALUES.map(v => unit.format(v)),
+ expectation.formattedValues);
+ }
+
+ // Regular (non-delta).
+ checkSizeUnit(Unit.byName.bandwidthInBytesPerSecond, {
+ unitName: 'bandwidthInBytesPerSecond',
+ asJSON: 'bytesPerSecond',
+ isDelta: false,
+ baseUnit: Unit.byName.bandwidthInBytesPerSecond,
+ correspondingDeltaUnit: Unit.byName.bandwidthInBytesPerSecondDelta,
+ improvementDirection: ImprovementDirection.DONT_CARE,
+ formattedValues: EXPECTED_REGULAR_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.bandwidthInBytesPerSecond_smallerIsBetter, {
+ unitName: 'bandwidthInBytesPerSecond_smallerIsBetter',
+ asJSON: 'bytesPerSecond_smallerIsBetter',
+ isDelta: false,
+ baseUnit: Unit.byName.bandwidthInBytesPerSecond,
+ correspondingDeltaUnit:
+ Unit.byName.bandwidthInBytesPerSecondDelta_smallerIsBetter,
+ improvementDirection: ImprovementDirection.SMALLER_IS_BETTER,
+ formattedValues: EXPECTED_REGULAR_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.bandwidthInBytesPerSecond_biggerIsBetter, {
+ unitName: 'bandwidthInBytesPerSecond_biggerIsBetter',
+ asJSON: 'bytesPerSecond_biggerIsBetter',
+ isDelta: false,
+ baseUnit: Unit.byName.bandwidthInBytesPerSecond,
+ correspondingDeltaUnit:
+ Unit.byName.bandwidthInBytesPerSecondDelta_biggerIsBetter,
+ improvementDirection: ImprovementDirection.BIGGER_IS_BETTER,
+ formattedValues: EXPECTED_REGULAR_FORMATTED_VALUES
+ });
+
+ // Delta.
+ checkSizeUnit(Unit.byName.bandwidthInBytesPerSecondDelta, {
+ unitName: 'bandwidthInBytesPerSecondDelta',
+ asJSON: 'bytesPerSecondDelta',
+ isDelta: true,
+ baseUnit: Unit.byName.bandwidthInBytesPerSecond,
+ correspondingDeltaUnit: Unit.byName.bandwidthInBytesPerSecondDelta,
+ improvementDirection: ImprovementDirection.DONT_CARE,
+ formattedValues: EXPECTED_DELTA_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.bandwidthInBytesPerSecondDelta_smallerIsBetter, {
+ unitName: 'bandwidthInBytesPerSecondDelta_smallerIsBetter',
+ asJSON: 'bytesPerSecondDelta_smallerIsBetter',
+ isDelta: true,
+ baseUnit: Unit.byName.bandwidthInBytesPerSecond,
+ correspondingDeltaUnit:
+ Unit.byName.bandwidthInBytesPerSecondDelta_smallerIsBetter,
+ improvementDirection: ImprovementDirection.SMALLER_IS_BETTER,
+ formattedValues: EXPECTED_DELTA_FORMATTED_VALUES
+ });
+ checkSizeUnit(Unit.byName.bandwidthInBytesPerSecondDelta_biggerIsBetter, {
+ unitName: 'bandwidthInBytesPerSecondDelta_biggerIsBetter',
+ asJSON: 'bytesPerSecondDelta_biggerIsBetter',
+ isDelta: true,
+ baseUnit: Unit.byName.bandwidthInBytesPerSecond,
+ correspondingDeltaUnit:
+ Unit.byName.bandwidthInBytesPerSecondDelta_biggerIsBetter,
+ improvementDirection: ImprovementDirection.BIGGER_IS_BETTER,
+ formattedValues: EXPECTED_DELTA_FORMATTED_VALUES
+ });
+ });
+
+ test('context', function() {
+ assert.strictEqual(Unit.byName.timeStampInMs.format(0, {}), '0.000 ms');
+ assert.strictEqual(Unit.byName.normalizedPercentageDelta.format(1.23456,
+ { maximumFractionDigits: 2 }), '+123.46%');
+ assert.strictEqual(Unit.byName.powerInWatts.format(999.999,
+ { minimumFractionDigits: 5 }), '999.99900 W');
+ assert.strictEqual(Unit.byName.powerInWatts_biggerIsBetter.format(8.88,
+ { minimumFractionDigits: 1 }), '8.88 W');
+ assert.strictEqual(Unit.byName.unitlessNumber.format(0.00789,
+ { maximumFractionDigits: 6 }), '0.00789');
+ assert.strictEqual(Unit.byName.unitlessNumber.format(41.9,
+ { maximumFractionDigits: 0 }), '42');
+ assert.strictEqual(
+ Unit.byName.energyInJoules.format(0.4444,
+ { minimumFractionDigits: 2, maximumFractionDigits: 3 }),
+ '444.40 mJ');
+ assert.strictEqual(
+ Unit.byName.energyInJoules.format(0.6,
+ { minimumFractionDigits: 2, maximumFractionDigits: 3 }),
+ '600.00 mJ');
+ assert.strictEqual(
+ Unit.byName.sizeInBytesDelta_smallerIsBetter.format(0,
+ { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
+ '\u00B10.0 B');
+ assert.strictEqual(
+ Unit.byName.sizeInBytes.format(25600000000,
+ { unitPrefix: UnitPrefixScale.BINARY.KIBI }),
+ '25,000,000.0 KiB');
+ assert.strictEqual(
+ Unit.byName.sizeInBytes.format(5243,
+ { unitPrefix: UnitPrefixScale.BINARY.MEBI,
+ minimumFractionDigits: 2 }),
+ '0.01 MiB');
+ assert.strictEqual(
+ Unit.byName.bandwidthInBytesPerSecondDelta_smallerIsBetter.format(0,
+ { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
+ '\u00B10.0 B/s');
+ assert.strictEqual(
+ Unit.byName.bandwidthInBytesPerSecond.format(25600000000,
+ { unitPrefix: UnitPrefixScale.DATA_SIZE.KIBI_BYTE }),
+ '25,000,000.0 KiB/s');
+ assert.strictEqual(
+ Unit.byName.bandwidthInBytesPerSecond.format(5243,
+ { unitPrefix: UnitPrefixScale.DATA_SIZE.MEBI_BYTE,
+ minimumFractionDigits: 2 }),
+ '0.01 MiB/s');
+ });
+
+ test('energyInJoules', function() {
+ assert.strictEqual(Unit.byName.energyInJoules.format(1000), '1.000 kJ');
+ assert.strictEqual(Unit.byName.energyInJoules.format(1), '1.000 J');
+ assert.strictEqual(Unit.byName.energyInJoules.format(0), '0.000 J');
+ assert.strictEqual(Unit.byName.energyInJoules.format(.005), '5.000 mJ');
+ assert.strictEqual(
+ Unit.byName.energyInJoules.format(.0005),
+ '500.000 ' + tr.b.GREEK_SMALL_LETTER_MU + 'J');
+ assert.strictEqual(
+ Unit.byName.energyInJoules.format(.0004),
+ '400.000 ' + tr.b.GREEK_SMALL_LETTER_MU + 'J');
+ });
+
+ test('powerInWatts', function() {
+ assert.strictEqual(Unit.byName.powerInWatts.format(1000), '1.000 kW');
+ assert.strictEqual(Unit.byName.powerInWatts.format(1), '1.000 W');
+ assert.strictEqual(Unit.byName.powerInWatts.format(0), '0.000 W');
+ assert.strictEqual(Unit.byName.powerInWatts.format(.001), '1.000 mW');
+ assert.strictEqual(Unit.byName.powerInWatts.format(.001005), '1.005 mW');
+ });
+
+ test('electricCurrentInAmperes', function() {
+ assert.strictEqual(
+ Unit.byName.electricCurrentInAmperes.format(1000), '1.000 kA');
+ assert.strictEqual(
+ Unit.byName.electricCurrentInAmperes.format(1), '1.000 A');
+ assert.strictEqual(
+ Unit.byName.electricCurrentInAmperes.format(0), '0.000 A');
+ assert.strictEqual(
+ Unit.byName.electricCurrentInAmperes.format(.001), '1.000 mA');
+ assert.strictEqual(
+ Unit.byName.electricCurrentInAmperes.format(.001005), '1.005 mA');
+ });
+
+ test('electricPotentialInVolts', function() {
+ assert.strictEqual(
+ Unit.byName.electricPotentialInVolts.format(1000), '1.000 kV');
+ assert.strictEqual(
+ Unit.byName.electricPotentialInVolts.format(1), '1.000 V');
+ assert.strictEqual(
+ Unit.byName.electricPotentialInVolts.format(0), '0.000 V');
+ assert.strictEqual(
+ Unit.byName.electricPotentialInVolts.format(.001), '1.000 mV');
+ assert.strictEqual(
+ Unit.byName.electricPotentialInVolts.format(.001005), '1.005 mV');
+ });
+
+ test('frequencyInHertz', function() {
+ assert.strictEqual(
+ Unit.byName.frequencyInHertz.format(1000), '1.000 kHz');
+ assert.strictEqual(Unit.byName.frequencyInHertz.format(1), '1.000 Hz');
+ assert.strictEqual(Unit.byName.frequencyInHertz.format(0), '0.000 Hz');
+ assert.strictEqual(Unit.byName.frequencyInHertz.format(.001), '1.000 mHz');
+ assert.strictEqual(
+ Unit.byName.frequencyInHertz.format(.001005), '1.005 mHz');
+ });
+
+ test('unitlessNumber', function() {
+ assert.strictEqual(Unit.byName.unitlessNumber.format(1), '1.000');
+ assert.strictEqual(Unit.byName.unitlessNumber.format(0), '0.000');
+ assert.strictEqual(Unit.byName.unitlessNumber.format(1.23), '1.230');
+ assert.strictEqual(Unit.byName.unitlessNumber.format(-1.23), '-1.230');
+ assert.strictEqual(Unit.byName.unitlessNumber.format(0), '0.000');
+ });
+
+ test('count', function() {
+ assert.strictEqual(Unit.byName.count.format(0), '0');
+ assert.strictEqual(Unit.byName.count.format(1), '1');
+ assert.strictEqual(Unit.byName.count.format(1.4), '1');
+ assert.strictEqual(Unit.byName.count.format(100), '100');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest.html b/chromium/third_party/catapult/tracing/tracing/base/unittest.html
new file mode 100644
index 00000000000..e8d214b2300
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script src="/chai/chai.js"></script>
+<script>
+ 'use strict';
+ /**
+ * Alias chai assert to the global assert.
+ */
+ if (tr.isNode) {
+ // In node, chai.js knows to act as a node module, whereas our HTML
+ // imports code expects chai to end up in the global scope. So, in Node,
+ // copy the chai exports into global.
+ const chaiAbsPath = HTMLImportsLoader.hrefToAbsolutePath(
+ '/chai/chai.js');
+ const chaiModule = require(chaiAbsPath);
+ for (const exportName in chaiModule) {
+ global[exportName] = chaiModule[exportName];
+ }
+ } else {
+ // https://github.com/catapult-project/catapult/issues/3097
+ chai.config.includeStack = true;
+
+ /**
+ * Catapult presubmit wanted me to put a jsdoc here. So nduca did.
+ */
+ global.assert = chai.assert;
+ }
+ global.assert.equal = () => {
+ // See https://github.com/catapult-project/catapult/issues/3235
+ throw new Error('Use assert.strictEqual instead of assert.equal since ' +
+ 'assert.equal will coerce its arguments.');
+ };
+</script>
+
+<link rel="import" href="/tracing/base/unittest/suite_loader.html">
+<link rel="import" href="/tracing/base/unittest/test_case.html">
+<link rel="import" href="/tracing/base/unittest/test_runner.html">
+<link rel="import" href="/tracing/base/unittest/test_suite.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/constants.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/constants.html
new file mode 100644
index 00000000000..f00bba28e1d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/constants.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b.unittest', function() {
+ const TestStatus = {
+ PENDING: 'pending-status',
+ RUNNING: 'running-status',
+ DONE_RUNNING: 'done-running-status'
+ };
+
+ const TestTypes = {
+ UNITTEST: 'unittest-type',
+ PERFTEST: 'perftest-type'
+ };
+
+ return {
+ TestStatus,
+ TestTypes,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/html_test_results.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/html_test_results.html
new file mode 100644
index 00000000000..66cf9e95109
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/html_test_results.html
@@ -0,0 +1,540 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/unittest/constants.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<style>
+ x-tr-b-unittest-test-results {
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+ }
+
+ x-tr-b-unittest-test-results > x-html-test-case-result.dark > #summary {
+ background-color: #eee;
+ }
+
+ x-html-test-case-result {
+ display: block;
+ }
+ x-html-test-case-result > #summary > #title,
+ x-html-test-case-result > #summary > #status,
+ x-html-test-case-result > #details > x-html-test-case-error > #message,
+ x-html-test-case-result > #details > x-html-test-case-error > #stack,
+ x-html-test-case-result > #details > x-html-test-case-error > #return-value,
+ x-html-test-case-result > #details > x-html-test-case-flaky > #message {
+ -webkit-user-select: auto;
+ }
+
+ x-html-test-case-result > #details > x-html-test-case-error,
+ x-html-test-case-result > #details > x-html-test-case-flaky {
+ display: block;
+ border: 1px solid grey;
+ border-radius: 5px;
+ font-family: monospace;
+ margin-bottom: 14px;
+ }
+
+ x-html-test-case-result > #details > x-html-test-case-error > #message,
+ x-html-test-case-result > #details > x-html-test-case-error > #stack,
+ x-html-test-case-result > #details > x-html-test-case-flaky > #message {
+ white-space: pre;
+ }
+
+ x-html-test-case-result > #details > x-html-test-case-html-result {
+ display: block;
+ }
+
+ .unittest-pending {
+ color: orange;
+ }
+ .unittest-running {
+ color: orange;
+ font-weight: bold;
+ }
+
+ .unittest-passed {
+ color: darkgreen;
+ }
+
+ .unittest-failed {
+ color: darkred;
+ font-weight: bold;
+ }
+
+ .unittest-flaky {
+ color: darkorange;
+ }
+
+ .unittest-skipped {
+ color: blue;
+ }
+
+ .unittest-exception {
+ color: red;
+ font-weight: bold;
+ }
+
+ .unittest-failure {
+ border: 1px solid grey;
+ border-radius: 5px;
+ padding: 5px;
+ }
+</style>
+<template id="x-html-test-case-result-template">
+ <div id="summary">
+ <span id="title"></span>&nbsp;
+ <span id="status"></span>&nbsp;
+ <span id="return-value"></span>
+ </div>
+ <div id="details"></div>
+</template>
+
+<template id="x-html-test-case-error-template">
+ <div id="stack"></div>
+</template>
+
+<template id="x-html-test-case-flaky-template">
+ <div id="message"></div>
+</template>
+
+<template id="x-html-test-case-skipped-template">
+ <div id="message"></div>
+</template>
+
+<script>
+'use strict';
+tr.exportTo('tr.b.unittest', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const TestStatus = tr.b.unittest.TestStatus;
+ const TestTypes = tr.b.unittest.TestTypes;
+
+ /**
+ * @constructor
+ */
+ const HTMLTestCaseResult = tr.ui.b.define('x-html-test-case-result');
+
+ HTMLTestCaseResult.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ Polymer.dom(this).appendChild(tr.ui.b.instantiateTemplate(
+ '#x-html-test-case-result-template', THIS_DOC));
+ this.testCase_ = undefined;
+ this.testCaseHRef_ = undefined;
+ this.duration_ = undefined;
+ this.testStatus_ = TestStatus.PENDING;
+ this.testReturnValue_ = undefined;
+ this.showHTMLOutput_ = false;
+ this.updateColorAndStatus_();
+ },
+
+ get showHTMLOutput() {
+ return this.showHTMLOutput_;
+ },
+
+ set showHTMLOutput(showHTMLOutput) {
+ this.showHTMLOutput_ = showHTMLOutput;
+ this.updateHTMLOutputDisplayState_();
+ },
+
+ get testCase() {
+ return this.testCase_;
+ },
+
+ set testCase(testCase) {
+ this.testCase_ = testCase;
+ this.updateTitle_();
+ },
+
+ get testCaseHRef() {
+ return this.testCaseHRef_;
+ },
+
+ set testCaseHRef(href) {
+ this.testCaseHRef_ = href;
+ this.updateTitle_();
+ },
+ updateTitle_() {
+ const titleEl = Polymer.dom(this).querySelector('#title');
+ if (this.testCase_ === undefined) {
+ Polymer.dom(titleEl).textContent = '';
+ return;
+ }
+
+ if (this.testCaseHRef_) {
+ Polymer.dom(titleEl).innerHTML =
+ '<a href="' + this.testCaseHRef_ + '">' +
+ this.testCase_.fullyQualifiedName + '</a>';
+ } else {
+ Polymer.dom(titleEl).textContent = this.testCase_.fullyQualifiedName;
+ }
+ },
+
+ addError(normalizedException) {
+ const errorEl = document.createElement('x-html-test-case-error');
+ Polymer.dom(errorEl).appendChild(tr.ui.b.instantiateTemplate(
+ '#x-html-test-case-error-template', THIS_DOC));
+ Polymer.dom(Polymer.dom(errorEl).querySelector('#stack')).
+ textContent = normalizedException.stack;
+ Polymer.dom(Polymer.dom(this).querySelector('#details')).appendChild(
+ errorEl);
+ this.updateColorAndStatus_();
+ },
+
+ addFlaky() {
+ const flakyEl = document.createElement('x-html-test-case-flaky');
+ Polymer.dom(flakyEl).appendChild(tr.ui.b.instantiateTemplate(
+ '#x-html-test-case-flaky-template', THIS_DOC));
+ Polymer.dom(Polymer.dom(flakyEl).querySelector('#message'))
+ .textContent = 'FLAKY';
+ Polymer.dom(Polymer.dom(this).querySelector('#details')).appendChild(
+ flakyEl);
+ this.updateColorAndStatus_();
+ },
+
+ addSkipped() {
+ const skippedEl = document.createElement('x-html-test-case-skipped');
+ Polymer.dom(skippedEl).appendChild(tr.ui.b.instantiateTemplate(
+ '#x-html-test-case-skipped-template', THIS_DOC));
+ Polymer.dom(Polymer.dom(skippedEl).querySelector('#message'))
+ .textContent = 'SKIPPED';
+ Polymer.dom(Polymer.dom(this).querySelector('#details')).appendChild(
+ skippedEl);
+ this.updateColorAndStatus_();
+ },
+
+ addHTMLOutput(element) {
+ const htmlResultEl = document.createElement(
+ 'x-html-test-case-html-result');
+ Polymer.dom(htmlResultEl).appendChild(element);
+ Polymer.dom(Polymer.dom(this).querySelector('#details'))
+ .appendChild(htmlResultEl);
+ },
+
+ updateHTMLOutputDisplayState_() {
+ const htmlResults =
+ Polymer.dom(this).querySelectorAll('x-html-test-case-html-result');
+ let display;
+ if (this.showHTMLOutput) {
+ display = '';
+ } else {
+ display = (this.testStatus_ === TestStatus.RUNNING) ? '' : 'none';
+ }
+ for (let i = 0; i < htmlResults.length; i++) {
+ htmlResults[i].style.display = display;
+ }
+ },
+
+ get hadErrors() {
+ return !!Polymer.dom(this).querySelector('x-html-test-case-error');
+ },
+
+ get isFlaky() {
+ return !!Polymer.dom(this).querySelector('x-html-test-case-flaky');
+ },
+
+ get isSkipped() {
+ return !!Polymer.dom(this).querySelector('x-html-test-case-skipped');
+ },
+
+ get duration() {
+ return this.duration_;
+ },
+
+ set duration(duration) {
+ this.duration_ = duration;
+ this.updateColorAndStatus_();
+ },
+
+ get testStatus() {
+ return this.testStatus_;
+ },
+
+ set testStatus(testStatus) {
+ this.testStatus_ = testStatus;
+ this.updateColorAndStatus_();
+ this.updateHTMLOutputDisplayState_();
+ },
+
+ updateColorAndStatus_() {
+ let colorCls;
+ let status;
+ if (this.hadErrors) {
+ colorCls = 'unittest-failed';
+ status = 'failed';
+ } else if (this.isFlaky) {
+ colorCls = 'unittest-flaky';
+ status = 'flaky';
+ } else if (this.isSkipped) {
+ colorCls = 'unittest-skipped';
+ status = 'skipped';
+ } else if (this.testStatus_ === TestStatus.PENDING) {
+ colorCls = 'unittest-pending';
+ status = 'pending';
+ } else if (this.testStatus_ === TestStatus.RUNNING) {
+ colorCls = 'unittest-running';
+ status = 'running';
+ } else { // DONE_RUNNING and no errors
+ colorCls = 'unittest-passed';
+ status = 'passed';
+ }
+
+ const statusEl = Polymer.dom(this).querySelector('#status');
+ if (this.duration_) {
+ Polymer.dom(statusEl).textContent = status + ' (' +
+ this.duration_.toFixed(2) + 'ms)';
+ } else {
+ Polymer.dom(statusEl).textContent = status;
+ }
+ statusEl.className = colorCls;
+ },
+
+ get testReturnValue() {
+ return this.testReturnValue_;
+ },
+
+ set testReturnValue(testReturnValue) {
+ this.testReturnValue_ = testReturnValue;
+ Polymer.dom(Polymer.dom(this).querySelector('#return-value'))
+ .textContent = testReturnValue;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ const HTMLTestResults = tr.ui.b.define('x-tr-b-unittest-test-results');
+
+ HTMLTestResults.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ decorate() {
+ this.testCaseResultsByCaseGUID_ = {};
+ this.currentTestCaseStartTime_ = undefined;
+ this.totalRunTime_ = 0;
+ this.numTestsThatPassed_ = 0;
+ this.numTestsThatFailed_ = 0;
+ this.numFlakyTests_ = 0;
+ this.numSkippedTests_ = 0;
+ this.showHTMLOutput_ = false;
+ this.showPendingAndPassedTests_ = false;
+ this.linkifyCallback_ = undefined;
+ this.headless_ = false;
+ },
+
+ get headless() {
+ return this.headless_;
+ },
+
+ set headless(headless) {
+ this.headless_ = headless;
+ },
+
+ getHRefForTestCase(testCase) {
+ /* Override this to create custom links */
+ return undefined;
+ },
+
+ get showHTMLOutput() {
+ return this.showHTMLOutput_;
+ },
+
+ set showHTMLOutput(showHTMLOutput) {
+ this.showHTMLOutput_ = showHTMLOutput;
+ const testCaseResults =
+ Polymer.dom(this).querySelectorAll('x-html-test-case-result');
+ for (let i = 0; i < testCaseResults.length; i++) {
+ testCaseResults[i].showHTMLOutput = showHTMLOutput;
+ }
+ },
+
+ get showPendingAndPassedTests() {
+ return this.showPendingAndPassedTests_;
+ },
+
+ set showPendingAndPassedTests(showPendingAndPassedTests) {
+ this.showPendingAndPassedTests_ = showPendingAndPassedTests;
+
+ const testCaseResults =
+ Polymer.dom(this).querySelectorAll('x-html-test-case-result');
+ for (let i = testCaseResults.length - 1; i >= 0; i--) {
+ this.updateDisplayStateForResult_(testCaseResults[i]);
+ }
+ },
+
+ updateDisplayStateForResult_(res) {
+ let display;
+ if (this.showPendingAndPassedTests_) {
+ if (res.testStatus === TestStatus.RUNNING ||
+ res.hadErrors) {
+ display = '';
+ } else {
+ display = 'none';
+ }
+ } else {
+ display = '';
+ }
+ res.style.display = display;
+ },
+
+ willRunTests(testCases) {
+ this.timeAtBeginningOfTest_ = window.performance.now();
+ testCases.forEach(function(testCase, i) {
+ const testCaseResult = new HTMLTestCaseResult();
+ testCaseResult.showHTMLOutput = this.showHTMLOutput_;
+ testCaseResult.testCase = testCase;
+ if ((i % 2) === 0) {
+ Polymer.dom(testCaseResult).classList.add('dark');
+ }
+
+ const href = this.getHRefForTestCase(testCase);
+ if (href) {
+ testCaseResult.testCaseHRef = href;
+ }
+ testCaseResult.testStatus = TestStatus.PENDING;
+ this.testCaseResultsByCaseGUID_[testCase.guid] = testCaseResult;
+ Polymer.dom(this).appendChild(testCaseResult);
+ this.updateDisplayStateForResult_(testCaseResult);
+ }, this);
+ },
+
+ willRunTest(testCase) {
+ this.currentTestCaseResult_ = this.testCaseResultsByCaseGUID_[
+ testCase.guid];
+ this.currentTestCaseStartTime_ = window.performance.now();
+ this.currentTestCaseResult_.testStatus = TestStatus.RUNNING;
+ this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+ this.log_(testCase.fullyQualifiedName + ': ');
+ },
+
+ addErrorForCurrentTest(error) {
+ this.log_('\n');
+
+ const normalizedException = tr.b.normalizeException(error);
+ this.log_('Exception: ' + normalizedException.message + '\n' +
+ normalizedException.stack);
+
+ this.currentTestCaseResult_.addError(normalizedException);
+ this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+ if (this.headless_) {
+ this.notifyTestResultToDevServer_('EXCEPT', normalizedException.stack);
+ }
+ },
+
+ addHTMLOutputForCurrentTest(element) {
+ this.currentTestCaseResult_.addHTMLOutput(element);
+ this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+ },
+
+ setCurrentTestFlaky() {
+ this.currentTestCaseResult_.addFlaky();
+ this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+ },
+
+ setCurrentTestSkipped() {
+ this.currentTestCaseResult_.addSkipped();
+ this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+ },
+
+ setReturnValueFromCurrentTest(returnValue) {
+ this.currentTestCaseResult_.testReturnValue = returnValue;
+ },
+
+ didCurrentTestEnd() {
+ const now = window.performance.now();
+ const testCaseResult = this.currentTestCaseResult_;
+ const testCaseDuration = now - this.currentTestCaseStartTime_;
+ this.currentTestCaseResult_.testStatus = TestStatus.DONE_RUNNING;
+ testCaseResult.duration = testCaseDuration;
+ this.totalRunTime_ = now - this.timeAtBeginningOfTest_;
+ let resultString;
+ if (testCaseResult.hadErrors) {
+ resultString = 'FAILED';
+ this.numTestsThatFailed_ += 1;
+ tr.b.dispatchSimpleEvent(this, 'testfailed');
+ } else if (testCaseResult.isFlaky) {
+ resultString = 'FLAKY';
+ this.numFlakyTests_ += 1;
+ tr.b.dispatchSimpleEvent(this, 'testflaky');
+ } else if (testCaseResult.isSkipped) {
+ resultString = 'SKIPPED';
+ this.numSkippedTests_ += 1;
+ tr.b.dispatchSimpleEvent(this, 'testskipped');
+ } else {
+ resultString = 'PASSED';
+ this.numTestsThatPassed_ += 1;
+ tr.b.dispatchSimpleEvent(this, 'testpassed');
+ }
+ this.log_('[' + resultString + ']\n');
+
+ if (this.headless_) {
+ this.notifyTestResultToDevServer_(resultString);
+ }
+
+ this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+ this.currentTestCaseResult_ = undefined;
+ },
+
+ didRunTests() {
+ this.log_('[DONE]\n');
+ if (this.headless_) {
+ this.notifyTestCompletionToDevServer_();
+ }
+ },
+
+ getStats() {
+ return {
+ numTestsThatPassed: this.numTestsThatPassed_,
+ numTestsThatFailed: this.numTestsThatFailed_,
+ numFlakyTests: this.numFlakyTests_,
+ numSkippedTests: this.numSkippedTests_,
+ totalRunTime: this.totalRunTime_
+ };
+ },
+
+ didAllTestsPass() {
+ // A test counts as failing if it failed or it was skipped.
+ return this.numTestsThatFailed_ + this.numSkippedTests_ === 0;
+ },
+
+ notifyTestResultToDevServer_(result, extraMsg) {
+ const req = new XMLHttpRequest();
+ const testName = this.currentTestCaseResult_.testCase.fullyQualifiedName;
+ const data = result + ' ' + testName + ' ' + (extraMsg || '');
+ tr.b.postAsync('/tracing/notify_test_result', data);
+ },
+
+ notifyTestCompletionToDevServer_() {
+ if (this.numTestsThatPassed_ + this.numTestsThatFailed_ +
+ this.numFlakyTests_ + this.numSkippedTests_ === 0) {
+ return;
+ }
+ let data = this.didAllTestsPass() ? 'ALL_PASSED' : 'HAD_FAILURES';
+ data += '\nPassed tests: ' + this.numTestsThatPassed_ +
+ ' Failed tests: ' + this.numTestsThatFailed_ +
+ ' Skipped tests: ' + this.numSkippedTests_ +
+ ' Flaky tests: ' + this.numFlakyTests_;
+
+ tr.b.postAsync('/tracing/notify_tests_completed', data);
+ },
+
+ log_(msg) {
+ tr.b.dispatchSimpleEvent(this, 'statschange');
+ }
+ };
+
+ return {
+ HTMLTestResults,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/interactive_test_runner.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/interactive_test_runner.html
new file mode 100644
index 00000000000..d83012033b3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/interactive_test_runner.html
@@ -0,0 +1,735 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unittest.html">
+<link rel="import" href="/tracing/base/unittest/html_test_results.html">
+<link rel="import" href="/tracing/base/unittest/suite_loader.html">
+<link rel="import" href="/tracing/base/unittest/test_runner.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<style>
+ x-base-interactive-test-runner {
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+ }
+
+ x-base-interactive-test-runner > * {
+ flex: 0 0 auto;
+ }
+ x-base-interactive-test-runner > #title {
+ font-size: 16pt;
+ }
+
+ x-base-interactive-test-runner {
+ font-family: sans-serif;
+ }
+
+ x-base-interactive-test-runner > h1 {
+ margin: 5px 0px 10px 0px;
+ }
+
+ x-base-interactive-test-runner > #stats {
+ }
+
+ x-base-interactive-test-runner > #controls {
+ display: block;
+ margin-bottom: 5px;
+ }
+
+ x-base-interactive-test-runner > #controls > ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ x-base-interactive-test-runner > #controls > ul > li {
+ float: left;
+ margin-right: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ }
+
+ x-base-interactive-test-runner > #shortform-results {
+ color: green;
+ height; 40px;
+ word-wrap: break-word;
+ }
+
+ x-base-interactive-test-runner > #shortform-results > .fail {
+ color: darkred;
+ font-weight: bold;
+ }
+
+ x-base-interactive-test-runner > #shortform-results > .flaky {
+ color: darkorange;
+ }
+
+ x-base-interactive-test-runner > #shortform-results > .skipped {
+ color: blue;
+ }
+
+ x-base-interactive-test-runner > #results-container {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ padding: 0 4px 0 4px;
+ }
+
+ .unittest-pending {
+ color: orange;
+ }
+ .unittest-running {
+ color: orange;
+ font-weight: bold;
+ }
+
+ .unittest-passed {
+ color: darkgreen;
+ }
+
+ .unittest-failed {
+ color: darkred;
+ font-weight: bold;
+ }
+
+ .unittest-flaky {
+ color: darkorange;
+ }
+
+ .unittest-exception {
+ color: red;
+ font-weight: bold;
+ }
+
+ .unittest-failure {
+ border: 1px solid grey;
+ border-radius: 5px;
+ padding: 5px;
+ }
+</style>
+
+<template id="x-base-interactive-test-runner-template">
+ <h1 id="title">Tests</h1>
+ <div id="stats"></div>
+ <div id="controls">
+ <ul id="links">
+ </ul>
+ <div style="clear: both;"></div>
+
+ <div>
+ <span>
+ <label>
+ <input type="radio" name="test-type-to-run" value="unit" />
+ Run unit tests
+ </label>
+ </span>
+ <span>
+ <label>
+ <input type="radio" name="test-type-to-run" value="perf" />
+ Run perf tests
+ </label>
+ </span>
+ <span>
+ <label>
+ <input type="radio" name="test-type-to-run" value="all" />
+ Run all tests
+ </label>
+ </span>
+ </div>
+ <span>
+ <label>
+ <input type="checkbox" id="short-format" /> Short format</label>
+ </span>
+ </div>
+ <div id="shortform-results">
+ </div>
+ <div id="results-container">
+ </div>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b.unittest', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const ALL_TEST_TYPES = 'all';
+
+ /**
+ * @constructor
+ */
+ const InteractiveTestRunner = tr.ui.b.define(
+ 'x-base-interactive-test-runner');
+
+ InteractiveTestRunner.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.allTests_ = undefined;
+
+ this.suppressStateChange_ = false;
+
+ this.testFilterString_ = '';
+ this.testTypeToRun_ = tr.b.unittest.TestTypes.UNITTEST;
+ this.shortFormat_ = false;
+ this.testSuiteName_ = '';
+
+ this.rerunPending_ = false;
+ this.runner_ = undefined;
+ this.results_ = undefined;
+ this.headless_ = false;
+
+ this.onResultsStatsChanged_ = this.onResultsStatsChanged_.bind(this);
+ this.onTestFailed_ = this.onTestFailed_.bind(this);
+ this.onTestFlaky_ = this.onTestFlaky_.bind(this);
+ this.onTestSkipped_ = this.onTestSkipped_.bind(this);
+ this.onTestPassed_ = this.onTestPassed_.bind(this);
+
+ Polymer.dom(this).appendChild(tr.ui.b.instantiateTemplate(
+ '#x-base-interactive-test-runner-template', THIS_DOC));
+
+ Polymer.dom(this).querySelector(
+ 'input[name=test-type-to-run][value=unit]').checked = true;
+ const testTypeToRunEls = Array.from(Polymer.dom(this).querySelectorAll(
+ 'input[name=test-type-to-run]'));
+
+ testTypeToRunEls.forEach(
+ function(inputEl) {
+ inputEl.addEventListener(
+ 'click', this.onTestTypeToRunClick_.bind(this));
+ }, this);
+
+ const shortFormatEl = Polymer.dom(this).querySelector('#short-format');
+ shortFormatEl.checked = this.shortFormat_;
+ shortFormatEl.addEventListener(
+ 'click', this.onShortFormatClick_.bind(this));
+ this.updateShortFormResultsDisplay_();
+
+ // Oh, DOM, how I love you. Title is such a convenient property name and I
+ // refuse to change my worldview because of tooltips.
+ this.__defineSetter__(
+ 'title',
+ function(title) {
+ Polymer.dom(Polymer.dom(this).querySelector('#title')).textContent =
+ title;
+ });
+ },
+
+ get allTests() {
+ return this.allTests_;
+ },
+
+ set allTests(allTests) {
+ this.allTests_ = allTests;
+ this.scheduleRerun_();
+ },
+
+ get testLinks() {
+ return this.testLinks_;
+ },
+ set testLinks(testLinks) {
+ this.testLinks_ = testLinks;
+ const linksEl = Polymer.dom(this).querySelector('#links');
+ Polymer.dom(linksEl).textContent = '';
+ this.testLinks_.forEach(function(l) {
+ const link = document.createElement('a');
+ link.href = l.linkPath;
+ Polymer.dom(link).textContent = l.title;
+
+ const li = document.createElement('li');
+ Polymer.dom(li).appendChild(link);
+
+ Polymer.dom(linksEl).appendChild(li);
+ }, this);
+ },
+
+ get testFilterString() {
+ return this.testFilterString_;
+ },
+
+ set testFilterString(testFilterString) {
+ this.testFilterString_ = testFilterString;
+ this.scheduleRerun_();
+ if (!this.suppressStateChange_) {
+ tr.b.dispatchSimpleEvent(this, 'statechange');
+ }
+ },
+
+ get shortFormat() {
+ return this.shortFormat_;
+ },
+
+ set shortFormat(shortFormat) {
+ this.shortFormat_ = shortFormat;
+ Polymer.dom(this).querySelector('#short-format').checked = shortFormat;
+ if (this.results_) {
+ this.results_.shortFormat = shortFormat;
+ }
+ if (!this.suppressStateChange_) {
+ tr.b.dispatchSimpleEvent(this, 'statechange');
+ }
+ },
+
+ onShortFormatClick_(e) {
+ this.shortFormat_ =
+ Polymer.dom(this).querySelector('#short-format').checked;
+ this.updateShortFormResultsDisplay_();
+ this.updateResultsGivenShortFormat_();
+ if (!this.suppressStateChange_) {
+ tr.b.dispatchSimpleEvent(this, 'statechange');
+ }
+ },
+
+ updateShortFormResultsDisplay_() {
+ const display = this.shortFormat_ ? '' : 'none';
+ Polymer.dom(this).querySelector('#shortform-results').style.display =
+ display;
+ },
+
+ updateResultsGivenShortFormat_() {
+ if (!this.results_) return;
+
+ if (this.testFilterString_.length || this.testSuiteName_.length) {
+ this.results_.showHTMLOutput = true;
+ } else {
+ this.results_.showHTMLOutput = false;
+ }
+ this.results_.showPendingAndPassedTests = this.shortFormat_;
+ },
+
+ get testTypeToRun() {
+ return this.testTypeToRun_;
+ },
+
+ set testTypeToRun(testTypeToRun) {
+ this.testTypeToRun_ = testTypeToRun;
+ let sel;
+ switch (testTypeToRun) {
+ case tr.b.unittest.TestTypes.UNITTEST:
+ sel = 'input[name=test-type-to-run][value=unit]';
+ break;
+ case tr.b.unittest.TestTypes.PERFTEST:
+ sel = 'input[name=test-type-to-run][value=perf]';
+ break;
+ case ALL_TEST_TYPES:
+ sel = 'input[name=test-type-to-run][value=all]';
+ break;
+ default:
+ throw new Error('Invalid test type to run: ' + testTypeToRun);
+ }
+ Polymer.dom(this).querySelector(sel).checked = true;
+ this.scheduleRerun_();
+ if (!this.suppressStateChange_) {
+ tr.b.dispatchSimpleEvent(this, 'statechange');
+ }
+ },
+
+ onTestTypeToRunClick_(e) {
+ switch (e.target.value) {
+ case 'unit':
+ this.testTypeToRun_ = tr.b.unittest.TestTypes.UNITTEST;
+ break;
+ case 'perf':
+ this.testTypeToRun_ = tr.b.unittest.TestTypes.PERFTEST;
+ break;
+ case 'all':
+ this.testTypeToRun_ = ALL_TEST_TYPES;
+ break;
+ default:
+ throw new Error('Inalid test type: ' + e.target.value);
+ }
+
+ this.scheduleRerun_();
+ if (!this.suppressStateChange_) {
+ tr.b.dispatchSimpleEvent(this, 'statechange');
+ }
+ },
+
+ onTestPassed_() {
+ Polymer.dom(Polymer.dom(this).querySelector('#shortform-results')).
+ appendChild(document.createTextNode('.'));
+ },
+
+ onTestFailed_() {
+ const span = document.createElement('span');
+ Polymer.dom(span).classList.add('fail');
+ Polymer.dom(span).appendChild(document.createTextNode('F'));
+ Polymer.dom(Polymer.dom(this).querySelector('#shortform-results'))
+ .appendChild(span);
+ },
+
+ onTestFlaky_() {
+ const span = document.createElement('span');
+ Polymer.dom(span).classList.add('flaky');
+ Polymer.dom(span).appendChild(document.createTextNode('~'));
+ Polymer.dom(Polymer.dom(this).querySelector('#shortform-results'))
+ .appendChild(span);
+ },
+
+ onTestSkipped_() {
+ const span = document.createElement('span');
+ Polymer.dom(span).classList.add('skipped');
+ Polymer.dom(span).appendChild(document.createTextNode('s'));
+ Polymer.dom(Polymer.dom(this).querySelector('#shortform-results'))
+ .appendChild(span);
+ },
+
+ onResultsStatsChanged_() {
+ const statsEl = Polymer.dom(this).querySelector('#stats');
+ const stats = this.results_.getStats();
+ const numTestsOverall = this.runner_.testCases.length;
+ const numTestsThatRan = stats.numTestsThatPassed +
+ stats.numTestsThatFailed +
+ stats.numFlakyTests +
+ stats.numSkippedTests;
+ Polymer.dom(statsEl).innerHTML =
+ '<span>' + numTestsThatRan + '/' + numTestsOverall +
+ '</span> tests run, ' +
+ '<span class="unittest-failed">' + stats.numTestsThatFailed +
+ '</span> failures, ' +
+ '<span class="unittest-flaky">' + stats.numFlakyTests +
+ '</span> flaky, ' +
+ '<span class="unittest-skipped">' + stats.numSkippedTests +
+ '</span> skipped, ' +
+ ' in ' + stats.totalRunTime.toFixed(2) + 'ms.';
+ },
+
+ scheduleRerun_() {
+ if (this.rerunPending_) return;
+ if (this.runner_) {
+ this.rerunPending_ = true;
+ this.runner_.beginToStopRunning();
+ const doRerun = function() {
+ this.rerunPending_ = false;
+ this.scheduleRerun_();
+ }.bind(this);
+ this.runner_.runCompletedPromise.then(
+ doRerun, doRerun);
+ return;
+ }
+ this.beginRunning_();
+ },
+
+ beginRunning_() {
+ const resultsContainer =
+ Polymer.dom(this).querySelector('#results-container');
+ if (this.results_) {
+ this.results_.removeEventListener('testpassed', this.onTestPassed_);
+ this.results_.removeEventListener('testfailed', this.onTestFailed_);
+ this.results_.removeEventListener('testflaky', this.onTestFlaky_);
+ this.results_.removeEventListener('testskipped', this.onTestSkipped_);
+ this.results_.removeEventListener('statschange',
+ this.onResultsStatsChanged_);
+ delete this.results_.getHRefForTestCase;
+ Polymer.dom(resultsContainer).removeChild(this.results_);
+ }
+
+ this.results_ = new tr.b.unittest.HTMLTestResults();
+ this.results_.headless = this.headless_;
+ this.results_.getHRefForTestCase = this.getHRefForTestCase.bind(this);
+ this.updateResultsGivenShortFormat_();
+
+ this.results_.shortFormat = this.shortFormat_;
+ this.results_.addEventListener('testpassed', this.onTestPassed_);
+ this.results_.addEventListener('testfailed', this.onTestFailed_);
+ this.results_.addEventListener('testflaky', this.onTestFlaky_);
+ this.results_.addEventListener('testskipped', this.onTestSkipped_);
+ this.results_.addEventListener('statschange',
+ this.onResultsStatsChanged_);
+ Polymer.dom(resultsContainer).appendChild(this.results_);
+
+ const tests = this.allTests_.filter(function(test) {
+ const i = test.fullyQualifiedName.indexOf(this.testFilterString_);
+ if (i === -1) return false;
+ if (this.testTypeToRun_ !== ALL_TEST_TYPES &&
+ test.testType !== this.testTypeToRun_) {
+ return false;
+ }
+ return true;
+ }, this);
+
+ this.runner_ = new tr.b.unittest.TestRunner(this.results_, tests);
+ this.runner_.beginRunning();
+
+ this.runner_.runCompletedPromise.then(
+ this.runCompleted_.bind(this),
+ this.runCompleted_.bind(this));
+ },
+
+ setState(state, opt_suppressStateChange) {
+ this.suppressStateChange_ = true;
+ if (state.testFilterString !== undefined) {
+ this.testFilterString = state.testFilterString;
+ } else {
+ this.testFilterString = '';
+ }
+
+ if (state.shortFormat === undefined) {
+ this.shortFormat = false;
+ } else {
+ this.shortFormat = state.shortFormat;
+ }
+
+ if (state.testTypeToRun === undefined) {
+ this.testTypeToRun = tr.b.unittest.TestTypes.UNITTEST;
+ } else {
+ this.testTypeToRun = state.testTypeToRun;
+ }
+
+ this.testSuiteName_ = state.testSuiteName || '';
+ this.headless_ = state.headless || false;
+
+ if (!opt_suppressStateChange) {
+ this.suppressStateChange_ = false;
+ }
+
+ this.onShortFormatClick_();
+ this.scheduleRerun_();
+ this.suppressStateChange_ = false;
+ },
+
+ getDefaultState() {
+ return {
+ testFilterString: '',
+ testSuiteName: '',
+ shortFormat: false,
+ testTypeToRun: tr.b.unittest.TestTypes.UNITTEST
+ };
+ },
+
+ getState() {
+ return {
+ testFilterString: this.testFilterString_,
+ testSuiteName: this.testSuiteName_,
+ shortFormat: this.shortFormat_,
+ testTypeToRun: this.testTypeToRun_
+ };
+ },
+
+ getHRefForTestCase(testCases) {
+ return undefined;
+ },
+
+ runCompleted_() {
+ this.runner_ = undefined;
+ }
+ };
+
+ function loadAndRunTests(runnerConfig) {
+ // The test runner no-ops pushState so keep it around.
+ const realWindowHistoryPushState = window.history.pushState.bind(
+ window.history);
+
+ function stateToSearchString(defaultState, state) {
+ const parts = [];
+ for (const k in state) {
+ if (state[k] === defaultState[k]) continue;
+ const v = state[k];
+ let kv;
+ if (v === true) {
+ kv = k;
+ } else if (v === false) {
+ kv = k + '=false';
+ } else if (v === '') {
+ continue;
+ } else {
+ kv = k + '=' + v;
+ }
+ parts.push(kv);
+ }
+ return parts.join('&');
+ }
+
+ function stateFromSearchString(string) {
+ const state = {};
+ string.split('&').forEach(function(part) {
+ if (part === '') return;
+ const kv = part.split('=');
+ let k;
+ let v;
+ if (kv.length === 1) {
+ k = kv[0];
+ v = true;
+ } else {
+ k = kv[0];
+ if (kv[1] === 'false') {
+ v = false;
+ } else {
+ v = kv[1];
+ }
+ }
+ state[k] = v;
+ });
+ return state;
+ }
+
+ function getSuiteRelpathsToLoad(state) {
+ if (state.testSuiteName) {
+ return new Promise(function(resolve) {
+ const parts = state.testSuiteName.split('.');
+ const testSuiteRelPath = '/' + parts.join('/') + '.html';
+
+ const suiteRelpathsToLoad = [testSuiteRelPath];
+ resolve(suiteRelpathsToLoad);
+ });
+ }
+ return runnerConfig.getAllSuiteRelPathsAsync();
+ }
+
+
+ function loadAndRunTestsImpl() {
+ const state = stateFromSearchString(
+ window.location.search.substring(1));
+ updateTitle(state);
+
+
+ showLoadingOverlay();
+
+ let loader;
+ let p = getSuiteRelpathsToLoad(state);
+ p = p.then(
+ function(suiteRelpathsToLoad) {
+ loader = new tr.b.unittest.SuiteLoader(suiteRelpathsToLoad);
+ return loader.allSuitesLoadedPromise;
+ },
+ function(e) {
+ hideLoadingOverlay();
+ throw e;
+ });
+ p = p.then(
+ function() {
+ hideLoadingOverlay();
+ // FIXME
+ window.addEventListener('WebComponentsReady', function() {
+ runTests(loader, state);
+ });
+ runTests(loader, state);
+ // Polymer.whenReady(function() {
+ // runTests(loader, state);
+ // });
+ },
+ function(err) {
+ hideLoadingOverlay();
+ tr.showPanic('Module loading failure', err);
+ throw err;
+ });
+ return p;
+ }
+
+ function showLoadingOverlay() {
+ const overlay = document.createElement('div');
+ overlay.id = 'tests-loading-overlay';
+ overlay.style.backgroundColor = 'white';
+ overlay.style.boxSizing = 'border-box';
+ overlay.style.color = 'black';
+ overlay.style.display = 'flex';
+ overlay.style.height = '100%';
+ overlay.style.left = 0;
+ overlay.style.padding = '8px';
+ overlay.style.position = 'fixed';
+ overlay.style.top = 0;
+ overlay.style.flexDirection = 'column';
+ overlay.style.width = '100%';
+
+ const element = document.createElement('div');
+ element.style.flex = '1 1 auto';
+ element.style.overflow = 'auto';
+ Polymer.dom(overlay).appendChild(element);
+
+ Polymer.dom(element).textContent = 'Loading tests...';
+ Polymer.dom(document.body).appendChild(overlay);
+ }
+ function hideLoadingOverlay() {
+ const overlay = Polymer.dom(document.body).querySelector(
+ '#tests-loading-overlay');
+ Polymer.dom(document.body).removeChild(overlay);
+ }
+
+ function updateTitle(state) {
+ const testFilterString = state.testFilterString || '';
+ const testSuiteName = state.testSuiteName || '';
+
+ let title;
+ if (testSuiteName && testFilterString.length) {
+ title = testFilterString + ' in ' + testSuiteName;
+ } else if (testSuiteName) {
+ title = testSuiteName;
+ } else if (testFilterString) {
+ title = testFilterString + ' in all tests';
+ } else {
+ title = runnerConfig.title;
+ }
+
+ if (state.shortFormat) title += '(s)';
+ document.title = title;
+ const runner = Polymer.dom(document).querySelector(
+ 'x-base-interactive-test-runner');
+ if (runner) runner.title = title;
+ }
+
+ function runTests(loader, state) {
+ const runner = new tr.b.unittest.InteractiveTestRunner();
+ runner.style.width = '100%';
+ runner.style.height = '100%';
+ runner.testLinks = runnerConfig.testLinks;
+ runner.allTests = loader.getAllTests();
+ Polymer.dom(document.body).appendChild(runner);
+
+ runner.setState(state);
+ updateTitle(state);
+
+ runner.addEventListener('statechange', function() {
+ const state = runner.getState();
+ const stateString = stateToSearchString(
+ runner.getDefaultState(), state);
+ if (window.location.search.substring(1) === stateString) return;
+
+ updateTitle(state);
+ let stateURL;
+ if (stateString.length > 0) {
+ stateURL = window.location.pathname + '?' + stateString;
+ } else {
+ stateURL = window.location.pathname;
+ }
+ realWindowHistoryPushState(state, document.title, stateURL);
+ });
+
+ window.addEventListener('popstate', function(state) {
+ runner.setState(state, true);
+ });
+
+ runner.getHRefForTestCase = function(testCase) {
+ const state = runner.getState();
+ if (state.testFilterString === '' &&
+ state.testSuiteName === '') {
+ state.testSuiteName = testCase.suite.name;
+ state.testFilterString = '';
+ state.shortFormat = false;
+ } else {
+ state.testSuiteName = testCase.suite.name;
+ state.testFilterString = testCase.name;
+ state.shortFormat = false;
+ }
+ const stateString = stateToSearchString(
+ runner.getDefaultState(), state);
+ if (stateString.length > 0) {
+ return window.location.pathname + '?' + stateString;
+ }
+ return window.location.pathname;
+ };
+ }
+
+ loadAndRunTestsImpl();
+ }
+
+ return {
+ InteractiveTestRunner,
+ loadAndRunTests,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/suite_loader.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/suite_loader.html
new file mode 100644
index 00000000000..6c14bd5afbf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/suite_loader.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/unittest/test_suite.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b.unittest', function() {
+ function HTMLImportsModuleLoader() {
+ }
+ HTMLImportsModuleLoader.prototype = {
+ loadModule(testRelpath, moduleName) {
+ return new Promise(function(resolve, reject) {
+ const importEl = document.createElement('link');
+ importEl.moduleName = moduleName;
+ Polymer.dom(importEl).setAttribute('rel', 'import');
+ Polymer.dom(importEl).setAttribute('href', testRelpath);
+
+ importEl.addEventListener('load', function() {
+ resolve({testRelpath,
+ moduleName});
+ });
+ importEl.addEventListener('error', function(e) {
+ reject('Error loading &#60;link rel="import" href="' +
+ testRelpath + '"');
+ });
+
+ Polymer.dom(tr.doc.head).appendChild(importEl);
+ });
+ },
+
+ getCurrentlyExecutingModuleName() {
+ if (!document.currentScript) {
+ throw new Error('Cannot call testSuite except during load.');
+ }
+ try {
+ throw new Error('');
+ } catch (e) {
+ const stack = e.stack.split('\n');
+ let url = stack[stack.length - 1].slice(7);
+ url = url.slice(0, url.lastIndexOf(':'));
+ url = url.slice(0, url.lastIndexOf(':')); // Yes, again.
+ return this.guessModuleNameFromURL_(url);
+ }
+ },
+
+ guessModuleNameFromURL_(url) {
+ const m = /.+?:\/\/.+?(\/.+)/.exec(url);
+ if (!m) {
+ throw new Error('Guessing module name failed');
+ }
+ const path = m[1];
+ if (path[0] !== '/') {
+ throw new Error('malformed path');
+ }
+ const i = path.indexOf('.html');
+ if (i < 0) {
+ throw new Error('Cannot define testSuites outside html imports');
+ }
+ return path.substring(1, i).split('/').join('.');
+ }
+ };
+
+ function HeadlessModuleLoader() {
+ this.currentlyExecutingModuleInfo_ = undefined;
+ }
+ HeadlessModuleLoader.prototype = {
+ loadModule(testRelpath, moduleName) {
+ return Promise.resolve().then(function() {
+ const moduleInfo = {
+ testRelpath,
+ moduleName
+ };
+ if (this.currentlyExecutingModuleInfo_ !== undefined) {
+ throw new Error('WAT');
+ }
+ this.currentlyExecutingModuleInfo_ = moduleInfo;
+
+ try {
+ loadHTML(testRelpath);
+ } catch (e) {
+ e.message = 'While loading ' + moduleName + ', ' + e.message;
+ e.stack = 'While loading ' + moduleName + ', ' + e.stack;
+ throw e;
+ } finally {
+ this.currentlyExecutingModuleInfo_ = undefined;
+ }
+
+ return moduleInfo;
+ }.bind(this));
+ },
+
+ getCurrentlyExecutingModuleName() {
+ if (this.currentlyExecutingModuleInfo_ === undefined) {
+ throw new Error('No currently loading module');
+ }
+ return this.currentlyExecutingModuleInfo_.moduleName;
+ }
+ };
+
+
+ function SuiteLoader(suiteRelpathsToLoad) {
+ tr.b.EventTarget.call(this);
+
+ this.currentModuleLoader_ = undefined;
+ this.testSuites = [];
+
+ if (tr.isHeadless) {
+ this.currentModuleLoader_ = new HeadlessModuleLoader();
+ } else {
+ this.currentModuleLoader_ = new HTMLImportsModuleLoader();
+ }
+
+ this.allSuitesLoadedPromise = this.beginLoadingModules_(
+ suiteRelpathsToLoad);
+ }
+
+ SuiteLoader.prototype = {
+ __proto__: tr.b.EventTarget.prototype,
+
+ beginLoadingModules_(testRelpaths) {
+ // Hooks!
+ this.bindGlobalHooks_();
+
+ // Load the modules.
+ const modulePromises = [];
+ for (let i = 0; i < testRelpaths.length; i++) {
+ const testRelpath = testRelpaths[i];
+ const moduleName = testRelpath.split('/').slice(-1)[0];
+
+ const p = this.currentModuleLoader_.loadModule(testRelpath, moduleName);
+ modulePromises.push(p);
+ }
+
+ const allModulesLoadedPromise = new Promise(function(resolve, reject) {
+ let remaining = modulePromises.length;
+ let resolved = false;
+ function oneMoreLoaded() {
+ if (resolved) return;
+ remaining--;
+ if (remaining > 0) return;
+ resolved = true;
+ resolve();
+ }
+
+ function oneRejected(e) {
+ if (resolved) return;
+ resolved = true;
+ reject(e);
+ }
+
+ modulePromises.forEach(function(modulePromise) {
+ modulePromise.then(oneMoreLoaded, oneRejected);
+ });
+ });
+
+ // Script errors errors abort load;
+ const scriptErrorPromise = new Promise(function(xresolve, xreject) {
+ this.scriptErrorPromiseResolver_ = {
+ resolve: xresolve,
+ reject: xreject
+ };
+ }.bind(this));
+ const donePromise = Promise.race([
+ allModulesLoadedPromise,
+ scriptErrorPromise
+ ]);
+
+ // Cleanup.
+ return donePromise.then(
+ function() {
+ this.scriptErrorPromiseResolver_ = undefined;
+ this.unbindGlobalHooks_();
+ }.bind(this),
+ function(e) {
+ this.scriptErrorPromiseResolver_ = undefined;
+ this.unbindGlobalHooks_();
+ throw e;
+ }.bind(this));
+ },
+
+ bindGlobalHooks_() {
+ if (global._currentSuiteLoader !== undefined) {
+ throw new Error('A suite loader exists already');
+ }
+ global._currentSuiteLoader = this;
+
+ this.oldGlobalOnError_ = global.onerror;
+ global.onerror = function(errorMsg, url, lineNumber) {
+ this.scriptErrorPromiseResolver_.reject(
+ new Error(errorMsg + '\n' + url + ':' + lineNumber));
+ if (this.oldGlobalOnError_) {
+ return this.oldGlobalOnError_(errorMsg, url, lineNumber);
+ }
+ return false;
+ }.bind(this);
+ },
+
+ unbindGlobalHooks_() {
+ global._currentSuiteLoader = undefined;
+
+ global.onerror = this.oldGlobalOnError_;
+ this.oldGlobalOnError_ = undefined;
+ },
+
+ constructAndRegisterTestSuite(suiteConstructor) {
+ const name = this.currentModuleLoader_.getCurrentlyExecutingModuleName();
+
+ const testSuite = new tr.b.unittest.TestSuite(
+ name, suiteConstructor);
+
+ this.testSuites.push(testSuite);
+
+ const e = new tr.b.Event('suite-loaded');
+ e.testSuite = testSuite;
+ this.dispatchEvent(e);
+ },
+
+ getAllTests() {
+ const tests = [];
+ this.testSuites.forEach(function(suite) {
+ tests.push.apply(tests, suite.tests);
+ });
+ return tests;
+ },
+
+ findTestWithFullyQualifiedName(fullyQualifiedName) {
+ for (let i = 0; i < this.testSuites.length; i++) {
+ const suite = this.testSuites[i];
+ for (let j = 0; j < suite.tests.length; j++) {
+ const test = suite.tests[j];
+ if (test.fullyQualifiedName === fullyQualifiedName) return test;
+ }
+ }
+ throw new Error('Test ' + fullyQualifiedName +
+ 'not found amongst ' + this.testSuites.length);
+ }
+ };
+
+ return {
+ SuiteLoader,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/test_case.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_case.html
new file mode 100644
index 00000000000..1a626608ce4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_case.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/unittest/constants.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b.unittest', function() {
+ const TestTypes = tr.b.unittest.TestTypes;
+
+ function TestCase(name, opt_testFn, opt_options) {
+ if (!name) {
+ throw new Error('Name must be provided');
+ }
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.suite_ = undefined; // Set by TestSuite.addTest.
+ this.name_ = name;
+
+ if (opt_options) {
+ this.options_ = opt_options;
+ } else {
+ this.options_ = {};
+ }
+
+ this.testFn_ = opt_testFn;
+ }
+
+ TestCase.parseFullyQualifiedName = function(fqn) {
+ const i = fqn.lastIndexOf('.');
+ if (i === -1) {
+ throw new Error('FullyQualifiedNames must have a period in them');
+ }
+ return {
+ suiteName: fqn.substr(0, i),
+ testCaseName: fqn.substr(i + 1)
+ };
+ };
+
+ TestCase.prototype = {
+ __proto__: Object.prototype,
+
+ get guid() {
+ return this.guid_;
+ },
+
+ get suite() {
+ return this.suite_;
+ },
+
+ set suite(suite) {
+ if (this.suite_ !== undefined) {
+ throw new Error('Suite can only be assigned once.');
+ }
+ this.suite_ = suite;
+ },
+
+ get testType() {
+ return TestTypes.UNITTEST;
+ },
+
+ get name() {
+ return this.name_;
+ },
+
+ get fullyQualifiedName() {
+ return this.suite_.name + '.' + this.name_;
+ },
+
+ get options() {
+ return this.options_;
+ },
+
+ setUp() {
+ if (this.options_.setUp) {
+ this.options_.setUp.call(this);
+ }
+ },
+
+ run(htmlHook) {
+ return this.testFn_();
+ },
+
+ tearDown() {
+ if (this.options_.tearDown) {
+ this.options_.tearDown.call(this);
+ }
+ },
+
+ // TODO(nduca): The routing of this is a bit awkward. Probably better
+ // to install a global function.
+ addHTMLOutput(element) {
+ tr.b.unittest.addHTMLOutputForCurrentTest(element);
+ }
+ };
+
+ function PerfTestCase(name, testFn, opt_options) {
+ TestCase.call(this, name, testFn, opt_options);
+ this.iterations = this.options.iterations || 10;
+ }
+
+ PerfTestCase.prototype = {
+ __proto__: TestCase.prototype,
+
+ get testType() {
+ return TestTypes.PERFTEST;
+ },
+
+ run() {
+ const durations = [];
+ const iterations = this.iterations;
+ for (let i = 0; i < iterations; ++i) {
+ const start = window.performance.now();
+ this.runOneIteration();
+ const duration = window.performance.now() - start;
+ durations.push(duration);
+ }
+
+ const durationStrings = durations.map(function(d) {
+ return d.toFixed(2) + 'ms';
+ });
+ const average = tr.b.math.Statistics.mean(durations);
+ const min = tr.b.math.Statistics.min(durations);
+
+ let summaryString = ' [';
+ summaryString += 'min ' + min.toFixed(2) + 'ms, ';
+ summaryString += 'avg ' + average.toFixed(2) + 'ms';
+ summaryString += ']';
+
+ return durationStrings.join(', ') + summaryString;
+ },
+
+ runOneIteration() {
+ this.testFn_();
+ }
+ };
+
+ return {
+ TestCase,
+ PerfTestCase,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/test_case_test.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_case_test.html
new file mode 100644
index 00000000000..c738b66bbbc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_case_test.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/unittest/test_case.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('parseFullyQualifiedName', function() {
+ const p = tr.b.unittest.TestCase.parseFullyQualifiedName('foo.bar');
+ assert.strictEqual(p.suiteName, 'foo');
+ assert.strictEqual(p.testCaseName, 'bar');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/test_runner.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_runner.html
new file mode 100644
index 00000000000..adcbdcca80e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_runner.html
@@ -0,0 +1,285 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/timing.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.b.unittest', function() {
+ let realTvOnAnimationFrameError;
+ let realGlobalOnError;
+ let realGlobalHistoryPushState;
+
+ const NUM_TESTS_PER_RIC = 16;
+
+ function installGlobalTestHooks(runner) {
+ realTvOnAnimationFrameError = tr.b.onAnimationFrameError;
+ tr.b.onAnimationFrameError = function(error) {
+ runner.results.addErrorForCurrentTest(error);
+ };
+
+ if (tr.isExported('global.onerror')) {
+ realGlobalOnError = global.onerror;
+ global.onerror = function(errorMsg, url, lineNumber) {
+ runner.results.addErrorForCurrentTest(
+ errorMsg + ' at ' + url + ':' + lineNumber);
+ if (realGlobalOnError) {
+ return realGlobalOnError(errorMsg, url, lineNumber);
+ }
+ return false;
+ };
+ }
+
+ if (tr.isExported('global.history')) {
+ realGlobalHistoryPushState = global.history.pushState;
+ global.history.pushState = function() {
+ };
+ }
+
+ tr.b.unittest.addHTMLOutputForCurrentTest = function(element) {
+ runner.results.addHTMLOutputForCurrentTest(element);
+ };
+
+ if (tr.isExported('global.sessionStorage')) {
+ global.sessionStorage.clear();
+ }
+
+ const e = new tr.b.Event('tr-unittest-will-run');
+ TestRunner.dispatchEvent(e);
+ }
+
+ function uninstallGlobalTestHooks() {
+ if (tr.isExported('global.onerror')) {
+ global.onerror = realGlobalOnError;
+ realGlobalOnError = undefined;
+ }
+
+ tr.b.onAnimationFrameError = realTvOnAnimationFrameError;
+ realTvOnAnimationFrameError = undefined;
+
+ if (tr.isExported('global.history')) {
+ global.history.pushState = realGlobalHistoryPushState;
+ realGlobalHistoryPushState = undefined;
+ }
+
+ tr.b.unittest.addHTMLOutputForCurrentTest = undefined;
+ }
+
+
+ function TestRunner(results, testCases) {
+ this.results_ = results;
+ this.testCases_ = testCases;
+ this.pendingTestCases_ = [];
+
+ this.runOneTestCaseScheduled_ = false;
+ this.numRunsSinceLastRIC_ = 0;
+
+ this.runCompletedPromise = undefined;
+ this.runCompletedResolver_ = undefined;
+
+ this.currentTestCase_ = undefined;
+ }
+
+ TestRunner.prototype = {
+ __proto__: Object.prototype,
+
+ beginRunning() {
+ if (this.pendingTestCases_.length) {
+ throw new Error('Tests still running!');
+ }
+
+ this.runCompletedPromise = new Promise(function(resolve, reject) {
+ this.runCompletedResolver_ = {
+ resolve,
+ reject
+ };
+ }.bind(this));
+
+ this.pendingTestCases_ = this.testCases_.slice(0);
+
+ this.results_.willRunTests(this.pendingTestCases_);
+
+ this.scheduleRunOneTestCase_();
+
+ return this.runCompletedPromise;
+ },
+
+ beginToStopRunning() {
+ if (!this.runCompletedResolver_) {
+ throw new Error('Still running');
+ }
+ this.pendingTestCases_ = [];
+ return this.runCompletedPromise;
+ },
+
+ get testCases() {
+ return this.testCases_;
+ },
+
+ get results() {
+ return this.results_;
+ },
+
+ scheduleRunOneTestCase_() {
+ if (this.runOneTestCaseScheduled_) return;
+ this.runOneTestCaseScheduled_ = true;
+
+ this.numRunsSinceLastRIC_++;
+ if (this.numRunsSinceLastRIC_ === NUM_TESTS_PER_RIC) {
+ this.numRunsSinceLastRIC_ = 0;
+ tr.b.idle().then(() => this.runOneTestCase_());
+ } else {
+ Promise.resolve().then(() => this.runOneTestCase_());
+ }
+ },
+
+ runOneTestCase_() {
+ this.runOneTestCaseScheduled_ = false;
+
+ if (this.pendingTestCases_.length === 0) {
+ this.didFinishRunningAllTests_();
+ return;
+ }
+
+ this.currentTestCase_ = this.pendingTestCases_.splice(0, 1)[0];
+ this.currentMark_ = tr.b.Timing.mark(
+ 'TestRunner', this.currentTestCase_.name);
+ this.results_.willRunTest(this.currentTestCase_);
+
+ if (this.isCurrentTestSkipped_()) {
+ this.results_.setCurrentTestSkipped();
+ this.results_.didCurrentTestEnd();
+ this.currentMark_.end();
+ this.currentTestCase_ = undefined;
+ this.scheduleRunOneTestCase_();
+ return;
+ }
+
+ if (this.isCurrentTestFlaky_()) {
+ this.results_.setCurrentTestFlaky();
+ this.results_.didCurrentTestEnd();
+ this.currentMark_.end();
+ this.currentTestCase_ = undefined;
+ this.scheduleRunOneTestCase_();
+ return;
+ }
+
+ if (!this.setUpCurrentTestCase_()) {
+ this.results_.didCurrentTestEnd();
+ this.currentMark_.end();
+ this.currentTestCase_ = undefined;
+ this.scheduleRunOneTestCase_();
+ return;
+ }
+
+ this.runCurrentTestCase_().then(
+ function pass(result) {
+ try {
+ this.tearDownCurrentTestCase_(true);
+ if (result) {
+ this.results_.setReturnValueFromCurrentTest(result);
+ }
+ this.results_.didCurrentTestEnd();
+ this.currentMark_.end();
+ this.currentTestCase_ = undefined;
+ this.scheduleRunOneTestCase_();
+ } catch (e) {
+ this.hadInternalError_(e);
+ throw e;
+ }
+ }.bind(this),
+ function fail(error) {
+ try {
+ this.results_.addErrorForCurrentTest(error);
+ this.tearDownCurrentTestCase_(false);
+ this.results_.didCurrentTestEnd();
+ this.currentMark_.end();
+ this.currentTestCase_ = undefined;
+ this.scheduleRunOneTestCase_();
+ } catch (e) {
+ this.hadInternalError_(e);
+ throw e;
+ }
+ }.bind(this));
+ },
+
+ isCurrentTestFlaky_() {
+ return !!this.currentTestCase_.options.flaky;
+ },
+
+ isCurrentTestSkipped_() {
+ return !!this.currentTestCase_.options.skipped;
+ },
+
+ setUpCurrentTestCase_() {
+ // Try setting it up. Return true if succeeded.
+ installGlobalTestHooks(this);
+ try {
+ this.currentTestCase_.setUp();
+ } catch (error) {
+ this.results_.addErrorForCurrentTest(error);
+ return false;
+ }
+ return true;
+ },
+
+ runCurrentTestCase_() {
+ return new Promise(function(resolve, reject) {
+ let maybePromise;
+ try {
+ maybePromise = this.currentTestCase_.run();
+ } catch (error) {
+ reject(error);
+ return;
+ }
+
+ if (maybePromise !== undefined && maybePromise.then) {
+ maybePromise.then(
+ function(result) {
+ resolve(result);
+ },
+ function(error) {
+ reject(error);
+ });
+ } else {
+ resolve(maybePromise);
+ }
+ }.bind(this));
+ },
+
+ hadInternalError_(outerE) {
+ this.results.didRunTests();
+
+ this.runCompletedResolver_.reject(outerE);
+ this.runCompletedResolver_ = undefined;
+ },
+
+ tearDownCurrentTestCase_() {
+ try {
+ this.currentTestCase_.tearDown();
+ } catch (error) {
+ this.results_.addErrorForCurrentTest(error);
+ }
+
+ uninstallGlobalTestHooks();
+ },
+
+ didFinishRunningAllTests_() {
+ this.results.didRunTests();
+ this.runCompletedResolver_.resolve();
+ this.runCompletedResolver_ = undefined;
+ }
+ };
+
+ tr.b.EventTarget.decorate(TestRunner);
+
+ return {
+ TestRunner,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/test_suite.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_suite.html
new file mode 100644
index 00000000000..237403fbf05
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/test_suite.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unittest/constants.html">
+<link rel="import" href="/tracing/base/unittest/test_case.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b.unittest', function() {
+ const TestCase = tr.b.unittest.TestCase;
+ const PerfTestCase = tr.b.unittest.PerfTestCase;
+
+ const TestTypes = tr.b.unittest.TestTypes;
+
+ function TestSuite(name, suiteConstructor) {
+ this.guid = tr.b.GUID.allocateSimple();
+ this.name_ = name;
+ this.tests_ = [];
+ this.testNames_ = {}; // For dupe checking.
+
+ global.flakyTest = function(testCaseOrName, opt_testFn, opt_options) {
+ if (testCaseOrName instanceof TestCase) {
+ testCaseOrName.options.flaky = true;
+ test(testCaseOrName);
+ } else {
+ const options = Object.assign({}, opt_options || {});
+ options.flaky = true;
+ test(testCaseOrName, opt_testFn, options);
+ }
+ }.bind(this);
+
+ global.test = function(testCaseOrName, opt_testFn, opt_options) {
+ if (testCaseOrName instanceof TestCase) {
+ if (opt_testFn !== undefined) {
+ throw new Error('opt_testFn cannot be given when giving a TestCase');
+ }
+ if (opt_options !== undefined) {
+ throw new Error('opt_options cannot be given when giving a TestCase');
+ }
+ this.addTest(testCaseOrName);
+ return;
+ }
+
+ let testName = testCaseOrName;
+ const testFn = opt_testFn;
+ const options = opt_options || {};
+ if (testFn === undefined) {
+ throw new Error('Must provide opt_testFn');
+ }
+
+ // If the test cares about DPI settings then we first push a test
+ // that fakes the DPI as the low or hi Dpi version, depending on what
+ // we're current using.
+ if (options.dpiAware) {
+ const defaultDevicePixelRatio = window.devicePixelRatio;
+ const dpi = defaultDevicePixelRatio > 1 ? 1 : 2;
+
+ const testWrapper = function() {
+ window.devicePixelRatio = dpi;
+ try {
+ testFn.bind(this).call();
+ } finally {
+ window.devicePixelRatio = defaultDevicePixelRatio;
+ }
+ };
+
+ let newName = name;
+ if (dpi === 1) {
+ newName += '_loDPI';
+ testName += '_hiDPI';
+ } else {
+ newName += '_hiDPI';
+ testName += '_loDPI';
+ }
+
+ this.addTest(new TestCase(newName,
+ testWrapper, options || {}));
+ }
+
+ this.addTest(new TestCase(testName,
+ testFn, options || {}));
+ }.bind(this);
+
+ global.timedPerfTest = function(name, testFn, options) {
+ if (options === undefined || options.iterations === undefined) {
+ throw new Error('timedPerfTest must have iteration option provided.');
+ }
+ this.addTest(new PerfTestCase(name, testFn, options));
+ }.bind(this);
+
+ try {
+ suiteConstructor.call();
+ } finally {
+ global.test = undefined;
+ global.timedPerfTest = undefined;
+ }
+ }
+
+ TestSuite.prototype = {
+ __proto__: Object.prototype,
+
+ get tests() {
+ return this.tests_;
+ },
+
+ addTest(test) {
+ if (test.suite !== undefined) {
+ throw new Error('Test suite is already assigned');
+ }
+ if (this.testNames_[test.name] !== undefined) {
+ throw new Error('Test name already used');
+ }
+ test.suite = this;
+ this.testNames_[test.name] = true;
+ this.tests_.push(test);
+ },
+
+ get name() {
+ return this.name_;
+ }
+ };
+
+ function testSuite(suiteConstructor) {
+ if (!global._currentSuiteLoader) {
+ throw new Error('testSuites can only be defined during suite loading');
+ }
+ global._currentSuiteLoader.constructAndRegisterTestSuite(suiteConstructor);
+ }
+
+ return {
+ TestSuite,
+ testSuite,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest/text_test_results.html b/chromium/third_party/catapult/tracing/tracing/base/unittest/text_test_results.html
new file mode 100644
index 00000000000..0bececfcd60
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest/text_test_results.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/unittest/constants.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+tr.exportTo('tr.b.unittest', function() {
+ /**
+ * @constructor
+ */
+ function TextTestResults() {
+ this.numTestsThatPassed_ = 0;
+ this.numTestsThatFailed_ = 0;
+ this.numFlakyTests_ = 0;
+ this.numSkippedTests_ = 0;
+ this.currentTestCaseHadErrors_ = false;
+ this.currentTestIsFlaky_ = false;
+ this.currentTestIsSkipped_ = false;
+ }
+
+ TextTestResults.prototype = {
+ get numTestsThatRan() {
+ return this.numTestsThatPassed_ + this.numTestsThatFailed_ +
+ this.numFlakyTests_ + this.numSkippedTests_;
+ },
+
+ get numTestsThatFailed() {
+ return this.numTestsThatFailed_;
+ },
+
+ get numTestsThatPassed() {
+ return this.numTestsThatPassed_;
+ },
+
+ get numFlakyTests() {
+ return this.numFlakyTests_;
+ },
+
+ get numSkippedTests() {
+ return this.numSkippedTests_;
+ },
+
+ willRunTests(testCases) {
+ },
+
+ willRunTest(testCase) {
+ this.write_(testCase.name + ' (' + testCase.suite.name + ') ... ');
+ this.currentTestCaseHadErrors_ = false;
+ this.currentTestIsFlaky_ = false;
+ this.currentTestIsSkipped_ = false;
+ },
+
+ addErrorForCurrentTest(error) {
+ if (!this.currentTestCaseHadErrors_) this.write_('FAIL\n');
+ const normalizedException = tr.b.normalizeException(error);
+ this.write_(normalizedException.stack + '\n');
+ this.currentTestCaseHadErrors_ = true;
+ },
+
+ addHTMLOutputForCurrentTest(element) {
+ this.curHTMLOutput_.push(element);
+ },
+
+ setCurrentTestFlaky() {
+ if (!this.currentTestIsFlaky_) this.write_('FLAKY\n');
+ this.currentTestIsFlaky_ = true;
+ },
+
+ setCurrentTestSkipped() {
+ if (!this.currentTestIsSkipped_) this.write_('SKIPPED\n');
+ this.currentTestIsSkipped_ = true;
+ },
+
+ setReturnValueFromCurrentTest(returnValue) {
+ this.write_('[RESULT] ' + JSON.stringify(returnValue) + '\n');
+ },
+
+ didCurrentTestEnd() {
+ if (this.currentTestCaseHadErrors_) {
+ this.numTestsThatFailed_ += 1;
+ } else if (this.currentTestIsFlaky_) {
+ this.numFlakyTests_ += 1;
+ } else if (this.currentTestIsSkipped_) {
+ this.numSkippedTests_ += 1;
+ } else {
+ this.numTestsThatPassed_ += 1;
+ this.write_('ok\n');
+ }
+ },
+
+ didRunTests() {
+ this.write_('\n------------------------------------------------------' +
+ '----------------\n');
+ if (this.numTestsThatRan === 1) {
+ this.write_('Ran 1 test\n');
+ } else {
+ this.write_('Ran ' + this.numTestsThatRan + ' tests\n');
+ }
+
+ const errorString = 'errors=' + this.numTestsThatFailed;
+ const flakyString = 'flaky=' + this.numFlakyTests;
+ const skippedString = 'skipped=' + this.numSkippedTests;
+ const messages = [];
+ if (this.numTestsThatFailed > 0) messages.push(errorString);
+ if (this.numFlakyTests > 0) messages.push(flakyString);
+ if (this.numSkippedTests > 0) messages.push(skippedString);
+ let details = '';
+ if (messages.length > 0) {
+ details += ' (' + messages.join(' ') + ')';
+ }
+
+ if (this.numTestsThatFailed > 0) {
+ this.write_('\nFAILED' + details);
+ } else {
+ this.write_('\nOK' + details);
+ }
+ },
+
+ write_(msg) {
+ if (tr.isVinn) {
+ global.write(msg);
+ } else {
+ console.log(msg);
+ }
+ }
+ };
+
+ return {
+ TextTestResults,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/unittest_test.html b/chromium/third_party/catapult/tracing/tracing/base/unittest_test.html
new file mode 100644
index 00000000000..553a804b4a6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/unittest_test.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/unittest.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('promise', function() {
+ return new Promise(function(resolve, reject) {
+ resolve();
+ });
+ });
+
+ test('async', function() {
+ return new Promise(function(resolve) {
+ tr.b.requestAnimationFrame(function() {
+ resolve();
+ });
+ });
+ });
+
+ test('assert_equal_is_forbidden', function() {
+ assert.throws(() => assert.equal(0, '0'));
+ });
+
+ /* To test failures remove comments
+ test('fail', function() {
+ assert.strictEqual(true, false);
+ });
+
+ test('rejected-promise', function() {
+ return new Promise(function(resolve, reject){
+ reject("Failure by rejection");
+ });
+ });
+
+ test('promise-that-throws-after-resolver', function() {
+ return new Promise(function(resolve, rejet){
+ throw new Error('blah');
+ });
+ });
+
+ */
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/url_json.html b/chromium/third_party/catapult/tracing/tracing/base/url_json.html
new file mode 100644
index 00000000000..c1b014e3d1c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/url_json.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ function encodeString(s) {
+ return encodeURIComponent(s).replace(/[(-.)]/, c =>
+ '%' + c.charCodeAt(0).toString(16));
+ }
+
+ function encodeStrings(dict) {
+ if (dict instanceof Array || !(dict instanceof Object)) {
+ throw new Error(
+ 'UrlJson only supports dictionaries of strings.');
+ }
+
+ const result = {};
+ for (const [key, value] of Object.entries(dict)) {
+ if ((value instanceof Array || !(value instanceof Object)) &&
+ typeof(value) !== 'string') {
+ throw new Error(
+ 'UrlJson only supports strings and dictionaries of strings.');
+ }
+
+ if (value instanceof Object) {
+ result[encodeString(key)] = encodeStrings(value);
+ } else {
+ result[encodeString(key)] = encodeString(value);
+ }
+ }
+ return result;
+ }
+
+ function decodeStrings(dict) {
+ const result = {};
+ for (const [key, value] of Object.entries(dict)) {
+ if (value instanceof Object) {
+ result[decodeURIComponent(key)] = decodeStrings(value);
+ } else if (typeof(value) === 'string') {
+ result[decodeURIComponent(key)] = decodeURIComponent(value);
+ } else {
+ throw new Error(
+ 'UrlJson only supports strings and dictionaries of strings.');
+ }
+ }
+ return result;
+ }
+
+ /*
+ * This implements a subset of JSON in a compact URL-safe format.
+ * Only strings and dictionaries of strings are supported.
+ * Numbers, arrays, booleans, and null are not supported.
+ * Instead of using url-unsafe characters {":,}
+ * This format uses url-safe characters (-.)
+ * If strings contain those characters, they will be URI-encoded like other
+ * url-unsafe characters.
+ * For example, the following object would be encoded as the following
+ * UrlJson:
+ * {"abc": {"def": "ghi", "jkl": "mno"}, "pqr": "stu"}
+ * abc-(def-ghi.jkl-mno).pqr-stu
+ */
+ class UrlJson {
+ static stringify(dict) {
+ dict = encodeStrings(dict);
+ const str = JSON.stringify(dict);
+ return str.slice(1, str.length - 1)
+ .replace(/{/g, '(')
+ .replace(/}/g, ')')
+ .replace(/ /g, '')
+ .replace(/"/g, '')
+ .replace(/,/g, '.')
+ .replace(/:/g, '-');
+ }
+
+ static parse(str) {
+ if (str === '') return {};
+ str = ('{"' + str + '"}')
+ .replace(/-\(/g, '": {"')
+ .replace(/-/g, '": "')
+ .replace(/\(/g, '{"')
+ .replace(/\)/g, '"}')
+ .replace(/}"/g, '}')
+ .replace(/\./g, '", "');
+ return decodeStrings(JSON.parse(str));
+ }
+ }
+
+ return {
+ UrlJson,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/url_json_test.html b/chromium/third_party/catapult/tracing/tracing/base/url_json_test.html
new file mode 100644
index 00000000000..8dccd0613e4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/url_json_test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/url_json.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ function urlJson2Json(s) {
+ return JSON.stringify(tr.b.UrlJson.parse(s));
+ }
+
+ test('stringifyInvalid', function() {
+ assert.throws(() => tr.b.UrlJson.stringify([]));
+ assert.throws(() => tr.b.UrlJson.stringify(0));
+ assert.throws(() => tr.b.UrlJson.stringify(null));
+ assert.throws(() => tr.b.UrlJson.stringify(true));
+ assert.throws(() => tr.b.UrlJson.stringify(false));
+ assert.throws(() => tr.b.UrlJson.stringify({a: []}));
+ assert.throws(() => tr.b.UrlJson.stringify({a: 0}));
+ assert.throws(() => tr.b.UrlJson.stringify({a: null}));
+ assert.throws(() => tr.b.UrlJson.stringify({a: true}));
+ assert.throws(() => tr.b.UrlJson.stringify({a: false}));
+ });
+
+ test('stringify', function() {
+ assert.strictEqual('', tr.b.UrlJson.stringify({}));
+ assert.strictEqual('a-', tr.b.UrlJson.stringify({a: ''}));
+ assert.strictEqual('a-0', tr.b.UrlJson.stringify({a: '0'}));
+ assert.strictEqual('a-%25', tr.b.UrlJson.stringify({a: '%'}));
+ assert.strictEqual('a-(b-)', tr.b.UrlJson.stringify({a: {b: ''}}));
+ assert.strictEqual('a-.b-', tr.b.UrlJson.stringify({a: '', b: ''}));
+ });
+
+ test('parse', function() {
+ assert.strictEqual('{}', urlJson2Json(''));
+ assert.strictEqual('{"a":""}', urlJson2Json('a-'));
+ assert.strictEqual('{"a":"0"}', urlJson2Json('a-0'));
+ assert.strictEqual('{"a":"%"}', urlJson2Json('a-%25'));
+ assert.strictEqual('{"a":{"b":""}}', urlJson2Json('a-(b-)'));
+ assert.strictEqual('{"a":"","b":""}', urlJson2Json('a-.b-'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/utils.html b/chromium/third_party/catapult/tracing/tracing/base/utils.html
new file mode 100644
index 00000000000..0bbce55a277
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/utils.html
@@ -0,0 +1,652 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ const URL_REGEX = /^(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b|file:\/\/)([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/; // eslint-disable-line max-len
+
+ function deepCopy(value) {
+ if (!(value instanceof Object)) {
+ if (value === undefined || value === null) return value;
+ if (typeof value === 'string') return value.substring();
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'number') return value;
+ throw new Error('Unrecognized: ' + typeof value);
+ }
+
+ const object = value;
+ if (object instanceof Array) {
+ const res = new Array(object.length);
+ for (let i = 0; i < object.length; i++) {
+ res[i] = deepCopy(object[i]);
+ }
+ return res;
+ }
+
+ if (object.__proto__ !== Object.prototype) {
+ throw new Error('Can only clone simple types');
+ }
+ const res = {};
+ for (const key in object) {
+ res[key] = deepCopy(object[key]);
+ }
+ return res;
+ }
+
+ function normalizeException(e) {
+ if (e === undefined || e === null) {
+ return {
+ typeName: 'UndefinedError',
+ message: 'Unknown: null or undefined exception',
+ stack: 'Unknown'
+ };
+ }
+
+ if (typeof(e) === 'string') {
+ return {
+ typeName: 'StringError',
+ message: e,
+ stack: [e]
+ };
+ }
+
+ let typeName;
+ if (e.name) {
+ typeName = e.name;
+ } else if (e.constructor) {
+ if (e.constructor.name) {
+ typeName = e.constructor.name;
+ } else {
+ typeName = 'AnonymousError';
+ }
+ } else {
+ typeName = 'ErrorWithNoConstructor';
+ }
+
+ const msg = e.message ? e.message : 'Unknown';
+ return {
+ typeName,
+ message: msg,
+ stack: e.stack ? e.stack : [msg]
+ };
+ }
+
+ function stackTraceAsString() {
+ return new Error().stack + '';
+ }
+ function stackTrace() {
+ let stack = stackTraceAsString();
+ stack = stack.split('\n');
+ return stack.slice(2);
+ }
+
+ function getUsingPath(path, fromDict) {
+ const parts = path.split('.');
+ let cur = fromDict;
+
+ for (let part; parts.length && (part = parts.shift());) {
+ if (!parts.length) {
+ return cur[part];
+ } else if (part in cur) {
+ cur = cur[part];
+ } else {
+ return undefined;
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * Format date as a string "YYYY-MM-DD HH:mm:ss". The timezone is implicitly
+ * UTC. This format is based on the ISO format, but without milliseconds and
+ * the 'T' is replaced with a space for legibility.
+ *
+ * @param {!Date} date
+ * @return {string}
+ */
+ function formatDate(date) {
+ return date.toISOString().replace('T', ' ').slice(0, 19);
+ }
+
+ /**
+ * Infinity and NaN are left out of JSON for security reasons that do not
+ * apply to our use cases. This helper function allows serializing them
+ * independently of null.
+ *
+ * @param {!number} n
+ * @return {!(number|string)}
+ */
+ function numberToJson(n) {
+ if (isNaN(n)) return 'NaN';
+ if (n === Infinity) return 'Infinity';
+ if (n === -Infinity) return '-Infinity';
+ return n;
+ }
+
+ /**
+ * Infinity and NaN are left out of JSON for security reasons that do not
+ * apply to our use cases. This helper function allows deserializing them
+ * independently of null.
+ *
+ * @param {!(number|string)} n
+ * @return {!number}
+ */
+ function numberFromJson(n) {
+ if (n === 'NaN' || n === null) return NaN;
+ if (n === 'Infinity') return Infinity;
+ if (n === '-Infinity') return -Infinity;
+ return n;
+ }
+
+ /**
+ * @param {Array.<T>} ary
+ * @returns {Array.<Object.<T, number>>} The run length encoding of the array
+ * as an array of {value, count} objects.
+ * @template T
+ */
+ function runLengthEncoding(ary) {
+ const encodedArray = [];
+ for (const element of ary) {
+ if (encodedArray.length === 0 ||
+ encodedArray[encodedArray.length - 1].value !== element) {
+ encodedArray.push({
+ value: element,
+ count: 1,
+ });
+ } else {
+ encodedArray[encodedArray.length - 1].count += 1;
+ }
+ }
+ return encodedArray;
+ }
+
+ /**
+ * @param {string} s
+ * @return {boolean}
+ */
+ function isUrl(s) {
+ return typeof(s) === 'string' && s.match(URL_REGEX) !== null;
+ }
+
+ /**
+ * Returns the only element in the iterable. If the iterable is empty or has
+ * more than one element, an error is thrown.
+ */
+ function getOnlyElement(iterable) {
+ const iterator = iterable[Symbol.iterator]();
+
+ const firstIteration = iterator.next();
+ if (firstIteration.done) {
+ throw new Error('getOnlyElement was passed an empty iterable.');
+ }
+
+ const secondIteration = iterator.next();
+ if (!secondIteration.done) {
+ throw new Error(
+ 'getOnlyElement was passed an iterable with multiple elements.');
+ }
+
+ return firstIteration.value;
+ }
+
+ /**
+ * Returns the first element in the iterable. If the iterable is empty, an
+ * error is thrown.
+ */
+ function getFirstElement(iterable) {
+ const iterator = iterable[Symbol.iterator]();
+ const result = iterator.next();
+ if (result.done) {
+ throw new Error('getFirstElement was passed an empty iterable.');
+ }
+
+ return result.value;
+ }
+
+ function compareArrays(x, y, elementCmp) {
+ const minLength = Math.min(x.length, y.length);
+ let i;
+ for (i = 0; i < minLength; i++) {
+ const tmp = elementCmp(x[i], y[i]);
+ if (tmp) return tmp;
+ }
+ if (x.length === y.length) return 0;
+
+ if (x[i] === undefined) return -1;
+
+ return 1;
+ }
+
+ /**
+ * Returns a new Map with items grouped by the return value of the
+ * specified function being called on each item.
+ * @param {!Array.<!*>} ary The array being iterated through
+ * @param {!function(!*):!*} callback The mapping function between the array
+ * value and the map key.
+ * @param {*=} opt_this
+ */
+ function groupIntoMap(ary, callback, opt_this, opt_arrayConstructor) {
+ const arrayConstructor = opt_arrayConstructor || Array;
+ const results = new Map();
+ for (const element of ary) {
+ const key = callback.call(opt_this, element);
+ let items = results.get(key);
+ if (items === undefined) {
+ items = new arrayConstructor();
+ results.set(key, items);
+ }
+ items.push(element);
+ }
+ return results;
+ }
+
+ function inPlaceFilter(array, predicate, opt_this) {
+ opt_this = opt_this || this;
+ let nextPosition = 0;
+ for (let i = 0; i < array.length; i++) {
+ if (!predicate.call(opt_this, array[i], i)) continue;
+ if (nextPosition < i) {
+ array[nextPosition] = array[i]; // Move elements only if necessary.
+ }
+ nextPosition++;
+ }
+
+ if (nextPosition < array.length) {
+ array.length = nextPosition; // Truncate the array only if necessary.
+ }
+ }
+
+ /**
+ * Convert an array of dictionaries to a dictionary of arrays.
+ *
+ * The keys of the resulting dictionary are a union of the keys of all
+ * dictionaries in the provided array. Each array in the resulting dictionary
+ * has the same length as the provided array and contains the values of its
+ * key in the dictionaries in the provided array. Example:
+ *
+ * INPUT:
+ *
+ * [
+ * {a: 6, b: 5 },
+ * undefined,
+ * {a: 4, b: 3, c: 2},
+ * { b: 1, c: 0}
+ * ]
+ *
+ * OUTPUT:
+ *
+ * {
+ * a: [6, undefined, 4, undefined],
+ * b: [5, undefined, 3, 1 ],
+ * c: [undefined, undefined, 2, 0 ]
+ * }
+ *
+ * @param {!Array} array Array of items to be inverted. If opt_dictGetter
+ * is not provided, all elements of the array must be either undefined,
+ * or dictionaries.
+ * @param {?(function(*): (!Object|undefined))=} opt_dictGetter Optional
+ * function mapping defined elements of array to dictionaries.
+ * @param {*=} opt_this Optional 'this' context for opt_dictGetter.
+ */
+ function invertArrayOfDicts(array, opt_dictGetter, opt_this) {
+ opt_this = opt_this || this;
+ const result = {};
+ for (let i = 0; i < array.length; i++) {
+ const item = array[i];
+ if (item === undefined) continue;
+ const dict = opt_dictGetter ? opt_dictGetter.call(opt_this, item) : item;
+ if (dict === undefined) continue;
+ for (const key in dict) {
+ let valueList = result[key];
+ if (valueList === undefined) {
+ result[key] = valueList = new Array(array.length);
+ }
+ valueList[i] = dict[key];
+ }
+ }
+ return result;
+ }
+
+ function setsEqual(a, b) {
+ if (!(a instanceof Set) || !(b instanceof Set)) return false;
+ if (a.size !== b.size) return false;
+ // Avoid Array.from() here -- it creates garbage.
+ for (const x of a) {
+ if (!b.has(x)) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Finds the first index in the array whose value is >= loVal.
+ *
+ * The key for the search is defined by the mapFn. This array must
+ * be prearranged such that ary.map(mapFn) would also be sorted in
+ * ascending order.
+ *
+ * @param {Array} ary An array of arbitrary objects.
+ * @param {function():*} mapFn Callback that produces a key value
+ * from an element in ary.
+ * @param {number} loVal Value for which to search.
+ * @return {Number} Offset o into ary where all ary[i] for i <= o
+ * are < loVal, or ary.length if loVal is greater than all elements in
+ * the array.
+ */
+ function findLowIndexInSortedArray(ary, mapFn, loVal) {
+ if (ary.length === 0) return 1;
+
+ let low = 0;
+ let high = ary.length - 1;
+ let i;
+ let comparison;
+ let hitPos = -1;
+ while (low <= high) {
+ i = Math.floor((low + high) / 2);
+ comparison = mapFn(ary[i]) - loVal;
+ if (comparison < 0) {
+ low = i + 1; continue;
+ } else if (comparison > 0) {
+ high = i - 1; continue;
+ } else {
+ hitPos = i;
+ high = i - 1;
+ }
+ }
+ // return where we hit, or failing that the low pos
+ return hitPos !== -1 ? hitPos : low;
+ }
+
+ /**
+ * Finds an index in an array of intervals that either intersects
+ * the provided loVal, or if no intersection is found, -1 or ary.length.
+ *
+ * The array of intervals is defined implicitly via two mapping functions
+ * over the provided ary. mapLoFn determines the lower value of the interval,
+ * mapWidthFn the width. Intersection is lower-inclusive, e.g. [lo,lo+w).
+ *
+ * The array of intervals formed by this mapping must be non-overlapping and
+ * sorted in ascending order by loVal.
+ *
+ * @param {Array} ary An array of objects that can be converted into sorted
+ * nonoverlapping ranges [x,y) using the mapLoFn and mapWidth.
+ * @param {function():*} mapLoFn Callback that produces the low value for the
+ * interval represented by an element in the array.
+ * @param {function():*} mapWidthFn Callback that produces the width for the
+ * interval represented by an element in the array.
+ * @param {number} loVal The low value for the search.
+ * @return {Number} An index in the array that intersects or is first-above
+ * loVal, -1 if none found and loVal is below than all the intervals,
+ * ary.length if loVal is greater than all the intervals.
+ */
+ function findIndexInSortedIntervals(ary, mapLoFn, mapWidthFn, loVal) {
+ const first = findLowIndexInSortedArray(ary, mapLoFn, loVal);
+ if (first === 0) {
+ if (loVal >= mapLoFn(ary[0]) &&
+ loVal < mapLoFn(ary[0]) + mapWidthFn(ary[0], 0)) {
+ return 0;
+ }
+ return -1;
+ }
+
+ if (first < ary.length) {
+ if (loVal >= mapLoFn(ary[first]) &&
+ loVal < mapLoFn(ary[first]) + mapWidthFn(ary[first], first)) {
+ return first;
+ }
+ if (loVal >= mapLoFn(ary[first - 1]) &&
+ loVal < mapLoFn(ary[first - 1]) +
+ mapWidthFn(ary[first - 1], first - 1)) {
+ return first - 1;
+ }
+ return ary.length;
+ }
+
+ if (first === ary.length) {
+ if (loVal >= mapLoFn(ary[first - 1]) &&
+ loVal < mapLoFn(ary[first - 1]) +
+ mapWidthFn(ary[first - 1], first - 1)) {
+ return first - 1;
+ }
+ return ary.length;
+ }
+
+ return ary.length;
+ }
+
+ /**
+ * Finds an index in an array of sorted closed intervals that either
+ * intersects the provided val, or if no intersection is found, -1 or
+ * ary.length.
+ *
+ * The array of intervals is defined implicitly via two mapping functions
+ * over the provided ary. mapLoFn determines the lower value of the interval,
+ * mapHiFn the high. Intersection is closed, e.g. [lo,hi], unlike with
+ * findIndexInSortedIntervals, which is right-open.
+ *
+ * The array of intervals formed by this mapping must be non-overlapping, and
+ * sorted in ascending order by val.
+ *
+ * @param {Array} ary An array of objects that can be converted into sorted
+ * nonoverlapping ranges [x,y) using the mapLoFn and mapWidth.
+ * @param {function():*} mapLoFn Callback that produces the low value for the
+ * interval represented by an element in the array.
+ * @param {function():*} mapHiFn Callback that produces the high for the
+ * interval represented by an element in the array.
+ * @param {number} val The value for the search.
+ * @return {Number} An index in the array that intersects or is first-above
+ * val, -1 if none found and val is below than all the intervals,
+ * ary.length if val is greater than all the intervals.
+ */
+ function findIndexInSortedClosedIntervals(ary, mapLoFn, mapHiFn, val) {
+ const i = findLowIndexInSortedArray(ary, mapLoFn, val);
+ if (i === 0) {
+ if (val >= mapLoFn(ary[0], 0) &&
+ val <= mapHiFn(ary[0], 0)) {
+ return 0;
+ }
+ return -1;
+ }
+
+ if (i < ary.length) {
+ if (val >= mapLoFn(ary[i - 1], i - 1) &&
+ val <= mapHiFn(ary[i - 1], i - 1)) {
+ return i - 1;
+ }
+ if (val >= mapLoFn(ary[i], i) &&
+ val <= mapHiFn(ary[i], i)) {
+ return i;
+ }
+ return ary.length;
+ }
+
+ if (i === ary.length) {
+ if (val >= mapLoFn(ary[i - 1], i - 1) &&
+ val <= mapHiFn(ary[i - 1], i - 1)) {
+ return i - 1;
+ }
+ return ary.length;
+ }
+
+ return ary.length;
+ }
+
+ /**
+ * Calls cb for all intervals in the implicit array of intervals
+ * defnied by ary, mapLoFn and mapHiFn that intersect the range
+ * [loVal,hiVal)
+ *
+ * This function uses the same scheme as findLowIndexInSortedArray
+ * to define the intervals. The same restrictions on sortedness and
+ * non-overlappingness apply.
+ *
+ * @param {Array} ary An array of objects that can be converted into sorted
+ * nonoverlapping ranges [x,y) using the mapLoFn and mapWidth.
+ * @param {function():*} mapLoFn Callback that produces the low value for the
+ * interval represented by an element in the array.
+ * @param {function():*} mapWidthFn Callback that produces the width for the
+ * interval represented by an element in the array.
+ * @param {number} loVal The low value for the search, inclusive.
+ * @param {number} hiVal The high value for the search, non inclusive.
+ * @param {function():*} cb The function to run for intersecting intervals.
+ */
+ function iterateOverIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal,
+ hiVal, cb) {
+ if (ary.length === 0) return;
+
+ if (loVal > hiVal) return;
+
+ let i = findLowIndexInSortedArray(ary, mapLoFn, loVal);
+ if (i === -1) {
+ return;
+ }
+ if (i > 0) {
+ const hi = mapLoFn(ary[i - 1]) + mapWidthFn(ary[i - 1], i - 1);
+ if (hi >= loVal) {
+ cb(ary[i - 1], i - 1);
+ }
+ }
+ if (i === ary.length) {
+ return;
+ }
+
+ for (let n = ary.length; i < n; i++) {
+ const lo = mapLoFn(ary[i]);
+ if (lo >= hiVal) break;
+ cb(ary[i], i);
+ }
+ }
+
+ /**
+ * Finds the element in the array whose value is closest to |val|.
+ *
+ * The same restrictions on sortedness as for findLowIndexInSortedArray apply.
+ *
+ * @param {Array} ary An array of arbitrary objects.
+ * @param {function():*} mapFn Callback that produces a key value
+ * from an element in ary.
+ * @param {number} val Value for which to search.
+ * @param {number} maxDiff Maximum allowed difference in value between |val|
+ * and an element's value.
+ * @return {object} Object in the array whose value is closest to |val|, or
+ * null if no object is within range.
+ */
+ function findClosestElementInSortedArray(ary, mapFn, val, maxDiff) {
+ if (ary.length === 0) return null;
+
+ let aftIdx = findLowIndexInSortedArray(ary, mapFn, val);
+ const befIdx = aftIdx > 0 ? aftIdx - 1 : 0;
+
+ if (aftIdx === ary.length) aftIdx -= 1;
+
+ const befDiff = Math.abs(val - mapFn(ary[befIdx]));
+ const aftDiff = Math.abs(val - mapFn(ary[aftIdx]));
+
+ if (befDiff > maxDiff && aftDiff > maxDiff) return null;
+
+ const idx = befDiff < aftDiff ? befIdx : aftIdx;
+ return ary[idx];
+ }
+
+ /**
+ * Finds the closest interval in the implicit array of intervals
+ * defined by ary, mapLoFn and mapHiFn.
+ *
+ * This function uses the same scheme as findLowIndexInSortedArray
+ * to define the intervals. The same restrictions on sortedness and
+ * non-overlappingness apply.
+ *
+ * @param {Array} ary An array of objects that can be converted into sorted
+ * nonoverlapping ranges [x,y) using the mapLoFn and mapHiFn.
+ * @param {function():*} mapLoFn Callback that produces the low value for the
+ * interval represented by an element in the array.
+ * @param {function():*} mapHiFn Callback that produces the high for the
+ * interval represented by an element in the array.
+ * @param {number} val The value for the search.
+ * @param {number} maxDiff Maximum allowed difference in value between |val|
+ * and an interval's low or high value.
+ * @return {interval} Interval in the array whose high or low value is closest
+ * to |val|, or null if no interval is within range.
+ */
+ function findClosestIntervalInSortedIntervals(ary, mapLoFn, mapHiFn, val,
+ maxDiff) {
+ if (ary.length === 0) return null;
+
+ let idx = findLowIndexInSortedArray(ary, mapLoFn, val);
+ if (idx > 0) idx -= 1;
+
+ const hiInt = ary[idx];
+ let loInt = hiInt;
+
+ if (val > mapHiFn(hiInt) && idx + 1 < ary.length) {
+ loInt = ary[idx + 1];
+ }
+
+ const loDiff = Math.abs(val - mapLoFn(loInt));
+ const hiDiff = Math.abs(val - mapHiFn(hiInt));
+
+ if (loDiff > maxDiff && hiDiff > maxDiff) return null;
+
+ if (loDiff < hiDiff) return loInt;
+
+ return hiInt;
+ }
+
+ /**
+ * Returns first index i in |array| such that |test| is true for array[i].
+ * Returns array.length if no such i is found. Assumes |test| is monotonic
+ * boolean on |array|, i.e. if test(array[i]) is true, then test(array[i + 1])
+ * is also true.
+ *
+ * @param {Array} array Array of elements to perform binary search on.
+ * @param {function(*):boolean} test Monotonic boolean test function.
+ */
+ function findFirstTrueIndexInSortedArray(array, test) {
+ let i0 = 0;
+ let i1 = array.length;
+ while (i0 < i1) {
+ const i = Math.trunc((i0 + i1) / 2);
+ if (test(array[i])) {
+ i1 = i; // Explore the left branch.
+ } else {
+ i0 = i + 1; // Explore the right branch.
+ }
+ }
+ return i1;
+ }
+
+ return {
+ compareArrays,
+ deepCopy,
+ findClosestElementInSortedArray,
+ findClosestIntervalInSortedIntervals,
+ findFirstTrueIndexInSortedArray,
+ findIndexInSortedClosedIntervals,
+ findIndexInSortedIntervals,
+ findLowIndexInSortedArray,
+ formatDate,
+ getFirstElement,
+ getOnlyElement,
+ getUsingPath,
+ groupIntoMap,
+ inPlaceFilter,
+ invertArrayOfDicts,
+ isUrl,
+ iterateOverIntersectingIntervals,
+ normalizeException,
+ numberFromJson,
+ numberToJson,
+ runLengthEncoding,
+ setsEqual,
+ stackTrace,
+ stackTraceAsString,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/utils_test.html b/chromium/third_party/catapult/tracing/tracing/base/utils_test.html
new file mode 100644
index 00000000000..a9b00f0c5c1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/utils_test.html
@@ -0,0 +1,433 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('getUsingPath', function() {
+ const z = tr.b.getUsingPath('x.y.z', {'x': {'y': {'z': 3}}});
+ assert.strictEqual(z, 3);
+
+ const w = tr.b.getUsingPath('x.w', {'x': {'y': {'z': 3}}});
+ assert.isUndefined(w);
+ });
+
+ test('testExceptionNaming', function() {
+ const err = new Error('asdf');
+ err.name = 'MyError';
+
+ const ex = tr.b.normalizeException(err);
+ assert.strictEqual(ex.typeName, 'MyError');
+ });
+
+ test('formatDate', function() {
+ assert.strictEqual(tr.b.formatDate(new Date(0)), '1970-01-01 00:00:00');
+ });
+
+ test('runLengthEncoding', function() {
+ assert.deepEqual(tr.b.runLengthEncoding([]), []);
+
+ let encoded = tr.b.runLengthEncoding([1, 1]);
+ assert.deepEqual(encoded.map(x => x.value), [1]);
+ assert.deepEqual(encoded.map(x => x.count), [2]);
+
+ encoded = tr.b.runLengthEncoding([1, 2]);
+ assert.deepEqual(encoded.map(x => x.value), [1, 2]);
+ assert.deepEqual(encoded.map(x => x.count), [1, 1]);
+
+ encoded = tr.b.runLengthEncoding([1, 1, 2, 2, 2, 3, 1, 1]);
+ assert.deepEqual(encoded.map(x => x.value), [1, 2, 3, 1]);
+ assert.deepEqual(encoded.map(x => x.count), [2, 3, 1, 2]);
+ });
+
+ test('isUrl', function() {
+ assert.isFalse(tr.b.isUrl());
+ assert.isFalse(tr.b.isUrl(null));
+ assert.isFalse(tr.b.isUrl(42));
+ assert.isFalse(tr.b.isUrl([]));
+ assert.isFalse(tr.b.isUrl({}));
+ assert.isFalse(tr.b.isUrl(''));
+ assert.isFalse(tr.b.isUrl('data:,'));
+ assert.isFalse(tr.b.isUrl('ftp://user:password@host:port/path'));
+ assert.isTrue(tr.b.isUrl('http://google.com/'));
+ assert.isTrue(tr.b.isUrl('https://www.google.com/'));
+ });
+
+ test('setsEqual', function() {
+ assert.isTrue(tr.b.setsEqual(new Set(), new Set()));
+ assert.isTrue(tr.b.setsEqual(new Set(['a']), new Set(['a'])));
+ assert.isFalse(tr.b.setsEqual(new Set(), undefined));
+ assert.isFalse(tr.b.setsEqual(new Set(), new Set(['a'])));
+ assert.isFalse(tr.b.setsEqual(new Set(['a']), new Set(['b'])));
+ });
+
+ test('compareArrays', function() {
+ function cmp(x, y) {
+ assert.isDefined(x);
+ assert.isDefined(y);
+ return x - y;
+ }
+
+ assert.isBelow(tr.b.compareArrays([1], [2], cmp), 0);
+ assert.isAbove(tr.b.compareArrays([2], [1], cmp), 0);
+
+ assert.isBelow(tr.b.compareArrays([1], [1, 2], cmp), 0);
+ assert.isAbove(tr.b.compareArrays([1, 2], [1], cmp), 0);
+
+ assert.isBelow(tr.b.compareArrays([], [1], cmp), 0);
+ assert.isAbove(tr.b.compareArrays([1], [], cmp), 0);
+
+ assert.isAbove(tr.b.compareArrays([2], [1], cmp), 0);
+
+ assert.strictEqual(tr.b.compareArrays([], [], cmp), 0);
+ assert.strictEqual(tr.b.compareArrays([1], [1], cmp), 0);
+ });
+
+ test('getOnlyElement_throwsIfEmpty', function() {
+ assert.throws(() => tr.b.getOnlyElement([]),
+ 'getOnlyElement was passed an empty iterable.');
+ });
+
+ test('getOnlyElement_oneItem', function() {
+ assert.strictEqual(tr.b.getOnlyElement([1]), 1);
+ });
+
+ test('getOnlyElement_twoItems', function() {
+ assert.throws(() => tr.b.getOnlyElement([1, 2]),
+ 'getOnlyElement was passed an iterable with multiple elements.');
+ });
+
+ test('getFirstElement_throwsIfEmpty', function() {
+ assert.throws(() => tr.b.getFirstElement([]),
+ 'getFirstElement was passed an empty iterable.');
+ });
+
+ test('getFirstElement_oneItem', function() {
+ assert.strictEqual(tr.b.getFirstElement([1]), 1);
+ });
+
+ test('getFirstElement_twoItems', function() {
+ assert.strictEqual(tr.b.getFirstElement([1, 2]), 1);
+ });
+
+ test('groupIntoMap', function() {
+ // Empty array
+ let srcArray = [];
+ const fn = function(curr) { return (curr % 2); };
+ const dstDict = {};
+
+ assert.deepEqual(tr.b.groupIntoMap(srcArray, fn), dstDict);
+
+ // Non-empty array
+ srcArray = [0, 1, 2, 3, 4, 5, 6];
+ const dstMap = new Map([
+ [0, [0, 2, 4, 6]],
+ [1, [1, 3, 5]]
+ ]);
+
+ assert.deepEqual(tr.b.groupIntoMap(srcArray, fn), dstMap);
+ });
+
+ test('inPlaceFilter_simple', function() {
+ const someThisArg = {};
+ const list = [1, 2, 3, 4];
+ tr.b.inPlaceFilter(list, function(item) {
+ assert.strictEqual(this, someThisArg);
+ return item % 2 === 0;
+ }, someThisArg);
+ assert.deepEqual(list, [2, 4]);
+ });
+
+ test('invertArrayOfDicts_defaultGetter', function() {
+ const array = [
+ {a: 6, b: 5},
+ undefined,
+ {a: 4, b: 3, c: 2},
+ {b: 1, c: 0}
+ ];
+ const dict = tr.b.invertArrayOfDicts(array);
+ assert.sameMembers(Object.keys(dict), ['a', 'b', 'c']);
+ assert.deepEqual(Array.from(dict.a), [6, undefined, 4, undefined]);
+ assert.deepEqual(Array.from(dict.b), [5, undefined, 3, 1]);
+ assert.deepEqual(Array.from(dict.c), [undefined, undefined, 2, 0]);
+ });
+
+ test('invertArrayOfDicts_customGetter', function() {
+ const fakeThis = { itemToDict: JSON.parse };
+ const array = [
+ '{"a": "test", "b": true}',
+ '{}',
+ '{invalid-json}',
+ '{"a": 42, "c": false}'
+ ];
+ const dict = tr.b.invertArrayOfDicts(array, function(item) {
+ try {
+ return this.itemToDict(item);
+ } catch (e) {
+ return undefined;
+ }
+ }, fakeThis);
+ assert.sameMembers(Object.keys(dict), ['a', 'b', 'c']);
+ assert.deepEqual(
+ Array.from(dict.a), ['test', undefined, undefined, 42]);
+ assert.deepEqual(
+ Array.from(dict.b), [true, undefined, undefined, undefined]);
+ assert.deepEqual(
+ Array.from(dict.c), [undefined, undefined, undefined, false]);
+ });
+
+ const ArrayOfIntervals = function(array) {
+ this.array = array;
+ };
+
+ ArrayOfIntervals.prototype = {
+ get(index) {
+ return this.array[index];
+ },
+
+ findLowElementIndex(ts) {
+ return tr.b.findLowIndexInSortedArray(
+ this.array,
+ function(x) { return x.lo; },
+ ts);
+ },
+
+ findIntervalIndex(ts) {
+ return tr.b.findIndexInSortedIntervals(
+ this.array,
+ function(x) { return x.lo; },
+ function(x) { return x.hi - x.lo; },
+ ts);
+ },
+
+ findIndexInClosedIntervals(ts) {
+ return tr.b.findIndexInSortedClosedIntervals(
+ this.array,
+ function(x) { return x.lo; },
+ function(x) { return x.hi; },
+ ts);
+ },
+
+ findIntersectingIntervals(tsA, tsB) {
+ const array = this.array;
+ const result = [];
+ tr.b.iterateOverIntersectingIntervals(
+ this.array,
+ function(x) { return x.lo; },
+ function(x) { return x.hi - x.lo; },
+ tsA,
+ tsB,
+ function(x) { result.push(array.indexOf(x)); });
+ return result;
+ },
+
+ findClosestElement(ts, tsDiff) {
+ return tr.b.findClosestElementInSortedArray(
+ this.array,
+ function(x) { return x.lo; },
+ ts,
+ tsDiff);
+ },
+
+ findClosestInterval(ts, tsDiff) {
+ return tr.b.findClosestIntervalInSortedIntervals(
+ this.array,
+ function(x) { return x.lo; },
+ function(x) { return x.hi; },
+ ts,
+ tsDiff);
+ }
+ };
+
+ test('findLowElementIndex', function() {
+ const array = new ArrayOfIntervals([
+ {lo: 10, hi: 15},
+ {lo: 20, hi: 30}
+ ]);
+
+ assert.strictEqual(array.findLowElementIndex(-100), 0);
+ assert.strictEqual(array.findLowElementIndex(0), 0);
+ assert.strictEqual(array.findLowElementIndex(10), 0);
+
+ assert.strictEqual(array.findLowElementIndex(10.1), 1);
+ assert.strictEqual(array.findLowElementIndex(15), 1);
+ assert.strictEqual(array.findLowElementIndex(20), 1);
+
+ assert.strictEqual(array.findLowElementIndex(20.1), 2);
+ assert.strictEqual(array.findLowElementIndex(21), 2);
+ assert.strictEqual(array.findLowElementIndex(100), 2);
+ });
+
+ test('findIntervalIndex', function() {
+ const array = new ArrayOfIntervals([
+ {lo: 10, hi: 15},
+ {lo: 20, hi: 30}
+ ]);
+
+ assert.strictEqual(array.findIntervalIndex(0), -1);
+ assert.strictEqual(array.findIntervalIndex(9.9), -1);
+
+ assert.strictEqual(array.findIntervalIndex(10), 0);
+ assert.strictEqual(array.findIntervalIndex(12), 0);
+ assert.strictEqual(array.findIntervalIndex(14.9), 0);
+
+ assert.strictEqual(array.findIntervalIndex(20), 1);
+ assert.strictEqual(array.findIntervalIndex(21), 1);
+ assert.strictEqual(array.findIntervalIndex(29.99), 1);
+
+ assert.strictEqual(array.findIntervalIndex(30), 2);
+ assert.strictEqual(array.findIntervalIndex(40), 2);
+
+
+ // misses, in between the intervals, return array length
+ assert.strictEqual(array.findIntervalIndex(15), 2);
+ assert.strictEqual(array.findIntervalIndex(19.9), 2);
+ });
+
+ test('findClosedIntervalIndex', function() {
+ const array = new ArrayOfIntervals([
+ {lo: 10, hi: 15},
+ {lo: 15, hi: 20},
+ {lo: 21, hi: 25}
+ ]);
+
+ assert.strictEqual(array.findIndexInClosedIntervals(0), -1);
+ assert.strictEqual(array.findIndexInClosedIntervals(9.999), -1);
+ assert.strictEqual(array.findIndexInClosedIntervals(10), 0);
+ assert.strictEqual(array.findIndexInClosedIntervals(14), 0);
+ assert.strictEqual(array.findIndexInClosedIntervals(15), 0);
+ assert.strictEqual(array.findIndexInClosedIntervals(15.00001), 1);
+ assert.strictEqual(array.findIndexInClosedIntervals(20.5), 3);
+ assert.strictEqual(array.findIndexInClosedIntervals(22), 2);
+ assert.strictEqual(array.findIndexInClosedIntervals(25), 2);
+ assert.strictEqual(array.findIndexInClosedIntervals(25.00001), 3);
+ });
+
+ test('findClosedInEmptyArray', function() {
+ const array = new ArrayOfIntervals([]);
+ assert.strictEqual(array.findIndexInClosedIntervals(0), 0);
+ });
+
+ test('findIntersectingIntervals', function() {
+ const array = new ArrayOfIntervals([
+ {lo: 10, hi: 15},
+ {lo: 20, hi: 30}
+ ]);
+
+ assert.deepEqual(array.findIntersectingIntervals(0, 0), []);
+ assert.deepEqual(array.findIntersectingIntervals(100, 0), []);
+ assert.deepEqual(array.findIntersectingIntervals(0, 10), []);
+
+ assert.deepEqual(array.findIntersectingIntervals(0, 10.1), [0]);
+ assert.deepEqual(array.findIntersectingIntervals(5, 15), [0]);
+ assert.deepEqual(array.findIntersectingIntervals(15, 20), [0]);
+
+ assert.deepEqual(array.findIntersectingIntervals(15.1, 20), []);
+
+ assert.deepEqual(array.findIntersectingIntervals(15.1, 20.1), [1]);
+ assert.deepEqual(array.findIntersectingIntervals(20, 30), [1]);
+ assert.deepEqual(array.findIntersectingIntervals(30, 100), [1]);
+
+ assert.deepEqual(array.findIntersectingIntervals(0, 100), [0, 1]);
+ assert.deepEqual(array.findIntersectingIntervals(15, 20.1), [0, 1]);
+ });
+
+ test('findClosestElement', function() {
+ const array = new ArrayOfIntervals([
+ {lo: 10, hi: 15},
+ {lo: 20, hi: 30}
+ ]);
+
+ // Test the helper method first.
+ assert.isUndefined(array.get(-1));
+ assert.strictEqual(array.get(0), array.array[0]);
+ assert.strictEqual(array.get(1), array.array[1]);
+ assert.isUndefined(array.get(2));
+
+ assert.isNull(array.findClosestElement(0, 0));
+ assert.isNull(array.findClosestElement(0, 9.9));
+ assert.isNull(array.findClosestElement(10, -10));
+
+ assert.strictEqual(array.get(0), array.findClosestElement(0, 10));
+ assert.strictEqual(array.get(0), array.findClosestElement(8, 5));
+ assert.strictEqual(array.get(0), array.findClosestElement(10, 0));
+ assert.strictEqual(array.get(0), array.findClosestElement(12, 2));
+
+ assert.isNull(array.findClosestElement(15, 3));
+ assert.isNotNull(array.findClosestElement(15, 5));
+
+ assert.strictEqual(array.get(1), array.findClosestElement(19, 1));
+ assert.strictEqual(array.get(1), array.findClosestElement(20, 0));
+ assert.strictEqual(array.get(1), array.findClosestElement(30, 15));
+
+ assert.isNull(array.findClosestElement(30, 9.9));
+ assert.isNull(array.findClosestElement(100, 50));
+ });
+
+ test('findClosestInterval', function() {
+ const array = new ArrayOfIntervals([
+ {lo: 10, hi: 15},
+ {lo: 20, hi: 30}
+ ]);
+
+ assert.isNull(array.findClosestInterval(0, 0));
+ assert.isNull(array.findClosestInterval(0, 9.9));
+ assert.isNull(array.findClosestInterval(0, -100));
+
+ assert.strictEqual(array.get(0), array.findClosestInterval(0, 10));
+ assert.strictEqual(array.get(0), array.findClosestInterval(10, 0));
+ assert.strictEqual(array.get(0), array.findClosestInterval(12, 3));
+ assert.strictEqual(array.get(0), array.findClosestInterval(12, 100));
+
+ assert.strictEqual(array.get(0), array.findClosestInterval(13, 3));
+ assert.strictEqual(array.get(0), array.findClosestInterval(13, 20));
+ assert.strictEqual(array.get(0), array.findClosestInterval(15, 0));
+
+ assert.isNull(array.findClosestInterval(17.5, 0));
+ assert.isNull(array.findClosestInterval(17.5, 2.4));
+ assert.isNotNull(array.findClosestInterval(17.5, 2.5));
+ assert.isNotNull(array.findClosestInterval(17.5, 10));
+
+ assert.strictEqual(array.get(1), array.findClosestInterval(19, 2));
+ assert.strictEqual(array.get(1), array.findClosestInterval(20, 0));
+ assert.strictEqual(array.get(1), array.findClosestInterval(24, 100));
+ assert.strictEqual(array.get(1), array.findClosestInterval(26, 100));
+
+ assert.strictEqual(array.get(1), array.findClosestInterval(30, 0));
+ assert.strictEqual(array.get(1), array.findClosestInterval(35, 10));
+ assert.strictEqual(array.get(1), array.findClosestInterval(50, 100));
+
+ assert.isNull(array.findClosestInterval(50, 19));
+ assert.isNull(array.findClosestInterval(100, 50));
+ assert.isNull(array.findClosestInterval(50, -100));
+ });
+
+ test('findFirstTrueIndexInSortedArray', function() {
+ const array = [1, 3, 7, 15, 30, 50, 80];
+ const objectArray = array.map(v => {return {value: v};});
+
+ assert.strictEqual(
+ tr.b.findFirstTrueIndexInSortedArray(objectArray, x => (x.value > 90)),
+ objectArray.length);
+ assert.strictEqual(
+ tr.b.findFirstTrueIndexInSortedArray(objectArray, x => (x.value > 15)),
+ 4);
+ assert.strictEqual(
+ tr.b.findFirstTrueIndexInSortedArray(objectArray, x => (x.value >= 15)),
+ 3);
+ assert.strictEqual(
+ tr.b.findFirstTrueIndexInSortedArray(objectArray, x => (x.value > 0)),
+ 0);
+ assert.strictEqual(
+ tr.b.findFirstTrueIndexInSortedArray(objectArray,
+ x => (x.value % 5 === 0)),
+ 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/view_state.html b/chromium/third_party/catapult/tracing/tracing/base/view_state.html
new file mode 100644
index 00000000000..a64bbc591fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/view_state.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/serializable.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ /*
+ * This is a base class for MVC Model classes.
+ * Subclasses must call super() and define() in their constructors to define
+ * managed properties.
+ * Call addUpdateListener(listener) to listen for update events.
+ *
+ * When clients set fields on an instance of a subclass of ViewState,
+ * an update event will be dispatched containing the delta.
+ *
+ * Defined properties can optionally contain instances of any ViewState
+ * subclass in Arrays, Maps, or Sets, recursively. Update events do not bubble
+ * up through parent ViewStateModels. This allows clients to choose whether to
+ * listen to every sub-ViewState or select instances. This also allows
+ * ViewState subclasses to define circular references if necessary, though
+ * that is not supported for serialization.
+ */
+ class ViewState extends tr.b.Serializable {
+ constructor() {
+ super();
+ tr.b.EventTarget.decorate(this);
+ }
+
+ setProperty_(name, value) {
+ this.update(new Map([[name, value]]));
+ }
+
+ async updateFromViewState(other) {
+ await this.update(other.properties_);
+ }
+
+ /**
+ * Updates properties, and, if any of them actually changed, dispatches
+ * an event with delta = {propertyName: {previous, current}}.
+ *
+ * @param {!(Object|Map)} delta
+ */
+ async update(delta) {
+ // This method only wants to iterate over delta, so convert it to a map.
+ if (!(delta instanceof Map)) delta = new Map(Object.entries(delta));
+
+ // Clients presumably want to test for changes to specific fields by name,
+ // which is easier with dictionaries, so the actualDelta is a dictionary.
+ const actualDelta = {};
+ for (const [name, current] of delta) {
+ const previous = this[name];
+ if (previous === current) continue;
+
+ actualDelta[name] = {previous, current};
+ tr.b.Serializable.prototype.setProperty_.call(this, name, current);
+ }
+
+ if (Object.keys(actualDelta).length === 0) return;
+
+ await tr.b.dispatchSimpleEventAsync(
+ this, this.updateEventName_, {delta: actualDelta});
+ }
+
+ get updateEventName_() {
+ return this.constructor.name + '.update';
+ }
+
+ /**
+ * @param {!function(!tr.b.Event)} listener
+ */
+ addUpdateListener(listener) {
+ this.addEventListener(this.updateEventName_, listener);
+ }
+
+ /**
+ * @param {!function(!tr.b.Event)} listener
+ */
+ removeUpdateListener(listener) {
+ this.removeEventListener(this.updateEventName_, listener);
+ }
+ }
+
+ return {
+ ViewState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/view_state_test.html b/chromium/third_party/catapult/tracing/tracing/base/view_state_test.html
new file mode 100644
index 00000000000..ec6e2769285
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/view_state_test.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/view_state.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ class Stuff extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('boole', false);
+ this.define('sub', []);
+ }
+ }
+
+ tr.b.ViewState.register(Stuff);
+
+ test('sync', function() {
+ const state = new Stuff();
+ let updateCount = 0;
+ let delta;
+ function listener(event) {
+ ++updateCount;
+ delta = event.delta;
+ }
+ state.addUpdateListener(listener);
+ assert.strictEqual(0, updateCount);
+
+ state.boole = true;
+ assert.strictEqual(1, updateCount);
+ assert.isFalse(delta.boole.previous);
+ assert.isTrue(delta.boole.current);
+ assert.isTrue(state.boole);
+
+ state.boole = true;
+ assert.strictEqual(1, updateCount);
+
+ state.sub = [new Stuff()];
+ assert.strictEqual(2, updateCount);
+ assert.lengthOf(delta.sub.previous, 0);
+ assert.lengthOf(delta.sub.current, 1);
+
+ state.sub.push(new Stuff());
+ assert.strictEqual(2, updateCount);
+
+ state.sub[0].addUpdateListener(listener);
+ assert.strictEqual(2, updateCount);
+
+ state.sub[0].boole = true;
+ assert.strictEqual(3, updateCount);
+ assert.isFalse(delta.boole.previous);
+ assert.isTrue(delta.boole.current);
+ assert.isTrue(state.sub[0].boole);
+ });
+
+ test('async', async function() {
+ const state = new Stuff();
+ let updateCount = 0;
+ let delta;
+ async function listener(event) {
+ ++updateCount;
+ delta = event.delta;
+ await Promise.resolve();
+ }
+ state.addUpdateListener(listener);
+ assert.strictEqual(0, updateCount);
+
+ await state.update({boole: true});
+ assert.strictEqual(1, updateCount);
+ assert.isFalse(delta.boole.previous);
+ assert.isTrue(delta.boole.current);
+ assert.isTrue(state.boole);
+
+ await state.update({boole: true});
+ assert.strictEqual(1, updateCount);
+
+ await state.update({sub: [new Stuff()]});
+ assert.strictEqual(2, updateCount);
+ assert.lengthOf(delta.sub.previous, 0);
+ assert.lengthOf(delta.sub.current, 1);
+
+ state.sub.push(new Stuff());
+ assert.strictEqual(2, updateCount);
+
+ state.sub[0].addUpdateListener(listener);
+ assert.strictEqual(2, updateCount);
+
+ await state.sub[0].update({boole: true});
+ assert.strictEqual(3, updateCount);
+ assert.isFalse(delta.boole.previous);
+ assert.isTrue(delta.boole.current);
+ assert.isTrue(state.sub[0].boole);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/base/xhr.html b/chromium/third_party/catapult/tracing/tracing/base/xhr.html
new file mode 100644
index 00000000000..f168ee43b09
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/base/xhr.html
@@ -0,0 +1,156 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.b', function() {
+ let fs;
+ if (tr.isNode) fs = require('fs');
+
+ function guessBinary(url) {
+ return /[.]gz$/.test(url) || /[.]zip$/.test(url);
+ }
+ function xhr(method, url, async, opt_data, forceBinary) {
+ const req = new XMLHttpRequest();
+ req.overrideMimeType('text/plain; charset=x-user-defined');
+ req.open(method, url, async);
+
+ const isBinary = forceBinary;
+
+ if (isBinary === undefined) {
+ guessBinary(url);
+ if (isBinary && async) req.responseType = 'arraybuffer';
+ }
+
+ const data = opt_data !== undefined ? opt_data : null;
+
+ if (!async) {
+ req.send(data);
+ if (req.status === 200) return req.responseText;
+ throw new Error('XHR failed with status ' + req.status +
+ ' for url ' + url);
+ }
+
+ const p = new Promise(function(resolve, reject) {
+ req.onreadystatechange = function(aEvt) {
+ if (req.readyState !== 4) return;
+ tr.b.timeout(0).then(() => {
+ if (req.status !== 200) {
+ reject(new Error('XHR failed with status ' + req.status +
+ ' for url ' + url));
+ return;
+ }
+ if (req.responseType === 'arraybuffer') {
+ resolve(req.response);
+ return;
+ }
+ resolve(req.responseText);
+ });
+ };
+ });
+ req.send(data);
+ return p;
+ }
+
+ function getAsync(url) {
+ // Browser.
+ if (!tr.isHeadless) return xhr('GET', url, true);
+
+ // Node or vinn prep.
+ let filename;
+ if (url.startsWith('file:///')) {
+ filename = url.substring(7);
+ } else {
+ filename = global.HTMLImportsLoader.hrefToAbsolutePath(url);
+ }
+ const isBinary = guessBinary(url);
+
+ // Node.
+ if (tr.isNode) {
+ const encoding = isBinary ? undefined : 'utf8';
+ return new Promise(function(resolve, reject) {
+ fs.readFile(filename, encoding, function(err, data) {
+ if (err) {
+ reject(err);
+ return;
+ }
+ resolve(data);
+ });
+ });
+ }
+
+ // Vinn.
+ return Promise.resolve().then(function() {
+ if (isBinary) return readbuffer(filename);
+ return read(filename);
+ });
+ }
+
+ function getSync(url, asTraceStream) {
+ // Browser.
+ if (!tr.isHeadless) return xhr('GET', url, false);
+
+ // Node or vinn prep.
+ let filename;
+ if (url.startsWith('file:///')) { // posix
+ filename = url.substring(7);
+ } else if (url.startsWith('file://') && url[8] === ':') { // win
+ filename = url.substring(7);
+ } else {
+ filename = global.HTMLImportsLoader.hrefToAbsolutePath(url);
+ }
+ const isBinary = guessBinary(url);
+
+ // Node.
+ if (tr.isNode) {
+ const encoding = isBinary ? undefined : 'utf8';
+ return fs.readFileSync(filename, encoding);
+ }
+
+ // Vinn.
+ try {
+ if (asTraceStream) {
+ return new tr.b.InMemoryTraceStream(
+ new Uint8Array(readbuffer(filename)), isBinary);
+ } else if (isBinary) {
+ return readbuffer(filename);
+ }
+ return read(filename);
+ } catch (ex) {
+ if (ex.message) {
+ ex.message += ' when reading ' + filename;
+ throw ex;
+ }
+ throw new Error(ex + ' when reading' + filename);
+ }
+ }
+
+ function postAsync(url, data) {
+ if (tr.isHeadless) {
+ throw new Error('Only supported inside a browser');
+ }
+ return xhr('POST', url, true, data);
+ }
+
+ function postTextAsync(url, data) {
+ if (tr.isHeadless) {
+ throw new Error('Only supported inside a browser');
+ }
+ return xhr('POST', url, true, data, false);
+ }
+
+ return {
+ getAsync,
+ getSync,
+ postAsync,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/core/auditor.html b/chromium/third_party/catapult/tracing/tracing/core/auditor.html
new file mode 100644
index 00000000000..dd3dd224a36
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/auditor.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for auditors.
+ */
+tr.exportTo('tr.c', function() {
+ function Auditor(model) {
+ this.model_ = model;
+ }
+
+ Auditor.prototype = {
+ __proto__: Object.prototype,
+
+ get model() {
+ return this.model_;
+ },
+
+ /**
+ * Called by the Model after baking slices. May modify model.
+ */
+ runAnnotate() {
+ },
+
+ /**
+ * Called by import to install userFriendlyCategoryDriver.
+ */
+ installUserFriendlyCategoryDriverIfNeeded() {
+ },
+
+ /**
+ * Called by the Model after importing. Should not modify model, except
+ * for adding interaction ranges and audits.
+ */
+ runAudit() {
+ }
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.defaultMetadata = {};
+ options.mandatoryBaseClass = Auditor;
+ tr.b.decorateExtensionRegistry(Auditor, options);
+
+ return {
+ Auditor,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/core/filter.html b/chromium/third_party/catapult/tracing/tracing/core/filter.html
new file mode 100644
index 00000000000..acbbb06e96f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/filter.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.c', function() {
+ function makeCaseInsensitiveRegex(pattern) {
+ // See https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/
+ // Regular_Expressions.
+ pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return new RegExp(pattern, 'i');
+ }
+
+ /**
+ * @constructor The generic base class for filtering a Model based on
+ * various rules. The base class returns true for everything.
+ */
+ function Filter() { }
+
+ Filter.prototype = {
+ __proto__: Object.prototype,
+
+ matchCounter(counter) {
+ return true;
+ },
+
+ matchCpu(cpu) {
+ return true;
+ },
+
+ matchProcess(process) {
+ return true;
+ },
+
+ matchSlice(slice) {
+ return true;
+ },
+
+ matchThread(thread) {
+ return true;
+ }
+ };
+
+ /**
+ * @constructor A filter that matches objects by their name or category
+ * case insensitive.
+ * .findAllObjectsMatchingFilter
+ */
+ function TitleOrCategoryFilter(text) {
+ Filter.call(this);
+ this.regex_ = makeCaseInsensitiveRegex(text);
+
+ if (!text.length) {
+ throw new Error('Filter text is empty.');
+ }
+ }
+ TitleOrCategoryFilter.prototype = {
+ __proto__: Filter.prototype,
+
+ matchSlice(slice) {
+ if (slice.title === undefined && slice.category === undefined) {
+ return false;
+ }
+
+ return this.regex_.test(slice.title) ||
+ (!!slice.category && this.regex_.test(slice.category));
+ }
+ };
+
+ /**
+ * @constructor A filter that matches objects with the exact given title.
+ */
+ function ExactTitleFilter(text) {
+ Filter.call(this);
+ this.text_ = text;
+
+ if (!text.length) {
+ throw new Error('Filter text is empty.');
+ }
+ }
+ ExactTitleFilter.prototype = {
+ __proto__: Filter.prototype,
+
+ matchSlice(slice) {
+ return slice.title === this.text_;
+ }
+ };
+
+ /**
+ * @constructor A filter that matches objects by their full text contents
+ * (title, category, args). Note that for performance this filter applies a
+ * regex against all the keys of the slice arguments instead of recursing
+ * through any embedded sub-objects.
+ */
+ function FullTextFilter(text) {
+ Filter.call(this);
+ this.regex_ = makeCaseInsensitiveRegex(text);
+ this.titleOrCategoryFilter_ = new TitleOrCategoryFilter(text);
+ }
+ FullTextFilter.prototype = {
+ __proto__: Filter.prototype,
+
+ matchObject_(obj) {
+ for (const key in obj) {
+ if (!obj.hasOwnProperty(key)) continue;
+ if (this.regex_.test(key)) return true;
+ if (this.regex_.test(obj[key])) return true;
+ }
+ return false;
+ },
+
+ matchSlice(slice) {
+ if (this.titleOrCategoryFilter_.matchSlice(slice)) return true;
+ return this.matchObject_(slice.args);
+ }
+ };
+
+ return {
+ Filter,
+ TitleOrCategoryFilter,
+ ExactTitleFilter,
+ FullTextFilter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/core/filter_test.html b/chromium/third_party/catapult/tracing/tracing/core/filter_test.html
new file mode 100644
index 00000000000..b69aa378497
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/filter_test.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unittest.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TitleOrCategoryFilter = tr.c.TitleOrCategoryFilter;
+ const ExactTitleFilter = tr.c.ExactTitleFilter;
+ const FullTextFilter = tr.c.FullTextFilter;
+
+ test('titleOrCategoryFilter', function() {
+ assert.throw(function() {
+ new TitleOrCategoryFilter();
+ });
+ assert.throw(function() {
+ new TitleOrCategoryFilter('');
+ });
+
+ const s0 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'a', start: 1, duration: 3});
+ assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s0));
+ assert.isTrue(new TitleOrCategoryFilter('cat').matchSlice(s0));
+ assert.isTrue(new TitleOrCategoryFilter('at').matchSlice(s0));
+ assert.isFalse(new TitleOrCategoryFilter('b').matchSlice(s0));
+ assert.isFalse(new TitleOrCategoryFilter('X').matchSlice(s0));
+
+ const s1 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'abc', start: 1, duration: 3});
+ assert.isTrue(new TitleOrCategoryFilter('abc').matchSlice(s1));
+ assert.isTrue(new TitleOrCategoryFilter('Abc').matchSlice(s1));
+ assert.isTrue(new TitleOrCategoryFilter('cat').matchSlice(s1));
+ assert.isTrue(new TitleOrCategoryFilter('Cat').matchSlice(s1));
+ assert.isFalse(new TitleOrCategoryFilter('cat1').matchSlice(s1));
+ assert.isFalse(new TitleOrCategoryFilter('X').matchSlice(s1));
+ });
+
+ test('exactTitleFilter', function() {
+ assert.throw(function() {
+ new ExactTitleFilter();
+ });
+ assert.throw(function() {
+ new ExactTitleFilter('');
+ });
+
+ const s0 = tr.c.TestUtils.newSliceEx({title: 'a', start: 1, duration: 3});
+ assert.isTrue(new ExactTitleFilter('a').matchSlice(s0));
+ assert.isFalse(new ExactTitleFilter('b').matchSlice(s0));
+ assert.isFalse(new ExactTitleFilter('A').matchSlice(s0));
+
+ const s1 = tr.c.TestUtils.newSliceEx({title: 'abc', start: 1, duration: 3});
+ assert.isTrue(new ExactTitleFilter('abc').matchSlice(s1));
+ assert.isFalse(new ExactTitleFilter('Abc').matchSlice(s1));
+ assert.isFalse(new ExactTitleFilter('bc').matchSlice(s1));
+ assert.isFalse(new ExactTitleFilter('a').matchSlice(s1));
+ });
+
+ test('fullTextFilter', function() {
+ assert.throw(function() {
+ new FullTextFilter();
+ });
+ assert.throw(function() {
+ new FullTextFilter('');
+ });
+
+ const s0 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'a', start: 1, duration: 3});
+ s0.args.key = 'value';
+ s0.args.anotherKey = 'anotherValue';
+ assert.isTrue(new FullTextFilter('cat').matchSlice(s0));
+ assert.isTrue(new FullTextFilter('a').matchSlice(s0));
+ assert.isTrue(new FullTextFilter('key').matchSlice(s0));
+ assert.isTrue(new FullTextFilter('value').matchSlice(s0));
+ assert.isTrue(new FullTextFilter('anotherValue').matchSlice(s0));
+ assert.isFalse(new FullTextFilter('not there').matchSlice(s0));
+
+ const s1 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'a', start: 1, duration: 3});
+ s1.args.key = 123;
+ assert.isTrue(new FullTextFilter('123').matchSlice(s1));
+
+ const s2 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'a', start: 1, duration: 3});
+ s2.args.key = ['innerValue1', 'innerValue2'];
+ assert.isTrue(new FullTextFilter('innerValue1').matchSlice(s2));
+ assert.isTrue(new FullTextFilter('innerValue2').matchSlice(s2));
+
+ const s3 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'a', start: 1, duration: 3});
+ s3.args.key = ['one', 'two', 'three'];
+ assert.isTrue(new FullTextFilter('two').matchSlice(s3));
+
+ const s4 = tr.c.TestUtils.newSliceEx(
+ {cat: 'cat', title: 'a', start: 1, duration: 3});
+ s4.args.key = undefined;
+ assert.isFalse(new FullTextFilter('not there').matchSlice(s4));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/core/scripting_controller.html b/chromium/third_party/catapult/tracing/tracing/core/scripting_controller.html
new file mode 100644
index 00000000000..ec4a6570a36
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/scripting_controller.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/core/scripting_object.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.c', function() {
+ function ScriptingController(brushingStateController) {
+ this.brushingStateController_ = brushingStateController;
+ this.scriptObjectNames_ = [];
+ this.scriptObjectValues_ = [];
+ this.brushingStateController.addEventListener(
+ 'model-changed', this.onModelChanged_.bind(this));
+
+ // Register all scripting objects.
+ const typeInfos = ScriptingObjectRegistry.getAllRegisteredTypeInfos();
+ typeInfos.forEach(function(typeInfo) {
+ this.addScriptObject(typeInfo.metadata.name, typeInfo.constructor);
+ // Also make the object available to the DevTools inspector.
+ global[typeInfo.metadata.name] = typeInfo.constructor;
+ }, this);
+ }
+
+ function ScriptingObjectRegistry() {
+ }
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(ScriptingObjectRegistry, options);
+
+ ScriptingController.prototype = {
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ onModelChanged_() {
+ this.scriptObjectValues_.forEach(function(v) {
+ if (v.onModelChanged) {
+ v.onModelChanged(this.brushingStateController.model);
+ }
+ }, this);
+ },
+
+ addScriptObject(name, value) {
+ this.scriptObjectNames_.push(name);
+ this.scriptObjectValues_.push(value);
+ },
+
+ executeCommand(command) {
+ const f = new Function(
+ this.scriptObjectNames_, 'return eval(' + command + ')');
+ return f.apply(null, this.scriptObjectValues_);
+ }
+ };
+
+ return {
+ ScriptingController,
+ ScriptingObjectRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/core/scripting_controller_test.html b/chromium/third_party/catapult/tracing/tracing/core/scripting_controller_test.html
new file mode 100644
index 00000000000..ab2737e0024
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/scripting_controller_test.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function FakeBrushingStateController() {
+ tr.b.EventTarget.call(this);
+
+ this.addAllEventsMatchingFilterToSelectionReturnValue = [];
+
+ this.viewport = undefined;
+ this.model = undefined;
+ this.selection = new tr.model.EventSet();
+ this.highlight = new tr.model.EventSet();
+ }
+ FakeBrushingStateController.prototype = {
+ __proto__: tr.b.EventTarget.prototype
+ };
+
+ test('scriptingControllerBasicArithmetic', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.c.ScriptingController(brushingStateController);
+ const result = controller.executeCommand('1 + 1');
+ assert.strictEqual(result, 2);
+ });
+
+ test('scriptingControllerNonLocalContext', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.c.ScriptingController(brushingStateController);
+ const x = 1;
+ controller.executeCommand('x = 2');
+ assert.strictEqual(x, 1);
+ });
+
+ test('scriptingControllerModifyGlobalContext', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.c.ScriptingController(brushingStateController);
+ global._x = 1;
+ controller.executeCommand('_x = 2');
+ assert.strictEqual(global._x, 2);
+ delete global._x;
+ });
+
+ test('scriptingControllerPersistentContext', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.c.ScriptingController(brushingStateController);
+ controller.executeCommand('a = 42');
+ const result = controller.executeCommand('a');
+ assert.strictEqual(result, 42);
+ });
+
+ test('scriptingControllerAddScriptObject', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.c.ScriptingController(brushingStateController);
+ controller.addScriptObject('z', 123);
+ const result = controller.executeCommand('z');
+ assert.strictEqual(result, 123);
+ });
+
+ test('scriptingControllerObjectRegistry', function() {
+ const brushingStateController = new FakeBrushingStateController();
+
+ tr.c.ScriptingObjectRegistry.register(
+ function() { return 123; },
+ {
+ name: 'testFunctionName'
+ }
+ );
+ const controller = new tr.c.ScriptingController(brushingStateController);
+ const result = controller.executeCommand('testFunctionName()');
+ assert.strictEqual(result, 123);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/core/scripting_object.html b/chromium/third_party/catapult/tracing/tracing/core/scripting_object.html
new file mode 100644
index 00000000000..e3dfade5ecb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/scripting_object.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.c', function() {
+ function ScriptingObject() {
+ }
+
+ ScriptingObject.prototype = {
+ onModelChanged(model) {
+ }
+ };
+
+ return {
+ ScriptingObject,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/core/test_utils.html b/chromium/third_party/catapult/tracing/tracing/core/test_utils.html
new file mode 100644
index 00000000000..ceaa4702765
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/core/test_utils.html
@@ -0,0 +1,498 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/profile_node.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/model/stack_frame.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/model/thread_time_slice.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for use in tracing tests.
+ */
+tr.exportTo('tr.c', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ function _getStartAndCpuDurationFromDict(
+ options, required, startFieldName, durationFieldName, endFieldName) {
+ if (options[startFieldName] === undefined) {
+ if (required) {
+ throw new Error('Too little information.');
+ } else {
+ return {start: undefined, duration: undefined};
+ }
+ }
+ if (options[durationFieldName] !== undefined &&
+ options[endFieldName] !== undefined) {
+ throw new Error('Too much information.');
+ }
+ if (options[durationFieldName] === undefined &&
+ options[endFieldName] === undefined) {
+ if (required) {
+ throw new Error('Too little information.');
+ } else {
+ return {start: undefined, duration: undefined};
+ }
+ }
+
+ let duration;
+ if (options[durationFieldName] !== undefined) {
+ duration = options[durationFieldName];
+ } else {
+ duration = options[endFieldName] - options[startFieldName];
+ }
+
+ return {
+ start: options[startFieldName],
+ duration
+ };
+ }
+
+ function _maybeGetCpuStartAndCpuDurationFromDict(options) {
+ return _getStartAndCpuDurationFromDict(
+ options, false, 'cpuStart', 'cpuDuration', 'cpuEnd');
+ }
+
+ function TestUtils() {
+ }
+
+ TestUtils.getStartAndDurationFromDict = function(options) {
+ return _getStartAndCpuDurationFromDict(
+ options, true, 'start', 'duration', 'end');
+ };
+
+ TestUtils.newAsyncSlice = function(start, duration, startThread, endThread) {
+ return TestUtils.newAsyncSliceNamed(
+ 'a', start, duration, startThread, endThread);
+ };
+
+ TestUtils.newAsyncSliceNamed = function(
+ name, start, duration, startThread, endThread) {
+ const asyncSliceConstructor =
+ tr.model.AsyncSlice.subTypes.getConstructor('', name);
+
+ const s = new asyncSliceConstructor('', name, 0, start);
+ s.duration = duration;
+ s.startThread = startThread;
+ s.endThread = endThread;
+ return s;
+ };
+
+ TestUtils.newAsyncSliceEx = function(options) {
+ const sd = TestUtils.getStartAndDurationFromDict(options);
+
+ const cat = options.cat ? options.cat : 'cat';
+ const title = options.title ? options.title : 'a';
+ const colorId = options.colorId || 0;
+
+ let isTopLevel;
+ if (options.isTopLevel !== undefined) {
+ isTopLevel = options.isTopLevel;
+ } else {
+ isTopLevel = false;
+ }
+
+ const asyncSliceConstructor =
+ tr.model.AsyncSlice.subTypes.getConstructor(cat, title);
+
+ const slice = new asyncSliceConstructor(
+ cat,
+ title,
+ colorId,
+ sd.start,
+ options.args ? options.args : {},
+ sd.duration, isTopLevel);
+
+ if (options.id) {
+ slice.id = options.id;
+ } else {
+ slice.id = tr.b.GUID.allocateSimple();
+ }
+
+ if (options.startStackFrame) {
+ slice.startStackFrame = options.startStackFrame;
+ }
+ if (options.endStackFrame) {
+ slice.endStackFrame = options.endStackFrame;
+ }
+ if (options.important) {
+ slice.important = options.important;
+ }
+ if (options.startThread) {
+ slice.startThread = options.startThread;
+ }
+ if (options.endThread) {
+ slice.endThread = options.endThread;
+ }
+ return slice;
+ };
+
+ TestUtils.newCounter = function(parent) {
+ return TestUtils.newCounterNamed(parent, 'a');
+ };
+
+ TestUtils.newCounterNamed = function(parent, name) {
+ const s = new tr.model.Counter(parent, name, null, name);
+ return s;
+ };
+
+ TestUtils.newCounterCategory = function(parent, category, name) {
+ const s = new tr.model.Counter(parent, name, category, name);
+ return s;
+ };
+
+ TestUtils.newCounterSeries = function() {
+ const s = new tr.model.CounterSeries('a', 0);
+ return s;
+ };
+
+ TestUtils.newFlowEventEx = function(options) {
+ if (options.start === undefined) throw new Error('Too little info');
+
+ const title = options.title ? options.title : 'a';
+
+ const colorId = options.colorId ? options.colorId : 0;
+
+ const sd = TestUtils.getStartAndDurationFromDict(options);
+
+ let id;
+ if (options.id !== undefined) {
+ id = options.id;
+ } else {
+ id = tr.b.GUID.allocateSimple();
+ }
+
+ const event = new tr.model.FlowEvent(
+ options.cat ? options.cat : 'cat',
+ id,
+ title,
+ colorId,
+ sd.start,
+ options.args ? options.args : {},
+ sd.duration);
+
+ if (options.startStackFrame) {
+ event.startStackFrame = options.startStackFrame;
+ }
+ if (options.endStackFrame) {
+ event.endStackFrame = options.endStackFrame;
+ }
+ if (options.important) {
+ event.important = options.important;
+ }
+ if (options.startSlice) {
+ event.startSlice = options.startSlice;
+ event.startSlice.outFlowEvents.push(event);
+ }
+ if (options.endSlice) {
+ event.endSlice = options.endSlice;
+ event.endSlice.inFlowEvents.push(event);
+ }
+ return event;
+ };
+
+ TestUtils.newThreadSlice = function(thread, state, start, duration, opt_cpu) {
+ const s = new tr.model.ThreadTimeSlice(
+ thread, state, 'cat', start, {}, duration);
+ if (opt_cpu) {
+ s.cpuOnWhichThreadWasRunning = opt_cpu;
+ }
+ return s;
+ };
+
+ TestUtils.newSampleNamed = function(
+ thread, sampleName, category, frameNames, start) {
+ let model;
+ if (thread.parent) {
+ model = thread.parent.model;
+ } else {
+ model = undefined;
+ }
+ const node = TestUtils.newProfileNodes(model, frameNames);
+ const s = new tr.model.Sample(
+ start, sampleName, node, thread, undefined, 1);
+ return s;
+ };
+
+ TestUtils.newSliceEx = function(options) {
+ const sd = TestUtils.getStartAndDurationFromDict(options);
+
+ const title = options.title ? options.title : 'a';
+
+ const colorId = options.colorId ? options.colorId : 0;
+
+ const cpuSD = _maybeGetCpuStartAndCpuDurationFromDict(options);
+
+ const cat = options.cat ? options.cat : 'cat';
+
+ const bindId = options.bindId ? options.bindId : 0;
+
+ let type;
+ if (options.type) {
+ type = options.type;
+ } else {
+ type = tr.model.ThreadSlice.subTypes.getConstructor(cat, title);
+ }
+
+ const slice = new type(
+ cat,
+ title,
+ colorId,
+ sd.start,
+ options.args ? options.args : {},
+ sd.duration,
+ cpuSD.start, cpuSD.duration,
+ undefined, bindId);
+
+ if (options.isTopLevel) slice.isTopLevel = true;
+
+ return slice;
+ };
+
+ TestUtils.newStackTrace = function(model, titles) {
+ let frame = undefined;
+ titles.forEach(function(title) {
+ frame = new tr.model.StackFrame(
+ frame, tr.b.GUID.allocateSimple(), title, 7);
+ if (model) model.addStackFrame(frame);
+ });
+ return frame;
+ };
+
+ TestUtils.newProfileNode = function(model, title, parentNode) {
+ return new tr.model.ProfileNode(
+ tr.b.GUID.allocateSimple(), title, parentNode);
+ };
+
+ TestUtils.newProfileNodes = function(model, titles) {
+ let node = undefined;
+ for (const title of titles) {
+ node = TestUtils.newProfileNode(model, title, node);
+ }
+ return node;
+ };
+
+ TestUtils.newSnapshot = function(model, options) {
+ return model.getOrCreateProcess(options.pid || 1).objects.addSnapshot(
+ new tr.model.ScopedId(options.scope || tr.model.OBJECT_DEFAULT_SCOPE,
+ options.id || '0x1'),
+ options.category || 'cat',
+ options.name || 'A',
+ options.ts || 0,
+ options.args || {},
+ options.baseTypeName);
+ };
+
+ TestUtils.findSliceNamed = function(slices, name) {
+ if (slices instanceof tr.model.SliceGroup) {
+ slices = slices.slices;
+ }
+ for (let i = 0; i < slices.length; i++) {
+ if (slices[i].title === name) return slices[i];
+ }
+ return undefined;
+ };
+
+ TestUtils.newInteractionRecord = function(parentModel, start, duration) {
+ return new tr.model.um.StubExpectation({
+ parentModel, start, duration});
+ };
+
+ TestUtils.newModel = function(customizeModelCallback) {
+ return TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneEmptyContainers: false,
+ customizeModelCallback
+ });
+ };
+
+ TestUtils.newModelWithEvents = function(events, opts) {
+ if (!(events instanceof Array)) events = [events];
+
+ opts = opts || {};
+
+ const io = new tr.importer.ImportOptions();
+ io.showImportWarnings = false;
+ io.customizeModelCallback = opts.customizeModelCallback;
+ io.trackDetailedModelStats = opts.trackDetailedModelStats === undefined ?
+ false : opts.trackDetailedModelStats;
+ io.shiftWorldToZero = opts.shiftWorldToZero === undefined ?
+ true : opts.shiftWorldToZero;
+ io.pruneEmptyContainers = opts.pruneEmptyContainers === undefined ?
+ true : opts.pruneEmptyContainers;
+ io.auditorConstructors = opts.auditorConstructors === undefined ?
+ [] : opts.auditorConstructors;
+
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m, io);
+ i.importTraces(events);
+ return m;
+ };
+
+ TestUtils.newModelWithAuditor = function(customizeModelCallback, auditor) {
+ return TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneEmptyContainers: false,
+ customizeModelCallback,
+ auditorConstructors: [auditor]
+ });
+ };
+
+ TestUtils.newFakeThread = function() {
+ const process = {model: {}};
+ return new tr.model.Thread(process);
+ };
+
+ /** @constructor */
+ TestUtils.SourceGenerator = function() {
+ this.sourceList_ = [];
+ this.currentLineCommentList_ = [];
+ this.currentIndent_ = 0;
+ this.currentLineEmpty_ = true;
+ };
+
+ TestUtils.SourceGenerator.prototype = {
+ push(/* arguments */) {
+ if (this.currentLineEmpty_) {
+ this.sourceList_.push(' '.repeat(this.currentIndent_));
+ this.currentLineEmpty_ = false;
+ }
+ this.sourceList_.push.apply(
+ this.sourceList_, Array.prototype.slice.call(arguments));
+ },
+
+ pushComment(/* arguments */) {
+ this.currentLineCommentList_.push.apply(
+ this.currentLineCommentList_, Array.prototype.slice.call(arguments));
+ },
+
+ build() {
+ this.finishLine_();
+ return this.sourceList_.join('');
+ },
+
+ breakLine() {
+ this.finishLine_();
+ this.push('\n');
+ this.currentLineEmpty_ = true;
+ },
+
+ finishLine_() {
+ if (this.currentLineCommentList_.length === 0) return;
+ this.push(' // ');
+ this.push.apply(this, this.currentLineCommentList_);
+ this.push('.');
+ this.currentLineCommentList_ = [];
+ },
+
+ indentBlock(spaces, breakLine, blockCallback, opt_this) {
+ opt_this = opt_this || this;
+ this.currentIndent_ += spaces;
+ if (breakLine) this.breakLine();
+ blockCallback.call(opt_this);
+ this.currentIndent_ -= spaces;
+ },
+
+ formatSingleLineList(list, itemCallback, opt_this) {
+ opt_this = opt_this || this;
+ this.push('[');
+ Array.from(list).forEach(function(item, index) {
+ if (index > 0) this.push(', ');
+ itemCallback.call(opt_this, item, index);
+ }, this);
+ this.push(']');
+ },
+
+ formatMultiLineList(list, itemCallback, opt_this) {
+ opt_this = opt_this || this;
+ this.push('[');
+ this.indentBlock(2, false /* don't break line */, function() {
+ Array.from(list).forEach(function(item, index) {
+ if (index > 0) this.push(',');
+ this.breakLine();
+ itemCallback.call(opt_this, item, index);
+ }, this);
+ }, this);
+ if (list.length > 0) this.breakLine();
+ this.push(']');
+ },
+
+ formatString(string) {
+ if (string === undefined) {
+ this.push('undefined');
+ } else {
+ this.push('\'', string, '\'');
+ }
+ }
+ };
+
+ TestUtils.addSourceListing = function(test, source) {
+ const testSourceEl = document.createElement('pre');
+ testSourceEl.style.fontFamily = 'monospace';
+ Polymer.dom(testSourceEl).textContent = source;
+
+ const copyButtonEl = document.createElement('button');
+ Polymer.dom(copyButtonEl).textContent = 'Copy into to clipboard';
+ copyButtonEl.addEventListener('click', function() {
+ const selection = window.getSelection();
+
+ // Store the original selection.
+ const originalRanges = new Array(selection.rangeCount);
+ for (let i = 0; i < originalRanges.length; i++) {
+ originalRanges[i] = selection.getRangeAt(i);
+ }
+
+ // Copy the generated test source code into clipboard.
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.selectNode(testSourceEl);
+ selection.addRange(range);
+ document.execCommand('copy');
+
+ // Restore the original selection.
+ selection.removeAllRanges();
+ for (let i = 0; i < originalRanges.length; i++) {
+ selection.addRange(originalRanges[i]);
+ }
+ });
+
+ const outputEl = document.createElement('div');
+ Polymer.dom(outputEl).appendChild(copyButtonEl);
+ Polymer.dom(outputEl).appendChild(testSourceEl);
+ test.addHTMLOutput(outputEl);
+ };
+
+ TestUtils.newInstantEvent = function(options) {
+ const title = options.title;
+ const start = options.start;
+ if ((title === undefined) ||
+ (title === '') ||
+ (start === undefined)) {
+ throw new Error('too little information');
+ }
+
+ const category = options.category || 'category';
+ const colorId = options.colorId || 0;
+ const args = options.args || {};
+ return new tr.model.InstantEvent(
+ category, title, colorId, start, args);
+ };
+
+ return {
+ TestUtils,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/__init__.py b/chromium/third_party/catapult/tracing/tracing/extras/__init__.py
new file mode 100644
index 00000000000..a22a6ee39a9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2017 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.
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category.html b/chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category.html
new file mode 100644
index 00000000000..bb153e81a05
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.ads', function() {
+ function Rule(expr, category) {
+ this.expr = expr;
+ this.category = category;
+ }
+
+ const CATEGORY_ADS = 'Ads';
+ const CATEGORY_ANALYTICS = 'Analytics';
+ const CATEGORY_CONTENT_RECOMMENDATION = 'Content Recommendation';
+ const CATEGORY_EXTENSIONS = 'Extensions';
+ const CATEGORY_OTHER = 'Other';
+ const CATEGORY_SOCIAL = 'Social';
+ const CATEGORY_TRACING = 'Tracing';
+
+ // Each rule is a tuple of (RegEx, Category). The first match will be used.
+ // For now we only assign 1 category to a domain; it may make sense to
+ // generalize this later to labeling domains with several categories.
+ const CATEGORY_DOMAIN_RULES = [
+ new Rule(/\.googleadservices\.com$/, CATEGORY_ADS),
+ new Rule(/\.moatads\.com$/, CATEGORY_ANALYTICS),
+ // Yahoo-Bing Contextual Ads
+ new Rule(/contextual\.media\.net$/, CATEGORY_ADS),
+ new Rule(/\.postrelease\.com$/, CATEGORY_CONTENT_RECOMMENDATION),
+ new Rule(/\.burt\.io$/, CATEGORY_ANALYTICS),
+ // Google DoubleClick, GPT
+ new Rule(/\.googletagservices\.com$/, CATEGORY_ADS),
+ new Rule(/\.ensighten\.com$/, CATEGORY_ANALYTICS),
+ // Amazon CloudFront
+ new Rule(/\.cloudfront\.net$/, CATEGORY_ADS),
+ new Rule(/\.viafoura\.net$/, CATEGORY_ANALYTICS),
+ // Google Content Ads/DoubleClick
+ new Rule(/\.googlesyndication\.com$/, CATEGORY_ADS),
+ // Google DoubleClick
+ new Rule(/\.doubleclick\.net$/, CATEGORY_ADS),
+ // Adobe Analytics/TagManager
+ new Rule(/\.adobetag\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/\.rubiconproject\.com$/, CATEGORY_ADS),
+ new Rule(/\.sharethrough\.com$/, CATEGORY_ADS),
+ new Rule(/\.facebook\.net$/, CATEGORY_SOCIAL),
+ new Rule(/\.facebook\.com$/, CATEGORY_SOCIAL),
+ new Rule(/\.outbrain\.com$/, CATEGORY_CONTENT_RECOMMENDATION),
+ // Google Content Ads/Others
+ new Rule(/\.gstatic\.com$/, CATEGORY_ADS),
+ new Rule(/\.newrelic\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/\.newsinc\.com$/, CATEGORY_ADS),
+ new Rule(/\.spingo\.com$/, CATEGORY_CONTENT_RECOMMENDATION),
+ new Rule(/\.visualrevenue\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/cdn\.livefyre\.com$/, CATEGORY_SOCIAL),
+ new Rule(/\.dynamicyield\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/cdn\.krxd\.net$/, CATEGORY_ANALYTICS),
+ new Rule(/\.chartbeat\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/\.clicktale\.net$/, CATEGORY_ANALYTICS),
+ new Rule(/\.gravity\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/\.brightcove\.com$/, CATEGORY_ANALYTICS),
+ new Rule(/^chrome:\/\//, CATEGORY_TRACING),
+ new Rule(/^chrome-extension:\/\//, CATEGORY_EXTENSIONS)
+ ];
+
+ function DomainCategory() {
+ }
+
+ DomainCategory.fromDomain = function(domain) {
+ for (let i = 0; i < CATEGORY_DOMAIN_RULES.length; i++) {
+ if (CATEGORY_DOMAIN_RULES[i].expr.test(domain)) {
+ return CATEGORY_DOMAIN_RULES[i].category;
+ }
+ }
+ return CATEGORY_OTHER;
+ };
+
+ return {
+ DomainCategory
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category_test.html b/chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category_test.html
new file mode 100644
index 00000000000..531726071dd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/ads/domain_category_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/ads/domain_category.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const DomainCategory = tr.e.ads.DomainCategory;
+ test('domainCategory', function() {
+ assert.strictEqual(
+ 'Ads',
+ DomainCategory.fromDomain('https://parnter.googleadservices.com'));
+ assert.strictEqual(
+ 'Analytics',
+ DomainCategory.fromDomain('https://x.adobetag.com'));
+ assert.strictEqual(
+ 'Social',
+ DomainCategory.fromDomain('http://x.facebook.net'));
+ assert.strictEqual(
+ 'Content Recommendation',
+ DomainCategory.fromDomain('http://xyz.outbrain.com'));
+ assert.strictEqual(
+ 'Tracing', DomainCategory.fromDomain('chrome://tracing'));
+ assert.strictEqual(
+ 'Extensions',
+ DomainCategory.fromDomain('chrome-extension://oiuy325oiu'));
+ assert.strictEqual(
+ 'Other',
+ DomainCategory.fromDomain('http://xyz.outbrain.com.au'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor.html b/chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor.html
new file mode 100644
index 00000000000..a1638d24baa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor.html
@@ -0,0 +1,767 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/model/alert.html">
+<link rel="import" href="/tracing/model/frame.html">
+<link rel="import" href="/tracing/model/helpers/android_model_helper.html">
+<link rel="import" href="/tracing/model/thread_time_slice.html">
+<link rel="import" href="/tracing/model/user_model/response_expectation.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for Android-specific Auditing.
+ */
+tr.exportTo('tr.e.audits', function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const Auditor = tr.c.Auditor;
+ const AndroidModelHelper = tr.model.helpers.AndroidModelHelper;
+ const ColorScheme = tr.b.ColorScheme;
+ const Statistics = tr.b.math.Statistics;
+ const FRAME_PERF_CLASS = tr.model.FRAME_PERF_CLASS;
+ const Alert = tr.model.Alert;
+ const EventInfo = tr.model.EventInfo;
+ const Scalar = tr.b.Scalar;
+ const timeDurationInMs = tr.b.Unit.byName.timeDurationInMs;
+
+ // TODO: extract from VSYNC, since not all devices have vsync near 60fps
+ const EXPECTED_FRAME_TIME_MS = 16.67;
+
+ function getStart(e) { return e.start; }
+ function getDuration(e) { return e.duration; }
+ // used for general UI thread responsiveness alerts, falls back to duration
+ function getCpuDuration(e) {
+ return (e.cpuDuration !== undefined) ? e.cpuDuration : e.duration;
+ }
+
+ function frameIsActivityStart(frame) {
+ return frame.associatedEvents.any(x => x.title === 'activityStart');
+ }
+
+ function frameMissedDeadline(frame) {
+ return frame.args.deadline && frame.args.deadline < frame.end;
+ }
+
+ /** Builder object for EventInfo docLink structures */
+ function DocLinkBuilder() {
+ this.docLinks = [];
+ }
+ DocLinkBuilder.prototype = {
+ addAppVideo(name, videoId) {
+ this.docLinks.push({
+ label: 'Video Link',
+ textContent: ('Android Performance Patterns: ' + name),
+ href: 'https://www.youtube.com/watch?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE&v=' + videoId // @suppress longLineCheck
+ });
+ return this;
+ },
+ addDacRef(name, link) {
+ this.docLinks.push({
+ label: 'Doc Link',
+ textContent: (name + ' documentation'),
+ href: 'https://developer.android.com/reference/' + link
+ });
+ return this;
+ },
+ build() {
+ return this.docLinks;
+ }
+ };
+
+ /**
+ * Auditor for Android-specific traces.
+ * @constructor
+ */
+ function AndroidAuditor(model) {
+ Auditor.call(this, model);
+
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ if (helper.apps.length || helper.surfaceFlinger) {
+ this.helper = helper;
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Rendering / RenderThread alerts - only available on SDK 22+
+ //////////////////////////////////////////////////////////////////////////////
+
+ AndroidAuditor.viewAlphaAlertInfo_ = new EventInfo(
+ 'Inefficient View alpha usage',
+ 'Setting an alpha between 0 and 1 has significant performance costs, if one of the fast alpha paths is not used.', // @suppress longLineCheck
+ new DocLinkBuilder()
+ .addAppVideo('Hidden Cost of Transparency', 'wIy8g8yNhNk')
+ .addDacRef('View#setAlpha()', 'android/view/View.html#setAlpha(float)') // @suppress longLineCheck
+ .build());
+ AndroidAuditor.saveLayerAlertInfo_ = new EventInfo(
+ 'Expensive rendering with Canvas#saveLayer()',
+ 'Canvas#saveLayer() incurs extremely high rendering cost. They disrupt the rendering pipeline when drawn, forcing a flush of drawing content. Instead use View hardware layers, or static Bitmaps. This enables the offscreen buffers to be reused in between frames, and avoids the disruptive render target switch.', // @suppress longLineCheck
+ new DocLinkBuilder()
+ .addAppVideo('Hidden Cost of Transparency', 'wIy8g8yNhNk')
+ .addDacRef('Canvas#saveLayerAlpha()', 'android/graphics/Canvas.html#saveLayerAlpha(android.graphics.RectF, int, int)') // @suppress longLineCheck
+ .build());
+ AndroidAuditor.getSaveLayerAlerts_ = function(frame) {
+ const badAlphaRegEx =
+ /^(.+) alpha caused (unclipped )?saveLayer (\d+)x(\d+)$/;
+ const saveLayerRegEx = /^(unclipped )?saveLayer (\d+)x(\d+)$/;
+
+ const ret = [];
+ const events = [];
+
+ frame.associatedEvents.forEach(function(slice) {
+ const match = badAlphaRegEx.exec(slice.title);
+ if (match) {
+ // due to bug in tracing code on SDK 22, ignore
+ // presence of 'unclipped' string in View alpha slices
+ const args = { 'view name': match[1],
+ 'width': parseInt(match[3]),
+ 'height': parseInt(match[4]) };
+ ret.push(new Alert(AndroidAuditor.viewAlphaAlertInfo_,
+ slice.start, [slice], args));
+ } else if (saveLayerRegEx.test(slice.title)) {
+ events.push(slice);
+ }
+ }, this);
+
+ if (events.length > ret.length) {
+ // more saveLayers than bad alpha can account for - add another alert
+
+ const unclippedSeen = Statistics.sum(events, function(slice) {
+ return saveLayerRegEx.exec(slice.title)[1] ? 1 : 0;
+ });
+ const clippedSeen = events.length - unclippedSeen;
+ const earliestStart = Statistics.min(events, function(slice) {
+ return slice.start;
+ });
+
+ const args = {
+ 'Unclipped saveLayer count (especially bad!)': unclippedSeen,
+ 'Clipped saveLayer count': clippedSeen
+ };
+
+ events.push(frame);
+ ret.push(new Alert(AndroidAuditor.saveLayerAlertInfo_,
+ earliestStart, events, args));
+ }
+
+ return ret;
+ };
+
+
+ AndroidAuditor.pathAlertInfo_ = new EventInfo(
+ 'Path texture churn',
+ 'Paths are drawn with a mask texture, so when a path is modified / newly drawn, that texture must be generated and uploaded to the GPU. Ensure that you cache paths between frames and do not unnecessarily call Path#reset(). You can cut down on this cost by sharing Path object instances between drawables/views.'); // @suppress longLineCheck
+ AndroidAuditor.getPathAlert_ = function(frame) {
+ const uploadRegEx = /^Generate Path Texture$/;
+
+ const events = frame.associatedEvents.filter(function(event) {
+ return event.title === 'Generate Path Texture';
+ });
+ const start = Statistics.min(events, getStart);
+ const duration = Statistics.sum(events, getDuration);
+
+ if (duration < 3) return undefined;
+
+ events.push(frame);
+ return new Alert(AndroidAuditor.pathAlertInfo_, start, events,
+ { 'Time spent': new Scalar(timeDurationInMs, duration) });
+ };
+
+
+ AndroidAuditor.uploadAlertInfo_ = new EventInfo(
+ 'Expensive Bitmap uploads',
+ 'Bitmaps that have been modified / newly drawn must be uploaded to the GPU. Since this is expensive if the total number of pixels uploaded is large, reduce the amount of Bitmap churn in this animation/context, per frame.'); // @suppress longLineCheck
+ AndroidAuditor.getUploadAlert_ = function(frame) {
+ const uploadRegEx = /^Upload (\d+)x(\d+) Texture$/;
+
+ const events = [];
+ let start = Number.POSITIVE_INFINITY;
+ let duration = 0;
+ let pixelsUploaded = 0;
+ frame.associatedEvents.forEach(function(event) {
+ const match = uploadRegEx.exec(event.title);
+ if (match) {
+ events.push(event);
+ start = Math.min(start, event.start);
+ duration += event.duration;
+ pixelsUploaded += parseInt(match[1]) * parseInt(match[2]);
+ }
+ });
+ if (events.length === 0 || duration < 3) return undefined;
+
+ const mPixels = (pixelsUploaded / 1000000).toFixed(2) + ' million';
+ const args = { 'Pixels uploaded': mPixels,
+ 'Time spent': new Scalar(timeDurationInMs, duration) };
+ events.push(frame);
+ return new Alert(AndroidAuditor.uploadAlertInfo_, start, events, args);
+ };
+
+ //////////////////////////////////////////////////////////////////////////////
+ // UI responsiveness alerts
+ //////////////////////////////////////////////////////////////////////////////
+
+ AndroidAuditor.ListViewInflateAlertInfo_ = new EventInfo(
+ 'Inflation during ListView recycling',
+ 'ListView item recycling involved inflating views. Ensure your Adapter#getView() recycles the incoming View, instead of constructing a new one.'); // @suppress longLineCheck
+ AndroidAuditor.ListViewBindAlertInfo_ = new EventInfo(
+ 'Inefficient ListView recycling/rebinding',
+ 'ListView recycling taking too much time per frame. Ensure your Adapter#getView() binds data efficiently.'); // @suppress longLineCheck
+ AndroidAuditor.getListViewAlert_ = function(frame) {
+ const events = frame.associatedEvents.filter(function(event) {
+ return event.title === 'obtainView' || event.title === 'setupListItem';
+ });
+ const duration = Statistics.sum(events, getCpuDuration);
+
+ if (events.length === 0 || duration < 3) return undefined;
+
+ // simplifying assumption - check for *any* inflation.
+ // TODO(ccraik): make 'inflate' slices associated events.
+ let hasInflation = false;
+ for (const event of events) {
+ if (event.findDescendentSlice('inflate')) {
+ hasInflation = true;
+ }
+ }
+
+ const start = Statistics.min(events, getStart);
+ const args = { 'Time spent': new Scalar(timeDurationInMs, duration) };
+ args['ListView items ' + (hasInflation ? 'inflated' : 'rebound')] =
+ events.length / 2;
+ const eventInfo = hasInflation ? AndroidAuditor.ListViewInflateAlertInfo_ :
+ AndroidAuditor.ListViewBindAlertInfo_;
+ events.push(frame);
+ return new Alert(eventInfo, start, events, args);
+ };
+
+
+ AndroidAuditor.measureLayoutAlertInfo_ = new EventInfo(
+ 'Expensive measure/layout pass',
+ 'Measure/Layout took a significant time, contributing to jank. Avoid triggering layout during animations.', // @suppress longLineCheck
+ new DocLinkBuilder()
+ .addAppVideo('Invalidations, Layouts, and Performance', 'we6poP0kw6E')
+ .build());
+ AndroidAuditor.getMeasureLayoutAlert_ = function(frame) {
+ const events = frame.associatedEvents.filter(function(event) {
+ return event.title === 'measure' || event.title === 'layout';
+ });
+ const duration = Statistics.sum(events, getCpuDuration);
+
+ if (events.length === 0 || duration < 3) return undefined;
+
+ const start = Statistics.min(events, getStart);
+ events.push(frame);
+ return new Alert(AndroidAuditor.measureLayoutAlertInfo_, start, events,
+ { 'Time spent': new Scalar(timeDurationInMs, duration) });
+ };
+
+
+ AndroidAuditor.viewDrawAlertInfo_ = new EventInfo(
+ 'Long View#draw()',
+ 'Recording the drawing commands of invalidated Views took a long time. Avoid significant work in View or Drawable custom drawing, especially allocations or drawing to Bitmaps.', // @suppress longLineCheck
+ new DocLinkBuilder()
+ .addAppVideo('Invalidations, Layouts, and Performance', 'we6poP0kw6E')
+ .addAppVideo('Avoiding Allocations in onDraw()', 'HAK5acHQ53E')
+ .build());
+ AndroidAuditor.getViewDrawAlert_ = function(frame) {
+ let slice = undefined;
+ for (const event of frame.associatedEvents) {
+ if (event.title === 'getDisplayList' ||
+ event.title === 'Record View#draw()') {
+ slice = event;
+ break;
+ }
+ }
+
+ if (!slice || getCpuDuration(slice) < 3) return undefined;
+ return new Alert(AndroidAuditor.viewDrawAlertInfo_, slice.start,
+ [slice, frame],
+ { 'Time spent': new Scalar(
+ timeDurationInMs, getCpuDuration(slice)) });
+ };
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Runtime alerts
+ //////////////////////////////////////////////////////////////////////////////
+
+ AndroidAuditor.blockingGcAlertInfo_ = new EventInfo(
+ 'Blocking Garbage Collection',
+ 'Blocking GCs are caused by object churn, and made worse by having large numbers of objects in the heap. Avoid allocating objects during animations/scrolling, and recycle Bitmaps to avoid triggering garbage collection.', // @suppress longLineCheck
+ new DocLinkBuilder()
+ .addAppVideo('Garbage Collection in Android', 'pzfzz50W5Uo')
+ .addAppVideo('Avoiding Allocations in onDraw()', 'HAK5acHQ53E')
+ .build());
+ AndroidAuditor.getBlockingGcAlert_ = function(frame) {
+ const events = frame.associatedEvents.filter(function(event) {
+ return event.title === 'DVM Suspend' ||
+ event.title === 'GC: Wait For Concurrent';
+ });
+ const blockedDuration = Statistics.sum(events, getDuration);
+ if (blockedDuration < 3) return undefined;
+
+ const start = Statistics.min(events, getStart);
+ events.push(frame);
+ return new Alert(AndroidAuditor.blockingGcAlertInfo_, start, events,
+ { 'Blocked duration': new Scalar(
+ timeDurationInMs, blockedDuration) });
+ };
+
+
+ AndroidAuditor.lockContentionAlertInfo_ = new EventInfo(
+ 'Lock contention',
+ 'UI thread lock contention is caused when another thread holds a lock that the UI thread is trying to use. UI thread progress is blocked until the lock is released. Inspect locking done within the UI thread, and ensure critical sections are short.'); // @suppress longLineCheck
+ AndroidAuditor.getLockContentionAlert_ = function(frame) {
+ const events = frame.associatedEvents.filter(function(event) {
+ return /^Lock Contention on /.test(event.title);
+ });
+
+ const blockedDuration = Statistics.sum(events, getDuration);
+ if (blockedDuration < 1) return undefined;
+
+ const start = Statistics.min(events, getStart);
+ events.push(frame);
+ return new Alert(AndroidAuditor.lockContentionAlertInfo_, start, events,
+ { 'Blocked duration': new Scalar(
+ timeDurationInMs, blockedDuration) });
+ };
+
+ AndroidAuditor.schedulingAlertInfo_ = new EventInfo(
+ 'Scheduling delay',
+ 'Work to produce this frame was descheduled for several milliseconds, contributing to jank. Ensure that code on the UI thread doesn\'t block on work being done on other threads, and that background threads (doing e.g. network or bitmap loading) are running at android.os.Process#THREAD_PRIORITY_BACKGROUND or lower so they are less likely to interrupt the UI thread. These background threads should show up with a priority number of 130 or higher in the scheduling section under the Kernel process.'); // @suppress longLineCheck
+ AndroidAuditor.getSchedulingAlert_ = function(frame) {
+ let totalDuration = 0;
+ const totalStats = {};
+ for (const ttr of frame.threadTimeRanges) {
+ const stats = ttr.thread.getSchedulingStatsForRange(ttr.start, ttr.end);
+ for (const [key, value] of Object.entries(stats)) {
+ if (!(key in totalStats)) {
+ totalStats[key] = 0;
+ }
+ totalStats[key] += value;
+ totalDuration += value;
+ }
+ }
+
+ // only alert if frame not running for > 3ms. Note that we expect a frame
+ // to never describe intentionally idle time.
+ if (!(SCHEDULING_STATE.RUNNING in totalStats) ||
+ totalDuration === 0 ||
+ totalDuration - totalStats[SCHEDULING_STATE.RUNNING] < 3) {
+ return;
+ }
+
+ const args = {};
+ for (const [key, value] of Object.entries(totalStats)) {
+ let newKey = key;
+ if (key === SCHEDULING_STATE.RUNNABLE) {
+ newKey = 'Not scheduled, but runnable';
+ } else if (key === SCHEDULING_STATE.UNINTR_SLEEP) {
+ newKey = 'Blocking I/O delay';
+ }
+ args[newKey] = new Scalar(timeDurationInMs, value);
+ }
+
+ return new Alert(AndroidAuditor.schedulingAlertInfo_, frame.start, [frame],
+ args);
+ };
+
+ AndroidAuditor.prototype = {
+ __proto__: Auditor.prototype,
+
+ renameAndSort_() {
+ this.model.kernel.important = false;// auto collapse
+ // SurfaceFlinger first, other processes sorted by slice count
+ this.model.getAllProcesses().forEach(function(process) {
+ if (this.helper.surfaceFlinger &&
+ process === this.helper.surfaceFlinger.process) {
+ if (!process.name) {
+ process.name = 'SurfaceFlinger';
+ }
+ process.sortIndex = Number.NEGATIVE_INFINITY;
+ process.important = false; // auto collapse
+ return;
+ }
+
+ const uiThread = process.getThread(process.pid);
+ if (!process.name && uiThread && uiThread.name) {
+ if (/^ndroid\./.test(uiThread.name)) {
+ uiThread.name = 'a' + uiThread.name;
+ }
+ process.name = uiThread.name;
+
+ uiThread.name = 'UI Thread';
+ }
+
+ process.sortIndex = 0;
+ for (const tid in process.threads) {
+ process.sortIndex -= process.threads[tid].sliceGroup.slices.length;
+ }
+ }, this);
+
+ // ensure sequential, relative order for UI/Render/Worker threads
+ this.model.getAllThreads().forEach(function(thread) {
+ if (thread.tid === thread.parent.pid) {
+ thread.sortIndex = -3;
+ }
+ if (thread.name === 'RenderThread') {
+ thread.sortIndex = -2;
+ }
+ if (/^hwuiTask/.test(thread.name)) {
+ thread.sortIndex = -1;
+ }
+ });
+ },
+
+ pushFramesAndJudgeJank_() {
+ let badFramesObserved = 0;
+ let framesObserved = 0;
+ const surfaceFlinger = this.helper.surfaceFlinger;
+
+ this.helper.apps.forEach(function(app) {
+ // override frame list
+ app.process.frames = app.getFrames();
+
+ app.process.frames.forEach(function(frame) {
+ if (frame.totalDuration > EXPECTED_FRAME_TIME_MS * 2) {
+ badFramesObserved += 2;
+ frame.perfClass = FRAME_PERF_CLASS.TERRIBLE;
+ } else if (frame.totalDuration > EXPECTED_FRAME_TIME_MS ||
+ frameMissedDeadline(frame)) {
+ badFramesObserved++;
+ frame.perfClass = FRAME_PERF_CLASS.BAD;
+ } else {
+ frame.perfClass = FRAME_PERF_CLASS.GOOD;
+ }
+ });
+ framesObserved += app.process.frames.length;
+ });
+
+ if (framesObserved) {
+ const portionBad = badFramesObserved / framesObserved;
+ if (portionBad > 0.3) {
+ this.model.faviconHue = 'red';
+ } else if (portionBad > 0.05) {
+ this.model.faviconHue = 'yellow';
+ } else {
+ this.model.faviconHue = 'green';
+ }
+ }
+ },
+
+ pushEventInfo_() {
+ const appAnnotator = new AppAnnotator();
+ this.helper.apps.forEach(function(app) {
+ if (app.uiThread) {
+ appAnnotator.applyEventInfos(app.uiThread.sliceGroup);
+ }
+ if (app.renderThread) {
+ appAnnotator.applyEventInfos(app.renderThread.sliceGroup);
+ }
+ });
+ },
+
+ runAnnotate() {
+ if (!this.helper) return;
+
+ this.renameAndSort_();
+ this.pushFramesAndJudgeJank_();
+ this.pushEventInfo_();
+
+ this.helper.iterateImportantSlices(function(slice) {
+ slice.important = true;
+ });
+ },
+
+ runAudit() {
+ if (!this.helper) return;
+
+ const alerts = this.model.alerts;
+ this.helper.apps.forEach(function(app) {
+ app.getFrames().forEach(function(frame) {
+ alerts.push.apply(alerts, AndroidAuditor.getSaveLayerAlerts_(frame));
+
+ // skip most alerts for neutral or good frames
+ if (frame.perfClass === FRAME_PERF_CLASS.NEUTRAL ||
+ frame.perfClass === FRAME_PERF_CLASS.GOOD) {
+ return;
+ }
+
+ let alert = AndroidAuditor.getPathAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getUploadAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getListViewAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getMeasureLayoutAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getViewDrawAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getBlockingGcAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getLockContentionAlert_(frame);
+ if (alert) alerts.push(alert);
+
+ alert = AndroidAuditor.getSchedulingAlert_(frame);
+ if (alert) alerts.push(alert);
+ });
+ }, this);
+
+ this.addRenderingInteractionRecords();
+ this.addInputInteractionRecords();
+ },
+
+ addRenderingInteractionRecords() {
+ const events = [];
+ this.helper.apps.forEach(function(app) {
+ events.push.apply(events, app.getAnimationAsyncSlices());
+ events.push.apply(events, app.getFrames());
+ });
+
+ const mergerFunction = function(events) {
+ const ir = new tr.model.um.ResponseExpectation(
+ this.model, 'Rendering',
+ events[0].min,
+ events[events.length - 1].max - events[0].min);
+ this.model.userModel.expectations.push(ir);
+ }.bind(this);
+ tr.b.math.mergeRanges(
+ tr.b.math.convertEventsToRanges(events), 30, mergerFunction);
+ },
+
+ addInputInteractionRecords() {
+ const inputSamples = [];
+ this.helper.apps.forEach(function(app) {
+ inputSamples.push.apply(inputSamples, app.getInputSamples());
+ });
+
+ const mergerFunction = function(events) {
+ const ir = new tr.model.um.ResponseExpectation(
+ this.model, 'Input',
+ events[0].min,
+ events[events.length - 1].max - events[0].min);
+ this.model.userModel.expectations.push(ir);
+ }.bind(this);
+ const inputRanges = inputSamples.map(function(sample) {
+ return tr.b.math.Range.fromExplicitRange(
+ sample.timestamp, sample.timestamp);
+ });
+ tr.b.math.mergeRanges(inputRanges, 30, mergerFunction);
+ }
+ };
+
+ Auditor.register(AndroidAuditor);
+
+ function AppAnnotator() {
+ this.titleInfoLookup = new Map();
+ this.titleParentLookup = new Map();
+ this.build_();
+ }
+
+ AppAnnotator.prototype = {
+ build_() {
+ const registerEventInfo = function(dict) {
+ this.titleInfoLookup.set(dict.title, new EventInfo(
+ dict.title, dict.description, dict.docLinks));
+ if (dict.parents) {
+ this.titleParentLookup.set(dict.title, dict.parents);
+ }
+ }.bind(this);
+
+ registerEventInfo({
+ title: 'inflate',
+ description: 'Constructing a View hierarchy from pre-processed XML via LayoutInflater#layout. This includes constructing all of the View objects in the hierarchy, and applying styled attributes.'}); // @suppress longLineCheck
+
+ //////////////////////////////////////////////////////////////////////////
+ // Adapter view
+ //////////////////////////////////////////////////////////////////////////
+ registerEventInfo({
+ title: 'obtainView',
+ description: 'Adapter#getView() called to bind content to a recycled View that is being presented.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'setupListItem',
+ description: 'Attached a newly-bound, recycled View to its parent ListView.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'setupGridItem',
+ description: 'Attached a newly-bound, recycled View to its parent GridView.'}); // @suppress longLineCheck
+
+ //////////////////////////////////////////////////////////////////////////
+ // Choreographer (tracing enabled on M+)
+ //////////////////////////////////////////////////////////////////////////
+ const choreographerLinks = new DocLinkBuilder()
+ .addDacRef('Choreographer', 'android/view/Choreographer.html') // @suppress longLineCheck
+ .build();
+ registerEventInfo({
+ title: 'Choreographer#doFrame',
+ docLinks: choreographerLinks,
+ description: 'Choreographer executes frame callbacks for inputs, animations, and rendering traversals. When this work is done, a frame will be presented to the user.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'input',
+ parents: ['Choreographer#doFrame'],
+ docLinks: choreographerLinks,
+ description: 'Input callbacks are processed. This generally encompasses dispatching input to Views, as well as any work the Views do to process this input/gesture.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'animation',
+ parents: ['Choreographer#doFrame'],
+ docLinks: choreographerLinks,
+ description: 'Animation callbacks are processed. This is generally minimal work, as animations determine progress for the frame, and push new state to animated objects (such as setting View properties).'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'traversals',
+ parents: ['Choreographer#doFrame'],
+ docLinks: choreographerLinks,
+ description: 'Primary draw traversals. This is the primary traversal of the View hierarchy, including layout and draw passes.'}); // @suppress longLineCheck
+
+ //////////////////////////////////////////////////////////////////////////
+ // performTraversals + sub methods
+ //////////////////////////////////////////////////////////////////////////
+ const traversalParents = ['Choreographer#doFrame', 'performTraversals'];
+ const layoutLinks = new DocLinkBuilder()
+ .addDacRef('View#Layout', 'android/view/View.html#Layout')
+ .build();
+ registerEventInfo({
+ title: 'performTraversals',
+ description: 'A drawing traversal of the View hierarchy, comprised of all layout and drawing needed to produce the frame.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'measure',
+ parents: traversalParents,
+ docLinks: layoutLinks,
+ description: 'First of two phases in view hierarchy layout. Views are asked to size themselves according to constraints supplied by their parent. Some ViewGroups may measure a child more than once to help satisfy their own constraints. Nesting ViewGroups that measure children more than once can lead to excessive and repeated work.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'layout',
+ parents: traversalParents,
+ docLinks: layoutLinks,
+ description: 'Second of two phases in view hierarchy layout, repositioning content and child Views into their new locations.'}); // @suppress longLineCheck
+ const drawString = 'Draw pass over the View hierarchy. Every invalidated View will have its drawing commands recorded. On Android versions prior to Lollipop, this would also include the issuing of draw commands to the GPU. Starting with Lollipop, it only includes the recording of commands, and syncing that information to the RenderThread.'; // @suppress longLineCheck
+ registerEventInfo({
+ title: 'draw',
+ parents: traversalParents,
+ description: drawString});
+
+ const recordString = 'Every invalidated View\'s drawing commands are recorded. Each will have View#draw() called, and is passed a Canvas that will record and store its drawing commands until it is next invalidated/rerecorded.'; // @suppress longLineCheck
+ registerEventInfo({
+ title: 'getDisplayList', // Legacy name for compatibility.
+ parents: ['draw'],
+ description: recordString});
+ registerEventInfo({
+ title: 'Record View#draw()',
+ parents: ['draw'],
+ description: recordString});
+
+ registerEventInfo({
+ title: 'drawDisplayList',
+ parents: ['draw'],
+ description: 'Execution of recorded draw commands to generate a frame. This represents the actual formation and issuing of drawing commands to the GPU. On Android L and higher devices, this work is done on a dedicated RenderThread, instead of on the UI Thread.'}); // @suppress longLineCheck
+
+ //////////////////////////////////////////////////////////////////////////
+ // RenderThread
+ //////////////////////////////////////////////////////////////////////////
+ registerEventInfo({
+ title: 'DrawFrame',
+ description: 'RenderThread portion of the standard UI/RenderThread split frame. This represents the actual formation and issuing of drawing commands to the GPU.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'doFrame',
+ description: 'RenderThread animation frame. Represents drawing work done by the RenderThread on a frame where the UI thread did not produce new drawing content.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'syncFrameState',
+ description: 'Sync stage between the UI thread and the RenderThread, where the UI thread hands off a frame (including information about modified Views). Time in this method primarily consists of uploading modified Bitmaps to the GPU. After this sync is completed, the UI thread is unblocked, and the RenderThread starts to render the frame.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'flush drawing commands',
+ description: 'Issuing the now complete drawing commands to the GPU.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'eglSwapBuffers',
+ description: 'Complete GPU rendering of the frame.'}); // @suppress longLineCheck
+
+ //////////////////////////////////////////////////////////////////////////
+ // RecyclerView
+ //////////////////////////////////////////////////////////////////////////
+ registerEventInfo({
+ title: 'RV Scroll',
+ description: 'RecyclerView is calculating a scroll. If there are too many of these in Systrace, some Views inside RecyclerView might be causing it. Try to avoid using EditText, focusable views or handle them with care.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'RV OnLayout',
+ description: 'OnLayout has been called by the View system. If this shows up too many times in Systrace, make sure the children of RecyclerView do not update themselves directly. This will cause a full re-layout but when it happens via the Adapter notifyItemChanged, RecyclerView can avoid full layout calculation.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'RV FullInvalidate',
+ description: 'NotifyDataSetChanged or equal has been called. If this is taking a long time, try sending granular notify adapter changes instead of just calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter might help.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'RV PartialInvalidate',
+ description: 'RecyclerView is rebinding a View. If this is taking a lot of time, consider optimizing your layout or make sure you are not doing extra operations in onBindViewHolder call.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'RV OnBindView',
+ description: 'RecyclerView is rebinding a View. If this is taking a lot of time, consider optimizing your layout or make sure you are not doing extra operations in onBindViewHolder call.'}); // @suppress longLineCheck
+ registerEventInfo({
+ title: 'RV CreateView',
+ description: 'RecyclerView is creating a new View. If too many of these are present: 1) There might be a problem in Recycling (e.g. custom Animations that set transient state and prevent recycling or ItemAnimator not implementing the contract properly. See Adapter#onFailedToRecycleView(ViewHolder). 2) There may be too many item view types. Try merging them. 3) There might be too many itemChange animations and not enough space in RecyclerPool. Try increasing your pool size and item cache size.'}); // @suppress longLineCheck
+
+ //////////////////////////////////////////////////////////////////////////
+ // Graphics + Composition
+ //////////////////////////////////////////////////////////////////////////
+ // TODO(ccraik): SurfaceFlinger work
+ registerEventInfo({
+ title: 'eglSwapBuffers',
+ description: 'The CPU has finished producing drawing commands, and is flushing drawing work to the GPU, and posting that buffer to the consumer (which is often SurfaceFlinger window composition). Once this is completed, the GPU can produce the frame content without any involvement from the CPU.'}); // @suppress longLineCheck
+ },
+
+ applyEventInfosRecursive_(parentNames, slice) {
+ const checkExpectedParentNames = function(expectedParentNames) {
+ if (!expectedParentNames) return true;
+ return expectedParentNames.some(function(name) {
+ return parentNames.has(name);
+ });
+ };
+
+ // Set EventInfo on the slice if it matches title, and parent.
+ if (this.titleInfoLookup.has(slice.title)) {
+ if (checkExpectedParentNames(this.titleParentLookup.get(slice.title))) {
+ slice.info = this.titleInfoLookup.get(slice.title);
+ }
+ }
+
+ // Push slice into parentNames, and recurse over subSlices.
+ if (slice.subSlices.length > 0) {
+ // Increment title in parentName dict.
+ if (!parentNames.has(slice.title)) {
+ parentNames.set(slice.title, 0);
+ }
+ parentNames.set(slice.title, parentNames.get(slice.title) + 1);
+
+ // Recurse over subSlices.
+ slice.subSlices.forEach(function(subSlice) {
+ this.applyEventInfosRecursive_(parentNames, subSlice);
+ }, this);
+
+ // Decrement title in parentName dict.
+ parentNames.set(slice.title, parentNames.get(slice.title) - 1);
+ if (parentNames.get(slice.title) === 0) {
+ delete parentNames[slice.title];
+ }
+ }
+ },
+
+ applyEventInfos(sliceGroup) {
+ sliceGroup.topLevelSlices.forEach(function(slice) {
+ this.applyEventInfosRecursive_(new Map(), slice);
+ }, this);
+ }
+ };
+
+ return {
+ AndroidAuditor,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor_test.html b/chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor_test.html
new file mode 100644
index 00000000000..bb042eac514
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/android/android_auditor_test.html
@@ -0,0 +1,424 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/android/android_auditor.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/frame.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const FRAME_PERF_CLASS = tr.model.FRAME_PERF_CLASS;
+ const newThreadSlice = tr.c.TestUtils.newThreadSlice;
+ const Scalar = tr.b.Scalar;
+ const timeDurationInMs = tr.b.Unit.byName.timeDurationInMs;
+
+ test('constructorSliceName', function() {
+ // verify 'constructor' slice name doesn't break the auditor
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'constructor', start: 200, duration: 5}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 0);
+ });
+
+ test('saveLayerAlert_badAlpha', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'doFrame', start: 200, duration: 5}));
+ renderThread.sliceGroup.pushSlice(newSliceEx({
+ title: 'BadAlphaView alpha caused saveLayer 480x320',
+ start: 203,
+ duration: 1
+ }));
+
+ // doesn't create alert, since bad alpha accounts for this savelayer
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'unclipped saveLayer 480x320', start: 204, duration: 1}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 1);
+
+ const alert = model.alerts[0];
+ assert.strictEqual(alert.args['view name'], 'BadAlphaView');
+ assert.strictEqual(alert.args.width, 480);
+ assert.strictEqual(alert.args.height, 320);
+ });
+
+ test('saveLayerAlert_canvas', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'doFrame', start: 200, duration: 5}));
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'saveLayer 480x320', start: 204, duration: 1}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 1);
+
+ const alert = model.alerts[0];
+ assert.strictEqual(alert.args['Clipped saveLayer count'], 1);
+ assert.strictEqual(alert.associatedEvents.length, 2);
+ });
+
+ test('generatePathAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'doFrame', start: 0, duration: 20}));
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Generate Path Texture', start: 0, duration: 3}));
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Generate Path Texture', start: 3, duration: 6}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 1);
+
+ const alert = model.alerts[0];
+ assert.deepEqual(alert.args['Time spent'],
+ new Scalar(timeDurationInMs, 9));
+ assert.strictEqual(alert.associatedEvents.length, 3);
+ });
+
+ test('uploadAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'doFrame', start: 0, duration: 20}));
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Upload 1000x1000 Texture', start: 0, duration: 15}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 1);
+
+ const alert = model.alerts[0];
+ assert.strictEqual(alert.args['Pixels uploaded'], '1.00 million');
+ assert.deepEqual(alert.args['Time spent'],
+ new Scalar(timeDurationInMs, 15));
+ assert.strictEqual(alert.associatedEvents.length, 2);
+ });
+
+ test('listViewAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'obtainView', start: 0, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'setupListItem', start: 5, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'obtainView', start: 10, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'setupListItem', start: 15, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 20, duration: 5}));
+
+ // short frame, so no alert should be generated
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'obtainView', start: 50, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'setupListItem', start: 55, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 60, duration: 1}));
+
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'obtainView', start: 100, duration: 10}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'inflate', start: 101, duration: 8}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'setupListItem', start: 110, duration: 10}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 120, duration: 5}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 2);
+ let alert = model.alerts[0];
+ assert.strictEqual(alert.args['ListView items rebound'], 2);
+ assert.deepEqual(alert.args['Time spent'],
+ new Scalar(timeDurationInMs, 20));
+ assert.strictEqual(alert.associatedEvents.length, 5);
+
+ alert = model.alerts[1];
+ assert.strictEqual(alert.args['ListView items inflated'], 1);
+ assert.deepEqual(alert.args['Time spent'],
+ new Scalar(timeDurationInMs, 20));
+ // note: inflate not assoc.
+ assert.strictEqual(alert.associatedEvents.length, 3);
+ });
+
+ test('measureLayoutAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 20}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'measure', start: 0, duration: 5}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'layout', start: 10, duration: 5}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 1);
+
+ const alert = model.alerts[0];
+ assert.deepEqual(alert.args['Time spent'],
+ new Scalar(timeDurationInMs, 10));
+ assert.strictEqual(alert.associatedEvents.length, 3);
+ });
+
+ test('viewDrawAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ // modern naming
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 20}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Record View#draw()', start: 0, duration: 10}));
+
+ // legacy naming
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 40, duration: 20}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'getDisplayList', start: 40, duration: 10}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 2);
+ assert.deepEqual(model.alerts[0].args['Time spent'],
+ new Scalar(timeDurationInMs, 10));
+ assert.deepEqual(model.alerts[1].args['Time spent'],
+ new Scalar(timeDurationInMs, 10));
+ });
+
+ test('blockingGcAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ const sliceGroup = uiThread.sliceGroup;
+ sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 20}));
+ sliceGroup.pushSlice(newSliceEx(
+ {title: 'DVM Suspend', start: 0, duration: 15}));
+
+ sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 50, duration: 20}));
+ sliceGroup.pushSlice(newSliceEx(
+ {title: 'GC: Wait For Concurrent', start: 50, duration: 15}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 2);
+ assert.deepEqual(model.alerts[0].args['Blocked duration'],
+ new Scalar(timeDurationInMs, 15));
+ assert.deepEqual(model.alerts[1].args['Blocked duration'],
+ new Scalar(timeDurationInMs, 15));
+ });
+
+ test('lockContentionAlert', function() {
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ const sliceGroup = uiThread.sliceGroup;
+ sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 20}));
+ sliceGroup.pushSlice(newSliceEx(
+ {title: 'Lock Contention on a lock', start: 0, duration: 15}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(model.alerts.length, 1);
+ assert.deepEqual(model.alerts[0].args['Blocked duration'],
+ new Scalar(timeDurationInMs, 15));
+ });
+
+ test('schedulingAlerts', function() {
+ let model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 20}));
+ uiThread.timeSlices = [
+ newThreadSlice(uiThread, SCHEDULING_STATE.RUNNING, 0, 6),
+ newThreadSlice(uiThread, SCHEDULING_STATE.RUNNABLE, 6, 10),
+ newThreadSlice(uiThread, SCHEDULING_STATE.RUNNING, 16, 4)];
+ }, tr.e.audits.AndroidAuditor);
+ assert.strictEqual(model.alerts.length, 1);
+ let alert = model.alerts[0];
+ assert.strictEqual(alert.info.title, 'Scheduling delay');
+ assert.deepEqual(alert.args['Not scheduled, but runnable'],
+ new Scalar(timeDurationInMs, 10));
+
+ model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 20}));
+ uiThread.timeSlices = [
+ newThreadSlice(uiThread, SCHEDULING_STATE.RUNNING, 0, 5),
+ newThreadSlice(uiThread, SCHEDULING_STATE.UNINTR_SLEEP, 5, 10),
+ newThreadSlice(uiThread, SCHEDULING_STATE.RUNNING, 15, 5)];
+ }, tr.e.audits.AndroidAuditor);
+ assert.strictEqual(model.alerts.length, 1);
+ alert = model.alerts[0];
+ assert.strictEqual(alert.info.title, 'Scheduling delay');
+ assert.deepEqual(alert.args['Blocking I/O delay'],
+ new Scalar(timeDurationInMs, 10));
+ });
+
+ test('addFramesToModel', function() {
+ let process;
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ process = model.getOrCreateProcess(1);
+ const uiThread = process.getOrCreateThread(1);
+
+ // High level choreographer frame signal
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Choreographer#doFrame', start: 0, duration: 8}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Choreographer#doFrame', start: 16, duration: 20}));
+
+ // Old devices only have 'performTraversals'
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 40, duration: 90}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.strictEqual(process.frames.length, 3);
+ assert.closeTo(process.frames[0].totalDuration, 8, 1e-5);
+ assert.closeTo(process.frames[1].totalDuration, 20, 1e-5);
+ assert.closeTo(process.frames[2].totalDuration, 90, 1e-5);
+
+ assert.strictEqual(process.frames[0].perfClass,
+ FRAME_PERF_CLASS.GOOD);
+ assert.strictEqual(process.frames[1].perfClass,
+ FRAME_PERF_CLASS.BAD);
+ assert.strictEqual(process.frames[2].perfClass,
+ FRAME_PERF_CLASS.TERRIBLE);
+ });
+
+ test('processRenameAndSort', function() {
+ let appProcess;
+ let sfProcess;
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ appProcess = model.getOrCreateProcess(1);
+ const uiThread = appProcess.getOrCreateThread(1);
+ uiThread.name = 'ndroid.systemui';
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 8}));
+
+ sfProcess = model.getOrCreateProcess(2);
+ const sfThread = sfProcess.getOrCreateThread(2);
+ sfThread.name = '/system/bin/surfaceflinger';
+ sfThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'doComposition', start: 8, duration: 2}));
+ }, tr.e.audits.AndroidAuditor);
+
+ // both processes should be renamed
+ assert.strictEqual(appProcess.name, 'android.systemui');
+ assert.strictEqual(sfProcess.name, 'SurfaceFlinger');
+
+ assert.isTrue(sfProcess.sortIndex < appProcess.sortIndex);
+ assert.isTrue(appProcess.important);
+ assert.isFalse(sfProcess.important);
+ });
+
+ test('eventInfo', function() {
+ const eventsExpectingInfo = [];
+ const eventsNotExpectingInfo = [];
+
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const appProcess = model.getOrCreateProcess(1);
+ const uiThread = appProcess.getOrCreateThread(1);
+ uiThread.name = 'ndroid.systemui';
+
+ const pushInfoSlice = function(slice) {
+ eventsExpectingInfo.push(slice);
+ uiThread.sliceGroup.pushSlice(slice);
+ };
+ const pushNonInfoSlice = function(slice) {
+ eventsNotExpectingInfo.push(slice);
+ uiThread.sliceGroup.pushSlice(slice);
+ };
+
+ pushInfoSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 10}));
+ pushInfoSlice(newSliceEx({title: 'measure', start: 0, duration: 2}));
+ pushInfoSlice(newSliceEx({title: 'layout', start: 2, duration: 1}));
+ pushInfoSlice(newSliceEx({title: 'draw', start: 3, duration: 7}));
+
+ // Out of place slices should not be tagged.
+ pushNonInfoSlice(newSliceEx({title: 'measure', start: 11, duration: 1}));
+ pushNonInfoSlice(newSliceEx({title: 'draw', start: 12, duration: 1}));
+ }, tr.e.audits.AndroidAuditor);
+
+ eventsExpectingInfo.forEach(function(event) {
+ assert.notEqual(event.info, undefined);
+ });
+
+ eventsNotExpectingInfo.forEach(function(event) {
+ assert.strictEqual(event.info, undefined);
+ });
+ });
+
+ test('drawingThreadPriorities', function() {
+ let uiThread;
+ let renderThread;
+ let workerThread;
+ let otherThread;
+ const model = tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const appProcess = model.getOrCreateProcess(1);
+
+ uiThread = appProcess.getOrCreateThread(1);
+ uiThread.name = 'ndroid.systemui';
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 0, duration: 4}));
+
+ renderThread = appProcess.getOrCreateThread(2);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'DrawFrame', start: 3, duration: 4}));
+
+ workerThread = appProcess.getOrCreateThread(3);
+ workerThread.name = 'hwuiTask1';
+ workerThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'work', start: 4, duration: 1}));
+
+ otherThread = appProcess.getOrCreateThread(4);
+ otherThread.name = 'other';
+ otherThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'otherWork', start: 0, duration: 2}));
+ }, tr.e.audits.AndroidAuditor);
+
+ assert.isTrue(uiThread.sortIndex < renderThread.sortIndex);
+ assert.isTrue(renderThread.sortIndex < workerThread.sortIndex);
+ assert.isTrue(workerThread.sortIndex < otherThread.sortIndex);
+ });
+
+ test('favicon', function() {
+ const createModelWithJank = function(percentageJank) {
+ return tr.c.TestUtils.newModelWithAuditor(function(model) {
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ for (let i = 0; i < 100; i++) {
+ const slice = newSliceEx({
+ title: 'performTraversals',
+ start: 30 * i,
+ duration: i <= percentageJank ? 24 : 8
+ });
+ uiThread.sliceGroup.pushSlice(slice);
+ }
+ }, tr.e.audits.AndroidAuditor);
+ };
+ assert.strictEqual(createModelWithJank(3).faviconHue, 'green');
+ assert.strictEqual(createModelWithJank(10).faviconHue, 'yellow');
+ assert.strictEqual(createModelWithJank(50).faviconHue, 'red');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context.html
new file mode 100644
index 00000000000..08a8842b1f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview BlameContext is the Trace Viewer side correspondence of
+ * Chrome's class base::trace_event::BlameContext. More specifically,
+ *
+ * BlameContextSnapshot, which inherits from ObjectSnapshot, is the base class
+ * of all snapshots of blame contexts traced in Chrome.
+ *
+ * BlameContextInstance, which inherits from ObjectInstance, gathers snapshots
+ * of the same blame context traced in Chrome.
+ *
+ * BlameContextSnapshot and BlameContextInstance should never be instantiated
+ * directly. Subclasses corresponding to different BlameContexts in Chrome
+ * should define their own BlameContextSnapshot and BlameContextInstance
+ * specializations for instantiation.
+ *
+ */
+tr.exportTo('tr.e.chrome', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ function BlameContextSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ BlameContextSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ /**
+ * Returns the parent in the context tree.
+ */
+ get parentContext() {
+ if (this.args.parent instanceof BlameContextSnapshot) {
+ return this.args.parent;
+ }
+ return undefined;
+ },
+
+ get userFriendlyName() {
+ return 'BlameContext';
+ }
+ };
+
+ function BlameContextInstance() {
+ ObjectInstance.apply(this, arguments);
+ }
+
+ BlameContextInstance.prototype = {
+ __proto__: ObjectInstance.prototype,
+
+ /**
+ * Returns the type of the blame context, to be overriden by subclasses.
+ */
+ get blameContextType() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ BlameContextSnapshot,
+ BlameContextInstance,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context_test.html
new file mode 100644
index 00000000000..8d6f4b5be5c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/blame_context_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/blame_context.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
+ const BlameContextInstance = tr.e.chrome.BlameContextInstance;
+
+ function TestBlameContextSnapshot() {
+ BlameContextSnapshot.apply(this, arguments);
+ }
+
+ TestBlameContextSnapshot.prototype = {
+ __proto__: BlameContextSnapshot.prototype,
+
+ get userFriendlyName() {
+ return 'Test';
+ }
+ };
+
+ tr.model.ObjectSnapshot.subTypes.register(
+ TestBlameContextSnapshot,
+ {typeName: 'Test'});
+
+ function TestBlameContextInstance() {
+ BlameContextInstance.apply(this, arguments);
+ }
+
+ TestBlameContextInstance.prototype = {
+ __proto__: BlameContextInstance.prototype,
+
+ get blameContextType() {
+ return 'Test';
+ }
+ };
+
+ tr.model.ObjectInstance.subTypes.register(
+ TestBlameContextInstance,
+ {typeName: 'Test'});
+
+ const TestUtils = tr.c.TestUtils;
+
+ test('parentContext', function() {
+ let parent;
+ let child;
+ TestUtils.newModel(function(model) {
+ parent = TestUtils.newSnapshot(model, {id: '0x1', name: 'Test'});
+ child = TestUtils.newSnapshot(model, {id: '0x2', name: 'Test',
+ args: {parent: {id_ref: '0x1'}}});
+ });
+
+ assert.isTrue(child.parentContext === parent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_blame_context_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_blame_context_test.html
new file mode 100644
index 00000000000..43b15ae43be
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_blame_context_test.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+
+ test('crossProcessCounterpart', function() {
+ let frameTreeNode;
+ let renderFrame;
+ TestUtils.newModel(function(model) {
+ // Add a toplevel to make the context tree consistent with the spec,
+ // though its functionality is not tested here.
+ TestUtils.newSnapshot(model, {
+ pid: 1, name: 'TopLevel', id: '0x1',
+ scope: 'PlatformThread', category: 'blink'});
+ renderFrame = TestUtils.newSnapshot(
+ model, {
+ pid: 1, name: 'RenderFrame', id: '0x2', scope: 'RenderFrame',
+ category: 'blink', args: {
+ parent: {scope: 'PlatformThread', id_ref: '0x1'}}});
+ frameTreeNode = TestUtils.newSnapshot(
+ model, {
+ pid: 2, name: 'FrameTreeNode', id: '0x3', scope: 'FrameTreeNode',
+ category: 'navigation', args: {
+ renderFrame: {scope: 'RenderFrame', id_ref: '0x2', pid_ref: 1}}});
+ });
+
+ assert.isTrue(frameTreeNode.renderFrame === renderFrame);
+ assert.isTrue(renderFrame.frameTreeNode === frameTreeNode);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_tree_node.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_tree_node.html
new file mode 100644
index 00000000000..bfd21093de5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/frame_tree_node.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/blame_context/blame_context.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Trace Viewer side's correspondence of Chrome's
+ * content::FrameTreeNode class.
+ *
+ */
+tr.exportTo('tr.e.chrome', function() {
+ const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
+ const BlameContextInstance = tr.e.chrome.BlameContextInstance;
+
+ function FrameTreeNodeSnapshot() {
+ BlameContextSnapshot.apply(this, arguments);
+ }
+
+ FrameTreeNodeSnapshot.prototype = {
+ __proto__: BlameContextSnapshot.prototype,
+
+ get renderFrame() {
+ if (this.args.renderFrame instanceof tr.e.chrome.RenderFrameSnapshot) {
+ return this.args.renderFrame;
+ }
+ return undefined;
+ },
+
+ get url() {
+ return this.args.url;
+ },
+
+ get userFriendlyName() {
+ return 'FrameTreeNode';
+ }
+ };
+
+ tr.model.ObjectSnapshot.subTypes.register(
+ FrameTreeNodeSnapshot,
+ {typeName: 'FrameTreeNode'});
+
+ function FrameTreeNodeInstance() {
+ BlameContextInstance.apply(this, arguments);
+ }
+
+ FrameTreeNodeInstance.prototype = {
+ __proto__: BlameContextInstance.prototype,
+
+ get blameContextType() {
+ return 'Frame';
+ }
+ };
+
+ tr.model.ObjectInstance.subTypes.register(
+ FrameTreeNodeInstance,
+ {typeName: 'FrameTreeNode'});
+
+ return {
+ FrameTreeNodeSnapshot,
+ FrameTreeNodeInstance,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/render_frame.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/render_frame.html
new file mode 100644
index 00000000000..1543403dae8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/render_frame.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/blame_context/blame_context.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Trace Viewer side's correspondence of Chrome's
+ * content::FrameBlameContext class.
+ *
+ */
+tr.exportTo('tr.e.chrome', function() {
+ const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
+ const BlameContextInstance = tr.e.chrome.BlameContextInstance;
+
+ function RenderFrameSnapshot() {
+ BlameContextSnapshot.apply(this, arguments);
+ }
+
+ RenderFrameSnapshot.prototype = {
+ __proto__: BlameContextSnapshot.prototype,
+
+ referencedAt(item, object, field) {
+ if (item instanceof tr.e.chrome.FrameTreeNodeSnapshot &&
+ object === item.args &&
+ field === 'renderFrame') {
+ this.args.frameTreeNode = item;
+ }
+ },
+
+ get frameTreeNode() {
+ if (this.args.frameTreeNode instanceof
+ tr.e.chrome.FrameTreeNodeSnapshot) {
+ return this.args.frameTreeNode;
+ }
+ return undefined;
+ },
+
+ get url() {
+ if (this.frameTreeNode) {
+ return this.frameTreeNode.url;
+ }
+ return undefined;
+ },
+
+ get userFriendlyName() {
+ return 'RenderFrame';
+ }
+ };
+
+ tr.model.ObjectSnapshot.subTypes.register(
+ RenderFrameSnapshot,
+ {typeName: 'RenderFrame'});
+
+ function RenderFrameInstance() {
+ BlameContextInstance.apply(this, arguments);
+ }
+
+ RenderFrameInstance.prototype = {
+ __proto__: BlameContextInstance.prototype,
+
+ get blameContextType() {
+ return 'Frame';
+ }
+ };
+
+ tr.model.ObjectInstance.subTypes.register(
+ RenderFrameInstance,
+ {typeName: 'RenderFrame'});
+
+ return {
+ RenderFrameSnapshot,
+ RenderFrameInstance,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/top_level.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/top_level.html
new file mode 100644
index 00000000000..b69bdb83b0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blame_context/top_level.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/blame_context/blame_context.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Trace Viewer side's correspondence of Chrome's
+ * content::TopLevelBlameContext class.
+ *
+ */
+tr.exportTo('tr.e.chrome', function() {
+ const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
+ const BlameContextInstance = tr.e.chrome.BlameContextInstance;
+
+ function TopLevelSnapshot() {
+ BlameContextSnapshot.apply(this, arguments);
+ }
+
+ TopLevelSnapshot.prototype = {
+ __proto__: BlameContextSnapshot.prototype,
+
+ get userFriendlyName() {
+ return 'TopLevel';
+ }
+ };
+
+ tr.model.ObjectSnapshot.subTypes.register(
+ TopLevelSnapshot,
+ {typeName: 'TopLevel'});
+
+ function TopLevelInstance() {
+ BlameContextInstance.apply(this, arguments);
+ }
+
+ TopLevelInstance.prototype = {
+ __proto__: BlameContextInstance.prototype,
+
+ get blameContextType() {
+ return 'TopLevel';
+ }
+ };
+
+ tr.model.ObjectInstance.subTypes.register(
+ TopLevelInstance,
+ {typeName: 'TopLevel'});
+
+ return {
+ TopLevelSnapshot,
+ TopLevelInstance,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice.html
new file mode 100644
index 00000000000..1916a699f7a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2017 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.
+-->
+<link rel="import" href="/tracing/model/async_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.blink', function() {
+ class BlinkSchedulerAsyncSlice extends tr.model.AsyncSlice {
+ get viewSubGroupGroupingKey() {
+ if (this.title.startsWith('FrameScheduler.')) {
+ return 'Frame' + this.id;
+ }
+ if (this.title.startsWith('Scheduler.')) {
+ return 'Renderer Scheduler';
+ }
+ return undefined;
+ }
+
+ get viewSubGroupTitle() {
+ // NOTE: Be careful with hardcoded (for performance) string length.
+ if (this.title.startsWith('FrameScheduler.')) {
+ return this.title.substring(15);
+ }
+ if (this.title.startsWith('Scheduler.')) {
+ return this.title.substring(10);
+ }
+ return this.title;
+ }
+ }
+
+ tr.model.AsyncSlice.subTypes.register(BlinkSchedulerAsyncSlice, {
+ categoryParts: [
+ 'renderer.scheduler',
+ 'disabled-by-default-renderer.scheduler',
+ 'disabled-by-default-renderer.scheduler.debug',
+ ]
+ });
+
+ return {
+ BlinkSchedulerAsyncSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice_test.html
new file mode 100644
index 00000000000..be078c3d1ed
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/blink/blink_scheduler_async_slice_test.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/blink/blink_scheduler_async_slice.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const BlinkSchedulerAsyncSlice = tr.e.blink.BlinkSchedulerAsyncSlice;
+
+ test('construct', function() {
+ assert.strictEqual(
+ AsyncSlice.subTypes.getConstructor(
+ 'renderer.scheduler', 'Scheduler.Foo'),
+ BlinkSchedulerAsyncSlice);
+ assert.strictEqual(
+ AsyncSlice.subTypes.getConstructor(
+ 'disabled-by-default-renderer.scheduler', 'Scheduler.Bar'),
+ BlinkSchedulerAsyncSlice);
+ assert.strictEqual(
+ AsyncSlice.subTypes.getConstructor(
+ 'disabled-by-default-renderer.scheduler.debug', 'Scheduler.Baz'),
+ BlinkSchedulerAsyncSlice);
+ });
+
+ test('subgroups', function() {
+ const rendererSlice = new BlinkSchedulerAsyncSlice(
+ 'renderer.scheduler', 'Scheduler.Foo', 7, 0, {}, 3);
+ assert.strictEqual(rendererSlice.viewSubGroupGroupingKey,
+ 'Renderer Scheduler');
+ assert.strictEqual(rendererSlice.viewSubGroupTitle, 'Foo');
+
+
+ const frameSlice = new BlinkSchedulerAsyncSlice(
+ 'renderer.scheduler', 'FrameScheduler.Bar', 7, 0, {}, 3);
+ frameSlice.id = ':ptr:0x1';
+ assert.strictEqual(frameSlice.viewSubGroupGroupingKey, 'Frame:ptr:0x1');
+ assert.strictEqual(frameSlice.viewSubGroupTitle, 'Bar');
+
+ const otherSlice = new BlinkSchedulerAsyncSlice(
+ 'renderer.scheduler', 'Something.Baz', 7, 0, {}, 3);
+ assert.isUndefined(otherSlice.viewSubGroupGroupingKey);
+ assert.strictEqual(otherSlice.viewSubGroupTitle, 'Something.Baz');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/cc.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/cc.html
new file mode 100644
index 00000000000..64b825f6a9f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/cc.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/constants.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/constants.html
new file mode 100644
index 00000000000..1f4aa35b7a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/constants.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const constants = {};
+ constants.ACTIVE_TREE = 0;
+ constants.PENDING_TREE = 1;
+
+ constants.HIGH_PRIORITY_BIN = 0;
+ constants.LOW_PRIORITY_BIN = 1;
+
+ constants.SEND_BEGIN_FRAME_EVENT =
+ 'ThreadProxy::ScheduledActionSendBeginMainFrame';
+ constants.BEGIN_MAIN_FRAME_EVENT = 'ThreadProxy::BeginMainFrame';
+
+ return {
+ constants
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/debug_colors.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/debug_colors.html
new file mode 100644
index 00000000000..07486dcd03c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/debug_colors.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Mapping of different tile configuration
+ * to border colors and widths.
+ */
+tr.exportTo('tr.e.cc', function() {
+ const tileTypes = {
+ highRes: 'highRes',
+ lowRes: 'lowRes',
+ extraHighRes: 'extraHighRes',
+ extraLowRes: 'extraLowRes',
+ missing: 'missing',
+ culled: 'culled',
+ solidColor: 'solidColor',
+ picture: 'picture',
+ directPicture: 'directPicture',
+ unknown: 'unknown'
+ };
+
+ const tileBorder = {
+ highRes: {
+ color: 'rgba(80, 200, 200, 0.7)',
+ width: 1
+ },
+ lowRes: {
+ color: 'rgba(212, 83, 192, 0.7)',
+ width: 2
+ },
+ extraHighRes: {
+ color: 'rgba(239, 231, 20, 0.7)',
+ width: 2
+ },
+ extraLowRes: {
+ color: 'rgba(93, 186, 18, 0.7)',
+ width: 2
+ },
+ missing: {
+ color: 'rgba(255, 0, 0, 0.7)',
+ width: 1
+ },
+ culled: {
+ color: 'rgba(160, 100, 0, 0.8)',
+ width: 1
+ },
+ solidColor: {
+ color: 'rgba(128, 128, 128, 0.7)',
+ width: 1
+ },
+ picture: {
+ color: 'rgba(64, 64, 64, 0.7)',
+ width: 1
+ },
+ directPicture: {
+ color: 'rgba(127, 255, 0, 1.0)',
+ width: 1
+ },
+ unknown: {
+ color: 'rgba(0, 0, 0, 1.0)',
+ width: 2
+ }
+ };
+
+ return {
+ tileTypes,
+ tileBorder
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list.html
new file mode 100644
index 00000000000..bb23acf149f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ function DisplayItemList(skp64, layerRect) {
+ tr.e.cc.Picture.apply(this, arguments);
+ }
+
+ DisplayItemList.prototype = {
+ __proto__: tr.e.cc.Picture.prototype
+ };
+
+ /**
+ * @constructor
+ */
+ function DisplayItemListSnapshot() {
+ tr.e.cc.PictureSnapshot.apply(this, arguments);
+ }
+
+ DisplayItemListSnapshot.prototype = {
+ __proto__: tr.e.cc.PictureSnapshot.prototype,
+
+ initialize() {
+ tr.e.cc.PictureSnapshot.prototype.initialize.call(this);
+ this.displayItems_ = this.args.params.items;
+ },
+
+ get items() {
+ return this.displayItems_;
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ DisplayItemListSnapshot,
+ {typeNames: ['cc::DisplayItemList']});
+
+ return {
+ DisplayItemListSnapshot,
+ DisplayItemList,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list_test.html
new file mode 100644
index 00000000000..78d5d21a57d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/display_item_list_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::DisplayItemList')[0];
+ const snapshot = instance.snapshots[0];
+
+ assert.instanceOf(snapshot, tr.e.cc.DisplayItemListSnapshot);
+ instance.wasDeleted(150);
+ });
+
+ test('getItems', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::DisplayItemList')[0];
+ const snapshot = instance.snapshots[0];
+
+ const items = snapshot.items;
+ assert.strictEqual(items.length, 2);
+
+ assert.strictEqual(items[0], 'BeginClipDisplayItem');
+ assert.strictEqual(items[1], 'EndClipDisplayItem');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice.html
new file mode 100644
index 00000000000..e3bd9bf2010
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice.html
@@ -0,0 +1,645 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/async_slice.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const EventSet = tr.model.EventSet;
+
+ const UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT';
+ const ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT';
+ const BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT';
+ const END_COMP_NAME = 'INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT';
+ const LEGACY_END_COMP_NAME =
+ 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT';
+
+ const MAIN_RENDERER_THREAD_NAME = 'CrRendererMain';
+ const COMPOSITOR_THREAD_NAME = 'Compositor';
+
+ const POSTTASK_FLOW_EVENT = 'disabled-by-default-toplevel.flow';
+ const IPC_FLOW_EVENT = 'disabled-by-default-ipc.flow';
+
+ const INPUT_EVENT_TYPE_NAMES = {
+ CHAR: 'Char',
+ CLICK: 'GestureClick',
+ CONTEXT_MENU: 'ContextMenu',
+ FLING_CANCEL: 'GestureFlingCancel',
+ FLING_START: 'GestureFlingStart',
+ KEY_DOWN: 'KeyDown',
+ KEY_DOWN_RAW: 'RawKeyDown',
+ KEY_UP: 'KeyUp',
+ LATENCY_SCROLL_UPDATE: 'ScrollUpdate',
+ MOUSE_DOWN: 'MouseDown',
+ MOUSE_ENTER: 'MouseEnter',
+ MOUSE_LEAVE: 'MouseLeave',
+ MOUSE_MOVE: 'MouseMove',
+ MOUSE_UP: 'MouseUp',
+ MOUSE_WHEEL: 'MouseWheel',
+ PINCH_BEGIN: 'GesturePinchBegin',
+ PINCH_END: 'GesturePinchEnd',
+ PINCH_UPDATE: 'GesturePinchUpdate',
+ SCROLL_BEGIN: 'GestureScrollBegin',
+ SCROLL_END: 'GestureScrollEnd',
+ SCROLL_UPDATE: 'GestureScrollUpdate',
+ SCROLL_UPDATE_RENDERER: 'ScrollUpdate',
+ SHOW_PRESS: 'GestureShowPress',
+ TAP: 'GestureTap',
+ TAP_CANCEL: 'GestureTapCancel',
+ TAP_DOWN: 'GestureTapDown',
+ TOUCH_CANCEL: 'TouchCancel',
+ TOUCH_END: 'TouchEnd',
+ TOUCH_MOVE: 'TouchMove',
+ TOUCH_START: 'TouchStart',
+ UNKNOWN: 'UNKNOWN'
+ };
+
+ function InputLatencyAsyncSlice() {
+ AsyncSlice.apply(this, arguments);
+ this.associatedEvents_ = new EventSet();
+ this.typeName_ = undefined;
+ if (!this.isLegacyEvent) {
+ this.determineModernTypeName_();
+ }
+ }
+
+ InputLatencyAsyncSlice.prototype = {
+ __proto__: AsyncSlice.prototype,
+
+ // Legacy InputLatencyAsyncSlices involve a top-level slice titled
+ // "InputLatency" containing a subSlice whose title starts with
+ // "InputLatency:". Modern InputLatencyAsyncSlices involve a single
+ // top-level slice whose title starts with "InputLatency::".
+ // Legacy subSlices are not available at construction time, so
+ // determineLegacyTypeName_() must be called at get time.
+ // So this returns false for the legacy subSlice events titled like
+ // "InputLatency:Foo" even though they are technically legacy events.
+ get isLegacyEvent() {
+ return this.title === 'InputLatency';
+ },
+
+ get typeName() {
+ if (!this.typeName_) {
+ this.determineLegacyTypeName_();
+ }
+ return this.typeName_;
+ },
+
+ checkTypeName_() {
+ if (!this.typeName_) {
+ throw new Error('Unable to determine typeName');
+ }
+ let found = false;
+ for (const typeName in INPUT_EVENT_TYPE_NAMES) {
+ if (this.typeName === INPUT_EVENT_TYPE_NAMES[typeName]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ this.typeName_ = INPUT_EVENT_TYPE_NAMES.UNKNOWN;
+ }
+ },
+
+ determineModernTypeName_() {
+ // This method works both on modern events titled like
+ // "InputLatency::Foo" and also on the legacy subSlices titled like
+ // "InputLatency:Foo". Modern events' titles contain 2 colons, whereas the
+ // legacy subSlices events contain 1 colon.
+
+ const lastColonIndex = this.title.lastIndexOf(':');
+ if (lastColonIndex < 0) return;
+
+ const characterAfterLastColonIndex = lastColonIndex + 1;
+ this.typeName_ = this.title.slice(characterAfterLastColonIndex);
+
+ // Check that the determined typeName is known.
+ this.checkTypeName_();
+ },
+
+ determineLegacyTypeName_() {
+ // Iterate over all descendent subSlices.
+ for (const subSlice of this.enumerateAllDescendents()) {
+ // If |subSlice| is not an InputLatencyAsyncSlice, then ignore it.
+ const subSliceIsAInputLatencyAsyncSlice = (
+ subSlice instanceof InputLatencyAsyncSlice);
+ if (!subSliceIsAInputLatencyAsyncSlice) continue;
+
+ // If |subSlice| does not have a typeName, then ignore it.
+ if (!subSlice.typeName) continue;
+
+ // If |this| already has a typeName and |subSlice| has a different
+ // typeName, then explode!
+ if (this.typeName_ && subSlice.typeName_) {
+ const subSliceHasDifferentTypeName = (
+ this.typeName_ !== subSlice.typeName_);
+ if (subSliceHasDifferentTypeName) {
+ throw new Error(
+ 'InputLatencyAsyncSlice.determineLegacyTypeName_() ' +
+ ' found multiple typeNames');
+ }
+ }
+
+ // The typeName of |this| top-level event is whatever the typeName of
+ // |subSlice| is. Set |this.typeName_| to the subSlice's typeName.
+ this.typeName_ = subSlice.typeName_;
+ }
+
+ // If typeName could not be determined, then explode!
+ if (!this.typeName_) {
+ throw new Error(
+ 'InputLatencyAsyncSlice.determineLegacyTypeName_() failed');
+ }
+
+ // Check that the determined typeName is known.
+ this.checkTypeName_();
+ },
+
+ getRendererHelper(sourceSlices) {
+ const traceModel = this.startThread.parent.model;
+ const modelHelper = traceModel.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (!modelHelper) return undefined;
+
+ let mainThread = undefined;
+ let compositorThread = undefined;
+
+ for (const i in sourceSlices) {
+ if (sourceSlices[i].parentContainer.name ===
+ MAIN_RENDERER_THREAD_NAME) {
+ mainThread = sourceSlices[i].parentContainer;
+ } else if (sourceSlices[i].parentContainer.name ===
+ COMPOSITOR_THREAD_NAME) {
+ compositorThread = sourceSlices[i].parentContainer;
+ }
+
+ if (mainThread && compositorThread) break;
+ }
+
+ const rendererHelpers = modelHelper.rendererHelpers;
+
+ const pids = Object.keys(rendererHelpers);
+ for (let i = 0; i < pids.length; i++) {
+ const pid = pids[i];
+ const rendererHelper = rendererHelpers[pid];
+ if (rendererHelper.mainThread === mainThread ||
+ rendererHelper.compositorThread === compositorThread) {
+ return rendererHelper;
+ }
+ }
+
+ return undefined;
+ },
+
+ addEntireSliceHierarchy(slice) {
+ this.associatedEvents_.push(slice);
+ slice.iterateAllSubsequentSlices(function(subsequentSlice) {
+ this.associatedEvents_.push(subsequentSlice);
+ }, this);
+ },
+
+ addDirectlyAssociatedEvents(flowEvents) {
+ const slices = [];
+
+ flowEvents.forEach(function(flowEvent) {
+ this.associatedEvents_.push(flowEvent);
+ const newSource = flowEvent.startSlice.mostTopLevelSlice;
+ if (slices.indexOf(newSource) === -1) {
+ slices.push(newSource);
+ }
+ }, this);
+
+ const lastFlowEvent = flowEvents[flowEvents.length - 1];
+ const lastSource = lastFlowEvent.endSlice.mostTopLevelSlice;
+ if (slices.indexOf(lastSource) === -1) {
+ slices.push(lastSource);
+ }
+
+ return slices;
+ },
+
+ // Find the Latency::ScrollUpdate slice that corresponds to the
+ // InputLatency::GestureScrollUpdate slice.
+ // The C++ CL that makes this connection is at:
+ // https://codereview.chromium.org/1178963003
+ addScrollUpdateEvents(rendererHelper) {
+ if (!rendererHelper || !rendererHelper.compositorThread) {
+ return;
+ }
+
+ const compositorThread = rendererHelper.compositorThread;
+ const gestureScrollUpdateStart = this.start;
+ const gestureScrollUpdateEnd = this.end;
+
+ const allCompositorAsyncSlices =
+ compositorThread.asyncSliceGroup.slices;
+
+ for (const i in allCompositorAsyncSlices) {
+ const slice = allCompositorAsyncSlices[i];
+
+ if (slice.title !== 'Latency::ScrollUpdate') continue;
+
+ const parentId = slice.args.data.
+ INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT.
+ sequence_number;
+
+ if (parentId === undefined) {
+ // Old trace, we can only rely on the timestamp to find the slice
+ if (slice.start < gestureScrollUpdateStart ||
+ slice.start >= gestureScrollUpdateEnd) {
+ continue;
+ }
+ } else {
+ // New trace, we can definitively find the latency slice by comparing
+ // its sequence number with gesture id
+ if (parseInt(parentId) !== parseInt(this.id)) {
+ continue;
+ }
+ }
+
+ slice.associatedEvents.forEach(function(event) {
+ this.associatedEvents_.push(event);
+ }, this);
+ break;
+ }
+ },
+
+ // Return true if the slice hierarchy is tracked by LatencyInfo of other
+ // input latency events. If the slice hierarchy is tracked by both, this
+ // function still returns true.
+ belongToOtherInputs(slice, flowEvents) {
+ let fromOtherInputs = false;
+
+ slice.iterateEntireHierarchy(function(subsequentSlice) {
+ if (fromOtherInputs) return;
+
+ subsequentSlice.inFlowEvents.forEach(function(inflow) {
+ if (fromOtherInputs) return;
+
+ if (inflow.category.indexOf('input') > -1) {
+ if (flowEvents.indexOf(inflow) === -1) {
+ fromOtherInputs = true;
+ }
+ }
+ }, this);
+ }, this);
+
+ return fromOtherInputs;
+ },
+
+ // Return true if |event| triggers slices of other inputs.
+ triggerOtherInputs(event, flowEvents) {
+ if (event.outFlowEvents === undefined ||
+ event.outFlowEvents.length === 0) {
+ return false;
+ }
+
+ // Once we fix the bug of flow event binding, there should exist one and
+ // only one outgoing flow (PostTask) from ScheduleBeginImplFrameDeadline
+ // and PostComposite.
+ const flow = event.outFlowEvents[0];
+
+ if (flow.category !== POSTTASK_FLOW_EVENT ||
+ !flow.endSlice) {
+ return false;
+ }
+
+ const endSlice = flow.endSlice;
+ if (this.belongToOtherInputs(endSlice.mostTopLevelSlice, flowEvents)) {
+ return true;
+ }
+
+ return false;
+ },
+
+ // Follow outgoing flow of subsequentSlices in the current hierarchy.
+ // We also handle cases where different inputs interfere with each other.
+ followSubsequentSlices(event, queue, visited, flowEvents) {
+ let stopFollowing = false;
+ let inputAck = false;
+
+ event.iterateAllSubsequentSlices(function(slice) {
+ if (stopFollowing) return;
+
+ // Do not follow TaskQueueManager::RunTask because it causes
+ // many false events to be included.
+ if (slice.title === 'TaskQueueManager::RunTask') return;
+
+ // Do not follow ScheduledActionSendBeginMainFrame because the real
+ // main thread BeginMainFrame is already traced by LatencyInfo flow.
+ if (slice.title === 'ThreadProxy::ScheduledActionSendBeginMainFrame') {
+ return;
+ }
+
+ // Do not follow ScheduleBeginImplFrameDeadline that triggers an
+ // OnBeginImplFrameDeadline that is tracked by another LatencyInfo.
+ if (slice.title === 'Scheduler::ScheduleBeginImplFrameDeadline') {
+ if (this.triggerOtherInputs(slice, flowEvents)) return;
+ }
+
+ // Do not follow PostComposite that triggers CompositeImmediately
+ // that is tracked by another LatencyInfo.
+ if (slice.title === 'CompositorImpl::PostComposite') {
+ if (this.triggerOtherInputs(slice, flowEvents)) return;
+ }
+
+ // Stop following the rest of the current slice hierarchy if
+ // FilterAndSendWebInputEvent occurs after ProcessInputEventAck.
+ if (slice.title === 'InputRouterImpl::ProcessInputEventAck') {
+ inputAck = true;
+ }
+ if (inputAck &&
+ slice.title === 'InputRouterImpl::FilterAndSendWebInputEvent') {
+ stopFollowing = true;
+ }
+
+ this.followCurrentSlice(slice, queue, visited);
+ }, this);
+ },
+
+ // Follow outgoing flow events of the current slice.
+ followCurrentSlice(event, queue, visited) {
+ event.outFlowEvents.forEach(function(outflow) {
+ if ((outflow.category === POSTTASK_FLOW_EVENT ||
+ outflow.category === IPC_FLOW_EVENT) &&
+ outflow.endSlice) {
+ this.associatedEvents_.push(outflow);
+
+ const nextEvent = outflow.endSlice.mostTopLevelSlice;
+ if (!visited.contains(nextEvent)) {
+ visited.push(nextEvent);
+ queue.push(nextEvent);
+ }
+ }
+ }, this);
+ },
+
+ backtraceFromDraw(beginImplFrame, visited) {
+ const pendingEventQueue = [];
+ pendingEventQueue.push(beginImplFrame.mostTopLevelSlice);
+
+ while (pendingEventQueue.length !== 0) {
+ const event = pendingEventQueue.pop();
+
+ this.addEntireSliceHierarchy(event);
+
+ // TODO(yuhao): For now, we backtrace all the way to the source input.
+ // But is this really needed? I will have an entry in the design
+ // doc to discuss this.
+ event.inFlowEvents.forEach(function(inflow) {
+ if (inflow.category === POSTTASK_FLOW_EVENT && inflow.startSlice) {
+ const nextEvent = inflow.startSlice.mostTopLevelSlice;
+ if (!visited.contains(nextEvent)) {
+ visited.push(nextEvent);
+ pendingEventQueue.push(nextEvent);
+ }
+ }
+ }, this);
+ }
+ },
+
+ sortRasterizerSlices(rasterWorkerThreads,
+ sortedRasterizerSlices) {
+ rasterWorkerThreads.forEach(function(rasterizer) {
+ Array.prototype.push.apply(sortedRasterizerSlices,
+ rasterizer.sliceGroup.slices);
+ }, this);
+
+ sortedRasterizerSlices.sort(function(a, b) {
+ if (a.start !== b.start) {
+ return a.start - b.start;
+ }
+ return a.guid - b.guid;
+ });
+ },
+
+ // Find rasterization slices that have the source_prepare_tiles_id
+ // same as the prepare_tiles_id of TileManager::PrepareTiles
+ // The C++ CL that makes this connection is at:
+ // https://codereview.chromium.org/1208683002/
+ addRasterizationEvents(prepareTiles, rendererHelper,
+ visited, flowEvents, sortedRasterizerSlices) {
+ if (!prepareTiles.args.prepare_tiles_id) return;
+
+ if (!rendererHelper || !rendererHelper.rasterWorkerThreads) {
+ return;
+ }
+
+ const rasterWorkerThreads = rendererHelper.rasterWorkerThreads;
+ const prepareTileId = prepareTiles.args.prepare_tiles_id;
+ const pendingEventQueue = [];
+
+ // Collect all the rasterizer tasks. Return the cached copy if possible.
+ if (sortedRasterizerSlices.length === 0) {
+ this.sortRasterizerSlices(rasterWorkerThreads, sortedRasterizerSlices);
+ }
+
+ // TODO(yuhao): Once TaskSetFinishedTaskImpl also get the prepareTileId
+ // we can simply track by checking id rather than counting.
+ let numFinishedTasks = 0;
+ const RASTER_TASK_TITLE = 'RasterizerTaskImpl::RunOnWorkerThread';
+ const IMAGEDECODE_TASK_TITLE = 'ImageDecodeTaskImpl::RunOnWorkerThread';
+ const FINISHED_TASK_TITLE = 'TaskSetFinishedTaskImpl::RunOnWorkerThread';
+
+ for (let i = 0; i < sortedRasterizerSlices.length; i++) {
+ const task = sortedRasterizerSlices[i];
+
+ if (task.title === RASTER_TASK_TITLE ||
+ task.title === IMAGEDECODE_TASK_TITLE) {
+ if (task.args.source_prepare_tiles_id === prepareTileId) {
+ this.addEntireSliceHierarchy(task.mostTopLevelSlice);
+ }
+ } else if (task.title === FINISHED_TASK_TITLE) {
+ if (task.start > prepareTiles.start) {
+ pendingEventQueue.push(task.mostTopLevelSlice);
+ if (++numFinishedTasks === 3) break;
+ }
+ }
+ }
+
+ // Trace PostTask from rasterizer tasks.
+ while (pendingEventQueue.length !== 0) {
+ const event = pendingEventQueue.pop();
+
+ this.addEntireSliceHierarchy(event);
+ this.followSubsequentSlices(event, pendingEventQueue, visited,
+ flowEvents);
+ }
+ },
+
+ addOtherCausallyRelatedEvents(rendererHelper, sourceSlices,
+ flowEvents, sortedRasterizerSlices) {
+ const pendingEventQueue = [];
+ // Keep track of visited nodes when traversing a DAG
+ const visitedEvents = new EventSet();
+ let beginImplFrame = undefined;
+ let prepareTiles = undefined;
+ sortedRasterizerSlices = [];
+
+ sourceSlices.forEach(function(sourceSlice) {
+ if (!visitedEvents.contains(sourceSlice)) {
+ visitedEvents.push(sourceSlice);
+ pendingEventQueue.push(sourceSlice);
+ }
+ }, this);
+
+ while (pendingEventQueue.length !== 0) {
+ const event = pendingEventQueue.pop();
+
+ // Push the current event chunk into associatedEvents.
+ this.addEntireSliceHierarchy(event);
+
+ this.followCurrentSlice(event, pendingEventQueue, visitedEvents);
+
+ this.followSubsequentSlices(event, pendingEventQueue, visitedEvents,
+ flowEvents);
+
+ // The rasterization work (CompositorTileWorker thread) and the
+ // Compositor tile manager are connect by the prepare_tiles_id
+ // instead of flow events.
+ const COMPOSITOR_PREPARE_TILES = 'TileManager::PrepareTiles';
+ prepareTiles = event.findDescendentSlice(COMPOSITOR_PREPARE_TILES);
+ if (prepareTiles) {
+ this.addRasterizationEvents(prepareTiles, rendererHelper,
+ visitedEvents, flowEvents, sortedRasterizerSlices);
+ }
+
+ // OnBeginImplFrameDeadline could be triggered by other inputs.
+ // For now, we backtrace from it.
+ // TODO(yuhao): There are more such slices that we need to backtrace
+ const COMPOSITOR_ON_BIFD = 'Scheduler::OnBeginImplFrameDeadline';
+ beginImplFrame = event.findDescendentSlice(COMPOSITOR_ON_BIFD);
+ if (beginImplFrame) {
+ this.backtraceFromDraw(beginImplFrame, visitedEvents);
+ }
+ }
+
+ // A separate pass on GestureScrollUpdate.
+ // Scroll update doesn't go through the main thread, but the compositor
+ // may go back to the main thread if there is an onscroll event handler.
+ // This is captured by a different flow event, which does not have the
+ // same ID as the Input Latency Event, but it is technically causally
+ // related to the GestureScrollUpdate input. Add them manually for now.
+ 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_;
+ }
+
+ // Step 1: Get events that are directly connected by the LatencyInfo
+ // flow events. This gives us a small set of events that are guaranteed
+ // to be associated with the input, but are almost certain incomplete.
+ // We call this set "source" event set.
+ // This step returns the "source" event set (sourceSlices), which is then
+ // used in the second step.
+ const sourceSlices = this.addDirectlyAssociatedEvents(flowEvents);
+
+ // Step 2: Start from the previously constructed "source" event set, we
+ // follow the toplevel (i.e., PostTask) and IPC flow events. Any slices
+ // that are reachable from the "source" event set via PostTasks or IPCs
+ // are conservatively considered associated with the input event.
+ // We then deal with specific cases where flow events either over include
+ // or miss capturing slices.
+ 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) {
+ // Old style.
+ allTypeNames.push('InputLatency:' + eventTypeName);
+
+ // New style.
+ allTypeNames.push('InputLatency::' + eventTypeName);
+ });
+
+ AsyncSlice.subTypes.register(
+ InputLatencyAsyncSlice,
+ {
+ typeNames: allTypeNames,
+ categoryParts: ['latencyInfo']
+ });
+
+ return {
+ InputLatencyAsyncSlice,
+ INPUT_EVENT_TYPE_NAMES,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice_test.html
new file mode 100644
index 00000000000..e65613ea647
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/input_latency_async_slice_test.html
@@ -0,0 +1,702 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/model_indices.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+ const newModel = tr.c.TestUtils.newModel;
+ const EventSet = tr.model.EventSet;
+
+ test('matchByType_oldStyle', function() {
+ const sOuter = newAsyncSliceEx({
+ title: 'InputLatency',
+ cat: 'benchmark',
+ start: 0,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {'time': 0},
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10}
+ }
+ }
+ });
+ assert.throws(function() {
+ sOuter.typeName;
+ });
+
+ const sInner = newAsyncSliceEx({
+ title: 'InputLatency:GestureScrollUpdate',
+ cat: 'benchmark',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ args: {
+ 'step': 'GestureScrollUpdate'
+ }
+ });
+ sOuter.subSlices.push(sInner);
+ assert.isTrue(sOuter instanceof tr.e.cc.InputLatencyAsyncSlice);
+ assert.isTrue(sInner instanceof tr.e.cc.InputLatencyAsyncSlice);
+ assert.strictEqual(sOuter.inputLatency, 10);
+ assert.strictEqual(
+ tr.e.cc.INPUT_EVENT_TYPE_NAMES.SCROLL_UPDATE, sInner.typeName);
+ assert.strictEqual(
+ tr.e.cc.INPUT_EVENT_TYPE_NAMES.SCROLL_UPDATE, sOuter.typeName);
+ });
+
+ test('matchByType_newStyle', function() {
+ const sInfo = newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ cat: 'benchmark',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {'time': 0},
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10}
+ }
+ }
+ });
+
+ assert.isTrue(sInfo instanceof tr.e.cc.InputLatencyAsyncSlice);
+ assert.strictEqual(sInfo.inputLatency, 10);
+ assert.strictEqual(
+ tr.e.cc.INPUT_EVENT_TYPE_NAMES.SCROLL_UPDATE, sInfo.typeName);
+ });
+
+ test('unknownType', function() {
+ const sInfo = newAsyncSliceEx({
+ title: 'InputLatency::BadTypeName',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {'time': 0},
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {'time': 0},
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10}
+ }
+ }
+ });
+ assert.strictEqual(tr.e.cc.INPUT_EVENT_TYPE_NAMES.UNKNOWN, sInfo.typeName);
+ });
+
+ test('getAssociatedEventsBypassRendererMain', function() {
+ const m = newModel(function(m) {
+ const pb = m.getOrCreateProcess(1);
+ const pr = m.getOrCreateProcess(2);
+ const mainBrowserThread = pb.getOrCreateThread(10);
+ const mainRendererThread = pr.getOrCreateThread(20);
+ const compositorThread = pr.getOrCreateThread(21);
+
+ mainBrowserThread.name = 'CrBrowserMain';
+ mainRendererThread.name = 'CrRendererMain';
+ compositorThread.name = 'Compositor';
+
+ // Expectation: None of s2 and s3 should be included
+ // CrBrowserMain: [s0] [s1]
+ // | /|\
+ // CrRendererMain: | [s2] [s3] |
+ // \|/ |
+ // Compositor: [s4]-------------|
+
+ m.s0 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's0', start: 0.0, duration: 1.0 }));
+ m.s1 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's1', start: 6.0, duration: 1.0 }));
+ m.s2 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's2', start: 2.0, duration: 1.0 }));
+ m.s3 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's3', start: 4.0, duration: 1.0 }));
+ m.s4 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's4', start: 0.5, duration: 1.0 }));
+
+ m.f1 = newFlowEventEx({
+ title: 'test1',
+ start: 0,
+ end: 10,
+ startSlice: m.s0,
+ endSlice: m.s4,
+ id: '0x100'
+ });
+
+ m.f2 = newFlowEventEx({
+ title: 'test2',
+ start: 20,
+ end: 30,
+ startSlice: m.s4,
+ endSlice: m.s1,
+ id: '0x100'
+ });
+
+ m.flowEvents.push(m.f1);
+ m.flowEvents.push(m.f2);
+
+ m.as0 = newAsyncSliceEx({
+ title: 'test1',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x101',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+
+ m.as1 = newAsyncSliceEx({
+ title: 'test2',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ startThread: compositorThread
+ });
+ });
+
+ assert.isTrue(m.as0.associatedEvents.length === 0);
+ assert.isTrue(m.as1.associatedEvents.equals(
+ new EventSet([m.f1, m.s0, m.f2, m.s4, m.s1])));
+ });
+
+ test('getAssociatedEventsBypassRendererMainWithOnScroll', function() {
+ const m = newModel(function(m) {
+ const pb = m.getOrCreateProcess(1);
+ const pr = m.getOrCreateProcess(2);
+ const mainBrowserThread = pb.getOrCreateThread(10);
+ const mainRendererThread = pr.getOrCreateThread(20);
+ const compositorThread = pr.getOrCreateThread(21);
+
+ mainBrowserThread.name = 'CrBrowserMain';
+ mainRendererThread.name = 'CrRendererMain';
+ compositorThread.name = 'Compositor';
+
+ // Expectation: s2 should be included but not s3
+ // GestureScrollUpdate: [ as1 ]
+ // CrBrowserMain: [s0] [s1]
+ // | /|\
+ // CrRendererMain: | [s2] [s3] |
+ // \|/ /|\ |
+ // Compositor: [s4]___|__________|
+ // ScrollUpdate: [ as2 ]
+
+ m.s0 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's0', start: 0.0, duration: 1.0 }));
+ m.s1 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's1', start: 6.0, duration: 1.0 }));
+ m.s2 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's2', start: 2.0, duration: 1.0 }));
+ m.s3 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's3', start: 4.0, duration: 1.0 }));
+ m.s4 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's4', start: 0.5, duration: 1.0 }));
+
+ m.f1 = newFlowEventEx({
+ title: 'f1',
+ start: 0,
+ end: 10,
+ startSlice: m.s0,
+ endSlice: m.s4,
+ id: '0x100'
+ });
+
+ m.f2 = newFlowEventEx({
+ title: 'f2',
+ start: 20,
+ end: 30,
+ startSlice: m.s4,
+ endSlice: m.s1,
+ id: '0x100'
+ });
+
+ m.f3 = newFlowEventEx({
+ title: 'f3',
+ start: 20,
+ end: 30,
+ startSlice: m.s4,
+ endSlice: m.s2,
+ id: '0x800'
+ });
+
+ m.flowEvents.push(m.f1);
+ m.flowEvents.push(m.f2);
+ m.flowEvents.push(m.f3);
+
+ m.as0 = mainBrowserThread.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ cat: 'benchmark,latencyInfo',
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT: 100
+ }
+ },
+ start: 2,
+ end: 10,
+ id: '0x101',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ }));
+ assert.strictEqual(tr.e.cc.INPUT_EVENT_TYPE_NAMES.SCROLL_UPDATE,
+ m.as0.typeName);
+
+ m.as1 = mainBrowserThread.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ cat: 'benchmark,latencyInfo',
+ args: {
+ data: {}
+ },
+ start: 0,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ }));
+ assert.strictEqual(tr.e.cc.INPUT_EVENT_TYPE_NAMES.SCROLL_UPDATE,
+ m.as1.typeName);
+
+ m.as2 = compositorThread.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'Latency::ScrollUpdate',
+ cat: 'benchmark,latencyInfo',
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT: 100
+ }
+ },
+ start: 1.5,
+ end: 8,
+ id: '0x800',
+ isTopLevel: true,
+ startThread: compositorThread
+ }));
+ assert.strictEqual(tr.e.cc.INPUT_EVENT_TYPE_NAMES.LATENCY_SCROLL_UPDATE,
+ m.as2.typeName);
+ });
+
+ assert.isTrue(m.as0.associatedEvents.length === 0);
+ assert.isTrue(m.as1.associatedEvents.equals(
+ new EventSet([m.f1, m.s0, m.f2, m.s4, m.s1, m.f3, m.s2])));
+ });
+
+ test('getAssociatedEventsWithoutCommit', function() {
+ const m = newModel(function(m) {
+ const pb = m.getOrCreateProcess(1);
+ const pr = m.getOrCreateProcess(2);
+ const mainBrowserThread = pb.getOrCreateThread(10);
+ const mainRendererThread = pr.getOrCreateThread(20);
+ const compositorThread = pr.getOrCreateThread(21);
+
+ mainBrowserThread.name = 'CrBrowserMain';
+ mainRendererThread.name = 'CrRendererMain';
+ compositorThread.name = 'Compositor';
+
+ // Expectation: none of s3 and s5 should be included
+ // CrBrowserMain: [s0] [s1]
+ // | /|\
+ // | __________|
+ // | |
+ // CrRendererMain: | [s2] [s3] [s5]
+ // \|/ /|\
+ // Compositor: [s4]___|
+
+ m.s0 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's0', start: 0.0, duration: 1.0 }));
+ m.s1 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's1', start: 6.0, duration: 1.0 }));
+ m.s2 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's2', start: 2.0, duration: 1.0 }));
+ m.s3 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's3', start: 4.0, duration: 1.0 }));
+ m.s4 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's4', start: 0.5, duration: 1.0 }));
+ m.s5 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's5', start: 100.0, duration: 1.0 }));
+
+ m.f1 = newFlowEventEx({
+ title: 'f1',
+ start: 0,
+ end: 10,
+ startSlice: m.s0,
+ endSlice: m.s4,
+ id: '0x100'
+ });
+
+ m.f2 = newFlowEventEx({
+ title: 'f2',
+ start: 20,
+ end: 30,
+ startSlice: m.s4,
+ endSlice: m.s2,
+ id: '0x100'
+ });
+
+ m.f3 = newFlowEventEx({
+ title: 'f3',
+ start: 20,
+ end: 30,
+ startSlice: m.s2,
+ endSlice: m.s1,
+ id: '0x100'
+ });
+
+ m.flowEvents.push(m.f1);
+ m.flowEvents.push(m.f2);
+ m.flowEvents.push(m.f3);
+
+ m.as0 = newAsyncSliceEx({
+ title: 'test1',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x101',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+
+ m.as1 = newAsyncSliceEx({
+ title: 'test2',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+ });
+
+ assert.isTrue(m.as0.associatedEvents.length === 0);
+ assert.isTrue(m.as1.associatedEvents.equals(
+ new EventSet([m.f1, m.s0, m.f2, m.s4, m.f3, m.s2, m.s1])));
+ });
+
+ test('getAssociatedEventsWillCommit', function() {
+ const m = newModel(function(m) {
+ const pb = m.getOrCreateProcess(1);
+ const pr = m.getOrCreateProcess(2);
+ const mainBrowserThread = pb.getOrCreateThread(10);
+ const mainRendererThread = pr.getOrCreateThread(20);
+ const compositorThread = pr.getOrCreateThread(21);
+
+ mainBrowserThread.name = 'CrBrowserMain';
+ mainRendererThread.name = 'CrRendererMain';
+ compositorThread.name = 'Compositor';
+
+ // Expectation: s3 should be included by getOtherCasuallyRelatedEvents,
+ // because there is a PostTask s7 -> s3, but s6 shouldn't be included.
+ // CrBrowserMain: [s0] [ s1 ]
+ // | /|\
+ // | |
+ // | [ s2 ]____ |
+ // CrRendererMain: | /|\ [s7] | | [s6]
+ // | | | | |
+ // | | | | |
+ // \|/ | \|/ \|/ |
+ // Compositor: [s4]__| [s3] [s5]__|
+
+ m.s0 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's0', start: 0.0, duration: 1.0 }));
+ m.s1 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's1', start: 6.0, duration: 1.0 }));
+ m.s2 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's2', start: 2.0, duration: 1.0 }));
+ m.s3 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's3', start: 4.0, duration: 1.0 }));
+ m.s4 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's4', start: 0.5, duration: 1.0 }));
+ m.s5 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's5', start: 5.5, duration: 1.0 }));
+ m.s6 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's6', start: 1000.0, duration: 1.0 }));
+ m.s7 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's7', start: 2.5, duration: 0.2 }));
+
+ mainBrowserThread.sliceGroup.createSubSlices();
+ mainRendererThread.sliceGroup.createSubSlices();
+ compositorThread.sliceGroup.createSubSlices();
+
+ m.f1 = newFlowEventEx({
+ title: 'f1',
+ start: 0,
+ end: 10,
+ startSlice: m.s0,
+ endSlice: m.s4,
+ id: '0x100'
+ });
+
+ m.f2 = newFlowEventEx({
+ title: 'f2',
+ start: 20,
+ end: 30,
+ startSlice: m.s4,
+ endSlice: m.s2,
+ id: '0x100'
+ });
+
+ m.f3 = newFlowEventEx({
+ title: 'f3',
+ start: 20,
+ end: 30,
+ startSlice: m.s2,
+ endSlice: m.s5,
+ id: '0x100'
+ });
+
+ m.f4 = newFlowEventEx({
+ title: 'f4',
+ start: 20,
+ end: 30,
+ startSlice: m.s5,
+ endSlice: m.s1,
+ id: '0x100'
+ });
+
+ m.f5 = newFlowEventEx({
+ title: 'f5',
+ cat: 'disabled-by-default-toplevel.flow',
+ start: 20,
+ end: 30,
+ startSlice: m.s7,
+ endSlice: m.s3,
+ id: '0xAAA'
+ });
+
+ m.flowEvents.push(m.f1);
+ m.flowEvents.push(m.f2);
+ m.flowEvents.push(m.f3);
+ m.flowEvents.push(m.f4);
+ m.flowEvents.push(m.f5);
+
+ m.as0 = newAsyncSliceEx({
+ title: 'test1',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x101',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+
+ m.as1 = newAsyncSliceEx({
+ title: 'test2',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+ });
+
+ assert.isTrue(m.as0.associatedEvents.length === 0);
+ assert.isTrue(m.as1.associatedEvents.equals(new EventSet(
+ [m.f1, m.f2, m.f3, m.f4, m.f5,
+ m.s0, m.s1, m.s2, m.s3, m.s4, m.s5, m.s7])));
+ });
+
+ test('getAssociatedEventsExcludeOtherInputs', function() {
+ const m = newModel(function(m) {
+ const pb = m.getOrCreateProcess(1);
+ const pr = m.getOrCreateProcess(2);
+ const mainBrowserThread = pb.getOrCreateThread(10);
+ const mainRendererThread = pr.getOrCreateThread(20);
+ const compositorThread = pr.getOrCreateThread(21);
+
+ mainBrowserThread.name = 'CrBrowserMain';
+ mainRendererThread.name = 'CrRendererMain';
+ compositorThread.name = 'Compositor';
+
+ // Expectation: s3 should be included by getOtherCasuallyRelatedEvents,
+ // because there is a PostTask s7 -> s3. Even though there is also a
+ // PostTask from s9 to s6, s6 shouldn't be included because it's tracked
+ // by LatencyInfo of another input.
+ // CrBrowserMain: [s0] [ s1 ] [s8]
+ // | /|\ |
+ // | | |
+ // | [ s2 ]____ | \|/
+ // CrRendererMain: | /|\ [s7] | | [s6]
+ // | | | | | /|\
+ // | | | | | |
+ // \|/ | \|/ \|/ | |
+ // Compositor: [s4]__| [s3] [ s5 ]_| |
+ // [s9]_________|
+
+ m.s0 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's0', start: 0.0, duration: 1.0 }));
+ m.s1 = mainBrowserThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's1', start: 6.0, duration: 1.0 }));
+ m.s2 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's2', start: 2.0, duration: 1.0 }));
+ m.s3 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's3', start: 4.0, duration: 1.0 }));
+ m.s4 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's4', start: 0.5, duration: 1.0 }));
+ m.s5 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's5', start: 5.5, duration: 1.0 }));
+ m.s6 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's6', start: 10.0, duration: 1.0 }));
+ m.s7 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's7', start: 2.5, duration: 0.2 }));
+ m.s8 = mainRendererThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 's8', start: 9.5, duration: 1.0 }));
+ m.s9 = compositorThread.sliceGroup.pushSlice(newSliceEx(
+ { title: 'Scheduler::ScheduleBeginImplFrameDeadline',
+ start: 5.7, duration: 0.2 }));
+
+ mainBrowserThread.sliceGroup.createSubSlices();
+ mainRendererThread.sliceGroup.createSubSlices();
+ compositorThread.sliceGroup.createSubSlices();
+
+ m.f1 = newFlowEventEx({
+ title: 'f1',
+ cat: 'input',
+ start: 0,
+ end: 10,
+ startSlice: m.s0,
+ endSlice: m.s4,
+ id: '0x100'
+ });
+
+ m.f2 = newFlowEventEx({
+ title: 'f2',
+ cat: 'input',
+ start: 20,
+ end: 30,
+ startSlice: m.s4,
+ endSlice: m.s2,
+ id: '0x100'
+ });
+
+ m.f3 = newFlowEventEx({
+ title: 'f3',
+ cat: 'input',
+ start: 20,
+ end: 30,
+ startSlice: m.s2,
+ endSlice: m.s5,
+ id: '0x100'
+ });
+
+ m.f4 = newFlowEventEx({
+ title: 'f4',
+ cat: 'input',
+ start: 20,
+ end: 30,
+ startSlice: m.s5,
+ endSlice: m.s1,
+ id: '0x100'
+ });
+
+ m.f5 = newFlowEventEx({
+ title: 'f5',
+ cat: 'disabled-by-default-toplevel.flow',
+ start: 20,
+ end: 30,
+ startSlice: m.s7,
+ endSlice: m.s3,
+ id: '0xAAA'
+ });
+
+ m.f6 = newFlowEventEx({
+ title: 'f6',
+ cat: 'disabled-by-default-toplevel.flow',
+ start: 20,
+ end: 30,
+ startSlice: m.s9,
+ endSlice: m.s6,
+ id: '0xAAB'
+ });
+
+ m.f7 = newFlowEventEx({
+ title: 'f7',
+ cat: 'input',
+ start: 20,
+ end: 30,
+ startSlice: m.s8,
+ endSlice: m.s6,
+ id: '0x102'
+ });
+
+ m.flowEvents.push(m.f1);
+ m.flowEvents.push(m.f2);
+ m.flowEvents.push(m.f3);
+ m.flowEvents.push(m.f4);
+ m.flowEvents.push(m.f5);
+ m.flowEvents.push(m.f6);
+ m.flowEvents.push(m.f7);
+
+ m.as0 = newAsyncSliceEx({
+ title: 'test1',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x101',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+
+ m.as1 = newAsyncSliceEx({
+ title: 'test2',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+
+ m.as2 = newAsyncSliceEx({
+ title: 'test2',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x102',
+ isTopLevel: true,
+ startThread: mainBrowserThread
+ });
+ });
+
+ assert.isTrue(m.as0.associatedEvents.length === 0);
+ assert.isTrue(m.as1.associatedEvents.equals(new EventSet(
+ [m.f1, m.f2, m.f3, m.f4, m.f5,
+ m.s0, m.s1, m.s2, m.s3, m.s4, m.s5, m.s7, m.s9])));
+ });
+
+ test('legacyEndComponent', function() {
+ const sInfo = newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ cat: 'benchmark',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {'time': 0},
+ INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT: {time: 10}
+ }
+ }
+ });
+
+ assert.isTrue(sInfo instanceof tr.e.cc.InputLatencyAsyncSlice);
+ assert.strictEqual(sInfo.inputLatency, 10);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_impl.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_impl.html
new file mode 100644
index 00000000000..caf63707186
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_impl.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/region.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile_coverage_rect.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const constants = tr.e.cc.constants;
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * @constructor
+ */
+ function LayerImplSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ LayerImplSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ tr.e.cc.preInitializeObject(this);
+
+ this.layerTreeImpl_ = undefined;
+ this.parentLayer = undefined;
+ },
+
+ initialize() {
+ // Defaults.
+ this.invalidation = new tr.e.cc.Region();
+ this.unrecordedRegion = new tr.e.cc.Region();
+ this.pictures = [];
+
+ // Import & validate this.args
+ tr.e.cc.moveRequiredFieldsFromArgsToToplevel(
+ this, ['layerId', 'layerQuad']);
+ tr.e.cc.moveOptionalFieldsFromArgsToToplevel(
+ this, ['children', 'maskLayer', 'replicaLayer',
+ 'idealContentsScale', 'geometryContentsScale',
+ 'layoutRects', 'usingGpuRasterization']);
+
+ // Leave gpu memory usage in both places.
+ this.gpuMemoryUsageInBytes = this.args.gpuMemoryUsage;
+
+ // Leave bounds in both places.
+ this.bounds = tr.b.math.Rect.fromXYWH(
+ 0, 0,
+ this.args.bounds.width, this.args.bounds.height);
+
+ if (this.args.animationBounds) {
+ // AnimationBounds[2] and [5] are the Z-component of the box.
+ this.animationBoundsRect = tr.b.math.Rect.fromXYWH(
+ this.args.animationBounds[0], this.args.animationBounds[1],
+ this.args.animationBounds[3], this.args.animationBounds[4]);
+ }
+
+ // After Slimming Paint v2, compositor no longer knows hierarchy
+ // information. If the children value is undefined, the tracing
+ // data comes from the new version of cc, otherwise we set the
+ // parentLayer as we did before SPv2.
+ if (this.children) {
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].parentLayer = this;
+ }
+ }
+ if (this.maskLayer) {
+ this.maskLayer.parentLayer = this;
+ }
+ if (this.replicaLayer) {
+ this.replicaLayer.parentLayer = this;
+ }
+ if (!this.geometryContentsScale) {
+ this.geometryContentsScale = 1.0;
+ }
+ if (!this.idealContentsScale) {
+ this.idealContentsScale = 1.0;
+ }
+
+ this.touchEventHandlerRegion = tr.e.cc.Region.fromArrayOrUndefined(
+ this.args.touchEventHandlerRegion);
+ this.wheelEventHandlerRegion = tr.e.cc.Region.fromArrayOrUndefined(
+ this.args.wheelEventHandlerRegion);
+ this.nonFastScrollableRegion = tr.e.cc.Region.fromArrayOrUndefined(
+ this.args.nonFastScrollableRegion);
+ },
+
+ get layerTreeImpl() {
+ if (this.layerTreeImpl_) {
+ return this.layerTreeImpl_;
+ }
+ if (this.parentLayer) {
+ return this.parentLayer.layerTreeImpl;
+ }
+ return undefined;
+ },
+ set layerTreeImpl(layerTreeImpl) {
+ this.layerTreeImpl_ = layerTreeImpl;
+ },
+
+ get activeLayer() {
+ if (this.layerTreeImpl.whichTree === constants.ACTIVE_TREE) {
+ return this;
+ }
+ const activeTree = this.layerTreeImpl.layerTreeHostImpl.activeTree;
+ return activeTree.findLayerWithId(this.layerId);
+ },
+
+ get pendingLayer() {
+ if (this.layerTreeImpl.whichTree === constants.PENDING_TREE) {
+ return this;
+ }
+ const pendingTree = this.layerTreeImpl.layerTreeHostImpl.pendingTree;
+ return pendingTree.findLayerWithId(this.layerId);
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function PictureLayerImplSnapshot() {
+ LayerImplSnapshot.apply(this, arguments);
+ }
+
+ PictureLayerImplSnapshot.prototype = {
+ __proto__: LayerImplSnapshot.prototype,
+
+ initialize() {
+ LayerImplSnapshot.prototype.initialize.call(this);
+
+ // Backward compatibility: the new format puts debug info fields under
+ // 'debugInfo', while the old format puts them directly under args.
+ if (this.args.debugInfo) {
+ for (const i in this.args.debugInfo) {
+ this.args[i] = this.args.debugInfo[i];
+ }
+ delete this.args.debugInfo;
+ }
+
+ if (this.args.annotatedInvalidationRects) {
+ this.invalidation = new tr.e.cc.Region();
+ for (const annotatedRect of this.args.annotatedInvalidationRects) {
+ const rect = annotatedRect.geometryRect;
+ rect.reason = annotatedRect.reason;
+ rect.client = annotatedRect.client;
+ this.invalidation.addRect(rect);
+ }
+ delete this.args.annotatedInvalidationRects;
+ } else if (this.args.invalidation) {
+ // Use unannotated invalidation rect if no annotated rects are
+ // available.
+ this.invalidation = tr.e.cc.Region.fromArray(this.args.invalidation);
+ }
+ delete this.args.invalidation;
+
+ if (this.args.unrecordedRegion) {
+ this.unrecordedRegion = tr.e.cc.Region.fromArray(
+ this.args.unrecordedRegion);
+ delete this.args.unrecordedRegion;
+ }
+
+ if (this.args.pictures) {
+ this.pictures = this.args.pictures;
+
+ // The picture list comes in with an unknown ordering. We resort based
+ // on timestamp order so we will draw the base picture first and the
+ // various fixes on top of that.
+ this.pictures.sort(function(a, b) { return a.ts - b.ts; });
+ }
+
+ this.tileCoverageRects = [];
+ if (this.args.coverageTiles) {
+ for (let i = 0; i < this.args.coverageTiles.length; ++i) {
+ const rect = this.args.coverageTiles[i].geometryRect.scale(
+ this.idealContentsScale);
+ const tile = this.args.coverageTiles[i].tile;
+ this.tileCoverageRects.push(new tr.e.cc.TileCoverageRect(rect, tile));
+ }
+ delete this.args.coverageTiles;
+ }
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ PictureLayerImplSnapshot,
+ {
+ typeName: 'cc::PictureLayerImpl'
+ });
+
+ ObjectSnapshot.subTypes.register(
+ LayerImplSnapshot,
+ {
+ typeNames: [
+ 'cc::LayerImpl',
+ 'cc::DelegatedRendererLayerImpl',
+ 'cc::HeadsUpDisplayLayerImpl',
+ 'cc::IOSurfaceLayerImpl',
+ 'cc::NinePatchLayerImpl',
+ 'cc::PictureImageLayerImpl',
+ 'cc::ScrollbarLayerImpl',
+ 'cc::SolidColorLayerImpl',
+ 'cc::SolidColorScrollbarLayerImpl',
+ 'cc::SurfaceLayerImpl',
+ 'cc::TextureLayerImpl',
+ 'cc::TiledLayerImpl',
+ 'cc::VideoLayerImpl',
+ 'cc::PaintedScrollbarLayerImpl',
+ 'ClankPatchLayer',
+ 'TabBorderLayer',
+ 'CounterLayer'
+ ]
+ });
+
+ return {
+ LayerImplSnapshot,
+ PictureLayerImplSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl.html
new file mode 100644
index 00000000000..d857a3b5bbd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/bbox2.html">
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the LayerTreeHostImpl model-level objects.
+ */
+tr.exportTo('tr.e.cc', function() {
+ const constants = tr.e.cc.constants;
+
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ /**
+ * @constructor
+ */
+ function LayerTreeHostImplSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ LayerTreeHostImplSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ tr.e.cc.preInitializeObject(this);
+ },
+
+ initialize() {
+ tr.e.cc.moveRequiredFieldsFromArgsToToplevel(
+ this, ['deviceViewportSize',
+ 'activeTree']);
+ tr.e.cc.moveOptionalFieldsFromArgsToToplevel(
+ this, ['pendingTree']);
+
+ // Move active_tiles into this.tiles. If that doesn't exist then, then as
+ // a backward compatability move tiles into this.tiles.
+ if (this.args.activeTiles !== undefined) {
+ this.activeTiles = this.args.activeTiles;
+ delete this.args.activeTiles;
+ } else if (this.args.tiles !== undefined) {
+ this.activeTiles = this.args.tiles;
+ delete this.args.tiles;
+ }
+
+ if (!this.activeTiles) {
+ this.activeTiles = [];
+ }
+
+ this.activeTree.layerTreeHostImpl = this;
+ this.activeTree.whichTree = constants.ACTIVE_TREE;
+ if (this.pendingTree) {
+ this.pendingTree.layerTreeHostImpl = this;
+ this.pendingTree.whichTree = constants.PENDING_TREE;
+ }
+ },
+
+ /**
+ * Get all of tile scales and their associated names.
+ */
+ getContentsScaleNames() {
+ const scales = {};
+ for (let i = 0; i < this.activeTiles.length; ++i) {
+ const tile = this.activeTiles[i];
+ // Return scale -> scale name mappings.
+ // Example:
+ // 0.25 -> LOW_RESOLUTION
+ // 1.0 -> HIGH_RESOLUTION
+ // 0.75 -> NON_IDEAL_RESOLUTION
+ scales[tile.contentsScale] = tile.resolution;
+ }
+ return scales;
+ },
+
+ getTree(whichTree) {
+ if (whichTree === constants.ACTIVE_TREE) {
+ return this.activeTree;
+ }
+ if (whichTree === constants.PENDING_TREE) {
+ return this.pendingTree;
+ }
+ throw new Exception('Unknown tree type + ' + whichTree);
+ },
+
+ get tilesHaveGpuMemoryUsageInfo() {
+ if (this.tilesHaveGpuMemoryUsageInfo_ !== undefined) {
+ return this.tilesHaveGpuMemoryUsageInfo_;
+ }
+
+ for (let i = 0; i < this.activeTiles.length; i++) {
+ if (this.activeTiles[i].gpuMemoryUsageInBytes === undefined) {
+ continue;
+ }
+ this.tilesHaveGpuMemoryUsageInfo_ = true;
+ return true;
+ }
+ this.tilesHaveGpuMemoryUsageInfo_ = false;
+ return false;
+ },
+
+ get gpuMemoryUsageInBytes() {
+ if (!this.tilesHaveGpuMemoryUsageInfo) return;
+
+ let usage = 0;
+ for (let i = 0; i < this.activeTiles.length; i++) {
+ const u = this.activeTiles[i].gpuMemoryUsageInBytes;
+ if (u !== undefined) usage += u;
+ }
+ return usage;
+ },
+
+ get userFriendlyName() {
+ let frameNumber;
+ if (!this.activeTree) {
+ frameNumber = this.objectInstance.snapshots.indexOf(this);
+ } else {
+ if (this.activeTree.sourceFrameNumber === undefined) {
+ frameNumber = this.objectInstance.snapshots.indexOf(this);
+ } else {
+ frameNumber = this.activeTree.sourceFrameNumber;
+ }
+ }
+ return 'cc::LayerTreeHostImpl frame ' + frameNumber;
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ LayerTreeHostImplSnapshot,
+ {typeName: 'cc::LayerTreeHostImpl'});
+
+ /**
+ * @constructor
+ */
+ function LayerTreeHostImplInstance() {
+ ObjectInstance.apply(this, arguments);
+
+ this.allLayersBBox_ = undefined;
+ }
+
+ LayerTreeHostImplInstance.prototype = {
+ __proto__: ObjectInstance.prototype,
+
+ get allContentsScales() {
+ if (this.allContentsScales_) {
+ return this.allContentsScales_;
+ }
+
+ const scales = {};
+ for (const tileID in this.allTileHistories_) {
+ const tileHistory = this.allTileHistories_[tileID];
+ scales[tileHistory.contentsScale] = true;
+ }
+ this.allContentsScales_ = Object.keys(scales);
+ return this.allContentsScales_;
+ },
+
+ get allLayersBBox() {
+ if (this.allLayersBBox_) {
+ return this.allLayersBBox_;
+ }
+ const bbox = new tr.b.math.BBox2();
+ function handleTree(tree) {
+ tree.renderSurfaceLayerList.forEach(function(layer) {
+ bbox.addQuad(layer.layerQuad);
+ });
+ }
+ this.snapshots.forEach(function(lthi) {
+ handleTree(lthi.activeTree);
+ if (lthi.pendingTree) {
+ handleTree(lthi.pendingTree);
+ }
+ });
+ this.allLayersBBox_ = bbox;
+ return this.allLayersBBox_;
+ }
+ };
+
+ ObjectInstance.subTypes.register(
+ LayerTreeHostImplInstance,
+ {typeName: 'cc::LayerTreeHostImpl'});
+
+ return {
+ LayerTreeHostImplSnapshot,
+ LayerTreeHostImplInstance,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test.html
new file mode 100644
index 00000000000..207ae0a4b11
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const snapshot = instance.snapshots[0];
+
+ assert.instanceOf(snapshot, tr.e.cc.LayerTreeHostImplSnapshot);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js
new file mode 100644
index 00000000000..b8e443aa0a1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js
@@ -0,0 +1,345 @@
+// Copyright (c) 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.
+
+'use strict';
+
+// A single LTHI sort of manually created from a Google search for cats.
+// This cannot be const because this file may be loaded more than once.
+global.g_catLTHIEvents = [
+ {
+ 'name': 'cc::Picture',
+ 'args': {
+ 'snapshot': {
+ 'params': {
+ 'opaque_rect': [
+ -15,
+ -15,
+ 0,
+ 0
+ ],
+ 'layer_rect': [
+ -15,
+ -15,
+ 1260,
+ 1697
+ ]
+ },
+ 'skp64': 'c2tpYXBpY3QWAAAAOAQAABQDAAADAAAAAWRhZXKoCQAACAAAHgMAAAAIAAAeAwAAAAwAACMAAHBBAABwQRwAAAMAAHDBAABwwQAghUQAQEFEAQAAAKAJAAAYAAAVAQAAAAAAAAAAAAAAAECDRACAPUQIAAAeAwAAABwAAAMAAAAAAAAAAABAg0QAAMhBAQAAAIwAAAAYAAAVAgAAAAAAAAAAAAAAAECDRAAAyEEEAAAcCAAAHgMAAAAcAAADAAAAAAAAyEEAQINEAADQQQEAAADYAAAADAAAIwAAAAAAAMhBGAAAFQIAAAAAAAAAAAAAAABAg0QAAMhBBAAAHBgAABUDAAAAAAAAAAAAyEEAQINEAADQQQgAAB4DAAAACAAAHgMAAAAMAAAjAABwwQAAcMEUAAAGAAAAAAAAAAAAAIhBAACIQQQAABwEAAAcCAAAHgMAAAAcAAADAACAQAAAAEAAAHxCAADAQQEAAABQAQAABAAAHAgAAB4DAAAAHAAAAwAAgEAAAABAAAB8QgAAwEEBAAAAeAEAAAQAABwIAAAeAwAAABwAAAMAAIBAAAAAQAAAfEIAAMBBAQAAAOABAABAAAAUBAAAAAwAAAA1AEgARgBSAFUARwAGAAAAAKDcPwCG4kEAAJBBAAAgQQDSlkEAONVBAEsHQgCxKUIAm0BCBAAAHAgAAB4DAAAAHAAAAwAAgEAAAABAAAB8QgAAwEEBAAAACAIAAAQAABwkAAAUBQAAAAIAAAADAAAAAQAAAAAAYD0AkM1BAACQQQDwhUIIAAAeAwAAAAgAAB4DAAAADAAAIwAAcMEAAHDBFAAABgAAAAABAAAAAACyQgAAsEEEAAAcBAAAHCQAABQFAAAAAgAAAAMAAAABAAAAAABgPQCQzUEAAJBBAJCxQlgAABQGAAAAFAAAADAAUgBRAEwAVwBSAFUATABRAEoACgAAAAAQE0AA3sVBAACQQQCQvUIAkNVCAJDlQgCQ9UIAkPtCAMgBQwDICUMAyA5DAMgRQwDIGUMkAAAUBQAAAAIAAAADAAAAAQAAAAAAYD0AkM1BAACQQQDII0MIAAAeAwAAAAgAAB4DAAAADAAAIwAAcMEAAHDBFAAABgAAAAACAAAAAAA5QwAAiEEEAAAcBAAAHAgAAB4DAAAAHAAAAwAALEMAAABAAIC/QwAAwEEBAAAAZAMAAAQAABwIAAAeAwAAABwAAAMAACxDAAAAQACAv0MAAMBBAQAAAIwDAAAEAAAcCAAAHgMAAAAcAAADAAAsQwAAAEAAgL9DAADAQQEAAAB0BAAAwAAAFAcAAAA2AAAAJgBEAFMAVwBYAFUASAADADAAUgBRAEwAVwBSAFUATABRAEoAAwA2AFEARABTAFYASwBSAFcAAAAbAAAAAKDcPwCG4kEAAJBBAAAyQ8CvO0MAa0NDwDpMQ0B3UUOAJ1pDAOJfQ8CuZ0PAHGxDwCt4Q6BigEPAuoRDwMCGQwBfiUPAq41DAImQQwCPkkMg55ZDgESbQ4B7nUOgQKFDwJilQ2B2qUNA3q1DwG+xQ+DHtUOgFLpDBAAAHAgAAB4DAAAAHAAAAwAALEMAAABAAIC/QwAAwEEBAAAAnAQAAAQAABwkAAAUBQAAAAIAAAADAAAAAQAAAAAAYD0AkM1BAACQQQCYwUMIAAAeAwAAAAgAAB4DAAAADAAAIwAAcMEAAHDBFAAABgAAAAADAAAAAADrQwAAiEEEAAAcBAAAHAgAAB4DAAAAHAAAAwCA5EMAAABAAAD6QwAAwEEBAAAAIAUAAAQAABwIAAAeAwAAABwAAAMAgORDAAAAQAAA+kMAAMBBAQAAAEgFAAAEAAAcCAAAHgMAAAAcAAADAIDkQwAAAEAAAPpDAADAQQEAAACkBQAANAAAFAcAAAAIAAAANgBEAFkASAAEAAAAAKDcPwCG4kEAAJBBAIDnQyBF60PAIu9DQMLyQwQAABwIAAAeAwAAABwAAAMAgORDAAAAQAAA+kMAAMBBAQAAAMwFAAAEAAAcJAAAFAUAAAACAAAAAwAAAAEAAAAAAGA9AJDNQQAAkEEAwvtDCAAAHgMAAAAIAAAeAwAAAAwAACMAAHDBAABwwRQAAAYAAAAABAAAAABAA0QAAIhBBAAAHAQAABwIAAAeAwAAABwAAAMAAABEAAAAQAAAC0QAAMBBAQAAAFAGAAAEAAAcCAAAHgMAAAAcAAADAAAARAAAAEAAAAtEAADAQQEAAAB4BgAABAAAHAgAAB4DAAAAHAAAAwAAAEQAAABAAAALRAAAwEEBAAAA1AYAADQAABQEAAAACAAAAC8AUgBEAEcABAAAAACg3D8AhuJBAACQQQCAAUTAXQNEIIQFRPByB0QEAAAcCAAAHgMAAAAcAAADAAAARAAAAEAAAAtEAADAQQEAAAD8BgAABAAAHDAAABQGAAAABgAAAEEAQgBBAAAAAwAAAACAmD4A3rVBAACAQQCADEQAQA5EAEAQRBgAABUIAAAAAECARAAAAEAAIINEAADAQQwAAA4JAAAAAQAAACQAABQKAAAAAgAAACIAAAABAAAAAICYPgDetUEAAIBBADCBRBgAABUDAAAAAAAAAAAA+kMAQINEAID6QxgAABULAAAAAICARAAA0EEAQINEAAD6QxgAABUMAAAAAICARAAA0EEAoIBEAAD6QxgAABUNAAAAAAAAAAAA/kMAQINEAIA9RBgAABUIAAAAAMBNRAAAAEAAQHhEAACoQQwAAA4JAAAAAgAAACQAABQOAAAAAgAAAJ0DAAABAAAAAIAmwABQ3kEAAIBBAEB4RCQAABQOAAAAAgAAAJ8DAAABAAAAAIAmwABQ3kEAAIBBAEB8RAgAAB4DAAAAHAAAAwDATUQAAEBAAEB4RAAAsEEBAAAAfAgAABAAAB8AAAAADwAAAB8AAAAEAAAcBAAAHAgAAB4DAAAAHAAAAwAAAAAAgPpDAECDRAAA+0MBAAAA1AgAAAwAACMAAAAAAID6QwwAACMAAACAAACAwBgAABUQAAAAAAAAAAAAAAAAQINEAACgQAQAABwIAAAeAwAAABwAAAMAAAAAAAD7QwBAg0QAgP1DAQAAACAJAAAMAAAjAAAAAAAA+0MYAAAVEAAAAAAAAAAAAAAAAECDRAAAoEAEAAAcCAAAHgMAAAAcAAADAAAAAACA/UMAQINEAAD+QwEAAABsCQAADAAAIwAAAAAAgP1DGAAAFRAAAAAAAAAAAAAAAABAg0QAAKBABAAAHBgAABURAAAAAAAAAACA+kMAQINEAAD7QxgAABUSAAAAAAAAAACA/UMAQINEAAD+QwQAABwEAAAcdGNhZiMAAAACAAAADVNrU3JjWGZlcm1vZGUQU2tMaW5lYXJHcmFkaWVudGNmcHQCAAAAAAENTHVjaWRhIEdyYW5kZQQNTHVjaWRhIEdyYW5kZQYMTHVjaWRhR3JhbmRl/v8AAAABCUhlbHZldGljYQQJSGVsdmV0aWNhBglIZWx2ZXRpY2H+/wAAeWFyYcQIAABwbXRiBQAAAD8AAAAWAAAAAAAAAKkAAACJUE5HDQoaCgAAAA1JSERSAAAAPwAAABYIBgAAAHf8RCEAAABwSURBVFiF7ZSxAcQgDMRk8KQMRQdD0lElxf8MueKsCSSMHXPOB1MSYIyh9vicvfcv/t6rdpGQAOcctYeEphZQkgCteb5Bxffe1R4SEiAi1B4SKt7123teuj/Wk6+dd42vnXedfMVXvCEVv9ZSe0h4ASOjDeti06sSAAAAAElFTkSuQmCCAAAAAAAAAAAAAAAMAAAADQAAAAAAAAAPAQAAiVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAYAAACdKY9CAAAA1klEQVQokZWSMWrDQBBF38hrxW5SGJdSkyqtyRXSB3QRnUUXEWzvK+QKwSCxxbIE5CpaZMmFsuBCK/DAr+a9zxQjwBY4AK/AjuX5A67ArwIOVVV9FEXxlmXZcYlu29bVdf1TluW3AO9N03zG4Ecpz/OzAKdpmr7W4DAiohUgwzAwjuMqnCQJgCiAJwQUgPce7/2qkKbpLAY7NMTawz5OxWSgN8a40BKLMcYBfQJ0WuuLtdYppViKtdZprS9AJ8AeODK/xkvkkp75NZwAAmz+IxFhAm7A7Q619kxK1JGuJQAAAABJRU5ErkJgggAAAAAAAAAAANcAAAAWAAAAAAAAAMcAAACJUE5HDQoaCgAAAA1JSERSAAAA1wAAABYIBgAAAFPJCaQAAACOSURBVHic7duxDYQwEETRObSVupJrgpQiiRwS0AIrS9Z7FUzytU78O8/zH+BzlSRjjNU7YCvXdb1xzTlXb4HtVJLc9716B2znWD0AdlVJchwag69VklTV6h2wHZcLmqgKmrhc0ERc0ERc0ERc0ERV0ERc0MSzEJqoCpq4XNBEVdDE5YImqoImlbz//YFvPbqBDWEab+OuAAAAAElFTkSuQmCCAAAAAAAAAAAALwAAABYAAAAAAAAApgAAAIlQTkcNChoKAAAADUlIRFIAAAAvAAAAFggGAAAAUFLFyQAAAG1JREFUWIXt17ENwCAUA9EL8qRMkiVoGZKKMgVZIKSwLPEmOKzfcLXWbkIJoNbq7vis977i55zuli0CGGO4O7YUd8AfAigl8w0CkOTu2BK9fGb1K3r5E+8SHZ9Z/YqOP2fjcpZ3Ocu7CNZ/MNEDHg4NYflXb+0AAAAASUVORK5CYIIAAAAAAAAAAAAAMAAAABYAAAAAAAAArgAAAIlQTkcNChoKAAAADUlIRFIAAAAwAAAAFggGAAAAhvcfrAAAAHVJREFUWIXtk7ERwCAMxGTwpAxFB0PSUSVFMoFT/JmLJpDOfuu9XyTGAVprao8Qc84nYO+tdgnjAGsttUeYohb4igOUkrfjjIBaq9ojjAOYmdojzBkBmV8o73pf0l/gjA1kDjhjA5kv8Aeo+QPUOMAYQ+0R5gZf3A3r3/HMCQAAAABJRU5ErkJgggAAAAAAAAAAAAAgdG5wEgAAAAAAQEEAAIA/AAAAAAAAAAAAAIBA/////wIwAwAAAAAAAAAAAAAAAAABAAAABAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQAAAAP8CMAMAAAAAAAAAAAACAAAATAAAAAAAAAAAAAAAAgAAAOXl5f/R0dH/EAAAAAAAAAAK1yM9AAAAAArXI70AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAyEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQQAAgD8AAAAAAAAAAAAAgECOjo7/ADADAAAAAAAAAGBBAACAPwAAAAAAAAAAAACAQAAAAP8BMIMCAwAAAAEAAAAAAIBBAACAPwAAAAAAAAAAAACAQAAAAP8BMIMCAwAAAAIAAAAAAGBBAACAPwAAAAAAAAAAAACAQAAAAP8BMIMCAwAAAAIAAAAAAGBBAACAPwAAAAAAAAAAAACAQH9/f/8BMIMCAwAAAAEAAAAAAEBBAACAPwAAAAAAAAAAAACAQPj4+P8AMAMAAAAAAAAAQEEAAIA/AAAAAAAAAAAAAIBAAAAAfwAwAwAAAAAAAABgQQAAgD8AAAAAAAAAAAAAgEAAAADMATCDAgMAAAACAAAAAABAQQAAgD8AAAAAAAAAAAAAgEDs7Oz/ADADAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQAAAAP8AMAMAAAAAAAAAQEEAAIA/AAAAAAAAAAAAAIBA/////wAwAwAAAAAAAACAQQAAgD8AAAAAAAAAAAAAgEAAAAD/ATCDAgMAAAABAAAAAABAQQAAgD8AAAAAAAAAAAAAgEAAAAA/ADAAAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQAAAAP8CMAMAAAAAAAAAAAACAAAATAAAAAAAAAAAAAAAAgAAAOXl5f/R0dH/EAAAAAAAAADNzEw+AAAAAM3MTL4AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAoEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQQAAgD8AAAAAAAAAAAAAgED/////ADACAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQI6Ojv8AMAIAAAAAACBodHACAAAAAgAAAAABAAABAAACAAAAAAoAAAAIAAAAAAAAAAUBAQEABQEBAQAAQIBEAAAAQAAgg0QAAABAACCDRAAAwEEAQIBEAADAQQBggEQAAEBAAACDRAAAQEAAAINEAAC4QQBggEQAALhBAECARAAAAEAAIINEAADAQQAAAAEAAAEAAAIAAAAACgAAAAgAAAAAAAAABQEBAQAFAQEBAADATUQAAABAAEB4RAAAAEAAQHhEAACoQQDATUQAAKhBAABORAAAQEAAAHhEAABAQAAAeEQAAKBBAABORAAAoEEAwE1EAAAAQABAeEQAAKhBAAAgZm9l' // @suppress longLineCheck
+ }
+ },
+ 'pid': 1,
+ 'ts': 100,
+ 'cat': 'disabled-by-default-cc.debug',
+ 'tid': 1,
+ 'ph': 'O',
+ 'id': 'PICTURE_1'
+ },
+ {
+ 'name': 'AnalyzeTask',
+ 'args': {
+ 'data': {
+ 'source_frame_number': 107,
+ 'tile_id': {
+ 'id_ref': 'TILE_1'
+ },
+ 'resolution': 'HIGH_RESOLUTION',
+ 'is_tile_in_pending_tree_now_bin': true
+ }
+ },
+ 'pid': 1,
+ 'ts': 101,
+ 'cat': 'cc',
+ 'tid': 1,
+ 'ph': 'B'
+ },
+ {
+ 'name': 'AnalyzeTask',
+ 'args': {},
+ 'pid': 1,
+ 'ts': 105,
+ 'cat': 'cc',
+ 'tid': 1,
+ 'ph': 'E'
+ },
+ {
+ 'name': 'RasterTask',
+ 'args': {
+ 'data': {
+ 'source_frame_number': 107,
+ 'tile_id': {
+ 'id_ref': 'TILE_1'
+ },
+ 'resolution': 'HIGH_RESOLUTION',
+ 'is_tile_in_pending_tree_now_bin': true
+ }
+ },
+ 'pid': 1,
+ 'ts': 110,
+ 'cat': 'cc',
+ 'tid': 1,
+ 'ph': 'B'
+ },
+ {
+ 'name': 'RasterTask',
+ 'args': {},
+ 'pid': 1,
+ 'ts': 150,
+ 'cat': 'cc',
+ 'tid': 1,
+ 'ph': 'E'
+ },
+ {
+ 'name': 'RasterTask',
+ 'args': {
+ 'data': {
+ 'source_frame_number': 107,
+ 'tile_id': {
+ 'id_ref': 'TILE_2'
+ },
+ 'resolution': 'HIGH_RESOLUTION',
+ 'is_tile_in_pending_tree_now_bin': true
+ }
+ },
+ 'pid': 1,
+ 'ts': 170,
+ 'cat': 'cc',
+ 'tid': 1,
+ 'ph': 'B'
+ },
+ {
+ 'name': 'RasterTask',
+ 'args': {},
+ 'pid': 1,
+ 'ts': 180,
+ 'cat': 'cc',
+ 'tid': 1,
+ 'ph': 'E'
+ },
+ {
+ 'name': 'cc::LayerTreeHostImpl',
+ 'args': {
+ 'snapshot': {
+ 'device_viewport_size': {
+ 'width': 2460,
+ 'height': 1606
+ },
+ 'active_tree': {
+ 'source_frame_number': 7,
+ 'root_layer': {
+ 'tilings': [
+ {
+ 'content_scale': 2,
+ 'content_bounds': {
+ 'width': 2460,
+ 'height': 3334
+ },
+ 'num_tiles': 1
+ },
+ {
+ 'content_scale': 0.25,
+ 'content_bounds': {
+ 'width': 308,
+ 'height': 417
+ },
+ 'num_tiles': 1
+ }
+ ],
+ 'coverage_tiles': [
+ {
+ 'geometry_rect': [0, 0, 256, 256],
+ 'tile': {
+ 'id_ref': 'TILE_1'
+ }
+ },
+ {
+ 'geometry_rect': [256, 0, 256, 256]
+ },
+ {
+ 'geometry_rect': [512, 0, 256, 256]
+ },
+ {
+ 'geometry_rect': [0, 256, 256, 512]
+ },
+ {
+ 'geometry_rect': [256, 256, 512, 512]
+ }
+ ],
+ 'gpu_memory_usage': 22069248,
+ 'draws_content': 1,
+ 'layer_id': 6,
+ 'invalidation': [10, 20, 30, 40],
+ 'bounds': {
+ 'width': 1230,
+ 'height': 1667
+ },
+ 'children': [
+ {
+ 'tilings': [
+ {
+ 'content_scale': 2,
+ 'content_bounds': {
+ 'width': 200,
+ 'height': 100
+ },
+ 'num_tiles': 1
+ }
+ ],
+ 'gpu_memory_usage': 128000,
+ 'draws_content': 1,
+ 'layer_id': 7,
+ 'invalidation': [],
+ 'bounds': {
+ 'width': 100,
+ 'height': 50
+ },
+ 'children': [
+ ],
+ 'ideal_contents_scale': 2,
+ 'layer_quad': [
+ 0,
+ 0,
+ 200,
+ 0,
+ 200,
+ 100,
+ 0,
+ 100
+ ],
+ 'pictures': [
+ ],
+ 'debug_info': {
+ 'annotated_invalidation_rects': [
+ {
+ 'geometry_rect': [11, 22, 33, 44],
+ 'reason': 'appeared',
+ 'client': 'client1'
+ },
+ {
+ 'geometry_rect': [22, 33, 44, 55],
+ 'reason': 'disappeared',
+ 'client': 'client2'
+ },
+ ]
+ },
+ 'id': 'cc::PictureLayerImpl/LAYER_2'
+ }
+ ],
+ 'ideal_contents_scale': 2,
+ 'layer_quad': [
+ 0,
+ -1022,
+ 2460,
+ -1022,
+ 2460,
+ 2312,
+ 0,
+ 2312
+ ],
+ 'pictures': [
+ {
+ 'id_ref': 'PICTURE_1'
+ }
+ ],
+ 'id': 'cc::PictureLayerImpl/LAYER_1'
+ },
+ 'render_surface_layer_list': [
+ {'id_ref': 'LAYER_1'},
+ {'id_ref': 'LAYER_2'}
+ ],
+ 'id': 'cc::LayerTreeImpl/0x7d246ee0'
+ },
+ 'tiles': [
+ {
+ 'active_priority': {
+ 'time_to_visible_in_seconds': 0,
+ 'resolution': 'HIGH_RESOLUTION',
+ 'distance_to_visible_in_pixels': 0
+ },
+ 'pending_priority': {
+ 'time_to_visible_in_seconds': 3.4028234663852886e+38,
+ 'resolution': 'NON_IDEAL_RESOLUTION',
+ 'distance_to_visible_in_pixels': 3.4028234663852886e+38
+ },
+ 'managed_state': {
+ 'resolution': 'HIGH_RESOLUTION',
+ 'is_solid_color': false,
+ 'is_using_gpu_memory': true,
+ 'has_resource': true,
+ 'scheduled_priority': 10,
+ 'distance_to_visible': 0,
+ 'gpu_memory_usage': 1024000
+ },
+ 'layer_id': '6',
+ 'picture_pile': {
+ 'id_ref': 'PICTURE_1'
+ },
+ 'contents_scale': 2,
+ 'content_rect': [0, 0, 1024, 1024],
+ 'id': 'cc::Tile/TILE_1'
+ },
+ {
+ 'active_priority': {
+ 'time_to_visible_in_seconds': 0,
+ 'resolution': 'HIGH_RESOLUTION',
+ 'distance_to_visible_in_pixels': 0
+ },
+ 'pending_priority': {
+ 'time_to_visible_in_seconds': 3.4028234663852886e+38,
+ 'resolution': 'NON_IDEAL_RESOLUTION',
+ 'distance_to_visible_in_pixels': 3.4028234663852886e+38
+ },
+ 'managed_state': {
+ 'resolution': 'HIGH_RESOLUTION',
+ 'is_solid_color': false,
+ 'is_using_gpu_memory': true,
+ 'has_resource': true,
+ 'scheduled_priority': 12,
+ 'distance_to_visible': 0,
+ 'gpu_memory_usage': 1024000
+ },
+ 'layer_id': '6',
+ 'picture_pile': {
+ 'id_ref': 'PICTURE_1'
+ },
+ 'contents_scale': 2,
+ 'content_rect': [0, 1024, 1024, 1024],
+ 'id': 'cc::Tile/TILE_2'
+ }
+ ]
+ }
+ },
+ 'pid': 1,
+ 'ts': 500,
+ 'cat': 'disabled-by-default-cc.debug',
+ 'tid': 28163,
+ 'ph': 'O',
+ 'id': 'LTHI_1'
+ },
+ {
+ 'name': 'cc::DisplayItemList',
+ 'args': {
+ 'snapshot': {
+ 'params': {
+ 'layer_rect': [
+ -15,
+ -15,
+ 1260,
+ 1697
+ ],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': '[base 64 encoded skia picture]'
+ }
+ },
+ 'pid': 1,
+ 'ts': 300,
+ 'cat': 'disabled-by-default-cc.debug',
+ 'tid': 1,
+ 'ph': 'O',
+ 'id': 'PICTURE_3'
+ }
+];
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_impl.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_impl.html
new file mode 100644
index 00000000000..8b2d4120737
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/layer_tree_impl.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_impl.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const constants = tr.e.cc.constants;
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * @constructor
+ */
+ function LayerTreeImplSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ LayerTreeImplSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ tr.e.cc.preInitializeObject(this);
+ this.layerTreeHostImpl = undefined;
+ this.whichTree = undefined;
+ this.sourceFrameNumber = undefined;
+ },
+
+ initialize() {
+ tr.e.cc.moveRequiredFieldsFromArgsToToplevel(
+ this, ['renderSurfaceLayerList']);
+ tr.e.cc.moveOptionalFieldsFromArgsToToplevel(
+ this, ['rootLayer', 'layers']);
+ if (this.args.sourceFrameNumber) {
+ this.sourceFrameNumber = this.args.sourceFrameNumber;
+ }
+
+ // Slimming Paint v2 removes the tree hierarchy and replace
+ // it with a layer list. The tracing data should have either
+ // rootLayer or layers available.
+ if (this.rootLayer) {
+ // The version before SPv2
+ this.rootLayer.layerTreeImpl = this;
+ } else {
+ // The SPv2 version, where the layer list contains all
+ // non-mask, non-replica layers.
+ for (let i = 0; i < this.layers.length; i++) {
+ this.layers[i].layerTreeImpl = this;
+ }
+ }
+
+ if (this.args.swapPromiseTraceIds &&
+ this.args.swapPromiseTraceIds.length) {
+ this.tracedInputLatencies = [];
+
+ const ownProcess = this.objectInstance.parent;
+ const modelHelper = ownProcess.model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (modelHelper) {
+ this._initializeTracedInputLatencies(modelHelper);
+ }
+ }
+ },
+
+ _initializeTracedInputLatencies(modelHelper) {
+ const latencyEvents = modelHelper.browserHelper.getLatencyEventsInRange(
+ modelHelper.model.bounds);
+
+ // Convert all ids to InputLatency Async objects.
+ latencyEvents.forEach(function(event) {
+ for (let i = 0; i < this.args.swapPromiseTraceIds.length; i++) {
+ if (!event.args.data || !event.args.data.trace_id) {
+ continue;
+ }
+ if (parseInt(event.args.data.trace_id) ===
+ this.args.swapPromiseTraceIds[i]) {
+ this.tracedInputLatencies.push(event);
+ }
+ }
+ }, this);
+ },
+
+ get hasSourceFrameBeenDrawnBefore() {
+ if (this.whichTree === tr.e.cc.constants.PENDING_TREE) {
+ return false;
+ }
+
+ // Old chrome's don't produce sourceFrameNumber.
+ if (this.sourceFrameNumber === undefined) return;
+
+ const thisLTHI = this.layerTreeHostImpl;
+ const thisLTHIIndex = thisLTHI.objectInstance.snapshots.indexOf(
+ thisLTHI);
+ const prevLTHIIndex = thisLTHIIndex - 1;
+ if (prevLTHIIndex < 0 ||
+ prevLTHIIndex >= thisLTHI.objectInstance.snapshots.length) {
+ return false;
+ }
+ const prevLTHI = thisLTHI.objectInstance.snapshots[prevLTHIIndex];
+ if (!prevLTHI.activeTree) return false;
+
+ // Old chrome's don't produce sourceFrameNumber.
+ if (prevLTHI.activeTree.sourceFrameNumber === undefined) return;
+ return prevLTHI.activeTree.sourceFrameNumber === this.sourceFrameNumber;
+ },
+
+ get otherTree() {
+ const other = this.whichTree === constants.ACTIVE_TREE ?
+ constants.PENDING_TREE : constants.ACTIVE_TREE;
+ return this.layerTreeHostImpl.getTree(other);
+ },
+
+ get gpuMemoryUsageInBytes() {
+ let totalBytes = 0;
+ this.iterLayers(function(layer) {
+ if (layer.gpuMemoryUsageInBytes !== undefined) {
+ totalBytes += layer.gpuMemoryUsageInBytes;
+ }
+ });
+ return totalBytes;
+ },
+
+ iterLayers(func, thisArg) {
+ const visitedLayers = {};
+ function visitLayer(layer, depth, isMask, isReplica) {
+ if (visitedLayers[layer.layerId]) return;
+
+ visitedLayers[layer.layerId] = true;
+ func.call(thisArg, layer, depth, isMask, isReplica);
+ if (layer.children) {
+ for (let i = 0; i < layer.children.length; i++) {
+ visitLayer(layer.children[i], depth + 1);
+ }
+ }
+ if (layer.maskLayer) {
+ visitLayer(layer.maskLayer, depth + 1, true, false);
+ }
+ if (layer.replicaLayer) {
+ visitLayer(layer.replicaLayer, depth + 1, false, true);
+ }
+ }
+ if (this.rootLayer) {
+ visitLayer(this.rootLayer, 0, false, false);
+ } else {
+ for (let i = 0; i < this.layers.length; i++) {
+ visitLayer(this.layers[i], 0, false, false);
+ }
+ }
+ },
+ findLayerWithId(id) {
+ let foundLayer = undefined;
+ function visitLayer(layer) {
+ if (layer.layerId === id) {
+ foundLayer = layer;
+ }
+ }
+ this.iterLayers(visitLayer);
+ return foundLayer;
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ LayerTreeImplSnapshot,
+ {typeName: 'cc::LayerTreeImpl'});
+
+ return {
+ LayerTreeImplSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture.html
new file mode 100644
index 00000000000..b1a6f2ef288
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture_as_image_data.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+tr.exportTo('tr.e.cc', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ // Number of pictures created. Used as an uniqueId because we are immutable.
+ const PictureCount = 0;
+ const OPS_TIMING_ITERATIONS = 3;
+
+ function Picture(skp64, layerRect) {
+ this.skp64_ = skp64;
+ this.layerRect_ = layerRect;
+
+ this.guid_ = tr.b.GUID.allocateSimple();
+ }
+
+ Picture.prototype = {
+ get canSave() {
+ return true;
+ },
+
+ get layerRect() {
+ return this.layerRect_;
+ },
+
+ get guid() {
+ return this.guid_;
+ },
+
+ getBase64SkpData() {
+ return this.skp64_;
+ },
+
+ getOps() {
+ if (!PictureSnapshot.CanGetOps()) {
+ console.error(PictureSnapshot.HowToEnablePictureDebugging());
+ return undefined;
+ }
+
+ const ops = window.chrome.skiaBenchmarking.getOps({
+ skp64: this.skp64_,
+ params: {
+ layer_rect: this.layerRect_.toArray()
+ }
+ });
+
+ if (!ops) {
+ console.error('Failed to get picture ops.');
+ }
+
+ return ops;
+ },
+
+ getOpTimings() {
+ if (!PictureSnapshot.CanGetOpTimings()) {
+ console.error(PictureSnapshot.HowToEnablePictureDebugging());
+ return undefined;
+ }
+
+ const opTimings = window.chrome.skiaBenchmarking.getOpTimings({
+ skp64: this.skp64_,
+ params: {
+ layer_rect: this.layerRect_.toArray()
+ }
+ });
+
+ if (!opTimings) {
+ console.error('Failed to get picture op timings.');
+ }
+
+ return opTimings;
+ },
+
+ /**
+ * Tag each op with the time it takes to rasterize.
+ *
+ * FIXME: We should use real statistics to get better numbers here, see
+ * https://code.google.com/p/trace-viewer/issues/detail?id=357
+ *
+ * @param {Array} ops Array of Skia operations.
+ * @return {Array} Skia ops where op.cmd_time contains the associated time
+ * for a given op.
+ */
+ tagOpsWithTimings(ops) {
+ const opTimings = [];
+ for (let iteration = 0; iteration < OPS_TIMING_ITERATIONS; iteration++) {
+ opTimings[iteration] = this.getOpTimings();
+ if (!opTimings[iteration] || !opTimings[iteration].cmd_times) {
+ return ops;
+ }
+ if (opTimings[iteration].cmd_times.length !== ops.length) {
+ return ops;
+ }
+ }
+
+ for (let opIndex = 0; opIndex < ops.length; opIndex++) {
+ let min = Number.MAX_VALUE;
+ for (let i = 0; i < OPS_TIMING_ITERATIONS; i++) {
+ min = Math.min(min, opTimings[i].cmd_times[opIndex]);
+ }
+ ops[opIndex].cmd_time = min;
+ }
+
+ return ops;
+ },
+
+ /**
+ * Rasterize the picture.
+ *
+ * @param {{opt_stopIndex: number, params}} The SkPicture operation to
+ * rasterize up to. If not defined, the entire SkPicture is rasterized.
+ * @param {{opt_showOverdraw: bool, params}} Defines whether pixel overdraw
+ should be visualized in the image.
+ * @param {function(tr.e.cc.PictureAsImageData)} The callback function that
+ * is called after rasterization is complete or fails.
+ */
+ rasterize(params, rasterCompleteCallback) {
+ if (!PictureSnapshot.CanRasterize() || !PictureSnapshot.CanGetOps()) {
+ rasterCompleteCallback(new tr.e.cc.PictureAsImageData(
+ this, tr.e.cc.PictureSnapshot.HowToEnablePictureDebugging()));
+ return;
+ }
+
+ if (!this.layerRect_.width || !this.layerRect_.height) {
+ rasterCompleteCallback(new tr.e.cc.PictureAsImageData(this, null));
+ return;
+ }
+
+ const raster = window.chrome.skiaBenchmarking.rasterize(
+ {
+ skp64: this.skp64_,
+ params: {
+ layer_rect: this.layerRect_.toArray()
+ }
+ },
+ {
+ stop: params.stopIndex === undefined ? -1 : params.stopIndex,
+ overdraw: !!params.showOverdraw,
+ params: { }
+ });
+
+ if (raster) {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ canvas.width = raster.width;
+ canvas.height = raster.height;
+ const imageData = ctx.createImageData(raster.width, raster.height);
+ imageData.data.set(new Uint8ClampedArray(raster.data));
+ rasterCompleteCallback(new tr.e.cc.PictureAsImageData(this, imageData));
+ } else {
+ const error = 'Failed to rasterize picture. ' +
+ 'Your recording may be from an old Chrome version. ' +
+ 'The SkPicture format is not backward compatible.';
+ rasterCompleteCallback(new tr.e.cc.PictureAsImageData(this, error));
+ }
+ }
+ };
+
+ function LayeredPicture(pictures) {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.pictures_ = pictures;
+ this.layerRect_ = undefined;
+ }
+
+ LayeredPicture.prototype = {
+ __proto__: Picture.prototype,
+
+ get canSave() {
+ return false;
+ },
+
+ get typeName() {
+ return 'cc::LayeredPicture';
+ },
+
+ get layerRect() {
+ if (this.layerRect_ !== undefined) {
+ return this.layerRect_;
+ }
+
+ this.layerRect_ = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+
+ for (let i = 0; i < this.pictures_.length; ++i) {
+ const rect = this.pictures_[i].layerRect;
+ this.layerRect_.x = Math.min(this.layerRect_.x, rect.x);
+ this.layerRect_.y = Math.min(this.layerRect_.y, rect.y);
+ this.layerRect_.width =
+ Math.max(this.layerRect_.width, rect.x + rect.width);
+ this.layerRect_.height =
+ Math.max(this.layerRect_.height, rect.y + rect.height);
+ }
+ return this.layerRect_;
+ },
+
+ get guid() {
+ return this.guid_;
+ },
+
+ getBase64SkpData() {
+ throw new Error('Not available with a LayeredPicture.');
+ },
+
+ getOps() {
+ let ops = [];
+ for (let i = 0; i < this.pictures_.length; ++i) {
+ ops = ops.concat(this.pictures_[i].getOps());
+ }
+ return ops;
+ },
+
+ getOpTimings() {
+ const opTimings = this.pictures_[0].getOpTimings();
+ for (let i = 1; i < this.pictures_.length; ++i) {
+ const timings = this.pictures_[i].getOpTimings();
+ opTimings.cmd_times = opTimings.cmd_times.concat(timings.cmd_times);
+ opTimings.total_time += timings.total_time;
+ }
+ return opTimings;
+ },
+
+ tagOpsWithTimings(ops) {
+ const opTimings = [];
+ for (let iteration = 0; iteration < OPS_TIMING_ITERATIONS; iteration++) {
+ opTimings[iteration] = this.getOpTimings();
+ if (!opTimings[iteration] || !opTimings[iteration].cmd_times) {
+ return ops;
+ }
+ }
+
+ for (let opIndex = 0; opIndex < ops.length; opIndex++) {
+ let min = Number.MAX_VALUE;
+ for (let i = 0; i < OPS_TIMING_ITERATIONS; i++) {
+ min = Math.min(min, opTimings[i].cmd_times[opIndex]);
+ }
+ ops[opIndex].cmd_time = min;
+ }
+ return ops;
+ },
+
+ rasterize(params, rasterCompleteCallback) {
+ this.picturesAsImageData_ = [];
+ const rasterCallback = function(pictureAsImageData) {
+ this.picturesAsImageData_.push(pictureAsImageData);
+ if (this.picturesAsImageData_.length !== this.pictures_.length) {
+ return;
+ }
+
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ canvas.width = this.layerRect.width;
+ canvas.height = this.layerRect.height;
+
+ // TODO(dsinclair): Verify these finish in the order started.
+ // Do the rasterize calls run sync or asyn? As the imageData
+ // going to be in the same order as the pictures_ list?
+ for (let i = 0; i < this.picturesAsImageData_.length; ++i) {
+ ctx.putImageData(this.picturesAsImageData_[i].imageData,
+ this.pictures_[i].layerRect.x,
+ this.pictures_[i].layerRect.y);
+ }
+ this.picturesAsImageData_ = [];
+
+ rasterCompleteCallback(new tr.e.cc.PictureAsImageData(this,
+ ctx.getImageData(this.layerRect.x, this.layerRect.y,
+ this.layerRect.width, this.layerRect.height)));
+ }.bind(this);
+
+ for (let i = 0; i < this.pictures_.length; ++i) {
+ this.pictures_[i].rasterize(params, rasterCallback);
+ }
+ }
+ };
+
+
+ /**
+ * @constructor
+ */
+ function PictureSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ PictureSnapshot.HasSkiaBenchmarking = function() {
+ return tr.isExported('chrome.skiaBenchmarking');
+ };
+
+ PictureSnapshot.CanRasterize = function() {
+ if (!PictureSnapshot.HasSkiaBenchmarking()) {
+ return false;
+ }
+ if (!window.chrome.skiaBenchmarking.rasterize) {
+ return false;
+ }
+ return true;
+ };
+
+ PictureSnapshot.CanGetOps = function() {
+ if (!PictureSnapshot.HasSkiaBenchmarking()) {
+ return false;
+ }
+ if (!window.chrome.skiaBenchmarking.getOps) {
+ return false;
+ }
+ return true;
+ };
+
+ PictureSnapshot.CanGetOpTimings = function() {
+ if (!PictureSnapshot.HasSkiaBenchmarking()) {
+ return false;
+ }
+ if (!window.chrome.skiaBenchmarking.getOpTimings) {
+ return false;
+ }
+ 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;
+ }
+ 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';
+ }
+ if (!PictureSnapshot.CanGetInfo()) {
+ return 'Your chrome is old: chrome.skiaBenchmarking.getInfo not found';
+ }
+ return undefined;
+ };
+
+ PictureSnapshot.CanDebugPicture = function() {
+ return PictureSnapshot.HowToEnablePictureDebugging() === undefined;
+ };
+
+ PictureSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ tr.e.cc.preInitializeObject(this);
+ this.rasterResult_ = undefined;
+ },
+
+ initialize() {
+ // If we have an alias args, that means this picture was represented
+ // by an alias, and the real args is in alias.args.
+ if (this.args.alias) {
+ this.args = this.args.alias.args;
+ }
+
+ if (!this.args.params.layerRect) {
+ throw new Error('Missing layer rect');
+ }
+
+ this.layerRect_ = this.args.params.layerRect;
+ this.picture_ = new Picture(this.args.skp64, this.args.params.layerRect);
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ },
+
+ get canSave() {
+ return this.picture_.canSave;
+ },
+
+ get layerRect() {
+ return this.layerRect_ ? this.layerRect_ : this.picture_.layerRect;
+ },
+
+ get guid() {
+ return this.picture_.guid;
+ },
+
+ getBase64SkpData() {
+ return this.picture_.getBase64SkpData();
+ },
+
+ getOps() {
+ return this.picture_.getOps();
+ },
+
+ getOpTimings() {
+ return this.picture_.getOpTimings();
+ },
+
+ tagOpsWithTimings(ops) {
+ return this.picture_.tagOpsWithTimings(ops);
+ },
+
+ rasterize(params, rasterCompleteCallback) {
+ this.picture_.rasterize(params, rasterCompleteCallback);
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ PictureSnapshot,
+ {typeNames: ['cc::Picture']});
+
+ return {
+ PictureSnapshot,
+ Picture,
+ LayeredPicture,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_as_image_data.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_as_image_data.html
new file mode 100644
index 00000000000..0d33fc2f70c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_as_image_data.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ /**
+ * @constructor
+ */
+ function PictureAsImageData(picture, errorOrImageData) {
+ this.picture_ = picture;
+ if (errorOrImageData instanceof ImageData) {
+ this.error_ = undefined;
+ this.imageData_ = errorOrImageData;
+ } else {
+ this.error_ = errorOrImageData;
+ this.imageData_ = undefined;
+ }
+ }
+
+ /**
+ * Creates a new pending PictureAsImageData (no image data and no error).
+ *
+ * @return {PictureAsImageData} a new pending PictureAsImageData.
+ */
+ PictureAsImageData.Pending = function(picture) {
+ return new PictureAsImageData(picture, undefined);
+ };
+
+ PictureAsImageData.prototype = {
+ get picture() {
+ return this.picture_;
+ },
+
+ get error() {
+ return this.error_;
+ },
+
+ get imageData() {
+ return this.imageData_;
+ },
+
+ isPending() {
+ return this.error_ === undefined && this.imageData_ === undefined;
+ },
+
+ asCanvas() {
+ if (!this.imageData_) return;
+
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ canvas.width = this.imageData_.width;
+ canvas.height = this.imageData_.height;
+ ctx.putImageData(this.imageData_, 0, 0);
+ return canvas;
+ }
+ };
+
+ return {
+ PictureAsImageData,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_test.html
new file mode 100644
index 00000000000..71b17b78389
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/picture_test.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+ const snapshot = instance.snapshots[0];
+
+ assert.instanceOf(snapshot, tr.e.cc.PictureSnapshot);
+ instance.wasDeleted(150);
+ });
+
+ test('getOps', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+ const snapshot = instance.snapshots[0];
+
+ const ops = snapshot.getOps();
+ if (!ops) return;
+ assert.strictEqual(ops.length, 142);
+
+ const op0 = ops[0];
+ assert.strictEqual(op0.cmd_string, 'Save');
+ assert.instanceOf(op0.info, Array);
+ });
+
+ function setUpFakeSkiaBenchmarking() {
+ if (tr.e.cc.PictureSnapshot.CanRasterize() &&
+ tr.e.cc.PictureSnapshot.CanGetOps()) {
+ return window.chrome;
+ }
+
+ const oldChrome = window.chrome;
+ window.chrome = {
+ skiaBenchmarking: {
+ rasterize() {},
+ getOps() {},
+ }
+ };
+ return oldChrome;
+ }
+
+ test('rasterizeZeroSize', function() {
+ if (!window) return;
+
+ const oldChrome = setUpFakeSkiaBenchmarking();
+ try {
+ const picture = new tr.e.cc.Picture(
+ '', {x: 0, y: 0, width: 0, height: 50});
+ let result;
+ picture.rasterize({}, function(data) { result = data; });
+ assert.strictEqual(result.picture, picture);
+ assert.isUndefined(result.imageData);
+ assert.isNull(result.error);
+ } finally {
+ window.chrome = oldChrome;
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/raster_task.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/raster_task.html
new file mode 100644
index 00000000000..cc506aac902
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/raster_task.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const knownRasterTaskNames = [
+ 'TileManager::RunRasterTask',
+ 'RasterWorkerPoolTaskImpl::RunRasterOnThread',
+ 'RasterWorkerPoolTaskImpl::Raster',
+ 'RasterTaskImpl::Raster',
+ 'cc::RasterTask',
+ 'RasterTask'
+ ];
+
+ const knownAnalysisTaskNames = [
+ 'TileManager::RunAnalyzeTask',
+ 'RasterWorkerPoolTaskImpl::RunAnalysisOnThread',
+ 'RasterWorkerPoolTaskImpl::Analyze',
+ 'RasterTaskImpl::Analyze',
+ 'cc::AnalyzeTask',
+ 'AnalyzeTask'
+ ];
+
+ function getTileFromRasterTaskSlice(slice) {
+ if (!(isSliceDoingRasterization(slice) || isSliceDoingAnalysis(slice))) {
+ return undefined;
+ }
+
+ let tileData;
+ if (slice.args.data) {
+ tileData = slice.args.data;
+ } else {
+ tileData = slice.args.tileData;
+ }
+ if (tileData === undefined) return undefined;
+ if (tileData.tile_id) return tileData.tile_id;
+
+ const tile = tileData.tileId;
+ if (!(tile instanceof tr.e.cc.TileSnapshot)) {
+ return undefined;
+ }
+ return tileData.tileId;
+ }
+
+ function isSliceDoingRasterization(slice) {
+ return knownRasterTaskNames.includes(slice.title);
+ }
+
+ function isSliceDoingAnalysis(slice) {
+ return knownAnalysisTaskNames.includes(slice.title);
+ }
+
+ return {
+ getTileFromRasterTaskSlice,
+ isSliceDoingRasterization,
+ isSliceDoingAnalysis
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/region.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/region.html
new file mode 100644
index 00000000000..e2601dd2641
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/region.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/rect.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ /**
+ * @constructor
+ */
+ function Region() {
+ this.rects = [];
+ }
+
+ Region.fromArray = function(array) {
+ if (array.length % 4 !== 0) {
+ throw new Error('Array must consist be a multiple of 4 in length');
+ }
+
+ const r = new Region();
+ for (let i = 0; i < array.length; i += 4) {
+ r.rects.push(tr.b.math.Rect.fromXYWH(array[i], array[i + 1],
+ array[i + 2], array[i + 3]));
+ }
+ return r;
+ };
+
+ /**
+ * @return {Region} If array is undefined, returns an empty region. Otherwise
+ * returns Region.fromArray(array).
+ */
+ Region.fromArrayOrUndefined = function(array) {
+ if (array === undefined) return new Region();
+ return Region.fromArray(array);
+ };
+
+ Region.prototype = {
+ __proto__: Region.prototype,
+
+ rectIntersects(r) {
+ for (let i = 0; i < this.rects.length; i++) {
+ if (this.rects[i].intersects(r)) return true;
+ }
+ return false;
+ },
+
+ addRect(r) {
+ this.rects.push(r);
+ }
+ };
+
+ return {
+ Region,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/render_pass.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/render_pass.html
new file mode 100644
index 00000000000..ec78354ae6b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/render_pass.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * @constructor
+ */
+ function RenderPassSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ RenderPassSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ tr.e.cc.preInitializeObject(this);
+ },
+
+ initialize() {
+ tr.e.cc.moveRequiredFieldsFromArgsToToplevel(
+ this, ['quadList']);
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ RenderPassSnapshot,
+ {typeName: 'cc::RenderPass'});
+
+ return {
+ RenderPassSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile.html
new file mode 100644
index 00000000000..f9c232ab974
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/extras/chrome/cc/debug_colors.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * @constructor
+ */
+ function TileSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ TileSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ tr.e.cc.preInitializeObject(this);
+ },
+
+ initialize() {
+ tr.e.cc.moveOptionalFieldsFromArgsToToplevel(
+ this, ['layerId', 'contentsScale', 'contentRect']);
+ if (this.args.managedState) {
+ this.resolution = this.args.managedState.resolution;
+ this.isSolidColor = this.args.managedState.isSolidColor;
+ this.isUsingGpuMemory = this.args.managedState.isUsingGpuMemory;
+ this.hasResource = this.args.managedState.hasResource;
+ this.scheduledPriority = this.args.scheduledPriority;
+ this.gpuMemoryUsageInBytes = this.args.gpuMemoryUsage;
+ } else {
+ this.resolution = this.args.resolution;
+ this.isSolidColor = this.args.drawInfo.isSolidColor;
+ this.isUsingGpuMemory = this.args.isUsingGpuMemory;
+ this.hasResource = this.args.hasResource;
+ this.scheduledPriority = this.args.scheduledPriority;
+ this.gpuMemoryUsageInBytes = this.args.gpuMemoryUsage;
+ }
+
+ // This check is for backward compatability. It can probably
+ // be removed once we're confident that most traces contain
+ // content_rect.
+ if (this.contentRect) {
+ this.layerRect = this.contentRect.scale(1.0 / this.contentsScale);
+ }
+
+ if (this.isSolidColor) {
+ this.type_ = tr.e.cc.tileTypes.solidColor;
+ } else if (!this.hasResource) {
+ this.type_ = tr.e.cc.tileTypes.missing;
+ } else if (this.resolution === 'HIGH_RESOLUTION') {
+ this.type_ = tr.e.cc.tileTypes.highRes;
+ } else if (this.resolution === 'LOW_RESOLUTION') {
+ this.type_ = tr.e.cc.tileTypes.lowRes;
+ } else {
+ this.type_ = tr.e.cc.tileTypes.unknown;
+ }
+ },
+
+ getTypeForLayer(layer) {
+ let type = this.type_;
+ if (type === tr.e.cc.tileTypes.unknown) {
+ if (this.contentsScale < layer.idealContentsScale) {
+ type = tr.e.cc.tileTypes.extraLowRes;
+ } else if (this.contentsScale > layer.idealContentsScale) {
+ type = tr.e.cc.tileTypes.extraHighRes;
+ }
+ }
+ return type;
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(TileSnapshot, {typeName: 'cc::Tile'});
+
+ return {
+ TileSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_coverage_rect.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_coverage_rect.html
new file mode 100644
index 00000000000..d1c65fa608a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_coverage_rect.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ /**
+ * This class represents a tile (from impl side) and its final rect on the
+ * layer. Note that the rect is determined by what is needed to cover all
+ * of the layer without overlap.
+ * @constructor
+ */
+ function TileCoverageRect(rect, tile) {
+ this.geometryRect = rect;
+ this.tile = tile;
+ }
+
+ return {
+ TileCoverageRect,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_test.html
new file mode 100644
index 00000000000..218af949862
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/tile_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+ const instance = p.objects.getAllInstancesNamed('cc::Tile')[0];
+ const snapshot = instance.snapshots[0];
+
+ assert.instanceOf(snapshot, tr.e.cc.TileSnapshot);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util.html
new file mode 100644
index 00000000000..e074e68287c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<script>
+
+'use strict';
+
+tr.exportTo('tr.e.cc', function() {
+ const convertedNameCache = {};
+ function convertNameToJSConvention(name) {
+ if (name in convertedNameCache) {
+ return convertedNameCache[name];
+ }
+
+ if (name[0] === '_' ||
+ name[name.length - 1] === '_') {
+ convertedNameCache[name] = name;
+ return name;
+ }
+
+ const words = name.split('_');
+ if (words.length === 1) {
+ convertedNameCache[name] = words[0];
+ return words[0];
+ }
+
+ for (let i = 1; i < words.length; i++) {
+ words[i] = words[i][0].toUpperCase() + words[i].substring(1);
+ }
+
+ convertedNameCache[name] = words.join('');
+ return convertedNameCache[name];
+ }
+
+ function moveRequiredFieldsFromArgsToToplevel(object, fields) {
+ for (let i = 0; i < fields.length; i++) {
+ const key = fields[i];
+ if (object.args[key] === undefined) {
+ throw Error('Expected field ' + key + ' not found in args');
+ }
+ if (object[key] !== undefined) {
+ throw Error('Field ' + key + ' already in object');
+ }
+ object[key] = object.args[key];
+ delete object.args[key];
+ }
+ }
+
+ function moveOptionalFieldsFromArgsToToplevel(object, fields) {
+ for (let i = 0; i < fields.length; i++) {
+ const key = fields[i];
+ if (object.args[key] === undefined) continue;
+ if (object[key] !== undefined) {
+ throw Error('Field ' + key + ' already in object');
+ }
+ object[key] = object.args[key];
+ delete object.args[key];
+ }
+ }
+
+ function preInitializeObject(object) {
+ preInitializeObjectInner(object.args, false);
+ }
+
+ function preInitializeObjectInner(object, hasRecursed) {
+ if (!(object instanceof Object)) return;
+
+ if (object instanceof Array) {
+ for (let i = 0; i < object.length; i++) {
+ preInitializeObjectInner(object[i], true);
+ }
+ return;
+ }
+
+ if (hasRecursed &&
+ (object instanceof tr.model.ObjectSnapshot ||
+ object instanceof tr.model.ObjectInstance)) {
+ return;
+ }
+
+ for (let key in object) {
+ const newKey = convertNameToJSConvention(key);
+ if (newKey !== key) {
+ const value = object[key];
+ delete object[key];
+ object[newKey] = value;
+ key = newKey;
+ }
+
+ // Convert objects with keys ending with Quad to tr.b.math.Quad type.
+ if (/Quad$/.test(key) && !(object[key] instanceof tr.b.math.Quad)) {
+ let q;
+ try {
+ q = tr.b.math.Quad.from8Array(object[key]);
+ } catch (e) {
+ }
+ object[key] = q;
+ continue;
+ }
+
+ // Convert objects with keys ending with Rect to tr.b.math.Rect type.
+ if (/Rect$/.test(key) && !(object[key] instanceof tr.b.math.Rect)) {
+ let r;
+ try {
+ r = tr.b.math.Rect.fromArray(object[key]);
+ } catch (e) {
+ }
+ object[key] = r;
+ }
+
+ preInitializeObjectInner(object[key], true);
+ }
+ }
+
+ return {
+ preInitializeObject,
+ convertNameToJSConvention,
+ moveRequiredFieldsFromArgsToToplevel,
+ moveOptionalFieldsFromArgsToToplevel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util_test.html
new file mode 100644
index 00000000000..9c0adeb0f76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cc/util_test.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+
+<script>
+
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('nameConvert', function() {
+ assert.strictEqual(tr.e.cc.convertNameToJSConvention('_foo'), '_foo');
+ assert.strictEqual(tr.e.cc.convertNameToJSConvention('foo_'), 'foo_');
+ assert.strictEqual(tr.e.cc.convertNameToJSConvention('foo'), 'foo');
+ assert.strictEqual(tr.e.cc.convertNameToJSConvention('foo_bar'), 'fooBar');
+ assert.strictEqual(tr.e.cc.convertNameToJSConvention('foo_bar_baz'),
+ 'fooBarBaz');
+ });
+
+ test('objectConvertNested', function() {
+ const object = {
+ un_disturbed: true,
+ args: {
+ foo_bar: {
+ a_field: 7
+ }
+ }
+ };
+ const expected = {
+ un_disturbed: true,
+ args: {
+ fooBar: {
+ aField: 7
+ }
+ }
+ };
+ tr.e.cc.preInitializeObject(object);
+ assert.deepEqual(object, expected);
+ });
+
+ test('arrayConvert', function() {
+ const object = {
+ un_disturbed: true,
+ args: [
+ {foo_bar: 7},
+ {foo_bar: 8}
+ ]
+ };
+ const expected = {
+ un_disturbed: true,
+ args: [
+ {fooBar: 7},
+ {fooBar: 8}
+ ]
+ };
+ tr.e.cc.preInitializeObject(object);
+ assert.deepEqual(object, expected);
+ });
+
+ test('quadCoversion', function() {
+ const object = {
+ args: {
+ some_quad: [1, 2, 3, 4, 5, 6, 7, 8]
+ }
+ };
+ tr.e.cc.preInitializeObject(object);
+ assert.instanceOf(object.args.someQuad, tr.b.math.Quad);
+ });
+
+ test('quadConversionNested', function() {
+ const object = {
+ args: {
+ nested_field: {
+ a_quad: [1, 2, 3, 4, 5, 6, 7, 8]
+ },
+ non_nested_quad: [1, 2, 3, 4, 5, 6, 7, 8]
+ }
+ };
+ tr.e.cc.preInitializeObject(object);
+ assert.instanceOf(object.args.nestedField.aQuad, tr.b.math.Quad);
+ assert.instanceOf(object.args.nonNestedQuad, tr.b.math.Quad);
+ });
+
+ test('rectCoversion', function() {
+ const object = {
+ args: {
+ some_rect: [1, 2, 3, 4]
+ }
+ };
+ tr.e.cc.preInitializeObject(object);
+ assert.instanceOf(object.args.someRect, tr.b.math.Rect);
+ });
+
+ test('rectCoversionNested', function() {
+ const object = {
+ args: {
+ nested_field: {
+ a_rect: [1, 2, 3, 4]
+ },
+ non_nested_rect: [1, 2, 3, 4]
+ }
+ };
+ tr.e.cc.preInitializeObject(object);
+ assert.instanceOf(object.args.nestedField.aRect, tr.b.math.Rect);
+ assert.instanceOf(object.args.nonNestedRect, tr.b.math.Rect);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor.html
new file mode 100644
index 00000000000..a786c061bf9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import"
+ href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/model/constants.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data Auditors.
+ */
+tr.exportTo('tr.e.audits', function() {
+ const Auditor = tr.c.Auditor;
+
+ /**
+ * Auditor for Chrome-specific traces.
+ * @constructor
+ */
+ function ChromeAuditor(model) {
+ Auditor.call(this, model);
+
+ const modelHelper = this.model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (modelHelper && modelHelper.browserHelper) {
+ // Must be a browserHelper in order to do audits.
+ 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;
+ }
+ }
+ },
+
+ /**
+ * Called by import to install userFriendlyCategoryDriver.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor_test.html
new file mode 100644
index 00000000000..5947cfb35b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_auditor_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ function createMainProcesses(m) {
+ m.browserProcess = m.getOrCreateProcess(1);
+ m.browserMain = m.browserProcess.getOrCreateThread(2);
+ m.browserMain.name = 'CrBrowserMain';
+
+ m.renderer1 = m.getOrCreateProcess(3);
+ m.renderer1Main = m.renderer1.getOrCreateThread(4);
+ m.renderer1Main.name = 'CrRendererMain';
+
+ m.renderer1Compositor = m.renderer1.getOrCreateThread(4);
+ m.renderer1Compositor.name = 'Compositor';
+ }
+
+ function newInputLatencyEvent(tsStart, tsEnd, opt_args) {
+ const e = new tr.model.AsyncSlice(
+ 'benchmark', 'InputLatency',
+ ColorScheme.getColorIdForGeneralPurposeString('InputLatency'),
+ tsStart, opt_args);
+ e.duration = tsEnd - tsStart;
+ return e;
+ }
+
+ function newImplRenderingStatsEvent(ts, opt_args) {
+ const e = new tr.model.ThreadSlice(
+ 'benchmark', 'BenchmarkInstrumentation::ImplThreadRenderingStats',
+ ColorScheme.getColorIdForGeneralPurposeString('x'),
+ ts, opt_args, 0);
+ return e;
+ }
+
+ test('simple', function() {
+ tr.c.TestUtils.newModelWithAuditor(function(m) {
+ createMainProcesses(m);
+ const bAsyncSlices = m.browserMain.asyncSliceGroup;
+ bAsyncSlices.push(newInputLatencyEvent(100, 130));
+ bAsyncSlices.push(newInputLatencyEvent(116, 150));
+ bAsyncSlices.push(newInputLatencyEvent(133, 166));
+ bAsyncSlices.push(newInputLatencyEvent(150, 183));
+ bAsyncSlices.push(newInputLatencyEvent(166, 200));
+ bAsyncSlices.push(newInputLatencyEvent(183, 216));
+
+ const rm1Slices = m.renderer1Compositor.sliceGroup;
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(113));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(130));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(147));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(163));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(180));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(197));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(213));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(230));
+ rm1Slices.pushSlice(newImplRenderingStatsEvent(247));
+ }, tr.e.audits.ChromeAuditor);
+ });
+
+ test('refsToBrowser', function() {
+ const events = [
+ // An object created and snapshotted in the browser process.
+ {ts: 1000, pid: 1, tid: 2, ph: 'N', cat: 'c', id: '0x1000', name: 'a',
+ args: {}},
+ {ts: 1100, pid: 1, tid: 2, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
+ args: {snapshot: {foo: 12345}}},
+ {ts: 1300, pid: 1, tid: 2, ph: 'D', cat: 'c', id: '0x1000', name: 'a',
+ args: {}},
+
+ // A reference to the object in the browser from the renderer process.
+ {ts: 1200, pid: 3, tid: 4, ph: 'X', cat: 'c', name: 'b', dur: 100,
+ args: {bar: {pid_ref: -1, id_ref: '0x1000'}}}
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false,
+ pruneEmptyContainers: false,
+ customizeModelCallback: createMainProcesses,
+ auditorConstructors: [tr.e.audits.ChromeAuditor]
+ });
+
+ const browserObject = m.browserProcess.objects.getObjectInstanceAt(
+ new tr.model.ScopedId('ptr', '0x1000'), 1.2);
+ assert.isDefined(browserObject);
+ const foo = browserObject.getSnapshotAt(1.2);
+ assert.isDefined(foo);
+
+ assert.strictEqual(m.renderer1Main.sliceGroup.slices.length, 1);
+ const slice = m.renderer1Main.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.args.bar, foo);
+ });
+
+ test('filterTracingUI', function() {
+ const m = tr.c.TestUtils.newModelWithAuditor(function(m) {
+ m.browserProcess = m.getOrCreateProcess(1);
+ m.browserMain = m.browserProcess.getOrCreateThread(2);
+ m.browserMain.name = 'CrBrowserMain';
+
+ m.renderer1 = m.getOrCreateProcess(3);
+ m.renderer1.labels = ['https://google.com'];
+ m.renderer1Main = m.renderer1.getOrCreateThread(4);
+ m.renderer1Main.name = 'CrRendererMain';
+
+ m.renderer2 = m.getOrCreateProcess(5);
+ m.renderer2.labels = ['chrome://tracing'];
+ m.renderer2Main = m.renderer2.getOrCreateThread(6);
+ m.renderer2Main.name = 'CrRendererMain';
+ }, tr.e.audits.ChromeAuditor);
+
+ assert.isTrue(m.renderer1.important);
+ assert.isFalse(m.renderer2.important);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_processes.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_processes.html
new file mode 100644
index 00000000000..0456b29126e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_processes.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/fixed_color_scheme.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.chrome.chrome_processes', function() {
+ const CHROME_PROCESS_NAMES = {
+ BROWSER: 'browser_process',
+ RENDERER: 'renderer_processes', // Intentionally plural.
+ ALL: 'all_processes',
+ GPU: 'gpu_process',
+ PPAPI: 'ppapi_process',
+ UNKNOWN: 'unknown_processes',
+ };
+
+ const PROCESS_COLOR_SCHEME_NAME = 'ChromeProcessNames';
+ const PROCESS_COLOR_SCHEME =
+ tr.b.FixedColorScheme.fromNames(Object.values(CHROME_PROCESS_NAMES));
+
+ tr.b.FixedColorSchemeRegistry.register(() => PROCESS_COLOR_SCHEME, {
+ name: PROCESS_COLOR_SCHEME_NAME,
+ });
+
+ /**
+ * Converts name to lower case and replaces spaces with underscores.
+ */
+ function canonicalizeName(name) {
+ return name.toLowerCase().replace(' ', '_');
+ }
+
+ /**
+ * Convert raw process name to canonical process names used in catapult.
+ *
+ * Examples:
+ * browser -> CHROME_PROCESS_NAME.BROWSER
+ * renderer -> CHROME_PROCESS_NAME.RENDERER
+ */
+ function canonicalizeProcessName(rawProcessName) {
+ if (!rawProcessName) return CHROME_PROCESS_NAMES.UNKNOWN;
+
+ const baseCanonicalName = canonicalizeName(rawProcessName);
+ switch (baseCanonicalName) {
+ case 'renderer': return CHROME_PROCESS_NAMES.RENDERER;
+ case 'browser': return CHROME_PROCESS_NAMES.BROWSER;
+ }
+
+ if (Object.values(CHROME_PROCESS_NAMES).includes(baseCanonicalName)) {
+ return baseCanonicalName;
+ }
+
+ return CHROME_PROCESS_NAMES.UNKNOWN;
+ }
+
+ return {
+ CHROME_PROCESS_NAMES,
+ PROCESS_COLOR_SCHEME,
+ PROCESS_COLOR_SCHEME_NAME,
+ canonicalizeName,
+ canonicalizeProcessName,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_test_utils.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_test_utils.html
new file mode 100644
index 00000000000..69128010fa9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_test_utils.html
@@ -0,0 +1,161 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data Auditors.
+ */
+tr.exportTo('tr.e.chrome', function() {
+ function ChromeTestUtils() {
+ }
+
+ ChromeTestUtils.newChromeModel = function(customizeModelCallback) {
+ return tr.c.TestUtils.newModel(function(model) {
+ model.browserProcess = model.getOrCreateProcess(1);
+ model.browserMain = model.browserProcess.getOrCreateThread(2);
+ model.browserMain.name = 'CrBrowserMain';
+
+ model.rendererProcess = model.getOrCreateProcess(2);
+ model.rendererMain = model.rendererProcess.getOrCreateThread(3);
+ model.rendererMain.name = 'CrRendererMain';
+
+ model.rendererCompositor = model.rendererProcess.getOrCreateThread(4);
+ model.rendererCompositor.name = 'Compositor';
+
+ model.rasterWorker1 = model.rendererProcess.getOrCreateThread(5);
+ model.rasterWorker1.name = 'CompositorTileWorker1';
+
+ customizeModelCallback(model);
+ });
+ };
+
+ ChromeTestUtils.addEvent = function(thread, dict) {
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ thread.asyncSliceGroup.push(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addNavigationStartEvent = function(model, dict) {
+ dict.title = 'NavigationTiming navigationStart';
+ const event = tr.c.TestUtils.newInstantEvent(dict);
+ model.instantEvents.push(event);
+ return event;
+ };
+
+ ChromeTestUtils.addFirstContentfulPaintEvent = function(model, dict) {
+ dict.title = 'firstContentfulPaint';
+ const event = tr.c.TestUtils.newInstantEvent(dict);
+ model.instantEvents.push(event);
+ return event;
+ };
+
+ ChromeTestUtils.addInputEvent = function(model, typeName, dict) {
+ dict.title = 'InputLatency::' + typeName;
+ dict.isTopLevel = (dict.isTopLevel === undefined);
+ dict.startThread = model.browserMain;
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.browserMain.asyncSliceGroup.push(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addFlingAnimationEvent = function(model, dict) {
+ dict.title = 'InputHandlerProxy::HandleGestureFling::started';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.rendererCompositor.asyncSliceGroup.push(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addRenderingEvent = function(model, dict) {
+ dict.title = dict.title || 'DummyEvent';
+ dict.type = tr.model.ThreadSlice;
+ const slice = tr.c.TestUtils.newSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addFrameEvent = function(model, dict) {
+ dict.title = tr.model.helpers.IMPL_RENDERING_STATS;
+ dict.type = tr.model.ThreadSlice;
+ const slice = tr.c.TestUtils.newSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addLoadingEvent = function(model, dict) {
+ dict.title = 'WebContentsImpl Loading';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.rendererMain.asyncSliceGroup.push(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addNetworkEvent = function(model, dict) {
+ dict.cat = 'netlog';
+ dict.title = 'Generic Network event';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.browserMain.asyncSliceGroup.push(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addCommitLoadEvent = function(model, dict) {
+ dict.title = 'RenderFrameImpl::didCommitProvisionalLoad';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addCreateChildFrameEvent = function(model, dict) {
+ dict.title = 'RenderFrameImpl::createChildFrame';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addStartProvisionalLoadEvent = function(model, dict) {
+ dict.title = 'RenderFrameImpl::didStartProvisionalLoad';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addFailProvisionalLoadEvent = function(model, dict) {
+ dict.title = 'RenderFrameImpl::didFailProvisionalLoad';
+ const slice = tr.c.TestUtils.newAsyncSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addFinishLoadEvent = function(model, dict) {
+ dict.title = 'RenderFrameImpl::didFinishLoad';
+ const slice = tr.c.TestUtils.newSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addLoadFinishedEvent = function(model, dict) {
+ dict.title = 'LoadFinished';
+ const slice = tr.c.TestUtils.newSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ ChromeTestUtils.addCreateThreadsEvent = function(model, dict) {
+ dict.title = 'BrowserMainLoop::CreateThreads';
+ const slice = tr.c.TestUtils.newSliceEx(dict);
+ model.rendererMain.sliceGroup.pushSlice(slice);
+ return slice;
+ };
+
+ return {
+ ChromeTestUtils,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver.html
new file mode 100644
index 00000000000..d81818658de
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/sinebow_color_generator.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'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',
+ ],
+
+ // Where do these go?
+ renderer_misc: [
+ 'DecodeFont',
+ 'ThreadState::completeSweep', // blink_gc
+ ],
+
+ // TODO(fmeawad): https://github.com/catapult-project/catapult/issues/2572
+ v8_runtime: [
+ // Dynamically populated.
+ ],
+
+ [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);
+ });
+ }
+
+ // keys: event.category part
+ // values: user friendly 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();
+
+ // Prime the color generator by iterating through all UFCs in alphabetical
+ // order.
+ for (const category of ChromeUserFriendlyCategoryDriver.ALL_TITLES) {
+ ChromeUserFriendlyCategoryDriver.getColor(category);
+ }
+
+ return {
+ ChromeUserFriendlyCategoryDriver,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver_test.html
new file mode 100644
index 00000000000..5251a7d2edc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/chrome_user_friendly_category_driver_test.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ufcFromEvent = tr.e.chrome.ChromeUserFriendlyCategoryDriver.fromEvent;
+
+ test('userFriendlyCategory', function() {
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'LocalFrameView::layout',
+ category: 'cat'
+ }), 'layout');
+
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'V8.Execute',
+ category: 'cat'
+ }), 'script_execute');
+
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'Paint',
+ category: 'cat'
+ }), 'record');
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'Document::updateStyle',
+ category: 'cat'
+ }), 'style');
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser',
+ category: 'cat'
+ }), 'parseHTML');
+
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'constructor',
+ category: 'cat'
+ }), 'other');
+ });
+
+ test('ufcFromTraceCategory', function() {
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'a',
+ category: 'netlog'
+ }), 'net');
+
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'a',
+ category: 'foobar,overhead'
+ }), 'overhead');
+
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'a',
+ category: 'startup'
+ }), 'startup');
+ });
+
+ test('ufcOther', function() {
+ assert.strictEqual(ufcFromEvent({
+ guid: tr.b.GUID.allocateSimple(),
+ title: 'a',
+ category: 'other'
+ }), 'other');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time.html
new file mode 100644
index 00000000000..24468cb10e0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_processes.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.chrome', function() {
+ class CpuTime {
+ /**
+ * Returns two level map of rail stage to initiator type to set of bounds of
+ * associated segments, intersected with |rangeOfInterest|.
+ *
+ * For each rail stage, we additionally have a key 'all_initiators' that
+ * returns all the segment bounds associated with that rail stage across all
+ * initiator types. For completeness, there is an additional rail stage
+ * 'all_stages' that has all the segment bounds across all rail stages.
+ *
+ * If a segment is not contained within |rangeOfInterest| it is not
+ * included.
+ *
+ * There is a unique segment bound for each segment in the map. For example,
+ * assume
+ * - |segmentA| is associated with both Click Response and Scroll Animation
+ * - |bound1| is the interesting bound of |segmentA| in Response -> Click
+ * set.
+ * - |bound2| is the interesting bound of |segmentA| in Animation -> Scroll
+ * set.
+ * Then bound1 === bound2. These segment bounds can therefore be used as
+ * keys in a map to represent the segment.
+ *
+ * Example return value (all bounds are intersected with |rangeOfInterest|):
+ *
+ * {
+ * 'Animation': {
+ * 'CSS': {Segment bounds for CSS Animation},
+ * 'Video': {Segment bounds for Video Animation},
+ * ...
+ * 'all_initiators': {All Animation segment bounds}
+ * },
+ * 'Response': {
+ * 'Click': {Segment bounds for Click Response},
+ * 'Scroll': {Segment bounds for Scroll Response},
+ * ...
+ * 'all_initiators': {All Response segment bounds}
+ * },
+ * ...
+ * 'all_stages': {
+ * 'all_initiators': {All segment bounds}
+ * }
+ * }
+ *
+ * @param {!Array.<!tr.model.um.Segment>} segments
+ * @param {!Array.<!tr.b.math.Range>} rangeOfInterest
+ * @returns {!Map.<string, Map.<string, Set.<!tr.b.math.Range>>}
+ */
+ static getStageToInitiatorToSegmentBounds(segments, rangeOfInterest) {
+ const stageToInitiatorToRanges = new Map();
+ stageToInitiatorToRanges.set('all_stages',
+ new Map([['all_initiators', new Set()]]));
+ const allRanges =
+ stageToInitiatorToRanges.get('all_stages').get('all_initiators');
+
+ for (const segment of segments) {
+ if (!rangeOfInterest.intersectsRangeInclusive(segment.range)) continue;
+ const intersectingRange =
+ rangeOfInterest.findIntersection(segment.range);
+ allRanges.add(intersectingRange);
+
+ for (const expectation of segment.expectations) {
+ const stageTitle = expectation.stageTitle;
+ if (!stageToInitiatorToRanges.has(stageTitle)) {
+ stageToInitiatorToRanges.set(stageTitle,
+ new Map([['all_initiators', new Set()]]));
+ }
+
+ const initiatorToRanges = stageToInitiatorToRanges.get(stageTitle);
+ initiatorToRanges.get('all_initiators').add(intersectingRange);
+
+ const initiatorType = expectation.initiatorType;
+ if (initiatorType) {
+ if (!initiatorToRanges.has(initiatorType)) {
+ initiatorToRanges.set(initiatorType, new Set());
+ }
+ initiatorToRanges.get(initiatorType).add(intersectingRange);
+ }
+ }
+ }
+ return stageToInitiatorToRanges;
+ }
+
+ /**
+ * Returns the root node of a MultiDimensionalView in TopDownTreeView for
+ * cpu time.
+ *
+ * The returned tree view is three dimensional (processType, threadType, and
+ * railStage + initiator). Rail stage and initiator are not separate
+ * dimensions because they are not independent - there is no such thing as
+ * CSS Response or Scroll Load.
+ *
+ * Each node in the tree view contains two values - cpuUsage and cpuTotal.
+ *
+ * See cpu_time_multidimensinoal_view.md for more details about the returned
+ * multidimensional view.
+ *
+ * @param {!tr.Model} model
+ * @param {!tr.b.math.Range} rangeOfInterest
+ * @returns {!tr.b.MultiDimensionalViewNode}
+ */
+ static constructMultiDimensionalView(model, rangeOfInterest) {
+ const mdvBuilder = new tr.b.MultiDimensionalViewBuilder(
+ 3 /* dimensions (process, thread and rail stage / initiator) */,
+ 2 /* valueCount (cpuUsage and cpuTotal) */);
+
+ const stageToInitiatorToRanges =
+ CpuTime.getStageToInitiatorToSegmentBounds(
+ model.userModel.segments, rangeOfInterest);
+
+ const allSegmentBoundsInRange =
+ stageToInitiatorToRanges.get('all_stages').get('all_initiators');
+
+ for (const [pid, process] of Object.entries(model.processes)) {
+ const processType =
+ tr.e.chrome.chrome_processes.canonicalizeProcessName(process.name);
+ for (const [tid, thread] of Object.entries(process.threads)) {
+ // Cache cpuTime for each segment bound.
+ const rangeToCpuTime = new Map();
+ for (const range of allSegmentBoundsInRange) {
+ rangeToCpuTime.set(range, thread.getCpuTimeForRange(range));
+ }
+
+ for (const [stage, initiatorToRanges] of stageToInitiatorToRanges) {
+ for (const [initiator, ranges] of initiatorToRanges) {
+ const cpuTime = tr.b.math.Statistics.sum(ranges,
+ range => rangeToCpuTime.get(range));
+ const duration = tr.b.math.Statistics.sum(ranges,
+ range => range.duration);
+ const cpuTimePerSecond = cpuTime / duration;
+ mdvBuilder.addPath(
+ [[processType], [thread.type], [stage, initiator]],
+ [cpuTimePerSecond, cpuTime],
+ tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL);
+ }
+ }
+ }
+ }
+
+ return mdvBuilder.buildTopDownTreeView();
+ }
+ }
+
+ return {
+ CpuTime,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_multidimensional_view.md b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_multidimensional_view.md
new file mode 100644
index 00000000000..dcc646c3f9a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_multidimensional_view.md
@@ -0,0 +1,79 @@
+<!-- Copyright 2017 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.
+-->
+
+# CPU Time MultiDimensionalView Explainer
+
+This document explains the MultiDimensionalView returned by `constructMultiDimensionalView` in `cpuTime.html`.
+
+The returned MultiDimensionalView is in TopDownTreeView mode. It is three
+dimensional (processType, threadType, and railStage + initiator). Rail stage and
+initiator are not separate dimensions because they are not independent - there
+is no such thing as CSS Response or Scroll Load.
+
+Each node in the tree view contains two values - cpuUsage and cpuTotal.
+
+When talking about multidimensional tree views, a useful abstration is "path",
+which uniquely determines a node in the tree: A path is a 3 element array, and
+each of these three elements is a possibly empty array of strings. Here is an
+example path:
+```
+[ ['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS'] ]
+ Dimension 1 Dimension 2 Dimension 3
+```
+
+We can arrive at the node denoted by this path in many different ways starting
+from the root node, so this path is not to be confused with the graph theoretic
+notion of path. Here is one of the ways to reach the node (we show the
+intermediate paths during the traversal inline):
+
+```javascript
+const node = treeRoot // [[], [], []]
+ .children[0] // access children along first dimension
+ .get('browser_process') // [['browser_process'], [], []]
+ .children[2] // access children along third dimension
+ .get('Animation') // [['browser_process'], [], ['Animation']]
+ .children[1] // Access children along second dimension
+ .get('CrBrowserMain') // [['browser_process'], ['CrBrowserMain'], ['Animation']]
+ .children[2] // Go further down along third dimension
+ .get('CSS') // [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']]
+```
+Now node.values contains the cpu time data for the browser main thread during
+the CSS Animation stage:
+- `node.values[0]` is `cpuUsage` - cpu time over per unit of wall clock time
+- `node.values[1]` is `cpuTotal` - total miliseconds of used cpu time
+
+The path for the node that hold data for all threads of renderer process
+during scroll response expectations is `[['renderer_process'], [], ['Response', 'Scroll']]`.
+
+As we can see, we simply have an empty array for the second dimension. This
+works similarly if we want to get data for all processes for a particular
+thread.
+
+However, if we want to access data for all rail stages and all initiator
+types, we have to use the special rail stage `all_stages`, and initiator
+type `all_initiators`. For example, to get cpu data during all Response
+stages for all processes and threads, we use the node at path
+ `[[], [], ['Response', 'all_initiators']]`
+
+To get cpu data for all rail stages for ChildIOThread, we use the path
+ `[[], ['ChildIOThread'], ['all_stages', 'all_initiators']]`
+
+This is because the tree view automatically aggregates cpu time
+data along each dimension by summing values on the children nodes. For
+aggregating rail stages and initiator types, summing is not the right thing
+to do since
+
+ 1. User Expectations can overlap (for example, one tab can go through a
+ Video Animation while another tab is concurrently going through a CSS
+ Animation - it's worth noting that user expectations are not scoped to a
+ tab.)
+
+ 2. Different rail stages have different durations (for example, if we
+ have 200ms of Video Animation with 50% cpuUsage, and 500ms of CSS
+ Animation with 60% cpuUage, cpuUsage for all Animations is clearly not
+ 110%.)
+
+We therefore more manually do the appropriate aggregations and store the
+data in `all_stages` and `all_initiators` nodes.
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test.html
new file mode 100644
index 00000000000..606463737ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test.html
@@ -0,0 +1,1503 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cpu_time.html">
+<link rel="import" href="/tracing/extras/chrome/cpu_time_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const getStageToInitiatorToSegmentBounds =
+ tr.e.chrome.CpuTime.getStageToInitiatorToSegmentBounds;
+
+ const INITIATOR_TYPE = tr.model.um.INITIATOR_TYPE;
+
+ const CHROME_PROCESS_NAMES =
+ tr.e.chrome.chrome_processes.CHROME_PROCESS_NAMES;
+
+ const constructMultiDimensionalView =
+ tr.e.chrome.CpuTime.constructMultiDimensionalView;
+
+ const buildModelFromSpec = tr.e.chrome.cpuTimeTestUtils.buildModelFromSpec;
+
+ test('getStageToInitiatorToSegmentBounds', () => {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ // This is needed for a model to have segments.
+ thread.name = 'CrBrowserMain';
+
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, INITIATOR_TYPE.CSS,
+ 100, // start time.
+ 300 // duration.
+ ));
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, INITIATOR_TYPE.VIDEO, 300, 100));
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, INITIATOR_TYPE.SCROLL, 400, 200));
+ });
+
+ const segments = model.userModel.segments;
+
+ const map = getStageToInitiatorToSegmentBounds(
+ model.userModel.segments, model.bounds);
+
+ // Ignoring Idle Expectations, we have the following segments:
+ // [100, 300]: CSS Animation
+ // [300, 400]: CSS Animation, Video Animation
+ // [400, 600]: Scroll Response
+ const allSegments = [...map.get('all_stages').get('all_initiators')];
+ assert.sameDeepMembers(
+ allSegments.map(s => [s.min, s.max]),
+ [[100, 300], [300, 400], [400, 600]]
+ );
+
+ const videoAnimationSegments =
+ [...map.get('Animation').get(INITIATOR_TYPE.VIDEO)];
+ assert.sameDeepMembers(
+ videoAnimationSegments.map(s => [s.min, s.max]),
+ [[300, 400]]);
+
+ const cssAnimationSegments =
+ [...map.get('Animation').get(INITIATOR_TYPE.CSS)];
+ assert.sameDeepMembers(
+ cssAnimationSegments.map(s => [s.min, s.max]),
+ [[100, 300], [300, 400]]);
+
+ const allAnimationSegments =
+ [...map.get('Animation').get('all_initiators')];
+ assert.sameDeepMembers(
+ allAnimationSegments.map(s => [s.min, s.max]),
+ [[100, 300], [300, 400]]);
+
+ const scrollResponseSegments =
+ [...map.get('Response').get(INITIATOR_TYPE.SCROLL)];
+ assert.sameDeepMembers(
+ scrollResponseSegments.map(s => [s.min, s.max]),
+ [[400, 600]]);
+
+ const allResponseSegments =
+ [...map.get('Response').get('all_initiators')];
+ assert.sameDeepMembers(
+ allResponseSegments.map(s => [s.min, s.max]),
+ [[400, 600]]);
+ });
+
+ test('getStageToInitiatorToSegmentBounds-rangeOfInterest', () => {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ // This is needed for a model to have segments.
+ thread.name = 'CrBrowserMain';
+
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, INITIATOR_TYPE.CSS,
+ 100, // start time.
+ 300 // duration.
+ ));
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, INITIATOR_TYPE.VIDEO, 300, 100));
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, INITIATOR_TYPE.SCROLL, 400, 200));
+ });
+
+ const segments = model.userModel.segments;
+
+ const map = getStageToInitiatorToSegmentBounds(model.userModel.segments,
+ tr.b.math.Range.fromExplicitRange(150, 350));
+
+ // Ignoring Idle Expectations, we have the following segments in range:
+ // [150, 300]: CSS Animation
+ // [300, 350]: CSS Animation, Video Animation
+ const allSegments = [...map.get('all_stages').get('all_initiators')];
+ assert.sameDeepMembers(
+ allSegments.map(s => [s.min, s.max]),
+ [[150, 300], [300, 350]]
+ );
+
+ const videoAnimationSegments =
+ [...map.get('Animation').get(INITIATOR_TYPE.VIDEO)];
+ assert.sameDeepMembers(
+ videoAnimationSegments.map(s => [s.min, s.max]),
+ [[300, 350]]);
+
+ const cssAnimationSegments =
+ [...map.get('Animation').get(INITIATOR_TYPE.CSS)];
+ assert.sameDeepMembers(
+ cssAnimationSegments.map(s => [s.min, s.max]),
+ [[150, 300], [300, 350]]);
+
+ const allAnimationSegments =
+ [...map.get('Animation').get('all_initiators')];
+ assert.sameDeepMembers(
+ allAnimationSegments.map(s => [s.min, s.max]),
+ [[150, 300], [300, 350]]);
+
+ // There should be no Response segments
+ assert.isFalse(map.has('Response'));
+ });
+
+ /**
+ * Given the root node of a top down multidimensional tree view, returns
+ * the node at |path|.
+ */
+ function getNodeValues_(root, path) {
+ let node = root;
+ for (let i = 0; i < path.length; i++) {
+ for (const component of path[i]) {
+ node = node.children[i].get(component);
+ }
+ }
+ return node.values;
+ }
+
+ const getCpuUsage_ = nodeValues => nodeValues[0].total;
+ const getCpuTotal_ = nodeValues => nodeValues[1].total;
+
+ /**
+ * Returns a simple model spec with one process (browser process) and one
+ * thread (CrBrowserMain).
+ *
+ * It does not contain any slices or expectations - those should be added
+ * manually.
+ *
+ * This is a function instead of just a variable because the test functions
+ * are meant to modify this modelSpec and insert suitable expectations.
+ */
+ function getSimpleModelSpec_() {
+ return {
+ processes: [
+ {
+ name: 'Browser',
+ pid: 12345,
+ threads: [
+ {
+ name: 'CrBrowserMain',
+ tid: 1,
+ slices: []
+ },
+ ],
+ },
+ ],
+ expectations: [],
+ };
+ }
+
+ test('constructMultiDimensionalView_' +
+ 'slicesDoNotStraddleExpecationBoundaries', () => {
+ const simpleModelSpec = getSimpleModelSpec_();
+ simpleModelSpec.processes[0].threads[0].slices.push(
+ {range: [150, 200], cpu: 30},
+ {range: [205, 255], cpu: 20}
+ );
+ simpleModelSpec.expectations.push({
+ stage: 'Animation',
+ initiatorType: INITIATOR_TYPE.CSS,
+ range: [100, 300],
+ });
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'CSS']];
+
+ const model = buildModelFromSpec(simpleModelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ // Total CPU Time is 30 + 20 from the two slices, and the total duration of
+ // CSS Animation expectation is 200 (because the range is [100, 300]). CPU
+ // Usage is therefore (30 + 20) / 200.
+ assert.closeTo(getCpuUsage_(values), (30 + 20) / 200, 1e-7);
+ assert.closeTo(getCpuTotal_(values), (30 + 20), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_' +
+ 'slicesStraddleExpectationBoundaries', () => {
+ const simpleModelSpec = getSimpleModelSpec_();
+ simpleModelSpec.processes[0].threads[0].slices.push(
+ {range: [150, 200], cpu: 30},
+ {range: [205, 255], cpu: 20}
+ );
+ simpleModelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [75, 175]}
+ );
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'Video']
+ ];
+
+ const model = buildModelFromSpec(simpleModelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ // Only half of the first slice is in the expectation range, so we get a
+ // total CPU contribution of 30 / 2 = 15. The second slice is not in the
+ // expectation at all, so CPU contribution from that slice is 0. So total
+ // CPU Usage during Video Animation expectation is 15.
+ // The total duration of the expectation is 100, so CPU usage is 15 / 100.
+ assert.closeTo(getCpuUsage_(values), 15 / 100, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 15, 1e-7);
+ });
+
+ test('constructMultiDimensionalView_' +
+ 'singleThread-disjointExpectationsOfSameInitiator', () => {
+ const simpleModelSpec = getSimpleModelSpec_();
+ simpleModelSpec.processes[0].threads[0].slices.push(
+ {range: [150, 200], cpu: 30},
+ {range: [205, 255], cpu: 20}
+ );
+ simpleModelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [100, 160]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [205, 225]}
+ );
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Response', 'Scroll']
+ ];
+
+ const model = buildModelFromSpec(simpleModelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ // 1/5 of the first slice and 2/5 of the second slice is in the expectation
+ // ranges, so total CPU is 0.2 * 30 + 0.4 * 20.
+ // Total duration of Scroll Response expectation is 60 + 20, since the two
+ // expectation ranges are disjoint.
+ assert.closeTo(getCpuUsage_(values),
+ (0.2 * 30 + 0.4 * 20) / (60 + 20), 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ 0.2 * 30 + 0.4 * 20, 1e-7);
+ });
+
+ test('constructMultiDimensionalView_' +
+ 'singleThread-overlappingExpectationsOfSameInitiators', () => {
+ const simpleModelSpec = getSimpleModelSpec_();
+ simpleModelSpec.processes[0].threads[0].slices.push(
+ {range: [150, 200], cpu: 30},
+ {range: [205, 255], cpu: 20}
+ );
+ simpleModelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [100, 190]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [160, 230]}
+ );
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'CSS']
+ ];
+
+ const model = buildModelFromSpec(simpleModelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ // The whole of the first slice is covered by CSS Animation expectation,
+ // even though the expectations are from two different ranges. The second
+ // slice is only half covered. Total CPU usage: 30 + 0.5 * 20.
+ // The total range covered by the expectation is [100, 230], so total
+ // duration is 130.
+ assert.closeTo(getCpuUsage_(values), (30 + 0.5 * 20) / 130, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 30 + 0.5 * 20, 1e-7);
+ });
+
+ test('constructMultiDimensionalView_' +
+ 'singleThread-overlappingExpectationsOfDifferentInitiators', () => {
+ const simpleModelSpec = getSimpleModelSpec_();
+ simpleModelSpec.processes[0].threads[0].slices.push(
+ {range: [150, 200], cpu: 30},
+ {range: [205, 255], cpu: 20}
+ );
+ simpleModelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [100, 190]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [160, 230]}
+ );
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(simpleModelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ (30 + 0.5 * 20) / 130, 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ 30 + 0.5 * 20, 1e-7);
+ });
+
+ test('constructMultiDimensionalView_' +
+ 'singleThread-allStages-customRangeOfInterest', () => {
+ const simpleModelSpec = getSimpleModelSpec_();
+ simpleModelSpec.processes[0].threads[0].slices.push(
+ {range: [150, 200], cpu: 30},
+ {range: [205, 255], cpu: 20}
+ );
+ simpleModelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [100, 190]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [160, 230]}
+ );
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['all_stages', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(simpleModelSpec);
+ const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(100, 210);
+ const root = constructMultiDimensionalView(model, rangeOfInterest);
+ const values = getNodeValues_(root, path);
+
+ // Only 1/10 of the second slice is included in the range of interest, so
+ // contribution from that slice is 0.1 * 20.
+ // The total range of expectation within range of interest is [100, 210], so
+ // total duration is 110.
+ assert.closeTo(getCpuUsage_(values), (30 + 0.1 * 20) / 110, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 30 + 0.1 * 20, 1e-7);
+ });
+
+ /**
+ * Returns a model spec where the browser process has two worker threads.
+ *
+ * This is a function instead of just a variable because the test functions
+ * are meant to modify this modelSpec and insert suitable expectations.
+ *
+ * Thread 1 looks like
+ *
+ * |0 |10 |20 |30 |40 |50 |60 |70 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 10ms and CPU time of 5ms.
+ *
+ * Thread 2 looks like
+ *
+ * |0 |50 |100 |150 |200 |250 |300 |350 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ * where each slice has a duration of 80ms and CPU time of 40ms.
+ */
+ function getMultipleThreadsOfSameTypeModelSpec_() {
+ return {
+ processes: [
+ {
+ name: 'Browser',
+ pid: 12345,
+ threads: [
+ {
+ name: 'CrBrowserMain',
+ tid: 1,
+ slices: [],
+ },
+ {
+ name: 'Worker/1',
+ tid: 42,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 20) {
+ slices.push({range: [i, i + 10], cpu: 5});
+ }
+ return slices;
+ })(),
+ },
+ {
+ name: 'Worker/2',
+ tid: 52,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 100) {
+ slices.push({range: [i, i + 80], cpu: 40});
+ }
+ return slices;
+ })(),
+ },
+ ],
+ },
+ ],
+
+ expectations: [],
+ };
+ }
+
+ test('constructMultiDimensionalView_multipleThreadsOfSameType_' +
+ 'singleExpectation', () => {
+ const modelSpec = getMultipleThreadsOfSameTypeModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Worker'], ['Animation', 'Video']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ // There are five thread-1 slices within [0, 90] and one thread-2 slice.
+ assert.closeTo(getCpuUsage_(values), (5 * 5 + 40) / 90, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 5 * 5 + 40, 1e-7);
+ });
+
+ test('constructMultiDimensionalView_multipleThreadsOfSameType_' +
+ 'disjointExpectationSameInitiator', () => {
+ const modelSpec = getMultipleThreadsOfSameTypeModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Worker'], ['Animation', 'CSS']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ // Both worker threads are of the same type ('Worker'), so their CPU times
+ // will be added together.
+ // There are 3 thread-1 slices and 6/8 of a thread-2 slice within
+ // [500, 560].
+ // There are 1 thread-1 slices and two thread-2 slices within [690, 890].
+ // Total expectation duration is 60 + 200.
+ assert.closeTo(getCpuUsage_(values),
+ ((5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2)) / (60 + 200), 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ (5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_multipleThreadsOfSameType_' +
+ 'overlappingExpectationsOfSameInitiator', () => {
+ const modelSpec = getMultipleThreadsOfSameTypeModelSpec_();
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Worker'], ['Response', 'Scroll']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ (5 * 4 + 40 * (70 / 80)) / 90, 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ 5 * 4 + 40 * (70 / 80), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_multipleThreadsOfSameType_' +
+ 'disjointExpectationsAllInitiators', () => {
+ const modelSpec = getMultipleThreadsOfSameTypeModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Worker'], ['Animation', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ ((5 * 5 + 40) + ((5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2))) /
+ (90 + 60 + 200), 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ (5 * 5 + 40) + ((5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2)),
+ 1e-7);
+ });
+
+
+ test('constructMultiDimensionalView_multipleThreadsOfSameType_' +
+ 'overlappingExpectationsAllInitiators', () => {
+ const modelSpec = getMultipleThreadsOfSameTypeModelSpec_();
+ // [Click R]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Worker'], ['Response', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ (5 * 5 + 40) / 100, 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ 5 * 5 + 40, 1e-7);
+ });
+
+
+ test('constructMultiDimensionalView_multipleThreadsOfSameType_' +
+ 'allStagesAllInitiators', () => {
+ const modelSpec = getMultipleThreadsOfSameTypeModelSpec_();
+ // [Video A] [Click R] [CSS A] [ CSS A ]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Worker'], ['all_stages', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values), (250 + 400) / 990, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 250 + 400, 1e-7);
+ });
+
+ /**
+ * Returns a model spec where there are two renderer processes, each with a
+ * renderer main thread.
+ *
+ * This is a function instead of just a variable because the test functions
+ * are meant to modify this modelSpec and insert suitable expectations.
+ *
+ * The main thread on renderer-1 looks like
+ *
+ * |0 |10 |20 |30 |40 |50 |60 |70 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 10ms and CPU time of 5ms.
+ *
+ * The main thread on renderer-2 looks like
+ *
+ * |0 |50 |100 |150 |200 |250 |300 |350 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 80ms and CPU time of 40ms.
+ */
+ function getMultipleProcessesOfSameTypeModelSpec_() {
+ return {
+ processes: [
+ {
+ name: 'Browser',
+ pid: 12345,
+ threads: [
+ {
+ name: 'CrBrowserMain',
+ tid: 1,
+ slices: [],
+ },
+ ],
+ },
+ {
+ name: 'Renderer',
+ pid: 20001,
+ threads: [
+ {
+ name: 'CrRendererMain',
+ tid: 42,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 20) {
+ slices.push({range: [i, i + 10], cpu: 5});
+ }
+ return slices;
+ })(),
+ },
+ ],
+ },
+ {
+ name: 'Renderer',
+ pid: 30001,
+ threads: [
+ {
+ name: 'CrRendererMain',
+ tid: 52,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 100) {
+ slices.push({range: [i, i + 80], cpu: 40});
+ }
+ return slices;
+ })(),
+ },
+ ]
+ },
+ ],
+
+ expectations: [],
+ };
+ }
+
+ test('constructMultiDimensionalView_multipleProcessesOfSameType_' +
+ 'singleExpectation', () => {
+ const modelSpec = getMultipleProcessesOfSameTypeModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['CrRendererMain'], ['Animation', 'Video']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values), (5 * 5 + 40) / 90, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 5 * 5 + 40, 1e-7);
+ });
+
+ test('constructMultiDimensionalView_multipleProcessesOfSameType_' +
+ 'disjointExpectationSameInitiator', () => {
+ const modelSpec = getMultipleProcessesOfSameTypeModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['CrRendererMain'], ['Animation', 'CSS']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ ((5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2)) / (60 + 200), 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ (5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_multipleProcessesOfSameType_' +
+ 'overlappingExpectationsOfSameInitiator', () => {
+ const modelSpec = getMultipleProcessesOfSameTypeModelSpec_();
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['CrRendererMain'], ['Response', 'Scroll']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ (5 * 4 + 40 * (70 / 80)) / 90, 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ 5 * 4 + 40 * (70 / 80), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_multipleProcessesOfSameType_' +
+ 'disjointExpectationsAllInitiators', () => {
+ const modelSpec = getMultipleProcessesOfSameTypeModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['CrRendererMain'], ['Animation', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ ((5 * 5 + 40) + ((5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2))) /
+ (90 + 60 + 200), 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ (5 * 5 + 40) + ((5 * 3 + 40 * (60 / 80)) + (5 * 10 + 40 * 2)),
+ 1e-7);
+ });
+
+
+ test('constructMultiDimensionalView_multipleProcessesOfSameType_' +
+ 'overlappingExpectationsAllInitiators', () => {
+ const modelSpec = getMultipleProcessesOfSameTypeModelSpec_();
+ // [Click R]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['CrRendererMain'], ['Response', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values), (5 * 5 + 40) / 100, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 5 * 5 + 40, 1e-7);
+ });
+
+
+ test('constructMultiDimensionalView_multipleProcessesOfSameType_' +
+ 'allStagesAllInitiators', () => {
+ const modelSpec = getMultipleProcessesOfSameTypeModelSpec_();
+ // [Video A] [Click R] [CSS A] [ CSS A ]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const path = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['CrRendererMain'], ['all_stages', 'all_initiators']
+ ];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values), (250 + 400) / 990, 1e-7);
+ assert.closeTo(getCpuTotal_(values), 250 + 400, 1e-7);
+ });
+
+ /**
+ * Returns a model spec where the browser process has a main thread and an IO
+ * thread.
+ *
+ * This is a function instead of just a variable because the test functions
+ * are meant to modify this modelSpec and insert suitable expectations.
+ *
+ * The browser main thread looks like
+ *
+ * |0 |10 |20 |30 |40 |50 |60 |70 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 10ms and CPU time of 5ms.
+ *
+ * The IO Thread looks like
+ *
+ * |0 |50 |100 |150 |200 |250 |300 |350 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 80ms and CPU time of 40ms.
+ */
+ function getAllThreadsOfSameProcessModelSpec_() {
+ return {
+ processes: [
+ {
+ name: 'Browser',
+ pid: 12345,
+ threads: [
+ {
+ name: 'CrBrowserMain',
+ tid: 1,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 20) {
+ slices.push({range: [i, i + 10], cpu: 5});
+ }
+ return slices;
+ })(),
+ },
+ {
+ name: 'Chrome_IOThread',
+ tid: 5,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 100) {
+ slices.push({range: [i, i + 80], cpu: 40});
+ }
+ return slices;
+ })(),
+ }
+ ],
+ },
+ ],
+
+ expectations: [],
+ };
+ }
+
+
+ test('constructMultiDimensionalView_AllThreadsOfSameProcess_' +
+ 'singleExpectation', () => {
+ const modelSpec = getAllThreadsOfSameProcessModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]}
+ );
+
+ const pathForAllThreads = [
+ [CHROME_PROCESS_NAMES.BROWSER], [], ['Animation', 'Video']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'Video']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Chrome_IOThread'], ['Animation', 'Video']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllThreadsOfSameProcess_' +
+ 'disjointExpectationSameInitiator', () => {
+ const modelSpec = getAllThreadsOfSameProcessModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const pathForAllThreads = [
+ [CHROME_PROCESS_NAMES.BROWSER], [], ['Animation', 'CSS']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'CSS']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Chrome_IOThread'], ['Animation', 'CSS']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllThreadsOfSameProcess_' +
+ 'overlappingExpectationsOfSameInitiator', () => {
+ const modelSpec = getAllThreadsOfSameProcessModelSpec_();
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const pathForAllThreads = [
+ [CHROME_PROCESS_NAMES.BROWSER], [], ['Response', 'Scroll']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Response', 'Scroll']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Chrome_IOThread'], ['Response', 'Scroll']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllThreadsOfSameProcess_' +
+ 'disjointExpectationsAllInitiators', () => {
+ const modelSpec = getAllThreadsOfSameProcessModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const pathForAllThreads = [
+ [CHROME_PROCESS_NAMES.BROWSER], [], ['Animation', 'all_initiators']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Animation', 'all_initiators']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Chrome_IOThread'], ['Animation', 'all_initiators']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllThreadsOfSameProcess_' +
+ 'overlappingExpectationsAllInitiators', () => {
+ const modelSpec = getAllThreadsOfSameProcessModelSpec_();
+ // [Click R]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const pathForAllThreads = [
+ [CHROME_PROCESS_NAMES.BROWSER], [], ['Response', 'all_initiators']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['Response', 'all_initiators']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Chrome_IOThread'], ['Response', 'all_initiators']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllThreadsOfSameProcess_' +
+ 'allStagesAllInitiators', () => {
+ const modelSpec = getAllThreadsOfSameProcessModelSpec_();
+ // [Video A] [Click R] [CSS A] [ CSS A ]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const pathForAllThreads = [
+ [CHROME_PROCESS_NAMES.BROWSER], [], ['all_stages', 'all_initiators']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['CrBrowserMain'], ['all_stages', 'all_initiators']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.BROWSER],
+ ['Chrome_IOThread'], ['all_stages', 'all_initiators']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ /**
+ * Returns a model spec where a renderer process and a GPU process both have a
+ * Chrome_ChildIOThread.
+ *
+ * This is a function instead of just a variable because the test functions
+ * are meant to modify this modelSpec and insert suitable expectations.
+ *
+ * The modelSpec includes a basic browser process because it not a valid
+ * chrome model otherwise.
+ *
+ * The renderer ChildIOThread looks like
+ *
+ * |0 |10 |20 |30 |40 |50 |60 |70 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 10ms and CPU time of 5ms.
+ *
+ * The GPU ChildIOThread looks like
+ *
+ * |0 |50 |100 |150 |200 |250 |300 |350 .... Time
+ * [ ] [ ] [ ] [ ] .... Slices
+ *
+ * where each slice has a duration of 80ms and CPU time of 40ms.
+ */
+ function getAllProcessesOfSameThreadModelSpec_() {
+ return {
+ processes: [
+ {
+ name: 'Browser',
+ pid: 12345,
+ threads: [
+ {
+ name: 'CrBrowserMain',
+ tid: 1,
+ slices: [],
+ },
+ ],
+ },
+ {
+ name: 'Renderer',
+ pid: 20001,
+ threads: [
+ {
+ name: 'Chrome_ChildIOThread',
+ tid: 42,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 20) {
+ slices.push({range: [i, i + 10], cpu: 5});
+ }
+ return slices;
+ })(),
+ },
+ ],
+ },
+ {
+ name: 'GPU Process',
+ pid: 30001,
+ threads: [
+ {
+ name: 'Chrome_ChildIOThread',
+ tid: 52,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 100) {
+ slices.push({range: [i, i + 80], cpu: 40});
+ }
+ return slices;
+ })(),
+ },
+ ]
+ },
+ ],
+ expectations: [],
+ };
+ }
+
+ test('constructMultiDimensionalView_AllProcessesOfSameThread_' +
+ 'singleExpectation', () => {
+ const modelSpec = getAllProcessesOfSameThreadModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]}
+ );
+
+ const pathForAllThreads = [
+ [], ['Chrome_ChildIOThread'], ['Animation', 'Video']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['Chrome_ChildIOThread'], ['Animation', 'Video']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.GPU],
+ ['Chrome_ChildIOThread'], ['Animation', 'Video']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllProcessesOfSameThread_' +
+ 'disjointExpectationSameInitiator', () => {
+ const modelSpec = getAllProcessesOfSameThreadModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const pathForAllThreads = [
+ [], ['Chrome_ChildIOThread'], ['Animation', 'CSS']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['Chrome_ChildIOThread'], ['Animation', 'CSS']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.GPU],
+ ['Chrome_ChildIOThread'], ['Animation', 'CSS']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllProcessesOfSameThread_' +
+ 'overlappingExpectationsOfSameInitiator', () => {
+ const modelSpec = getAllProcessesOfSameThreadModelSpec_();
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const pathForAllThreads = [
+ [], ['Chrome_ChildIOThread'], ['Response', 'Scroll']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['Chrome_ChildIOThread'], ['Response', 'Scroll']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.GPU],
+ ['Chrome_ChildIOThread'], ['Response', 'Scroll']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllProcessesOfSameThread_' +
+ 'disjointExpectationsAllInitiators', () => {
+ const modelSpec = getAllProcessesOfSameThreadModelSpec_();
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const pathForAllThreads = [
+ [], ['Chrome_ChildIOThread'], ['Animation', 'all_initiators']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['Chrome_ChildIOThread'], ['Animation', 'all_initiators']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.GPU],
+ ['Chrome_ChildIOThread'], ['Animation', 'all_initiators']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllProcessesOfSameThread_' +
+ 'overlappingExpectationsAllInitiators', () => {
+ const modelSpec = getAllProcessesOfSameThreadModelSpec_();
+ // [Click R]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]}
+ );
+
+ const pathForAllThreads = [
+ [], ['Chrome_ChildIOThread'], ['Response', 'all_initiators']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['Chrome_ChildIOThread'], ['Response', 'all_initiators']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.GPU],
+ ['Chrome_ChildIOThread'], ['Response', 'all_initiators']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_AllProcessesOfSameThread_' +
+ 'allStagesAllInitiators', () => {
+ const modelSpec = getAllProcessesOfSameThreadModelSpec_();
+ // [Video A] [Click R] [CSS A] [ CSS A ]
+ // [Scroll R]
+ // [Scroll R]
+ modelSpec.expectations.push(
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ );
+
+ const pathForAllThreads = [
+ [], ['Chrome_ChildIOThread'], ['all_stages', 'all_initiators']];
+
+ const pathForThread1 = [
+ [CHROME_PROCESS_NAMES.RENDERER],
+ ['Chrome_ChildIOThread'], ['all_stages', 'all_initiators']];
+
+ const pathForThread2 = [
+ [CHROME_PROCESS_NAMES.GPU],
+ ['Chrome_ChildIOThread'], ['all_stages', 'all_initiators']];
+
+ const model = buildModelFromSpec(modelSpec);
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const valueForAllThreads = getNodeValues_(root, pathForAllThreads);
+ const valueForThread1 = getNodeValues_(root, pathForThread1);
+ const valueForThread2 = getNodeValues_(root, pathForThread2);
+
+ assert.closeTo(getCpuUsage_(valueForAllThreads),
+ getCpuUsage_(valueForThread1) + getCpuUsage_(valueForThread2), 1e-7);
+ assert.closeTo(getCpuTotal_(valueForAllThreads),
+ getCpuTotal_(valueForThread1) + getCpuTotal_(valueForThread2), 1e-7);
+ });
+
+ test('constructMultiDimensionalView_completeAggregation', () => {
+ const modelSpec = {
+ processes: [
+ {
+ name: 'Browser',
+ pid: 12345,
+ threads: [
+ {
+ name: 'CrBrowserMain',
+ tid: 1,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 50) {
+ slices.push({range: [i, i + 50], cpu: 30});
+ }
+ return slices;
+ })(),
+ },
+ ],
+ },
+ {
+ name: 'Renderer',
+ pid: 20001,
+ threads: [
+ {
+ name: 'Chrome_ChildIOThread',
+ tid: 42,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 20) {
+ slices.push({range: [i, i + 10], cpu: 5});
+ }
+ return slices;
+ })(),
+ },
+ ],
+ },
+ {
+ name: 'GPU Process',
+ pid: 30001,
+ threads: [
+ {
+ name: 'Chrome_ChildIOThread',
+ tid: 52,
+ slices: (() => {
+ const slices = [];
+ for (let i = 0; i < 1000; i += 100) {
+ slices.push({range: [i, i + 80], cpu: 40});
+ }
+ return slices;
+ })(),
+ },
+ ]
+ },
+ ],
+
+ // [Video A] [Click R] [CSS A] [ CSS A ]
+ // [Scroll R]
+ // [Scroll R]
+ expectations: [
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.VIDEO,
+ range: [0, 90]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.CLICK,
+ range: [200, 220]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [210, 260]},
+ {stage: 'Response', initiatorType: INITIATOR_TYPE.SCROLL,
+ range: [250, 300]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [500, 560]},
+ {stage: 'Animation', initiatorType: INITIATOR_TYPE.CSS,
+ range: [690, 890]}
+ ],
+ };
+
+ const model = buildModelFromSpec(modelSpec);
+ const path = [[], [], ['all_stages', 'all_initiators']];
+ const root = constructMultiDimensionalView(model, model.bounds);
+ const values = getNodeValues_(root, path);
+
+ assert.closeTo(getCpuUsage_(values),
+ (30 * 20 + 5 * 50 + 40 * 10) / 1000, 1e-7);
+ assert.closeTo(getCpuTotal_(values),
+ (30 * 20 + 5 * 50 + 40 * 10), 1e-7);
+ });
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test_utils.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test_utils.html
new file mode 100644
index 00000000000..3066677e6e7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/cpu_time_test_utils.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.chrome.cpuTimeTestUtils', function() {
+ /**
+ * Takes a model spec, and returns a fully constructed model.
+ *
+ * A model spec is a minimal representation of a model containing only the
+ * information needed to set up scenarios for testing cpu time metrics - it
+ * specifies
+ * - All the processes with name, pid, and threads
+ * - All the threads with name, tid, and top level slices
+ * - All the top level slices with range and cpu duration
+ * - All the User Expectations (except Idle Expectations) with range, stage,
+ * and initiator.
+ *
+ * Example model spec:
+ * {
+ * processes: [
+ * {
+ * name: 'Browser',
+ * pid: 12345,
+ * threads: [
+ * {
+ * name: 'CrBrowserMain',
+ * tid: 1,
+ * slices: [
+ * {range: [150, 200], cpu: 30},
+ * {range: [205, 255], cpu: 20}
+ * ]
+ * },
+ * ],
+ * },
+ * ],
+ * expectations: [
+ * {stage: 'Animation', initiatorType: 'Video', range: [0, 90]},
+ * {stage: 'Response', initiatorType: 'Click', range: [200, 220]},
+ * ]
+ * }
+ */
+ function buildModelFromSpec(modelSpec) {
+ return tr.c.TestUtils.newModel(model => {
+ // Create processes, threads, and slices
+ for (const processSpec of modelSpec.processes) {
+ const process = model.getOrCreateProcess(processSpec.pid);
+ process.name = processSpec.name;
+
+ for (const threadSpec of processSpec.threads) {
+ const thread = process.getOrCreateThread(threadSpec.tid);
+ thread.name = threadSpec.name;
+
+ for (const sliceSpec of threadSpec.slices) {
+ // Sanity checks on sliceSpec
+ const sliceStart = sliceSpec.range[0];
+ const sliceEnd = sliceSpec.range[1];
+ const duration = sliceEnd - sliceStart;
+ const cpuDuration = sliceSpec.cpu;
+ assert(sliceEnd >= sliceStart,
+ 'Slice end time is earlier than slice start time: ' +
+ `sliceStart: ${sliceStart}, sliceEnd: ${sliceEnd}`);
+ assert(duration >= cpuDuration,
+ `Cpu duration (${cpuDuration}) is larger than ` +
+ `slice duration (${duration})`);
+
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: sliceSpec.range[0],
+ duration: sliceSpec.range[1] - sliceSpec.range[0],
+ // We currently do not have all the data about exactly when a
+ // thread was scheduled or descheduled - for example, if a slice
+ // got descheduled twice over its duration, the trace will not
+ // contain information to indicate that. Without loss of
+ // generality, we therefore make the assumption that the cpuStart
+ // is at the beginning of the slice, and total cpu time of a slice
+ // is uniformly spread over its duration.
+ cpuStart: sliceSpec.range[0],
+ cpuDuration: sliceSpec.cpu
+ }));
+ }
+ }
+ }
+
+ const expectations = model.userModel.expectations;
+ for (const expSpec of modelSpec.expectations) {
+ // This switch statement is not exhaustive. You may have to add more
+ // cases here when you add new scenarios.
+ switch (expSpec.stage) {
+ case 'Animation':
+ expectations.push(new tr.model.um.AnimationExpectation(
+ model, expSpec.initiatorType,
+ expSpec.range[0], expSpec.range[1] - expSpec.range[0]
+ ));
+ break;
+ case 'Idle':
+ expectations.push(new tr.model.um.IdleExpectation(
+ model, expSpec.range[0], expSpec.range[1] - expSpec.range[0]
+ ));
+ break;
+ case 'Response':
+ expectations.push(new tr.model.um.ResponseExpectation(
+ model, expSpec.initiatorType,
+ expSpec.range[0], expSpec.range[1] - expSpec.range[0]
+ ));
+ break;
+ default:
+ throw new Error('Internal Error: Stage ' + expSpec.stage +
+ 'not handled yet. You should add another case here.');
+ }
+ }
+ });
+ }
+
+ return {
+ buildModelFromSpec,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency.html
new file mode 100644
index 00000000000..81d9803fa71
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency.html
@@ -0,0 +1,367 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.e.chrome', function() {
+ // TODO(dproy): Because title and category are properties of TimedEvent
+ // subclasses and not TimedEvent itself, we have to write our own "has title
+ // and category" function rather than having it provided by TimedEvent.
+ // This should be fixed.
+ // https://github.com/catapult-project/catapult/issues/2784
+ 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;
+ }
+
+ /**
+ * Returns a map of renderer PIDs to array of timestamps at which the
+ * renderer became interactive.
+ */
+ 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);
+ }
+ 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;
+ }
+ if (expectation.timeToInteractive === undefined) continue;
+ if (interactiveTimestampsMap.get(expectation.renderProcess.pid) ===
+ undefined) {
+ interactiveTimestampsMap.set(expectation.renderProcess.pid, []);
+ }
+ interactiveTimestampsMap.get(expectation.renderProcess.pid).push(
+ expectation.timeToInteractive);
+ }
+ return interactiveTimestampsMap;
+ }
+
+ /**
+ * Returns an Array of task windows that start with the supplied interactive
+ * timestamps.
+ *
+ * A task window is defined as the range of time from the time when the page
+ * became interactive until either
+ *
+ * 1. The beginning of the next navigationStart event or
+ * 2. The end of the trace
+ *
+ * This function only works when timestamps are from the same renderer. If
+ * windows for multiple renderers need to be computed, the timestamps should
+ * be separated for each renderer and this function should be called
+ * separately for each.
+ *
+ * @param {!Array.<number>} interactiveTimestamps
+ * @param {!Array.<number>} navStartTimestamps
+ * @param {!number} traceEndTimestamp
+ * @returns {!Array.<tr.b.math.Range>}
+ */
+ function getPostInteractiveTaskWindows(
+ interactiveTimestamps, navStartTimestamps, traceEndTimestamp) {
+ let navStartTsIndex = 0;
+ let lastTaskWindowEndTs = undefined;
+ const taskWindows = [];
+ for (const currTTI of interactiveTimestamps) {
+ // Find the first navigation start event after the interactive
+ // timestamp.
+ while (navStartTsIndex < navStartTimestamps.length &&
+ navStartTimestamps[navStartTsIndex] < currTTI) {
+ navStartTsIndex++;
+ }
+
+ const taskWindowEndTs = navStartTsIndex < navStartTimestamps.length ?
+ navStartTimestamps[navStartTsIndex] : traceEndTimestamp;
+
+ if (taskWindowEndTs === lastTaskWindowEndTs) {
+ // This is the case where we have two different interactive timestamps
+ // with no navigationStart event between them. This is only possible
+ // when two different pages are sharing the same renderer process (and
+ // therefore the same renderer scheduler). We cannot define a proper
+ // task window in this case to calculate Estimated Input Latency.
+ throw Error('Encountered two consecutive interactive timestamps ' +
+ 'with no navigationStart between them. ' +
+ 'PostInteractiveTaskWindow is not well defined in this case.');
+ }
+
+ taskWindows.push(tr.b.math.Range.fromExplicitRange(
+ currTTI, taskWindowEndTs));
+ lastTaskWindowEndTs = taskWindowEndTs;
+ }
+ return taskWindows;
+ }
+
+ /**
+ * Returns the contribution of the given task to expected queueing time
+ * in the given time window.
+ *
+ * The result is probabilityOf(task) * expectedQueueTimeDueTo(task),
+ * where
+ * - probabilityOf(task) = probability of input arriving while the given
+ * task is running.
+ * - expectedQueueingTimeDueTo(task) = expected time until the end of the
+ * given task for input arriving while the task is running.
+ *
+ * We assume that input arrival time is uniformly distributed in the given
+ * time window.
+ *
+ * @param {!tr.b.math.Range} A time window.
+ * @param {!Array.<!{start: number, end: number, weight: number}>} A list of
+ * weighted tasks. The weight of a task must be between 0.0 and 1.0.
+ * @returns {number}
+ */
+ function contributionToEQT(window, task) {
+ const startInWindow = Math.max(window.min, task.start);
+ const endInWindow = Math.min(window.max, task.end);
+ const durationInWindow = endInWindow - startInWindow;
+ if (durationInWindow <= 0) return 0;
+ const probabilityOfTask = durationInWindow / (window.max - window.min);
+ const minQueueingTime = task.end - endInWindow;
+ const maxQueueingTime = task.end - startInWindow;
+ const expectedQueueingTimeDueToTask =
+ (maxQueueingTime + minQueueingTime) / 2;
+ return probabilityOfTask * expectedQueueingTimeDueToTask;
+ }
+
+ /**
+ * Returns weighted expected queueing time (EQT) for the given time window and
+ * the given set of weighted tasks. The tasks must not overlap.
+ *
+ * The weighted EQT is computed as
+ * sum(contributionToEQT(window, task) * task.weight)
+ * for all tasks in weightedTasks, where
+ * - contributionToEQT is the function defined above.
+ * - task.weight is an arbitrary number between 0.0 and 1.0. This is useful
+ * for computing contribution of chrome subcomponents (e.g. GC) to
+ * the expected queueing time for EQT diagnostics.
+ *
+ * We assume that input arrival time is uniformly distributed in the given
+ * time window.
+ *
+ * @param {!tr.b.math.Range} A time window.
+ * @param {!Array.<!{start: number, end: number, weight: number}>} A list of
+ * weighted tasks. The weight of a task must be between 0.0 and 1.0.
+ * @returns {number}
+ */
+ function weightedExpectedQueueingTime(window, weightedTasks) {
+ let result = 0;
+ for (const task of weightedTasks) {
+ result += contributionToEQT(window, task) * task.weight;
+ }
+ return result;
+ }
+
+ /**
+ * Returns expected queueing time for the given time window and
+ * the given set of tasks. The tasks must not overlap.
+ *
+ * @param {!tr.b.math.Range} A time window.
+ * @param {!Array.<!{start: number, end: number}>} A list of tasks.
+ * @returns {number}
+ */
+ function expectedQueueingTime(window, tasks) {
+ return weightedExpectedQueueingTime(window, tasks.map(function(task) {
+ return { start: task.start, end: task.end, weight: 1 };
+ }));
+ }
+
+ /**
+ * Object of this calss represents the sliding window and maintains its
+ * main invariant: windowEQT = firstTaskEQT + innerEQT + lastTaskEQT.
+ * It is intended to be used only in maxExpectedQueueingTimeInSlidingWindow().
+ */
+ class SlidingWindow {
+ /**
+ * @param {number} The starting time of the sliding window.
+ * @param {number} The window size.
+ * @param {!Array.<!{start: number, end: number}>} A list of tasks sorted by
+ * task start time.
+ */
+ constructor(startTime, windowSize, sortedTasks) {
+ /**
+ * @private @const {number} The window size.
+ */
+ this.windowSize_ = windowSize;
+ /**
+ * @private @const {!Array.<!{start: number, end: number}>} The tasks.
+ */
+ this.sortedTasks_ = sortedTasks;
+ /**
+ * @private {!tr.b.math.Range} The endpoints of the sliding window.
+ */
+ this.range_ = tr.b.math.Range.fromExplicitRange(
+ startTime, startTime + windowSize);
+ /**
+ * @private {number} The index of the first task in the sortedTasks that
+ * ends after this window starts:
+ * this.range_.min < this.sortedTasks_[this.firstTaskIndex_].end.
+ */
+ this.firstTaskIndex_ =
+ sortedTasks.findIndex(task => startTime < task.end);
+ if (this.firstTaskIndex_ === -1) {
+ this.firstTaskIndex_ = sortedTasks.length;
+ }
+ /**
+ * @private {number} The index of the last task in the sortedTasks that
+ * starts before this window ends:
+ * this.range.max > this.sortedTasks_[lastTaskIndex_].start.
+ */
+ this.lastTaskIndex_ = -1;
+ while (this.lastTaskIndex_ + 1 < sortedTasks.length &&
+ sortedTasks[this.lastTaskIndex_ + 1].start < startTime + windowSize) {
+ this.lastTaskIndex_++;
+ }
+ /**
+ * @private {number} The sum of EQT contributions for all tasks between
+ * the first task and the last task (excluding the first and the last
+ * tasks). All such tasks are completely inside the window.
+ */
+ this.innerEQT_ = 0;
+ for (let i = this.firstTaskIndex_ + 1; i < this.lastTaskIndex_; i++) {
+ this.innerEQT_ += contributionToEQT(this.range_, sortedTasks[i]);
+ }
+ }
+
+ /**
+ * @returns the EQT for this window.
+ */
+ get getEQT() {
+ let firstTaskEQT = 0;
+ if (this.firstTaskIndex_ < this.sortedTasks_.length) {
+ firstTaskEQT = contributionToEQT(this.range_,
+ this.sortedTasks_[this.firstTaskIndex_]);
+ }
+ let lastTaskEQT = 0;
+ if (this.firstTaskIndex_ < this.lastTaskIndex_) {
+ lastTaskEQT = contributionToEQT(this.range_,
+ this.sortedTasks_[this.lastTaskIndex_]);
+ }
+ return firstTaskEQT + this.innerEQT_ + lastTaskEQT;
+ }
+
+ /**
+ * Moves the window to the given time t.
+ * @param {number} The time.
+ */
+ slide(t) {
+ this.range_ = tr.b.math.Range.fromExplicitRange(t, t + this.windowSize_);
+ if (this.firstTaskIndex_ < this.sortedTasks_.length &&
+ this.sortedTasks_[this.firstTaskIndex_].end <= t) {
+ // The first task moved out of the window.
+ this.firstTaskIndex_++;
+ if (this.firstTaskIndex_ < this.lastTaskIndex_) {
+ // The new first window was accounted in innerEQT. Undo that.
+ this.innerEQT_ -= contributionToEQT(this.range_,
+ this.sortedTasks_[this.firstTaskIndex_]);
+ }
+ }
+ if (this.lastTaskIndex_ + 1 < this.sortedTasks_.length &&
+ this.sortedTasks_[this.lastTaskIndex_ + 1].start <
+ t + this.windowSize_) {
+ // A new task moved in the window.
+ if (this.firstTaskIndex_ < this.lastTaskIndex_) {
+ // The old last task is completely inside the window.
+ // Account it in innerEQT.
+ this.innerEQT_ += contributionToEQT(this.range_,
+ this.sortedTasks_[this.lastTaskIndex_]);
+ }
+ this.lastTaskIndex_++;
+ }
+ }
+ }
+
+ /**
+ * Returns maximum expected queueing time for time window of the given size
+ * that slides from the startTime to the endTime:
+ * max { expectedQueueingTime(window(t, t + windowSize), tasks),
+ * for all startTime <= t && t + w <= endTime }.
+ * See https://goo.gl/jmWpMl for the description of the algorithm.
+ *
+ * @param {number} start time for the sliding window.
+ * @param {number} end time for the sliding window.
+ * @param {number} the size of the sliding window.
+ * @param {!Array.<!{start: number, end: number}>} A list of tasks.
+ * The tasks must not overlap.
+ * @returns {number}
+ */
+ 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++) {
+ // Ensure that the previous task finishes not later than the current task.
+ // This can happen with short trace events late in the timeline, when
+ // floating point errors increasingly come into play.
+ 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;
+ }
+ }
+
+ // Collect all time points that the sliding window needs to stop at.
+ // See https://goo.gl/jmWpMl for justification.
+ 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);
+
+ // Slide the window and compute maxEQT.
+ 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
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency_test.html
new file mode 100644
index 00000000000..3031ec2c4a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/estimated_input_latency_test.html
@@ -0,0 +1,384 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/extras/chrome/estimated_input_latency.html">
+<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const getInteractiveTimestamps = tr.e.chrome.getInteractiveTimestamps;
+ const getPostInteractiveTaskWindows =
+ tr.e.chrome.getPostInteractiveTaskWindows;
+ const getNavStartTimestamps = tr.e.chrome.getNavStartTimestamps;
+ const expectedQueueingTime = tr.e.chrome.expectedQueueingTime;
+ const maxExpectedQueueingTimeInSlidingWindow =
+ tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow;
+ const weightedExpectedQueueingTime = tr.e.chrome.weightedExpectedQueueingTime;
+ const assertRangeEquals = tr.b.assertRangeEquals;
+
+ function newSchedulerTask(startTime, duration) {
+ return tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: startTime,
+ duration
+ });
+ }
+
+ /**
+ * Adds a FrameLoader snapshot to rendererProcess that is used by test FMP
+ * candidate slices.
+ */
+ function addTestFrame(rendererProcess) {
+ rendererProcess.objects.addSnapshot(
+ 'ptr', 'loading', 'FrameLoader', 300, {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ }
+
+ function addNavigationStart(mainThread, startNavTime) {
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: startNavTime,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ }
+
+ function addNetworkRequest(rendererMain, start, duration) {
+ const networkEvents = [];
+ rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start,
+ duration,
+ }));
+ }
+
+ function addFMPCandidate(mainThread, fmpTime) {
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: fmpTime,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ }
+
+ function addDomContentLoadedEnd(mainThread, dclTime) {
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: dclTime,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ }
+
+ function addSchedulerTask(mainThread, startTime, duration) {
+ mainThread.sliceGroup.pushSlice(newSchedulerTask(startTime, duration));
+ }
+
+ function addDummyTask(mainThread, startTime) {
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'dummy',
+ title: 'dummyTitle',
+ start: startTime,
+ duration: 0.0
+ }));
+ }
+
+ test('getNavStartTimestamps', () => {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(model => {
+ const mainThread = model.rendererMain;
+ addNavigationStart(mainThread, 0);
+ addNavigationStart(mainThread, 10);
+ addNavigationStart(mainThread, 30);
+ });
+
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelper = chromeHelper.rendererHelpers[
+ model.rendererProcess.pid];
+ const navStartTimestamps = getNavStartTimestamps(rendererHelper);
+
+ // It is ok to assert equality for floating point numbers here because
+ // the timestamps should remain unmodified.
+ assert.deepEqual(navStartTimestamps, [0, 10, 30]);
+ });
+
+ /**
+ * Checks getInteractiveTimestamps works as intended. If the definition of
+ * TTI metric changes, this test may begin to fail and we may need to adjust
+ * our EIL implementation.
+ */
+ test('getInteractiveTimestamps', () => {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(model => {
+ addTestFrame(model.rendererProcess);
+
+ const mainThread = model.rendererMain;
+ addNavigationStart(mainThread, 0);
+ addNetworkRequest(mainThread, 0, 50);
+ addFMPCandidate(mainThread, 5000);
+ addDomContentLoadedEnd(mainThread, 5000);
+
+ addNavigationStart(mainThread, 100000);
+ addNetworkRequest(mainThread, 100000, 50);
+ addFMPCandidate(mainThread, 110000);
+ addDomContentLoadedEnd(mainThread, 110000);
+
+ // To detect when a page has become interactive, we need to find a large
+ // enough window of no long tasks. Adding a dummy task sufficiently far
+ // away extends the bounds of the model so that it can contain this
+ // window. In a non-test scenario, we always record traces for long enough
+ // that this is not an issue.
+ addDummyTask(mainThread, 900000);
+ });
+
+ const interactiveTimestampsMap = getInteractiveTimestamps(model);
+ const interactiveTimestamps =
+ interactiveTimestampsMap.get(model.rendererProcess.pid);
+ assert.deepEqual(
+ interactiveTimestamps.sort((a, b) => a - b), [5000, 110000]);
+ });
+
+ test('getInteractiveTimestampsMultiRenderer', () => {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcesses = [];
+ // ChromeModel creates one renderer. We create rest of them here.
+ for (let i = 1; i <= 5; i++) {
+ const rendererProcess = model.getOrCreateProcess(i + 100);
+ const mainThread = rendererProcess.getOrCreateThread(i + 100 + 10);
+ mainThread.name = 'CrRendererMain';
+
+ addTestFrame(rendererProcess);
+ addNavigationStart(mainThread, i * 1000);
+ addNetworkRequest(mainThread, i * 1000, 50);
+ addFMPCandidate(mainThread, i * 1000 + 50);
+ addDomContentLoadedEnd(mainThread, i * 1000 + 2000);
+ addNavigationStart(mainThread, i * 10000);
+ addNetworkRequest(mainThread, i * 10000, 50);
+ addFMPCandidate(mainThread, i * 10000 + 2000);
+ addDomContentLoadedEnd(mainThread, i * 10000 + 2000);
+ addDummyTask(mainThread, 100000);
+ }
+ });
+
+ const interactiveTimestampsMap = getInteractiveTimestamps(model);
+ assert.deepEqual(interactiveTimestampsMap.get(101), [3000, 12000]);
+ assert.deepEqual(interactiveTimestampsMap.get(102), [4000, 22000]);
+ assert.deepEqual(interactiveTimestampsMap.get(103), [5000, 32000]);
+ assert.deepEqual(interactiveTimestampsMap.get(104), [6000, 42000]);
+ assert.deepEqual(interactiveTimestampsMap.get(105), [7000, 52000]);
+ });
+
+ test('singlePostInteractiveWindow', () => {
+ const interactiveTimestamps = [50];
+ const navStartTimestamps = [0];
+ const traceEndTimestamp = [100];
+ const windows = getPostInteractiveTaskWindows(
+ interactiveTimestamps, navStartTimestamps, traceEndTimestamp);
+ assert.strictEqual(windows.length, 1);
+ assertRangeEquals(windows[0], tr.b.math.Range.fromExplicitRange(50, 100));
+ });
+
+ test('multiplePostInteractiveWindows', () => {
+ const interactiveTimestamps = [50, 80];
+ const navStartTimestamps = [0, 70];
+ const traceEndTimestamp = [100];
+ const windows = getPostInteractiveTaskWindows(
+ interactiveTimestamps, navStartTimestamps, traceEndTimestamp);
+ assert.strictEqual(windows.length, 2);
+ assertRangeEquals(windows[0], tr.b.math.Range.fromExplicitRange(50, 70));
+ assertRangeEquals(windows[1], tr.b.math.Range.fromExplicitRange(80, 100));
+ });
+
+ test('postInteractiveWindowWithOneNavigationNeverReachingInteractive', () => {
+ const interactiveTimestamps = [50, 90];
+ const navStartTimestamps = [0, 60, 70];
+ const traceEndTimestamp = [100];
+ const windows = getPostInteractiveTaskWindows(
+ interactiveTimestamps, navStartTimestamps, traceEndTimestamp);
+ assert.strictEqual(windows.length, 2);
+ assertRangeEquals(windows[0], tr.b.math.Range.fromExplicitRange(50, 60));
+ assertRangeEquals(windows[1], tr.b.math.Range.fromExplicitRange(90, 100));
+ });
+
+ test('twoInteractiveTimeStampsWithNoNavStartInBetween', () => {
+ const interactiveTimestamps = [50, 75];
+ const navStartTimestamps = [0];
+ const traceEndTimestamp = [100];
+ assert.throws(() => getPostInteractiveTaskWindows(
+ interactiveTimestamps, navStartTimestamps, traceEndTimestamp));
+ });
+
+ test('expectedQueueingTime_noTasks', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(0, expectedQueueingTime(window, []), 1e-6);
+ });
+
+ test('expectedQueueingTime_singleTask', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(1000 / 2,
+ expectedQueueingTime(window, [{start: 0, end: 1000}]),
+ 1e-6);
+ });
+
+ test('expectedQueueingTime_singleTaskStartingBeforeWindow', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(1000 / 2,
+ expectedQueueingTime(window, [{start: -1, end: 1000}]),
+ 1e-6);
+ });
+
+ test('expectedQueueingTime_singleTaskEndingAfterWindow', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(1500,
+ expectedQueueingTime(window, [{start: 0, end: 2000}]),
+ 1e-6);
+ });
+
+ test('expectedQueueingTime_singleTaskInsideWindow', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(10 / 1000 * 10 / 2,
+ expectedQueueingTime(window, [{start: 500, end: 510}]),
+ 1e-6);
+ });
+
+ test('expectedQueueingTime_twoTasksInsideWindow', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(10 / 1000 * 10 / 2 + 100 / 1000 * 100 / 2,
+ expectedQueueingTime(window,
+ [{start: 500, end: 510}, {start: 600, end: 700}]),
+ 1e-6);
+ });
+
+ test('expectedQueueingTime_twoTasksPartiallyInsideWindow', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(10 / 1000 * 10 / 2 + 100 / 1000 * (100 + 200) / 2,
+ expectedQueueingTime(window,
+ [{start: 500, end: 510}, {start: 900, end: 1100}]),
+ 1e-6);
+ });
+
+ test('weightedExpectedQueueingTime', () => {
+ const window = tr.b.math.Range.fromExplicitRange(0, 1000);
+ assert.closeTo(1000 / 2 * 0.7,
+ weightedExpectedQueueingTime(window,
+ [{start: 0, end: 1000, weight: 0.7}]),
+ 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_taskOutsideRange', () => {
+ assert.closeTo(0,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
+ [{start: 2000, end: 3000}]),
+ 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_taskInsideRange', () => {
+ assert.closeTo(100 / 100 * 100 / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
+ [{start: 200, end: 300}]),
+ 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_longTask', () => {
+ assert.closeTo(100 / 100 * (100 + 200) / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
+ [{start: 200, end: 400}]),
+ 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_twoTasks', () => {
+ assert.closeTo(2 * 10 / 100 * 10 / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
+ [{start: 200, end: 210}, {start: 290, end: 300}]),
+ 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_taskLargerThanRange', () => {
+ assert.closeTo(100 / 100 * (1200 + 1100) / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
+ [{start: -200, end: 1200}]),
+ 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_multipleTasks', () => {
+ assert.closeTo(40 / 100 * 40 / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
+ {start: 500, end: 510},
+ {start: 510, end: 520},
+ {start: 520, end: 530},
+ {start: 615, end: 655},
+ {start: 1000, end: 2000},
+ ]), 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_threeTasks', () => {
+ assert.closeTo(40 / 100 * 40 / 2 + 20 / 100 * (50 + 30) / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
+ {start: 500, end: 510},
+ {start: 520, end: 560},
+ {start: 600, end: 650},
+ ]), 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_taskEndsAfterRange', () => {
+ assert.closeTo(1 / 100 * (200 + 199) / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
+ {start: 500, end: 502},
+ {start: 999, end: 1199},
+ ]), 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_taskStartsBeforeRange', () => {
+ assert.closeTo(3 / 100 * 1 / 2 + 20 / 100 * 20 / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100, [
+ {start: -10, end: 1},
+ {start: 1, end: 2},
+ {start: 2, end: 3},
+ {start: 80, end: 100},
+ {start: 999, end: 1099},
+ ]), 1e-6);
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_nonFittingWindowThrows', () => {
+ assert.throws(() => maxExpectedQueueingTimeInSlidingWindow(0, 10, 100,
+ [{start: 0, end: 100}]),
+ 'The sliding window must fit in the specified time range'
+ );
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_emptyWindowThrows', () => {
+ assert.throws(() => maxExpectedQueueingTimeInSlidingWindow(0, 10, 0,
+ [{start: 0, end: 100}]),
+ 'The window size must be positive number'
+ );
+ });
+
+ test('maxExpectedQueueingTimeInSlidingWindow_smallOverlapIsTolerated', () => {
+ // Allow small floating-point precision error when comparing task
+ // end-points for overlapping.
+ assert.closeTo((100.01 + 0.01) / 2,
+ maxExpectedQueueingTimeInSlidingWindow(0, 1000, 100,
+ [{start: 0, end: 100.02}, {start: 100.0, end: 200}]),
+ 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils.html
new file mode 100644
index 00000000000..01281cc7cf7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.chrome', function() {
+ // We want to ignore chrome internal URLs when computing metrics.
+ const CHROME_INTERNAL_URLS = [
+ // Blank URLs are usually initial empty document.
+ '',
+ 'about:blank',
+ // Chrome on Android creates main frames with the below URL for plugins.
+ 'data:text/html,pluginplaceholderdata',
+ // Special URL used to start a navigation to an unreachable error page.
+ 'chrome-error://chromewebdata/'
+ ];
+
+
+ // Title for top level tasks in the scheduler. Any input event queued during a
+ // top level scheduler task cannot be handled until the end of that task.
+ const SCHEDULER_TOP_LEVEL_TASK_TITLE = 'ThreadControllerImpl::RunTask';
+
+ const SCHEDULOER_TOP_LEVEL_TASKS = new Set([
+ // Current title for the scheduler top level task.
+ SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ // Previous names scheduler top level tasks, kept for backwards
+ // compatibility.
+ 'ThreadControllerImpl::DoWork',
+ 'TaskQueueManager::ProcessTaskFromWorkQueue'
+ ]);
+
+ /**
+ * Utility class providing methods to efficiently find events.
+ * TODO(4023) This should be merged with thread/process helper.
+ */
+ class EventFinderUtils {
+ /**
+ * Returns true if |category| is one of the categories of |event|, and
+ * |event| has title |title|.
+ *
+ * TODO(dproy): Make this a method on a suitable parent class of the
+ * event/slice classes that are used with this function.
+ */
+ static hasCategoryAndName(event, category, title) {
+ return event.title === title && event.category &&
+ tr.b.getCategoryParts(event.category).includes(category);
+ }
+
+ /**
+ * Returns the list of main thread slices of |rendererHelper|
+ * with title |eventTitle| and category |eventCategory|.
+ * Returned slices do not include telemetry internal events.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @param {string} eventTitle
+ * @param {string} eventCategory
+ * @returns {Array<!tr.model.ThreadSlice>}
+ */
+ static* getMainThreadEvents(
+ rendererHelper, eventTitle, eventCategory) {
+ if (!rendererHelper.mainThread) return;
+ // Events yielded by childEvents() are sorted by start time.
+ for (const ev of rendererHelper.mainThread.sliceGroup.childEvents()) {
+ if (rendererHelper.isTelemetryInternalEvent(ev)) continue;
+ if (!this.hasCategoryAndName(ev, eventCategory, eventTitle)) {
+ continue;
+ }
+ yield ev;
+ }
+ }
+
+ /**
+ * Returns a map of frame id to main thread slices of |rendererHelper| with
+ * title |eventTitle| and categry |eventCategory|, sorted by start
+ * time. Returned slices do not include telemetry internal events.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @param {string} eventTitle
+ * @param {string} eventCategory
+ * @returns {Map.<string, Array<!tr.model.ThreadSlice>>}
+ */
+ 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;
+ }
+
+ /**
+ * Returns a map of navigation id to main thread slices of |rendererHelper|
+ * with title |eventTitle| and categry |eventCategory|.
+ * Returned slices do not include telemetry internal events.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @param {string} eventTitle
+ * @param {string} eventCategory
+ * @returns {Map.<string, tr.model.ThreadSlice>}
+ */
+ static getSortedMainThreadEventsByNavId(
+ rendererHelper, eventTitle, eventCategory) {
+ const eventsByNavId = new Map();
+ const events = this.getMainThreadEvents(
+ rendererHelper, eventTitle, eventCategory);
+ for (const ev of events) {
+ if (ev.args.data === undefined) continue;
+ const navIdRef = ev.args.data.navigationId;
+ if (navIdRef === undefined) continue;
+ eventsByNavId.set(navIdRef, ev);
+ }
+
+ return eventsByNavId;
+ }
+
+ /**
+ * Returns latest event in |sortedEvents| that starts on or before
+ * |timestamp|, or undefined if no such event exists.
+ *
+ * @param {!Array<!tr.model.TimedEvent>} sortedEvents - events sorted by
+ * start time.
+ * @param {number} timestamp
+ * @returns {tr.model.TimedEvent|undefined}
+ */
+ static findLastEventStartingOnOrBeforeTimestamp(sortedEvents, timestamp) {
+ const firstIndexAfterTimestamp =
+ tr.b.findFirstTrueIndexInSortedArray(
+ sortedEvents, e => e.start > timestamp);
+ // We found the first index after the timestamp, so the index immediately
+ // before it is the index we're looking for. If the first index after
+ // timestamp is 0, then there is no index on or before timestamp.
+ if (firstIndexAfterTimestamp === 0) return undefined;
+ return sortedEvents[firstIndexAfterTimestamp - 1];
+ }
+
+ /**
+ * Returns latest event in |sortedEvents| that starts before
+ * |timestamp|, or undefined if no such event exists.
+ *
+ * @param {!Array<!tr.model.TimedEvent>} sortedEvents - events sorted by
+ * start time.
+ * @param {number} timestamp
+ * @returns {tr.model.TimedEvent|undefined}
+ */
+ static findLastEventStartingBeforeTimestamp(sortedEvents, timestamp) {
+ const firstIndexAfterTimestamp =
+ tr.b.findFirstTrueIndexInSortedArray(
+ sortedEvents, e => e.start >= timestamp);
+ // We found the first index after the timestamp, so the index immediately
+ // before it is the index we're looking for. If the first index after
+ // timestamp is 0, then there is no index on or before timestamp.
+ if (firstIndexAfterTimestamp === 0) return undefined;
+ return sortedEvents[firstIndexAfterTimestamp - 1];
+ }
+
+
+ /**
+ * Returns earliest event in |sortedEvents| that starts on or after
+ * |timestamp|, or undefined if no such event exists.
+ *
+ * @param {!Array<!tr.model.TimedEvent>} sortedEvents - events sorted by
+ * start time.
+ * @param {number} timestamp
+ * @returns {tr.model.Event|undefined}
+ */
+ static findNextEventStartingOnOrAfterTimestamp(sortedEvents, timestamp) {
+ const firstIndexOnOrAfterTimestamp =
+ tr.b.findFirstTrueIndexInSortedArray(
+ sortedEvents, e => e.start >= timestamp);
+
+ if (firstIndexOnOrAfterTimestamp === sortedEvents.length) {
+ return undefined;
+ }
+ return sortedEvents[firstIndexOnOrAfterTimestamp];
+ }
+
+ /**
+ * Returns earliest event in |sortedEvents| that starts after |timestamp|,
+ * or undefined if no such event exists.
+ *
+ * @param {!Array<!tr.model.TimedEvent>} sortedEvents - events sorted by
+ * start time.
+ * @param {number} timestamp
+ * @returns {tr.model.Event|undefined}
+ */
+ static findNextEventStartingAfterTimestamp(sortedEvents, timestamp) {
+ const firstIndexOnOrAfterTimestamp =
+ tr.b.findFirstTrueIndexInSortedArray(
+ sortedEvents, e => e.start > timestamp);
+
+ if (firstIndexOnOrAfterTimestamp === sortedEvents.length) {
+ return undefined;
+ }
+ return sortedEvents[firstIndexOnOrAfterTimestamp];
+ }
+
+ /**
+ * Returns a list of top level scheduler tasks.
+ * It is used by TTI and EQT metrics.
+ * @param {!tr.model.Thread} mainThread - the main thead of the renderer.
+ * @returns {!Array<tr.model.Slice>}
+ */
+ static findToplevelSchedulerTasks(mainThread) {
+ const tasks = [];
+ tasks.push(...mainThread.findTopmostSlices(
+ slice => slice.category === 'toplevel' &&
+ SCHEDULOER_TOP_LEVEL_TASKS.has(slice.title)));
+ return tasks;
+ }
+ }
+
+ return {
+ EventFinderUtils,
+ CHROME_INTERNAL_URLS,
+ SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils_test.html
new file mode 100644
index 00000000000..f3d078cceff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/event_finder_utils_test.html
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+const EventFinderUtils = tr.e.chrome.EventFinderUtils;
+
+tr.b.unittest.testSuite(function() {
+ test('getSortedMainThreadEventsByFrame', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const rendererProcess = model.getOrCreateProcess(1);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0x0'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 300,
+ duration: 0.0,
+ args: {frame: '0x1'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 400,
+ duration: 0.0,
+ args: {frame: '0x0'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'devtools.timeline',
+ title: 'navigationStart',
+ start: 500,
+ duration: 0.0,
+ args: {frame: '0x0'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'firstPaint',
+ start: 600,
+ duration: 0.0,
+ args: {frame: '0x0'}
+ }));
+ });
+
+
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelper = chromeHelper.rendererHelpers[1];
+ const frameToSlices = EventFinderUtils.getSortedMainThreadEventsByFrame(
+ rendererHelper, 'navigationStart', 'blink.user_timing');
+
+ assert.strictEqual(frameToSlices.size, 2);
+ assert.strictEqual(frameToSlices.get('0x0').length, 2);
+ assert.strictEqual(frameToSlices.get('0x0')[0].start, 200);
+ assert.strictEqual(frameToSlices.get('0x0')[1].start, 400);
+ assert.strictEqual(frameToSlices.get('0x1').length, 1);
+ assert.strictEqual(frameToSlices.get('0x1')[0].start, 300);
+ });
+
+ test('getSortedMainThreadEventsByNavId', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const rendererProcess = model.getOrCreateProcess(1);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0x0', data: { navigationId: '0xa' }}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 300,
+ duration: 0.0,
+ args: {frame: '0x1', data: { navigationId: '0xb' }}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 400,
+ duration: 0.0,
+ args: {frame: '0x0', data: { navigationId: '0xc' }}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'devtools.timeline',
+ title: 'navigationStart',
+ start: 500,
+ duration: 0.0,
+ args: {frame: '0x0', data: { navigationId: '0xd' }}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'firstPaint',
+ start: 600,
+ duration: 0.0,
+ args: {frame: '0x0'}
+ }));
+ });
+
+
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelper = chromeHelper.rendererHelpers[1];
+ const navIdToSlices = EventFinderUtils.getSortedMainThreadEventsByNavId(
+ rendererHelper, 'navigationStart', 'blink.user_timing');
+
+ assert.strictEqual(navIdToSlices.size, 3);
+ assert.strictEqual(navIdToSlices.get('0xa').start, 200);
+ assert.strictEqual(navIdToSlices.get('0xb').start, 300);
+ assert.strictEqual(navIdToSlices.get('0xc').start, 400);
+ });
+
+ test('multipleCategoriesOnAnEvent', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const rendererProcess = model.getOrCreateProcess(1);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing,rail',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0x0'}
+ }));
+ });
+
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelper = chromeHelper.rendererHelpers[1];
+ const frameToSlices = EventFinderUtils.getSortedMainThreadEventsByFrame(
+ rendererHelper, 'navigationStart', 'blink.user_timing');
+
+ assert.strictEqual(frameToSlices.get('0x0')[0].start, 200);
+ });
+
+ test('findLastEventStartingOnOrBeforeTimestamp', () => {
+ const sortedEvents = [50, 100, 150, 200].map(ts =>
+ tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: ts,
+ duration: 40.0}));
+
+ assert.strictEqual(
+ EventFinderUtils.findLastEventStartingOnOrBeforeTimestamp(
+ sortedEvents, 100).start,
+ 100);
+ assert.strictEqual(
+ EventFinderUtils.findLastEventStartingOnOrBeforeTimestamp(
+ sortedEvents, 99).start,
+ 50);
+ assert.isUndefined(
+ EventFinderUtils.findLastEventStartingOnOrBeforeTimestamp(
+ sortedEvents, 49));
+ });
+
+ test('findNextEventStartingAfterTimestamp', () => {
+ const sortedEvents = [50, 50, 50, 200].map(ts =>
+ tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: ts,
+ duration: 40.0}));
+
+ assert.strictEqual(
+ EventFinderUtils.findNextEventStartingAfterTimestamp(
+ sortedEvents, 49).start,
+ 50);
+ assert.strictEqual(
+ EventFinderUtils.findNextEventStartingAfterTimestamp(
+ sortedEvents, 50).start,
+ 200);
+ assert.isUndefined(
+ EventFinderUtils.findNextEventStartingAfterTimestamp(
+ sortedEvents, 201));
+ });
+
+ test('findToplevelSchedulerTasks', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const rendererProcess = model.getOrCreateProcess(1);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: 'ThreadControllerImpl::RunTask',
+ start: 10,
+ duration: 1,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: 'ThreadControllerImpl::DoWork',
+ start: 20,
+ duration: 2,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: 'TaskQueueManager::ProcessTaskFromWorkQueue',
+ start: 30,
+ duration: 3,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: 'MessageLoop::RunTask',
+ start: 40,
+ duration: 4,
+ }));
+ // The category of 'ThreadControllerImpl::DoWork' slice was changed to
+ // 'sequence_manager' because it is not longer a scheduler top level task.
+ // See crbug.com/871204 for context.
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'sequence_manager',
+ title: 'ThreadControllerImpl::DoWork',
+ start: 50,
+ duration: 5,
+ }));
+ });
+
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelper = chromeHelper.rendererHelpers[1];
+ const tasks = EventFinderUtils.findToplevelSchedulerTasks(
+ rendererHelper.mainThread);
+
+ assert.strictEqual(tasks.length, 3);
+ assert.strictEqual(tasks[0].title,
+ 'ThreadControllerImpl::RunTask');
+ assert.strictEqual(tasks[0].start, 10);
+ assert.strictEqual(tasks[0].duration, 1);
+ assert.strictEqual(tasks[1].title,
+ 'ThreadControllerImpl::DoWork');
+ assert.strictEqual(tasks[1].start, 20);
+ assert.strictEqual(tasks[1].duration, 2);
+ assert.strictEqual(tasks[2].title,
+ 'TaskQueueManager::ProcessTaskFromWorkQueue');
+ assert.strictEqual(tasks[2].start, 30);
+ assert.strictEqual(tasks[2].duration, 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice.html
new file mode 100644
index 00000000000..bfea13a7166
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/model/async_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.gpu', function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+
+ function GpuAsyncSlice() {
+ AsyncSlice.apply(this, arguments);
+ }
+
+ GpuAsyncSlice.prototype = {
+ __proto__: AsyncSlice.prototype,
+
+ get viewSubGroupTitle() {
+ if (this.args.channel) {
+ if (this.category === 'disabled-by-default-gpu.device') {
+ return 'Device.' + this.args.channel;
+ }
+ return 'Service.' + this.args.channel;
+ }
+ return this.title;
+ }
+ };
+
+ AsyncSlice.subTypes.register(
+ GpuAsyncSlice,
+ {
+ categoryParts: ['disabled-by-default-gpu.device',
+ 'disabled-by-default-gpu.service']
+ });
+
+ return {
+ GpuAsyncSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice_test.html
new file mode 100644
index 00000000000..53edf5ded5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/gpu_async_slice_test.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/gpu/gpu_async_slice.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const GpuAsyncSlice = tr.e.gpu.GpuAsyncSlice;
+
+ test('construct', function() {
+ assert.strictEqual(AsyncSlice.subTypes.getConstructor(
+ 'disabled-by-default-gpu.device', 'gpu1'),
+ GpuAsyncSlice);
+ assert.strictEqual(AsyncSlice.subTypes.getConstructor(
+ 'disabled-by-default-gpu.service', 'gpu2'),
+ GpuAsyncSlice);
+ });
+
+ test('subgroup', function() {
+ const sDevice = new GpuAsyncSlice('disabled-by-default-gpu.device', 'gpu1',
+ 7, 0, {'channel': 'test_channel1'});
+
+ const sService = new GpuAsyncSlice(
+ 'disabled-by-default-gpu.service', 'gpu2', 7, 0,
+ {'channel': 'test_channel2'});
+
+ assert.strictEqual(sDevice.viewSubGroupTitle, 'Device.test_channel1');
+ assert.strictEqual(sService.viewSubGroupTitle, 'Service.test_channel2');
+ });
+
+ test('import', function() {
+ const events = [
+ {name: 'trace1', args: {}, pid: 1, ts: 100,
+ cat: 'disabled-by-default-gpu.service', tid: 2, ph: 'b', id: 71},
+ {name: 'trace1', args: {}, pid: 1, ts: 200,
+ cat: 'disabled-by-default-gpu.service', tid: 2, ph: 'e', id: 71},
+ {name: 'trace2', args: {'channel': 'test_channel'}, pid: 1, ts: 100,
+ cat: 'disabled-by-default-gpu.service', tid: 2, ph: 'b', id: 72},
+ {name: 'trace2', args: {'channel': 'test_channel'}, pid: 1, ts: 200,
+ cat: 'disabled-by-default-gpu.service', tid: 2, ph: 'e', id: 72},
+ {name: 'trace3', args: {'channel': 'test_channel'}, pid: 1, ts: 100,
+ cat: 'disabled-by-default-gpu.device', tid: 2, ph: 'b', id: 73},
+ {name: 'trace3', args: {'channel': 'test_channel'}, pid: 1, ts: 200,
+ cat: 'disabled-by-default-gpu.device', tid: 2, ph: 'e', id: 73}
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([events]);
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ assert.strictEqual(t2.asyncSliceGroup.length, 3);
+
+ const slice1 = t2.asyncSliceGroup.slices[0];
+ const slice2 = t2.asyncSliceGroup.slices[1];
+ const slice3 = t2.asyncSliceGroup.slices[2];
+
+ assert.instanceOf(slice1, GpuAsyncSlice);
+ assert.instanceOf(slice2, GpuAsyncSlice);
+ assert.instanceOf(slice3, GpuAsyncSlice);
+
+ assert.strictEqual(slice1.viewSubGroupTitle, 'trace1');
+ assert.strictEqual(slice2.viewSubGroupTitle, 'Service.test_channel');
+ assert.strictEqual(slice3.viewSubGroupTitle, 'Device.test_channel');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state.html
new file mode 100644
index 00000000000..b82c85abdf9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.gpu', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * @constructor
+ */
+ function StateSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ }
+
+ StateSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ preInitialize() {
+ this.screenshot_ = undefined;
+ },
+
+ initialize() {
+ if (this.args.screenshot) {
+ this.screenshot_ = this.args.screenshot;
+ }
+ },
+
+ /**
+ * @return {String} a base64 encoded screenshot if available.
+ */
+ get screenshot() {
+ return this.screenshot_;
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ StateSnapshot,
+ {typeName: 'gpu::State'});
+
+ return {
+ StateSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test.html
new file mode 100644
index 00000000000..ac5f34dfe8c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/gpu/state.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script src="/tracing/extras/chrome/gpu/state_test_data.js"></script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_gpuStateTrace]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('gpu::State')[0];
+ const snapshot = instance.snapshots[0];
+
+ assert.instanceOf(snapshot, tr.e.gpu.StateSnapshot);
+ assert.typeOf(snapshot.screenshot, 'string');
+ instance.wasDeleted(150);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test_data.js b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test_data.js
new file mode 100644
index 00000000000..9327b49e8eb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/gpu/state_test_data.js
@@ -0,0 +1,22 @@
+// Copyright (c) 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.
+
+'use strict';
+
+const g_gpuStateTrace = [
+ {
+ 'cat': 'disabled-by-default-gpu.debug',
+ 'pid': 23969,
+ 'tid': 1799,
+ 'ts': 1427012847340,
+ 'ph': 'O',
+ 'name': 'gpu::State',
+ 'args': {
+ 'snapshot': {
+ 'screenshot': 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90JCREbHlyxtxQAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAqUlEQVRIx+2WiwrAIAhF/f9vHmywQTMft8w2CJKIoPDozR50fmy0AcsAiMjs9UDOJgG4jwG8SMuUXoMAZcVYBmgPqDYNwLo3JHqcHufbRETZKireOSbDQAA+zgKE7lyiCQDtcQygS6PKYIp3vZ5MvgB0mhmQu8kcgAXE6bYBoZYFmPnhgg5n4En/h0RmvdmX3eKAMYZ3HtGD0+NU3w2BR795dPe/aANuuwDyW5SiCgNBiQAAAABJRU5ErkJggg==' // @suppress longLineCheck
+ }
+ },
+ 'id': '0x7d229bc0'
+ }
+];
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object.html
new file mode 100644
index 00000000000..60911317e14
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'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');
+ }
+ this.tableRow_ = args.row;
+ this.tableCol_ = args.col;
+ this.tableRowSpan_ = args.rowSpan;
+ this.tableColSpan_ = args.colSpan;
+
+ if (args.children) {
+ args.children.forEach(function(child) {
+ this.childLayoutObjects_.push(new LayoutObject(snapshot, child));
+ }.bind(this));
+ }
+
+ for (const property in args) {
+ if (!KNOWN_PROPERTIES[property]) {
+ this.otherProperties_[property] = args[property];
+ }
+ }
+ }
+
+ LayoutObject.prototype = {
+ get snapshot() {
+ return this.snapshot_;
+ },
+
+ get id() {
+ return this.id_;
+ },
+
+ get name() {
+ return this.name_;
+ },
+
+ get tag() {
+ return this.tag_;
+ },
+
+ get relativeRect() {
+ return this.relativeRect_;
+ },
+
+ get absoluteRect() {
+ return this.absoluteRect_;
+ },
+
+ get isPositioned() {
+ return this.isPositioned_;
+ },
+
+ get isFloat() {
+ return this.isFloat_;
+ },
+
+ get isStickyPositioned() {
+ return this.isStickyPositioned_;
+ },
+
+ get isRelativePositioned() {
+ return this.isRelativePositioned_;
+ },
+
+ get isAnonymous() {
+ return this.isAnonymous_;
+ },
+
+ get tableRow() {
+ return this.tableRow_;
+ },
+
+ get tableCol() {
+ return this.tableCol_;
+ },
+
+ get tableRowSpan() {
+ return this.tableRowSpan_;
+ },
+
+ get tableColSpan() {
+ return this.tableColSpan_;
+ },
+
+ get htmlId() {
+ return this.htmlId_;
+ },
+
+ get classNames() {
+ return this.classNames_;
+ },
+
+ get needsLayoutReasons() {
+ return this.needsLayoutReasons_;
+ },
+
+ get hasChildLayoutObjects() {
+ return this.childLayoutObjects_.length > 0;
+ },
+
+ get childLayoutObjects() {
+ return this.childLayoutObjects_;
+ },
+
+ traverseTree(cb, opt_this) {
+ cb.call(opt_this, this);
+ if (!this.hasChildLayoutObjects) return;
+ this.childLayoutObjects.forEach(function(child) {
+ child.traverseTree(cb, opt_this);
+ });
+ },
+
+ get otherPropertyNames() {
+ const names = [];
+ for (const name in this.otherProperties_) {
+ names.push(name);
+ }
+ return names;
+ },
+
+ getProperty(name) {
+ return this.otherProperties_[name];
+ },
+
+ get previousSnapshotLayoutObject() {
+ if (!this.snapshot.previousSnapshot) return undefined;
+ return this.snapshot.previousSnapshot.getLayoutObjectById(this.id);
+ },
+
+ get nextSnapshotLayoutObject() {
+ if (!this.snapshot.nextSnapshot) return undefined;
+ return this.snapshot.nextSnapshot.getLayoutObjectById(this.id);
+ }
+ };
+
+ return {
+ LayoutObject,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object_test.html
new file mode 100644
index 00000000000..d1c888f17ef
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_object_test.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/layout_object.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function FakeSnapshot() {
+ this.layoutObjectsById = {};
+ }
+ FakeSnapshot.prototype = {
+ getLayoutObjectById(id) {
+ return this.layoutObjectsById[id];
+ }
+ };
+
+ test('instantiate', function() {
+ const snapshot = new FakeSnapshot();
+ const layoutObject = new tr.e.chrome.LayoutObject(snapshot, {
+ address: 'deadbeef',
+ name: 'LayoutView',
+ other: 1,
+ children: [
+ {
+ address: 'beef2',
+ name: 'LayoutBlockFlow',
+ other: 2
+ }
+ ]
+ });
+ snapshot.layoutObjectsById[layoutObject.id] = layoutObject;
+
+ assert.strictEqual(snapshot, layoutObject.snapshot);
+ assert.strictEqual('LayoutView', layoutObject.name);
+ assert.strictEqual('deadbeef', layoutObject.id);
+ assert.strictEqual(1, layoutObject.otherPropertyNames.length);
+ assert.strictEqual(1, layoutObject.getProperty('other'));
+ assert.isTrue(layoutObject.hasChildLayoutObjects);
+ const child = layoutObject.childLayoutObjects[0];
+ assert.strictEqual('LayoutBlockFlow', child.name);
+ assert.strictEqual('beef2', child.id);
+ assert.strictEqual(1, child.otherPropertyNames.length);
+ assert.strictEqual(2, child.getProperty('other'));
+
+ assert.isUndefined(layoutObject.previousSnapshotLayoutObject);
+ assert.isUndefined(layoutObject.nextSnapshotLayoutObject);
+
+ let count = 0;
+ layoutObject.traverseTree(function(node) {
+ count += 1;
+ });
+ assert.strictEqual(2, count);
+
+ const nextSnapshot = new FakeSnapshot();
+ nextSnapshot.previousSnapshot = snapshot;
+ snapshot.nextSnapshot = nextSnapshot;
+
+ const nextLayoutObject = new tr.e.chrome.LayoutObject(nextSnapshot, {
+ address: 'deadbeef',
+ name: 'LayoutView'
+ });
+ nextSnapshot.layoutObjectsById[nextLayoutObject.id] = nextLayoutObject;
+
+ assert.strictEqual(layoutObject,
+ nextLayoutObject.previousSnapshotLayoutObject);
+ assert.strictEqual(nextLayoutObject, layoutObject.nextSnapshotLayoutObject);
+ assert.isUndefined(layoutObject.previousSnapshotLayoutObject);
+ assert.isUndefined(nextLayoutObject.nextSnapshotLayoutObject);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_tree.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_tree.html
new file mode 100644
index 00000000000..94a2f505d3c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/layout_tree.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_registry.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.chrome', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ function LayoutTreeInstance() {
+ ObjectInstance.apply(this, arguments);
+ }
+
+ LayoutTreeInstance.prototype = {
+ __proto__: ObjectInstance.prototype,
+ };
+
+ ObjectInstance.subTypes.register(
+ LayoutTreeInstance, {typeName: 'LayoutTree'});
+
+ function LayoutTreeSnapshot() {
+ ObjectSnapshot.apply(this, arguments);
+ this.rootLayoutObject = new tr.e.chrome.LayoutObject(this, this.args);
+ }
+
+ LayoutTreeSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+ };
+
+ ObjectSnapshot.subTypes.register(
+ LayoutTreeSnapshot, {typeName: 'LayoutTree'});
+
+ return {
+ LayoutTreeInstance,
+ LayoutTreeSnapshot,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer.html
new file mode 100644
index 00000000000..5a4bc02f396
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Some generic slice titles appear very frequently in traces,
+ * like MessageLoop::RunTask. Some of these slices have arguments that give
+ * useful context. This class combine such arguments with the slice title to
+ * generate a more useful title.
+ */
+tr.exportTo('tr.e.chrome', function() {
+ function SliceTitleFixer() {
+ }
+
+ // AsyncSlice uses virtual functions to accomplish something similar to what
+ // we're doing here. If this function ever becomes too complex we may consider
+ // using a similar pattern.
+ SliceTitleFixer.fromEvent = function(event) {
+ if (event.args && event.args.src_func) {
+ return event.title + ' <- ' + event.args.src_func;
+ }
+ return event.title;
+ };
+
+ return {
+ SliceTitleFixer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer_test.html
new file mode 100644
index 00000000000..8b8ef3c9554
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/slice_title_fixer_test.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/slice_title_fixer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const stfFromEvent = tr.e.chrome.SliceTitleFixer.fromEvent;
+
+ test('sliceTitleFixer', function() {
+ assert.strictEqual(stfFromEvent({
+ title: 'SomeDummy:event'
+ }), 'SomeDummy:event');
+
+ assert.strictEqual(stfFromEvent({
+ title: 'EventWith:sourceFunction',
+ args: {'src_func': 'theSource'}
+ }), 'EventWith:sourceFunction <- theSource');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive.html
new file mode 100644
index 00000000000..ea025e67697
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive.html
@@ -0,0 +1,397 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.chrome', function() {
+ // Required quiet window size for Time to Interactive.
+ const TIME_TO_INTERACTIVE_WINDOW_SIZE_MS = 5000;
+
+ // Number of requests tolerated before network is considered busy.
+ const ACTIVE_REQUEST_TOLERANCE = 2;
+
+ // Minimum gap between task clusters for determining First CPU Idle.
+ const FCI_MIN_CLUSTER_SEPARATION_MS = 1000;
+
+ // Minimum duration of a task cluster to consider it heavy.
+ const TASK_CLUSTER_HEAVINESS_THRESHOLD_MS = 250;
+
+ /**
+ * Enum for endpoint types.
+ * @enum {string}
+ */
+ const ENDPOINT_TYPES = {
+ LONG_TASK_START: 'LONG_TASK_START',
+ LONG_TASK_END: 'LONG_TASK_END',
+ REQUEST_START: 'REQUEST_START',
+ REQUEST_END: 'REQUEST_END'
+ };
+
+ /**
+ * @typedef {Object} Endpoint
+ * @property {number} time - timestamp of endpoint.
+ * @property {string} type - A type defined in |ENDPOINT_TYPES|.
+ * originates from.
+ */
+
+ /**
+ * Returns an array of endpoints. An endpoint is either the start or end
+ * timestamp of a TimedEvent.
+ *
+ * @param {Array.<!tr.model.TimedEvent>} events - Events to extract endpoints
+ * from.
+ * @param {string} startType - Endpoint type for a start time endpoint.
+ * @param {string} endType - Endpoint type for an end time endpoint.
+ * @returns {Array.<!Endpoint>}
+ */
+ 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;
+ }
+
+ /**
+ * Returns true if at time |timestamp| we have a long enough quiet window for
+ * determining Time to Interactive.
+ *
+ * @param {number} timestamp - Timestamp of interest.
+ * @param {number} networkQuietWindowStart - Most recent timestamp when the
+ * renderer became network 2-quiet. Undefined if renderer is not network
+ * 2-quiet at |timestamp|.
+ * @param {number} mainThreadQuietWindowStart - End of most recent long task.
+ * Undefined if a long task is in progress at |timestamp|.
+ * @returns {boolean}
+ */
+ function reachedTTIQuiscence_(timestamp,
+ networkQuietWindowStart, mainThreadQuietWindowStart) {
+ if (networkQuietWindowStart === undefined ||
+ mainThreadQuietWindowStart === undefined) {
+ return false;
+ }
+
+ const mainThreadQuietForLongEnough =
+ timestamp - mainThreadQuietWindowStart >=
+ TIME_TO_INTERACTIVE_WINDOW_SIZE_MS;
+
+ const networkQuietForLongEnough =
+ timestamp - networkQuietWindowStart >=
+ TIME_TO_INTERACTIVE_WINDOW_SIZE_MS;
+
+ return mainThreadQuietForLongEnough && networkQuietForLongEnough;
+ }
+
+ /**
+ * Returns the timestamp for when the page first becomes Interactive.
+ *
+ * See https://goo.gl/IN68jL for detailed explanation of definition and
+ * motivations. This metric was previously known as "Consistently
+ * Interactive". To summarize, we look for a 5 second window starting from
+ * between |searchBegin| and |searchEnd| such that there is no long task
+ * overlapping with that window, and the entire window is network 2-quiet. A
+ * long task is defined as a main thread task with wall duration longer than
+ * 50ms, and network 2-quiet means the window never has more than 2 concurrent
+ * in-flight network requests. Interactive 'candidate' is defined as end of
+ * the last long task before this quiet window, or as |searchBegin| if there
+ * is no long task before that window. We define Interactive as max of
+ * Interactive candidate and domContentLoadedEnd.
+ *
+ * Returns undefined if no suitable quiet window is found.
+ *
+ * @param {number} searchBegin - The left boundary for our search for quiet
+ * window. Ideally, this is First Meaningful Paint, but if FMP is not
+ * available, First Contentful Paint or domContentLoadedEnd may be used as an
+ * approximation.
+ * @param {number} searchEnd - The right boundary for our search for quiet
+ * window. This is either the start of next navigationStart request, or end of
+ * traces.
+ * @param {number} domContentLoadedEnd - Timestamp of domContentLoadedEnd
+ * event for the main loading frame.
+ * @param {Array.<!tr.model.TimedEvent>} longTasks - Main thread tasks longer
+ * than 50ms overlapping with the search window.
+ * @param {Array.<!tr.model.TimedEvent>} networkRequests - Resource
+ * requests overlapping with the search window.
+ * @returns {number|undefined}
+ */
+ function findInteractiveTime(searchBegin, searchEnd,
+ domContentLoadedEnd, longTasksInWindow, networkRequests) {
+ // It is sufficient to only look at the end points of long tasks and network
+ // requests. These are the points where any meaningful changes happen.
+ const longTaskEndpoints = getEndpoints_(longTasksInWindow,
+ ENDPOINT_TYPES.LONG_TASK_START, ENDPOINT_TYPES.LONG_TASK_END);
+ const networkRequestEndpoints = getEndpoints_(networkRequests,
+ ENDPOINT_TYPES.REQUEST_START, ENDPOINT_TYPES.REQUEST_END);
+ const endpoints = longTaskEndpoints.concat(networkRequestEndpoints);
+ endpoints.sort((a, b) => a.time - b.time);
+
+ let networkQuietWindowStart = searchBegin;
+ let mainThreadQuietWindowStart = searchBegin;
+ let interactiveCandidate = undefined;
+ let activeRequests = 0;
+
+ for (const endpoint of endpoints) {
+ if (reachedTTIQuiscence_(endpoint.time,
+ networkQuietWindowStart, mainThreadQuietWindowStart)) {
+ interactiveCandidate = mainThreadQuietWindowStart;
+ break;
+ }
+
+ switch (endpoint.type) {
+ case ENDPOINT_TYPES.LONG_TASK_START:
+ mainThreadQuietWindowStart = undefined;
+ break;
+ case ENDPOINT_TYPES.LONG_TASK_END:
+ mainThreadQuietWindowStart = endpoint.time;
+ break;
+ case ENDPOINT_TYPES.REQUEST_START:
+ activeRequests++;
+ if (activeRequests > ACTIVE_REQUEST_TOLERANCE) {
+ networkQuietWindowStart = undefined;
+ }
+ break;
+ case ENDPOINT_TYPES.REQUEST_END:
+ activeRequests--;
+ if (activeRequests === ACTIVE_REQUEST_TOLERANCE) {
+ // Just became network 2-quiet.
+ networkQuietWindowStart = endpoint.time;
+ }
+ break;
+ default:
+ // This should never happen.
+ throw new Error('Internal Error: Unhandled endpoint type.');
+ }
+ }
+
+ if (interactiveCandidate === undefined &&
+ reachedTTIQuiscence_(searchEnd,
+ networkQuietWindowStart, mainThreadQuietWindowStart)) {
+ interactiveCandidate = mainThreadQuietWindowStart;
+ }
+
+ if (interactiveCandidate === undefined) return undefined;
+
+ return Math.max(interactiveCandidate, domContentLoadedEnd);
+ }
+
+ /**
+ * Returns the required quiet window size for First CPU Idle at
+ * |timeSinceSearchBeginMs| after searchBegin.
+ *
+ * Required quiet window size is modeled as an exponential decay. See
+ * https://goo.gl/kQXGLW for development of the exact equation used here.
+ *
+ * @param {number} timeSinceSearchBeginMs - Time since the beginning of search
+ * window for First CPU Idle.
+ */
+ function requiredFCIWindowSizeMs(timeSinceSearchBeginMs) {
+ const timeCoefficient = 1 / 15 * Math.log(2);
+
+ const timeSinceSearchBeginSeconds = tr.b.convertUnit(timeSinceSearchBeginMs,
+ tr.b.UnitPrefixScale.METRIC.MILLI, tr.b.UnitPrefixScale.METRIC.NONE);
+ const windowSizeSeconds =
+ 4 * Math.exp(- timeCoefficient * timeSinceSearchBeginSeconds) + 1;
+ return tr.b.convertUnit(windowSizeSeconds,
+ tr.b.UnitPrefixScale.METRIC.NONE, tr.b.UnitPrefixScale.METRIC.MILLI);
+ }
+
+ /**
+ * TaskCluster represents a group of long tasks such that they are at least 1s
+ * away from any other long task that is not in the group.
+ *
+ * A TaskCluster instance is meant to be immutable once constructed.
+ */
+ class TaskCluster {
+ /**
+ * Create a TaskCluster.
+ * @param {Array.<!tr.model.TimedEvent>} tasksInClusterSorted - Tasks in the
+ * cluster. Assumed sorted by start time.
+ */
+ constructor(tasksInClusterSorted) {
+ if (tasksInClusterSorted.length === 0) {
+ throw new Error('Internal Error: TaskCluster must have non zero tasks');
+ }
+
+ for (let i = 0; i < tasksInClusterSorted.length - 1; i++) {
+ const durationBetweenTasks = tasksInClusterSorted[i + 1].start -
+ tasksInClusterSorted[i].end;
+ if (durationBetweenTasks >= FCI_MIN_CLUSTER_SEPARATION_MS) {
+ throw new Error('Internal Error: Tasks in a TaskCluster cannot be ' +
+ 'more than ' + FCI_MIN_CLUSTER_SEPARATION_MS +
+ ' miliseconds apart');
+ }
+
+ // Ideally the condition below should be durationBetweenTasks < 0, but
+ // sometimes, for rounding errors, the end time of one task may very
+ // slightly extend past the start time of the next.
+ if (durationBetweenTasks < -1e7) {
+ throw new Error('Internal Error: List of tasks used to construct ' +
+ 'TaskCluster must be sorted.');
+ }
+ }
+
+ this._clusterTasks = tasksInClusterSorted;
+ }
+
+ /**
+ * @type{number}
+ */
+ get start() {
+ return this._clusterTasks[0].start;
+ }
+
+ /**
+ * @type{number}
+ */
+ get end() {
+ return this._clusterTasks[this._clusterTasks.length - 1].end;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ isHeavy() {
+ return this.end - this.start > TASK_CLUSTER_HEAVINESS_THRESHOLD_MS;
+ }
+ }
+
+
+ /**
+ * Returns all the task clusters of |sortedLongTasks|.
+ *
+ * @param {Array.<!tr.model.TimedEvent>} sortedLongTasks - Main thread tasks
+ * longer than 50ms, sorted by task start time.
+ * @returns {Array.<!TaskCluster>}
+ */
+ function findFCITaskClusters(sortedLongTasks) {
+ const clusters = [];
+ if (sortedLongTasks.length === 0) return clusters;
+
+ const firstTask = sortedLongTasks[0];
+ const restOfTasks = sortedLongTasks.slice(1);
+
+ let currentClusterTasks = [firstTask];
+
+ for (const currTask of restOfTasks) {
+ const prevTask = currentClusterTasks[currentClusterTasks.length - 1];
+ if (currTask.start - prevTask.end < FCI_MIN_CLUSTER_SEPARATION_MS) {
+ // Add task to current task cluster.
+ currentClusterTasks.push(currTask);
+ } else {
+ // Wrap up the current cluster and add task to a fresh cluster.
+ clusters.push(new TaskCluster(currentClusterTasks));
+ currentClusterTasks = [currTask];
+ }
+ }
+
+ clusters.push(new TaskCluster(currentClusterTasks));
+
+ return clusters;
+ }
+
+ /**
+ * Returns true if at time |timestamp|, assuming |timestamp| is not in the
+ * middle of a heavy task cluster, we have a long enough quiet window for
+ * determining First CPU Idle.
+ *
+ * @param {number} timestamp - Timestamp of interest. We assume |timestamp| is
+ * not in the middle of a heavy task cluster.
+ * @param {number} mainThreadQuietWindowStart - Most recent timestamp when we
+ * considered the main thread to be quiet. Usually end of most recent
+ * heavy task cluster or |searchBegin| where there are no heavy task
+ * cluster.
+ * @param {number} searchBegin - Left boundary for quiet window search.
+ * @returns {boolean}
+ */
+ function reachedFCIQuiescence_(timestamp, mainThreadQuietWindowStart,
+ searchBegin) {
+ const quietWindowSize = timestamp - mainThreadQuietWindowStart;
+ const timeSinceSearchBegin = mainThreadQuietWindowStart - searchBegin;
+ const requiredWindowSize = requiredFCIWindowSizeMs(timeSinceSearchBegin);
+ return quietWindowSize > requiredWindowSize;
+ }
+
+ /**
+ * Returns the timestamp for First CPU Idle for a page.
+ *
+ * See https://goo.gl/1a1XwZ for definition, and https://goo.gl/IN68jL for an
+ * explanation of how the definition was developed. This metric was previously
+ * known as "First Interactive". To summarize, in order to find First
+ * Interactive we look for a long enough window between |searchBegin| and
+ * |searchEnd| such that it doesn't contain a heavy task cluster. The required
+ * length of the window decreases the further we move away from |searchBegin|.
+ * A "task cluster" is defined as a group of long tasks such that they are at
+ * least 1s away from any other long task that is not in the group. A task
+ * cluster is considered "heavy" if the duration of the task cluster (the time
+ * interval from the beginning of first task to the end of last task) is
+ * longer than 250ms. We call the beginning of the quiet window FCI candidate,
+ * and define First Cpu Idle as max of FCI candidate and
+ * domContentLoadedEnd.
+ *
+ * Returns undefined if no suitable quiet window is found.
+ *
+ * @param {number} searchBegin - The left boundary for our search for quiet
+ * window. Ideally, this is First Meaningful Paint, but if FMP is not
+ * available, First Contentful Paint or domContentLoadedEnd may be used as
+ * an approximation.
+ * @param {number} searchEnd - The right boundary for our search for quiet
+ * window. This is either the start of next navigationStart request, or
+ * end of traces.
+ * @param {number} domContentLoadedEnd - Timestamp of domContentLoadedEnd
+ * event for the main loading frame.
+ * @param {Array.<!tr.model.TimedEvent>} longTasks - Main thread tasks longer
+ * than 50ms overlapping with the search window.
+ * @returns {number|undefined}
+ */
+ function findFirstCpuIdleTime(searchBegin, searchEnd,
+ domContentLoadedEnd, longTasksInWindow) {
+ const sortedLongTasks = longTasksInWindow.sort((a, b) => a.start - b.start);
+ const taskClusters = findFCITaskClusters(sortedLongTasks);
+ const heavyTaskClusters = taskClusters.filter(cluster => cluster.isHeavy());
+
+ let quietWindowBegin = searchBegin;
+ let fiCandidate = undefined;
+ for (const cluster of heavyTaskClusters) {
+ if (reachedFCIQuiescence_(cluster.start, quietWindowBegin, searchBegin)) {
+ fiCandidate = quietWindowBegin;
+ break;
+ }
+ quietWindowBegin = cluster.end;
+ }
+
+ if (fiCandidate === undefined) {
+ // Check if quiescence was reached at the end of the search window.
+ if (reachedFCIQuiescence_(searchEnd, quietWindowBegin, searchBegin)) {
+ fiCandidate = quietWindowBegin;
+ } else {
+ // We never reached quiescence.
+ return undefined;
+ }
+ }
+
+ return Math.max(fiCandidate, domContentLoadedEnd);
+ }
+
+ return {
+ findInteractiveTime,
+ findFirstCpuIdleTime,
+ requiredFCIWindowSizeMs,
+ findFCITaskClusters,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive_test.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive_test.html
new file mode 100644
index 00000000000..655bd23fc25
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome/time_to_interactive_test.html
@@ -0,0 +1,329 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/time_to_interactive.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const findInteractiveTime = tr.e.chrome.findInteractiveTime;
+ const findFirstCpuIdleTime = tr.e.chrome.findFirstCpuIdleTime;
+ const requiredFCIWindowSizeMs = tr.e.chrome.requiredFCIWindowSizeMs;
+ const findFCITaskClusters = tr.e.chrome.findFCITaskClusters;
+
+ test('findInteractiveTime_' +
+ 'doesNotWaitForTasksFarAwayWithoutNetworkRequests', () => {
+ const longTasks = [
+ {start: 50, end: 200},
+ {start: 300, end: 400},
+ // More than 5000ms gap here.
+ {start: 7000, end: 7200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(
+ findInteractiveTime(0, 10000, 50, longTasks, []),
+ 400, 1e-7);
+ });
+
+ test('findInteractiveTime_' +
+ 'waitsForTasksFarAwayWhenNetworkIsBusy', () => {
+ const longTasks = [
+ {start: 50, end: 200},
+ {start: 300, end: 400},
+ // More than 5000ms gap here.
+ {start: 7000, end: 7200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ const networkRequests = [
+ {start: 300, end: 3000},
+ {start: 500, end: 2900},
+ {start: 600, end: 3100},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ // Last network 2-quiet region starts at 2900, while the next long task
+ // starts at 7000. We don't have 5 seconds of quiet window before that long
+ // task.
+ assert.closeTo(findInteractiveTime(
+ 0, 20000, 50, longTasks, networkRequests), 7200, 1e-7);
+ });
+
+ test('findInteractiveTime_' +
+ 'noQuietWindowBecauseOfLongTask', () => {
+ const longTasks = [
+ {start: 50, end: 200},
+ {start: 300, end: 400},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ // windowEnd is 2400, so we don't get 5 seconds on quiet window after 400.
+ assert.isUndefined(findInteractiveTime(
+ 0, 2400, 50, longTasks, []));
+ });
+
+ test('findInteractiveTime_' +
+ 'noQuietWindowBecauseOfNetwork', () => {
+ const networkRequests = [
+ {start: 50, end: 200},
+ {start: 51, end: 300},
+ {start: 52, end: 400},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ // windowEnd is 2200, and beginning of last network 2-quiet region is 200.
+ // We don't have a 5 second quiet window.
+ assert.isUndefined(findInteractiveTime(
+ 0, 2200, 50, [], networkRequests));
+ });
+
+ test('findInteractiveTime_' +
+ 'network2QuietHappensAfterLastLongTask', () => {
+ const longTasks = [
+ {start: 50, end: 200},
+ {start: 300, end: 400},
+ // More than 5000ms gap here.
+ {start: 7000, end: 7200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ const networkRequests = [
+ {start: 300, end: 8000},
+ {start: 500, end: 8100},
+ {start: 600, end: 8300},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findInteractiveTime(
+ 0, 20000, 50, longTasks, networkRequests), 7200, 1e-7);
+ });
+
+
+ test('findInteractiveTime_' +
+ 'longEnoughNetwork2QuietBeforeLastLongTask', () => {
+ const longTasks = [
+ {start: 50, end: 200},
+ {start: 300, end: 400},
+ // More than 5000ms gap here.
+ {start: 7000, end: 7200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ const networkRequests = [
+ {start: 300, end: 1000},
+ {start: 500, end: 1100},
+ {start: 600, end: 1200},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+
+ // 1000ms is the beginning of last network 2-quiet region, and we have a 5s
+ // window between that and the start of next long task.
+ assert.closeTo(findInteractiveTime(
+ 0, 20000, 50, longTasks, networkRequests), 400, 1e-7);
+ });
+
+ test('findInteractiveTime_' +
+ 'isLowerBoundedAtDCL', () => {
+ const longTasks = [
+ {start: 50, end: 200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ // End of last long task is 200, but FCI is still 5000 because of
+ // DOMContentLoadedEnd being at 5000.
+ assert.closeTo(findInteractiveTime(
+ 0, 20000, 5000, longTasks, []), 5000, 1e-7);
+ });
+
+
+ test('findInteractiveTime_' +
+ 'returnsWindowStartWhenNoLongTasks', () => {
+ // Note that it is possible for DOMContentLoadedEnd to happen before
+ // windowStart, since windowStart is usually First Meaningful Paint. If
+ // there are no long tasks, and DCL is before windowStart, FCI should be
+ // windowStart.
+ assert.closeTo(findInteractiveTime(
+ 1000, 20000, 500, [], []), 1000, 1e-7);
+ });
+
+ test('findInteractiveTime' +
+ 'networkRequestStartsBeforeWindowStart', () => {
+ const longTasks = [
+ {start: 7000, end: 7200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ const networkRequests = [
+ {start: 0, end: 6000},
+ {start: 10, end: 6100},
+ {start: 20, end: 6200}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ // windowStart is now 1000. All three network requests start before
+ // windowStart, but should still manage to block netowrk 2-quiet.
+ assert.closeTo(findInteractiveTime(
+ 1000, 20000, 1050, longTasks, networkRequests), 7200, 1e-7);
+ });
+
+ test('requiredFCIWindowSizeMs', () => {
+ // The required window size function is an exponential decay, and is
+ // uniquely determined by values at three points.
+ assert.closeTo(requiredFCIWindowSizeMs(0), 5000, 1e-7);
+ assert.closeTo(requiredFCIWindowSizeMs(15000), 3000, 1e-7);
+ assert.closeTo(requiredFCIWindowSizeMs(Infinity), 1000, 1e-7);
+ });
+
+ test('findFCITaskClusters_MultipleLongTasks', () => {
+ const longTasks = [
+ {start: 50, end: 200},
+ {start: 500, end: 700},
+ {start: 1710, end: 1800},
+ {start: 1900, end: 2000},
+ {start: 2200, end: 2300},
+ {start: 5000, end: 5100},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ const clusters = findFCITaskClusters(longTasks);
+ assert.strictEqual(clusters.length, 3);
+ assert.strictEqual(clusters[0].start, 50);
+ assert.strictEqual(clusters[0].end, 700);
+ assert.strictEqual(clusters[1].start, 1710);
+ assert.strictEqual(clusters[1].end, 2300);
+ assert.strictEqual(clusters[2].start, 5000);
+ assert.strictEqual(clusters[2].end, 5100);
+ });
+
+ test('findFCITaskClusters_NoLongTasks', () => {
+ const clusters = findFCITaskClusters([]);
+ assert.strictEqual(clusters.length, 0);
+ });
+
+ test('findFirstInteractiveTimestamp_noLongTasks', () => {
+ const searchBegin = 0;
+ const searchEnd = 20000;
+ const dclEnd = 0;
+ const longTasks = [];
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), 0, 1e-7);
+ });
+
+ test('findFirstCpuIdleTime_' +
+ 'firstInteractiveReachedInBetweenTaskClusters', () => {
+ const searchBegin = 0;
+ const searchEnd = 20000;
+ const dclEnd = 0;
+ const longTasks = [
+ // Cluster 1.
+ {start: 50, end: 150},
+ {start: 200, end: 300},
+ {start: 500, end: 700},
+ // Cluster 2.
+ {start: 2000, end: 2100},
+ {start: 2900, end: 3000},
+ // Cluster 3.
+ {start: 8000, end: 8100},
+ {start: 8200, end: 8500},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), 3000, 1e-7);
+ });
+
+ test('findFirstCpuIdleTime_' +
+ 'FirstInteractiveReachedAfterLastHeavyCluster', () => {
+ const searchBegin = 0;
+ const searchEnd = 20000;
+ const dclEnd = 0;
+ const longTasks = [
+ // Cluster 1.
+ {start: 1000, end: 1100},
+ {start: 1150, end: 1350},
+ // Cluster 2.
+ {start: 4000, end: 4400},
+ // Cluster 3.
+ {start: 7000, end: 7400}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), 7400, 1e-7);
+ });
+
+
+ test('findFirstInteractiveTimestamp_noFirstInteractiveReached', () => {
+ const searchBegin = 0;
+ const searchEnd = 10000;
+ const dclEnd = 0;
+ const longTasks = [
+ // Four spaced out heavy task clusters.
+ {start: 2000, end: 2300},
+ {start: 4000, end: 4300},
+ {start: 6000, end: 6300},
+ {start: 8000, end: 8300}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.isUndefined(findFirstCpuIdleTime(searchBegin, searchEnd,
+ dclEnd, longTasks));
+ });
+
+ test('findFirstInteractiveTimestamp_lowerBoundedAtDCL', () => {
+ const searchBegin = 0;
+ const searchEnd = 10000;
+ const dclEnd = 8500;
+ const longTasks = [
+ {start: 2000, end: 2100},
+ {start: 4000, end: 4100}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), dclEnd, 1e-7);
+ });
+
+ test('findFirstInteractiveTimestamp_doesNotAssumeLongTasksAreSorted', () => {
+ const searchBegin = 0;
+ const searchEnd = 20000;
+ const dclEnd = 0;
+ const longTasks = [
+ {start: 2000, end: 2400},
+ {start: 1000, end: 1400}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), 2400, 1e-7);
+ });
+
+ test('findFirstCpuIdleTime_' +
+ 'lightTaskClustersDoesNotBlockFirstInteractive', () => {
+ const searchBegin = 0;
+ const searchEnd = 20000;
+ const dclEnd = 0;
+ const longTasks = [
+ // Cluster 1 - heavy (duration 350ms).
+ {start: 1000, end: 1100},
+ {start: 1150, end: 1350},
+ // Cluster 2 - light (duration 200ms).
+ {start: 4000, end: 4200},
+ // Cluster 3 - heavy (duration 400ms).
+ {start: 7000, end: 7400}
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), 1350, 1e-7);
+ });
+
+ test('findFirstCpuIdleTime_' +
+ 'requiredWindowSizeIsAtLeast5sAtTheBeginning', () => {
+ // Search begin intentionally not 0 to catch errors in calculating
+ // timeSinceSearchBegin.
+ const searchBegin = 10000;
+ const searchEnd = 30000;
+ const dclEnd = 5000;
+ const longTasks = [
+ // Heavy Task Cluster (duration 501ms). Starts 4999 ms after
+ // |searchBegin|.
+ {start: 14999, end: 15100},
+ {start: 15200, end: 15500},
+ ].map(options => tr.c.TestUtils.newSliceEx(options));
+
+ assert.closeTo(findFirstCpuIdleTime(searchBegin, searchEnd, dclEnd,
+ longTasks), 15500, 1e-7);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/chrome_config.html b/chromium/third_party/catapult/tracing/tracing/extras/chrome_config.html
new file mode 100644
index 00000000000..f6969a549cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/chrome_config.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!--
+The chrome config is heavily used:
+ - chrome://tracing,
+ - trace2html, which in turn implies
+ - adb_profile_chrome
+ - telemetry
+-->
+
+<link rel="import" href="/tracing/extras/android/android_auditor.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/blame_context.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
+<link rel="import" href="/tracing/extras/chrome/blink/blink_scheduler_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_auditor.html">
+<link rel="import" href="/tracing/extras/chrome/layout_object.html">
+<link rel="import" href="/tracing/extras/chrome/layout_tree.html">
+<link rel="import" href="/tracing/extras/cpu/cpu_usage_auditor.html">
+<link rel="import" href="/tracing/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/tracing/extras/importer/fuchsia_importer.html">
+<link rel="import" href="/tracing/extras/importer/gzip_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace2html_importer.html">
+<link rel="import" href="/tracing/extras/importer/v8/v8_log_importer.html">
+<link rel="import" href="/tracing/extras/importer/zip_importer.html">
+<link rel="import" href="/tracing/extras/lean_config.html">
+<link rel="import" href="/tracing/extras/measure/measure.html">
+<link rel="import" href="/tracing/extras/net/net.html">
+<link rel="import" href="/tracing/extras/systrace_config.html">
+<link rel="import" href="/tracing/extras/v8_config.html">
+<link rel="import" href="/tracing/extras/vsync/vsync_auditor.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/cpu/cpu_usage_auditor.html b/chromium/third_party/catapult/tracing/tracing/extras/cpu/cpu_usage_auditor.html
new file mode 100644
index 00000000000..d32e69cc998
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/cpu/cpu_usage_auditor.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/model/resource_usage_series.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.audits', function() {
+ /**
+ * Auditor that analyzes the model and, if possible, adds data to it
+ * showing CPU usage.
+ */
+ class CpuUsageAuditor extends tr.c.Auditor {
+ constructor(model) {
+ super();
+ this.model_ = model;
+ }
+
+ runAnnotate() {
+ this.model_.device.cpuUsageSeries = this.computeCpuUsageSeries_(
+ this.model_.bounds.min, this.model_.bounds.max,
+ this.computeCpuUsage_());
+ }
+
+ /**
+ * Compute the bin size given the start and end times of the trace.
+ */
+ computeBinSize_(start, end) {
+ const MIN_BIN_SIZE_MS = 1.0;
+ const MAX_NUM_BINS = 100000;
+ const interval = end - start;
+ let binSize = MIN_BIN_SIZE_MS;
+ while (binSize * MAX_NUM_BINS < interval) binSize *= 2;
+ return binSize;
+ }
+
+ /**
+ * Returns a CPU usage series from a given set of CPU usage slices.
+ * Slices are in the format created by getCpuUsage below.
+ */
+ computeCpuUsageSeries_(start, end, usageRecords) {
+ const series = new tr.model.ResourceUsageSeries();
+ if (start === undefined || usageRecords.length === 0) return series;
+ const binSize = this.computeBinSize_(start, end);
+ const numBins = Math.ceil((end - start) / binSize);
+ const arr = new Array(numBins).fill(0);
+ for (const record of usageRecords) {
+ const firstIndex = Math.ceil((record.start - start) / binSize);
+ const lastIndex = Math.floor((record.end - start) / binSize);
+ for (let i = firstIndex; i <= lastIndex; i++) arr[i] += record.usage;
+ }
+ for (let i = 0; i < numBins; i++) {
+ series.addUsageSample(start + (i * binSize), arr[i]);
+ }
+ return series;
+ }
+
+ /**
+ * Returns a list of CPU usage slices based on tracing data. Thus, this
+ * effectively counts only processes that are traced (will not count
+ * e.g. background processes)
+ */
+ computeCpuUsage_() {
+ const model = this.model_;
+ const result = [];
+ for (const pid in model.processes) {
+ for (const e of model.processes[pid].getDescendantEvents()) {
+ if (!(e instanceof tr.model.ThreadSlice) || e.duration === 0 ||
+ e.cpuDuration === undefined) {
+ continue;
+ }
+
+ // This slice contains the most fine-grained CPU usage information
+ // for the area of the trace that it covers but that is not covered
+ // by its subslices.
+ // The math goes this way:
+ // s.selfTime : duration of slice s not spent in its subslices.
+ // s.cpuSelfTime : cpuDuration over slice s but not its subslices.
+ //
+ // We're looking for
+ // s.cpuSelfTimeRatio: average CPU usage over the area covered by
+ // s but not any of its subslices.
+ // = s.cpuSelfTime / s.selfTime
+ if (e.selfTime === 0 || e.selfTime === undefined ||
+ e.cpuSelfTime === undefined) {
+ continue;
+ }
+ const usage = tr.b.math.clamp(e.cpuSelfTime / e.selfTime, 0, 1);
+
+ // Go through the area covered by this slice but not its subslices
+ // and add the cpuSelfTimeRatio contribution over this area.
+ let lastTime = e.start;
+ for (const subslice of e.subSlices) {
+ result.push({usage, start: lastTime, end: subslice.start});
+ lastTime = subslice.end;
+ }
+ result.push({usage, start: lastTime, end: e.end});
+ }
+ }
+ return result;
+ }
+ }
+
+ tr.c.Auditor.register(CpuUsageAuditor);
+
+ return {
+ CpuUsageAuditor
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/full_config.html b/chromium/third_party/catapult/tracing/tracing/extras/full_config.html
new file mode 100644
index 00000000000..2f157189922
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/full_config.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!-- The full config is all the configs slammed together. -->
+<link rel="import" href="/tracing/extras/chrome_config.html">
+<link rel="import" href="/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html">
+<link rel="import" href="/tracing/extras/lean_config.html">
+<link rel="import" href="/tracing/extras/systrace_config.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer.html
new file mode 100644
index 00000000000..518adfc2f35
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer.html
@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.importer.android.atrace_process_dump', function() {
+ const IMPORT_PRIORITY = tr.e.importer.linux_perf.IMPORT_PRIORITY + 1;
+ const HEADER = 'ATRACE_PROCESS_DUMP';
+
+ const PROTECTION_FLAG_LETTERS = {
+ '-': 0,
+ 'r': tr.model.VMRegion.PROTECTION_FLAG_READ,
+ 'w': tr.model.VMRegion.PROTECTION_FLAG_WRITE,
+ 'x': tr.model.VMRegion.PROTECTION_FLAG_EXECUTE,
+ 's': tr.model.VMRegion.PROTECTION_FLAG_MAYSHARE,
+ };
+
+ class AtraceProcessDumpImporter extends tr.importer.Importer {
+ constructor(model, data) {
+ super(model, data);
+ this.importPriority = IMPORT_PRIORITY;
+ this.model_ = model;
+ this.raw_data_ = data;
+ this.clock_sync_markers_ = {};
+ this.snapshots_ = [];
+ this.processes_ = {};
+ }
+
+ static canImport(events) {
+ if (!(typeof(events) === 'string' || events instanceof String)) {
+ return false;
+ }
+ return events.startsWith(HEADER);
+ }
+
+ get importerName() {
+ return 'AtraceProcessDumpImporter';
+ }
+
+ get model() {
+ return this.model_;
+ }
+
+ lazyParseData() {
+ if (this.raw_data_ === undefined) {
+ return;
+ }
+ const dump = JSON.parse(this.raw_data_.slice(HEADER.length + 1));
+ this.clock_sync_markers_ = dump.clock_sync_markers;
+ this.snapshots_ = dump.dump.snapshots;
+ this.processes_ = dump.dump.processes;
+ this.raw_data_ = undefined;
+ }
+
+ importClockSyncMarkers() {
+ this.lazyParseData();
+ for (const syncId in this.clock_sync_markers_) {
+ const ts = parseInt(this.clock_sync_markers_[syncId]);
+ this.model_.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.LINUX_CLOCK_MONOTONIC, syncId, ts);
+ }
+ }
+
+ setProcessMemoryDumpTotals_(pmd, processInfo) {
+ pmd.totals = {
+ 'residentBytes': processInfo.rss * 1024,
+ 'platformSpecific': {
+ 'vm': processInfo.vm * 1024
+ }
+ // TODO(kraynov): Add OOM scores and make UI able to show it.
+ };
+ const totals = pmd.totals.platformSpecific;
+
+ function importGpuMetric(name) {
+ if (processInfo[name] !== undefined && processInfo[name] > 0) {
+ totals[name] = processInfo[name] * 1024;
+ totals[name + '_pss'] = processInfo[name + '_pss'] * 1024;
+ }
+ }
+ importGpuMetric('gpu_egl');
+ importGpuMetric('gpu_gl');
+ importGpuMetric('gpu_etc');
+
+ if (processInfo.pss !== undefined) {
+ // Full stats.
+ totals.pss = processInfo.pss * 1024;
+ totals.swp = processInfo.swp * 1024;
+ totals.pc = processInfo.pc * 1024;
+ totals.pd = processInfo.pd * 1024;
+ totals.sc = processInfo.sc * 1024;
+ totals.sd = processInfo.sd * 1024;
+ }
+ }
+
+ setProcessMemoryDumpVmRegions_(pmd, processInfo) {
+ if (processInfo.mmaps === undefined) {
+ return;
+ }
+ const vmRegions = [];
+ for (const memoryMap of processInfo.mmaps) {
+ const addr = memoryMap.vm.split('-').map(x => parseInt(x, 16));
+ let flags = 0;
+ for (const letter of memoryMap.flags) {
+ flags |= PROTECTION_FLAG_LETTERS[letter];
+ }
+ const totals = {
+ 'proportionalResident': memoryMap.pss * 1024,
+ 'privateCleanResident': memoryMap.pc * 1024,
+ 'privateDirtyResident': memoryMap.pd * 1024,
+ 'sharedCleanResident': memoryMap.sc * 1024,
+ 'sharedDirtyResident': memoryMap.sd * 1024,
+ 'swapped': memoryMap.swp * 1024,
+ };
+ vmRegions.push(new tr.model.VMRegion(
+ addr[0], addr[1] - addr[0], flags, memoryMap.file, totals));
+ }
+ pmd.vmRegions =
+ tr.model.VMRegionClassificationNode.fromRegions(vmRegions);
+ }
+
+ importEvents() {
+ this.lazyParseData();
+ // Assign process and thread names.
+ for (const [pid, process] of Object.entries(this.processes_)) {
+ const modelProcess = this.model_.getProcess(pid);
+ if (modelProcess === undefined) {
+ continue;
+ }
+ modelProcess.name = process.name;
+
+ const threads = process.threads;
+ if (threads === undefined) {
+ continue;
+ }
+ for (const [tid, thread] of Object.entries(threads)) {
+ const modelThread = modelProcess.threads[tid];
+ if (modelThread === undefined) {
+ continue;
+ }
+ modelThread.name = thread.name;
+ }
+ }
+
+ // Memory dumps.
+ const memCounter =
+ this.model_.kernel.getOrCreateCounter('global', 'SystemMemory');
+ const memUsedSeries = new tr.model.CounterSeries('Used (KB)', 0);
+ const memSwappedSeries = new tr.model.CounterSeries('Swapped (KB)', 0);
+ memCounter.addSeries(memUsedSeries);
+ memCounter.addSeries(memSwappedSeries);
+
+ for (const snapshot of this.snapshots_) {
+ const ts = parseInt(snapshot.ts);
+ const memoryDump = snapshot.memdump;
+
+ if (memoryDump === undefined) {
+ const memInfo = snapshot.meminfo;
+ if (memInfo === undefined) {
+ continue;
+ }
+
+ // See Android com.android.server.am.ActivityManagerService class
+ // for calculation formula in 'dumpsys meminfo'.
+ //
+ // The formula below excludes Cached PSS because it's too expensive
+ // to calculate and it's not volatile during short systrace run.
+ // Cached PSS is a total PSS of apps being primary targets for
+ // OOM killer and treated by 'dumpsys meminfo' as a free memory.
+ const memCaches = memInfo.Buffers + memInfo.Cached - memInfo.Mapped;
+ const memUsed = memInfo.MemTotal - memInfo.MemFree - memCaches;
+ const memSwapped = memInfo.SwapTotal - memInfo.SwapFree;
+
+ memUsedSeries.addCounterSample(ts, memUsed);
+ memSwappedSeries.addCounterSample(ts, memSwapped);
+ continue;
+ }
+
+ const gmd = new tr.model.GlobalMemoryDump(this.model_, ts);
+ this.model_.globalMemoryDumps.push(gmd);
+
+ for (const [pid, processInfo] of Object.entries(memoryDump)) {
+ if (processInfo.rss === undefined) {
+ // Memory stats aren't available.
+ continue;
+ }
+ const modelProcess = this.model_.getProcess(pid);
+ if (modelProcess === undefined) {
+ continue;
+ }
+ const pmd = new tr.model.ProcessMemoryDump(gmd, modelProcess, ts);
+ gmd.processMemoryDumps[pid] = pmd;
+ modelProcess.memoryDumps.push(pmd);
+ this.setProcessMemoryDumpTotals_(pmd, processInfo);
+ this.setProcessMemoryDumpVmRegions_(pmd, processInfo);
+ }
+ }
+ }
+ }
+
+ tr.importer.Importer.register(AtraceProcessDumpImporter);
+
+ return {
+ AtraceProcessDumpImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer_test.html
new file mode 100644
index 00000000000..ecf3e8a1584
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/atrace_process_dump_importer_test.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/android/atrace_process_dump_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Importer =
+ tr.e.importer.android.atrace_process_dump.AtraceProcessDumpImporter;
+
+ const TEST_DATA =
+ 'ATRACE_PROCESS_DUMP\n' +
+ '{"clock_sync_markers": {"very-unique-id": 1}, "dump":' +
+ '{"start_ts": "21917568", "snapshots": [' +
+ '{"ts": "21917568", "memdump": {' +
+ ' "788": {' +
+ ' "vm": 2629440, "rss": 249348,' +
+ ' "oom_sc": 0, "oom_sc_adj": -900,' +
+ ' "min_flt": 15844983, "maj_flt": 54832,' +
+ ' "utime": 831123, "stime": 1117125,' +
+ ' "mmaps": [{"vm": "a1000-a5000", "file": "foo", "flags": "rw-s",' +
+ ' "pss": 100, "rss": 200, "swp": 10,' +
+ ' "pc": 50, "pd": 60, "sc": 40, "sd": 45}]' +
+ '}}}],' +
+ '"processes": {' +
+ '"37": {"name": "kworker/5:0H"},' +
+ '"788": {"name": "system_server", "exe": "/system/bin/app_process64",' +
+ ' "threads": {' +
+ ' "788": {"name": "system_server"},' +
+ ' "793": {"name": "Signal Catcher"}' +
+ '}}}}}';
+
+ test('canImport', function() {
+ const canImport = Importer.canImport;
+ assert.isTrue(canImport(TEST_DATA));
+ assert.isFalse(canImport('\n'));
+ });
+
+ test('lazyParseData', function() {
+ const importer = new Importer(null, TEST_DATA);
+ importer.lazyParseData();
+ assert.isUndefined(importer.raw_data_);
+ assert.lengthOf(Object.keys(importer.clock_sync_markers_), 1);
+ assert.lengthOf(importer.snapshots_, 1);
+ assert.lengthOf(Object.keys(importer.processes_), 2);
+ assert.lengthOf(Object.keys(importer.processes_['37']), 1);
+ assert.lengthOf(Object.keys(importer.processes_['788']), 3);
+ assert.lengthOf(Object.keys(importer.processes_['788'].threads), 2);
+
+ const snapshot = importer.snapshots_[0];
+ assert.strictEqual(snapshot.ts, '21917568');
+ assert.lengthOf(Object.keys(snapshot.memdump), 1);
+ assert.lengthOf(Object.keys(snapshot.memdump['788']), 9);
+ });
+
+ test('importEvents', function() {
+ const model = new tr.Model();
+ const proc788 = model.getOrCreateProcess(788);
+ const importer = new Importer(model, TEST_DATA);
+ importer.importEvents();
+ assert.strictEqual(proc788.name, 'system_server');
+ assert.lengthOf(model.globalMemoryDumps, 1);
+
+ const gmd = model.globalMemoryDumps[0];
+ assert.lengthOf(Object.keys(gmd.processMemoryDumps), 1);
+ const pmd = gmd.processMemoryDumps['788'];
+ assert.strictEqual(pmd.totals.residentBytes, 249348 * 1024);
+
+ const vmRegions = pmd.vmRegions.allRegionsForTesting;
+ assert.lengthOf(vmRegions, 1);
+ const vmRegion = vmRegions[0];
+ assert.deepEqual(vmRegion.byteStats, pmd.vmRegions.byteStats);
+ assert.strictEqual(vmRegion.byteStats.swapped, 10240);
+ assert.strictEqual(vmRegion.mappedFile, 'foo');
+ assert.strictEqual(vmRegion.protectionFlags, 134);
+ assert.strictEqual(vmRegion.sizeInBytes, 16384);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/android/event_log_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/event_log_importer.html
new file mode 100644
index 00000000000..677ad5f9912
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/event_log_importer.html
@@ -0,0 +1,324 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/importer/simple_line_reader.html">
+<link rel="import" href="/tracing/model/activity.html">
+<link rel="import" href="/tracing/model/model.html">
+
+
+<script>
+/**
+ * @fileoverview Imports android event log data into the trace model.
+ * Android event log data contains information about activities that
+ * are launched/paused, processes that are started, memory usage, etc.
+ *
+ * The current implementation only parses activity events, with the goal of
+ * determining which Activity is running in the foreground for a process.
+ *
+ * This importer assumes the events arrive as a string. The unit tests provide
+ * examples of the trace format.
+ */
+'use strict';
+
+tr.exportTo('tr.e.importer.android', function() {
+ const Importer = tr.importer.Importer;
+
+ const ACTIVITY_STATE = {
+ NONE: 'none',
+ CREATED: 'created',
+ STARTED: 'started',
+ RESUMED: 'resumed',
+ PAUSED: 'paused',
+ STOPPED: 'stopped',
+ DESTROYED: 'destroyed'
+ };
+
+ const activityMap = {};
+
+ /**
+ * Imports android event log data (adb logcat -b events)
+ * @constructor
+ */
+ function EventLogImporter(model, events) {
+ this.model_ = model;
+ this.events_ = events;
+ this.importPriority = 3;
+ }
+
+ // Generic format of event log entries.
+ // Sample event log entry that this matches (split over 2 lines):
+ // 08-11 13:12:31.405 880 2645 I am_focused_activity: [0,com.google.android.googlequicksearchbox/com.google.android.launcher.GEL] // @suppress longLineCheck
+ const eventLogActivityRE = new RegExp(
+ '(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d+)' +
+ '\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s*' +
+ '(am_\\w+)\\s*:(.*)');
+
+ // 08-28 03:58:21.834 888 3177 I am_create_activity: [0,5972200,30,com.nxp.taginfolite/.activities.MainView,android.intent.action.MAIN,NULL,NULL,270532608] // @suppress longLineCheck
+ // Store the name of the created activity only
+ const amCreateRE = new RegExp('\s*\\[.*,.*,.*,(.*),.*,.*,.*,.*\\]');
+
+ // 07-22 12:22:19.504 920 2504 I am_focused_activity: [0,com.android.systemui/.recents.RecentsActivity] // @suppress longLineCheck
+ // Store the name of the focused activity only
+ const amFocusedRE = new RegExp('\s*\\[\\d+,(.*)\\]');
+
+ // 07-21 19:56:12.315 920 2261 I am_proc_start: [0,19942,10062,com.google.android.talk,broadcast,com.google.android.talk/com.google.android.apps.hangouts.realtimechat.RealTimeChatService$AlarmReceiver] // @suppress longLineCheck
+ // We care about proc starts on behalf of activities, and store the activity
+ const amProcStartRE = new RegExp('\s*\\[\\d+,\\d+,\\d+,.*,activity,(.*)\\]');
+
+ // 07-22 12:21:43.490 2893 2893 I am_on_resume_called: [0,com.google.android.launcher.GEL] // @suppress longLineCheck
+ // Store the activity name only
+ const amOnResumeRE = new RegExp('\s*\\[\\d+,(.*)\\]');
+
+ // 07-22 12:22:19.545 2893 2893 I am_on_paused_called: [0,com.google.android.launcher.GEL] // @suppress longLineCheck
+ // Store the activity name only
+ const amOnPauseRE = new RegExp('\s*\\[\\d+,(.*)\\]');
+
+ // 08-28 03:51:54.456 888 907 I am_activity_launch_time: [0,185307115,com.google.android.googlequicksearchbox/com.google.android.launcher.GEL,1174,1174] // @suppress longLineCheck
+ // Store the activity name and launch times
+ const amLaunchTimeRE = new RegExp('\s*\\[\\d+,\\d+,(.*),(\\d+),(\\d+)');
+
+ // 08-28 03:58:15.854 888 902 I am_destroy_activity: [0,203516597,29,com.android.chrome/com.google.android.apps.chrome.Main,finish-idle] // @suppress longLineCheck
+ // Store the activity name only
+ const amDestroyRE = new RegExp('\s*\\[\\d+,\\d+,\\d+,(.*)\\]');
+
+ /**
+ * @return {boolean} True when events is an android event log array.
+ */
+ EventLogImporter.canImport = function(events) {
+ if (!(typeof(events) === 'string' || events instanceof String)) {
+ return false;
+ }
+
+ // Prevent the importer from matching this file in vulcanized traces.
+ if (/^<!DOCTYPE html>/.test(events)) return false;
+
+ return eventLogActivityRE.test(events);
+ };
+
+ EventLogImporter.prototype = {
+ __proto__: Importer.prototype,
+
+ get importerName() {
+ return 'EventLogImporter';
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ /**
+ * @return {string} the full activity name (including package) from
+ * a component
+ */
+ getFullActivityName(component) {
+ const componentSplit = component.split('/');
+ if (componentSplit[1].startsWith('.')) {
+ return componentSplit[0] + componentSplit[1];
+ }
+
+ return componentSplit[1];
+ },
+
+ /**
+ * @return {string} the process name of a component
+ */
+ getProcName(component) {
+ const componentSplit = component.split('/');
+ return componentSplit[0];
+ },
+
+ findOrCreateActivity(activityName) {
+ if (activityName in activityMap) {
+ return activityMap[activityName];
+ }
+ const activity = {
+ state: ACTIVITY_STATE.NONE,
+ name: activityName
+ };
+ activityMap[activityName] = activity;
+ return activity;
+ },
+
+ deleteActivity(activityName) {
+ delete activityMap[activityName];
+ },
+
+ handleCreateActivity(ts, activityName) {
+ const activity = this.findOrCreateActivity(activityName);
+ activity.state = ACTIVITY_STATE.CREATED;
+ activity.createdTs = ts;
+ },
+
+ handleFocusActivity(ts, procName, activityName) {
+ const activity = this.findOrCreateActivity(activityName);
+ activity.lastFocusedTs = ts;
+ },
+
+ handleProcStartForActivity(ts, activityName) {
+ const activity = this.findOrCreateActivity(activityName);
+ activity.procStartTs = ts;
+ },
+
+ handleOnResumeCalled(ts, pid, activityName) {
+ const activity = this.findOrCreateActivity(activityName);
+ activity.state = ACTIVITY_STATE.RESUMED;
+ activity.lastResumeTs = ts;
+ // on_resume_called shows the actual PID; use this
+ // to link the activity up with a process later
+ activity.pid = pid;
+ },
+
+ handleOnPauseCalled(ts, activityName) {
+ const activity = this.findOrCreateActivity(activityName);
+ activity.state = ACTIVITY_STATE.PAUSED;
+ activity.lastPauseTs = ts;
+ // Create a new AndroidActivity representing the foreground state,
+ // but only if the pause happened within the model bounds
+ if (ts > this.model_.bounds.min && ts < this.model_.bounds.max) {
+ this.addActivityToProcess(activity);
+ }
+ },
+
+ handleLaunchTime(ts, activityName, launchTime) {
+ const activity = this.findOrCreateActivity(activityName);
+ activity.launchTime = launchTime;
+ },
+
+ handleDestroyActivity(ts, activityName) {
+ this.deleteActivity(activityName);
+ },
+
+ addActivityToProcess(activity) {
+ if (activity.pid === undefined) return;
+ const process = this.model_.getOrCreateProcess(activity.pid);
+ // The range of the activity is the time from resume to time
+ // of pause; limit the start time to the beginning of the model
+ const range = tr.b.math.Range.fromExplicitRange(
+ Math.max(this.model_.bounds.min, activity.lastResumeTs),
+ activity.lastPauseTs);
+ const newActivity = new tr.model.Activity(activity.name,
+ 'Android Activity', range,
+ {created: activity.createdTs,
+ procstart: activity.procStartTs,
+ lastfocus: activity.lastFocusedTs});
+ process.activities.push(newActivity);
+ },
+
+ parseAmLine_(line) {
+ let match = eventLogActivityRE.exec(line);
+ if (!match) return;
+
+ // Possible activity life-cycles:
+ // 1) Launch from scratch:
+ // - am_create_activity
+ // - am_focused_activity
+ // - am_proc_start
+ // - am_proc_bound
+ // - am_restart_activity
+ // - am_on_resume_called
+ // 2) Re-open existing activity
+ // - am_focused_activity
+ // - am_on_resume_called
+
+ // HACK: event log date format is "MM-DD" and doesn't contain the year;
+ // to figure out the year, take the min bound of the model, convert
+ // to real-time and use that as the year.
+ // The Android event log will eventually contain the year once this
+ // CL is in a release:
+ // https://android-review.googlesource.com/#/c/168900
+ const firstRealtimeTs = this.model_.bounds.min -
+ this.model_.realtime_to_monotonic_offset_ms;
+ const year = new Date(firstRealtimeTs).getFullYear();
+ const ts = match[1].substring(0, 5) + '-' + year + ' ' +
+ match[1].substring(5, match[1].length);
+
+ const monotonicTs = Date.parse(ts) +
+ this.model_.realtime_to_monotonic_offset_ms;
+
+ const pid = match[2];
+ const action = match[5];
+ const data = match[6];
+
+ if (action === 'am_create_activity') {
+ match = amCreateRE.exec(data);
+ if (match && match.length >= 2) {
+ this.handleCreateActivity(monotonicTs,
+ this.getFullActivityName(match[1]));
+ }
+ } else if (action === 'am_focused_activity') {
+ match = amFocusedRE.exec(data);
+ if (match && match.length >= 2) {
+ this.handleFocusActivity(monotonicTs,
+ this.getProcName(match[1]), this.getFullActivityName(match[1]));
+ }
+ } else if (action === 'am_proc_start') {
+ match = amProcStartRE.exec(data);
+ if (match && match.length >= 2) {
+ this.handleProcStartForActivity(monotonicTs,
+ this.getFullActivityName(match[1]));
+ }
+ } else if (action === 'am_on_resume_called') {
+ match = amOnResumeRE.exec(data);
+ if (match && match.length >= 2) {
+ this.handleOnResumeCalled(monotonicTs, pid, match[1]);
+ }
+ } else if (action === 'am_on_paused_called') {
+ match = amOnPauseRE.exec(data);
+ if (match && match.length >= 2) {
+ this.handleOnPauseCalled(monotonicTs, match[1]);
+ }
+ } else if (action === 'am_activity_launch_time') {
+ match = amLaunchTimeRE.exec(data);
+ this.handleLaunchTime(monotonicTs,
+ this.getFullActivityName(match[1]), match[2]);
+ } else if (action === 'am_destroy_activity') {
+ match = amDestroyRE.exec(data);
+ if (match && match.length === 2) {
+ this.handleDestroyActivity(monotonicTs,
+ this.getFullActivityName(match[1]));
+ }
+ }
+ },
+
+ importEvents() {
+ // Check if we have a mapping from real-time to CLOCK_MONOTONIC
+ if (isNaN(this.model_.realtime_to_monotonic_offset_ms)) {
+ this.model_.importWarning({
+ type: 'eveng_log_clock_sync',
+ message: 'Need a trace_event_clock_sync to map realtime to import.'
+ });
+ return;
+ }
+ // Since the event log typically spans a much larger timeframe
+ // than the ftrace data, we want to calculate the bounds of the existing
+ // model, and dump all event log data outside of those bounds
+ this.model_.updateBounds();
+
+ const lines = this.events_.split('\n');
+ lines.forEach(this.parseAmLine_, this);
+
+ // Iterate over all created activities that are not destroyed yet
+ for (const activityName in activityMap) {
+ const activity = activityMap[activityName];
+ // If we're still in the foreground, store the activity anyway
+ if (activity.state === ACTIVITY_STATE.RESUMED) {
+ // Set the pause timestamp to the end of the model bounds
+ activity.lastPauseTs = this.model_.bounds.max;
+ this.addActivityToProcess(activity);
+ }
+ }
+ }
+ };
+
+ Importer.register(EventLogImporter);
+
+ return {
+ EventLogImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer.html
new file mode 100644
index 00000000000..7c9cdf5e89f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/importer/simple_line_reader.html">
+<link rel="import" href="/tracing/model/activity.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+/**
+ * @fileoverview
+ */
+'use strict';
+
+tr.exportTo('tr.e.importer.android.process_data', function() {
+ const Importer = tr.importer.Importer;
+
+ const PROCESS_DUMP_HEADER = 'PROCESS DUMP';
+
+ /**
+ * Imports android process data
+ * @constructor
+ */
+ function ProcessDataImporter(model, processData) {
+ this.model_ = model;
+ this.processDataLines = processData.split('\n');
+ this.importPriority = 3;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ ProcessDataImporter.canImport = function(events) {
+ if (!(typeof(events) === 'string' || events instanceof String)) {
+ return false;
+ }
+
+ if (events.split('\n')[0] === PROCESS_DUMP_HEADER) {
+ return true;
+ }
+
+ return false;
+ };
+
+ ProcessDataImporter.prototype = {
+ __proto__: Importer.prototype,
+
+ get importerName() {
+ return 'ProcessDataImporter';
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ parseEventData(data) {
+ const allDumpedProcesses = {};
+
+ let parseProcesses = false;
+ let parseThreads = false;
+ let legacy = false;
+
+ // Skip header on line 1.
+ for (let i = 1; i < data.length; i++) {
+ const cols = data[i].split(/\s+/);
+ if (cols[0].startsWith('USER')) {
+ if (parseProcesses) {
+ parseProcesses = false;
+ parseThreads = true;
+ } else {
+ parseThreads = false;
+ parseProcesses = true;
+ }
+
+ const colCount = cols.length;
+ if (parseProcesses && colCount === 9) {
+ legacy = false;
+ } else if (parseProcesses && colCount === 8) {
+ legacy = true;
+ }
+ continue;
+ }
+
+ if (parseProcesses) {
+ const pid = Number(cols[1]);
+ if (allDumpedProcesses[pid] === undefined) {
+ allDumpedProcesses[pid] = {};
+ }
+ allDumpedProcesses[pid] = {
+ 'name': cols[8], pid, 'comm': cols[9]
+ };
+ continue;
+ }
+
+ if (parseThreads) {
+ let pid;
+ let tid;
+ let name;
+
+ // In legacy ps dumps, the PID is actually shown in the PPID column
+ // for thread lines, but PID column for the process line
+ if (legacy) {
+ pid = Number(cols[1]);
+ if (allDumpedProcesses[pid] !== undefined) {
+ // this is a process line in the dump.
+ tid = pid;
+ } else {
+ // Thread line, so swap.
+ tid = pid;
+ pid = Number(cols[2]); // i.e. the PPID column.
+ }
+ name = cols.slice(8).join(' ');
+ } else {
+ pid = Number(cols[1]);
+ tid = Number(cols[2]);
+ name = cols.slice(3).join(' ');
+ }
+
+ // Thread data for a process we didn't identify, skip.
+ if (allDumpedProcesses[pid] === undefined) continue;
+
+ if (allDumpedProcesses[pid].threads === undefined) {
+ allDumpedProcesses[pid].threads = {};
+ }
+ allDumpedProcesses[pid].threads[tid] = {tid, name};
+ continue;
+ }
+ }
+ return allDumpedProcesses;
+ },
+
+ importEvents() {
+ // Merge the snapshots into a single object, indexable by PID.
+
+ const allDumpedProcesses = this.parseEventData(this.processDataLines);
+
+ // Update the model with the new process / thread name data.
+ const modelProcesses = this.model_.getAllProcesses();
+ for (let i = 0; i < modelProcesses.length; i++) {
+ const modelProcess = modelProcesses[i];
+ const pid = modelProcess.pid;
+ const dumpedProcess = allDumpedProcesses[pid];
+ if (dumpedProcess === undefined) {
+ // There's a process in the model that the ps dump doesn't know about,
+ // nothing we can do so skip it.
+ continue;
+ }
+
+ modelProcess.name = dumpedProcess.name;
+ const processDumpThreads = dumpedProcess.threads;
+ if (processDumpThreads !== undefined) {
+ for (const tid in modelProcess.threads) {
+ const modelThread = modelProcess.threads[tid];
+ if (Number(pid) === Number(tid)) {
+ modelThread.name = 'UI thread';
+ } else if (modelThread.name === '<...>') {
+ if (processDumpThreads[tid] !== undefined) {
+ // Use the stored thread name that we have.
+ modelThread.name = processDumpThreads[tid].name;
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ Importer.register(ProcessDataImporter);
+
+ return {
+ ProcessDataImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer_test.html
new file mode 100644
index 00000000000..f902b06229c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/android/process_data_importer_test.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/android/process_data_importer.html">
+
+<script>
+'use strict';
+
+const TEST_DATA =
+ 'PROCESS DUMP\n' +
+ 'USER PID PPID VSZ RSS WCHAN PC S NAME COMM\n' +
+ 'root 1 0 0 0 SyS_epoll_wait ffffff S test1 init\n' +
+ 'root 2 0 0 0 kthreadd 0 S test2 2\n' +
+ 'root 3 2 0 0 smpboot_thread_fn 0 S test3 3\n' +
+ 'USER PID TID CMD\n' +
+ 'root 1 1 test1\n' +
+ 'root 2 2 test2\n' +
+ 'root 3 3 test3\n' +
+ 'u0_a48 1 4 test1_t1\n' +
+ 'u0_a10 2 5 test2_t1\n' +
+ 'u0_a10 3 6 test3_t1\n' +
+ 'u0_a10 1 7 test1_t2\n' +
+ 'u0_a10 2 8 test2_t2\n' +
+ 'u0_a10 3 9 test3_t2\n' +
+ 'u0_a10 3 10 test3_t3\n';
+
+const LEGACY_TEST_DATA =
+ 'PROCESS DUMP\n' +
+ 'USER PID PPID VSIZE RSS WCHAN PC NAME\n' +
+ 'root 1 0 1324 856 SyS_epoll_ 00070cd8 S test1\n' +
+ 'root 2 0 0 0 kthreadd 00000000 S test2\n' +
+ 'root 3 0 0 0 smpboot_th 00000000 S test3\n' +
+ 'USER PID PPID VSIZE RSS WCHAN PC NAME\n' +
+ 'root 1 0 1324 856 SyS_epoll_ 00070cd8 S test1\n' +
+ 'root 2 0 0 0 kthreadd 00000000 S test2\n' +
+ 'root 3 0 0 0 smpboot_th 00000000 S test3\n' +
+ 'u0_a35 4 1 726452 34352 SyS_epoll_ ffffffff S test1_t1\n' +
+ 'u0_a35 5 2 726452 34352 SyS_epoll_ ffffffff S test2_t1\n' +
+ 'u0_a53 6 3 846540 54996 SyS_epoll_ ffffffff S test3_t1\n' +
+ 'u0_a53 7 1 846540 54996 futex_wait ffffffff S test1_t2\n' +
+ 'u0_a53 8 2 846540 54996 futex_wait ffffffff S test2_t2\n' +
+ 'u0_a53 9 3 846540 54996 futex_wait ffffffff S test3_t2\n' +
+ 'root 10 3 4692 244 poll_sched ffffffff S test3_t3\n';
+
+tr.b.unittest.testSuite(function() {
+ test('canImport', function() {
+ const canImport =
+ tr.e.importer.android.process_data.ProcessDataImporter.canImport;
+ assert.isTrue(canImport(TEST_DATA));
+ assert.isTrue(canImport(LEGACY_TEST_DATA));
+ assert.isFalse(canImport(''));
+ assert.isFalse(canImport('\n'));
+ });
+
+ test('procinfoImport', function() {
+ const importer = new tr.e.importer.android.process_data.ProcessDataImporter(
+ null, TEST_DATA);
+ const result = importer.parseEventData(importer.processDataLines);
+ verifyParsedDump(result);
+ });
+
+ test('procinfoImportLegacy', function() {
+ const importer = new tr.e.importer.android.process_data.ProcessDataImporter(
+ null, LEGACY_TEST_DATA);
+ const result = importer.parseEventData(importer.processDataLines);
+ verifyParsedDump(result);
+ });
+
+ function verifyParsedDump(result) {
+ assert.isDefined(result);
+
+ // indexable by PID, so invalid PIDs should be undefined.
+ assert.isUndefined(result[0]);
+ assert.isUndefined(result[100]);
+
+ let proc = result[1];
+ assert.isDefined(proc);
+ assert.strictEqual(proc.name, 'test1');
+ assert.strictEqual(proc.pid, 1);
+ assert.isDefined(proc.threads);
+ let thread = proc.threads[1];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test1');
+ assert.strictEqual(thread.tid, 1);
+ thread = proc.threads[4];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test1_t1');
+ assert.strictEqual(thread.tid, 4);
+ thread = proc.threads[7];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test1_t2');
+ assert.strictEqual(thread.tid, 7);
+
+ proc = result[2];
+ assert.isDefined(proc);
+ assert.strictEqual(proc.name, 'test2');
+ assert.strictEqual(proc.pid, 2);
+ assert.isDefined(proc.threads);
+ thread = proc.threads[2];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test2');
+ assert.strictEqual(thread.tid, 2);
+ thread = proc.threads[5];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test2_t1');
+ assert.strictEqual(thread.tid, 5);
+ thread = proc.threads[8];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test2_t2');
+ assert.strictEqual(thread.tid, 8);
+
+ proc = result[3];
+ assert.isDefined(proc);
+ assert.strictEqual(proc.name, 'test3');
+ assert.strictEqual(proc.pid, 3);
+ assert.isDefined(proc.threads);
+ thread = proc.threads[3];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test3');
+ assert.strictEqual(thread.tid, 3);
+ thread = proc.threads[6];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test3_t1');
+ assert.strictEqual(thread.tid, 6);
+ thread = proc.threads[9];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test3_t2');
+ assert.strictEqual(thread.tid, 9);
+ thread = proc.threads[10];
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'test3_t3');
+ assert.strictEqual(thread.tid, 10);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer.html
new file mode 100644
index 00000000000..1235a4e41d5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+
+<script>
+/**
+ * @fileoverview Imports text files in the BattOr format into the
+ * Model. This format is output by the battor_agent executable and library.
+ *
+ * This importer assumes the events arrive as a string. The unit tests provide
+ * examples of the trace format.
+ */
+'use strict';
+
+tr.exportTo('tr.e.importer.battor', function() {
+ /**
+ * Imports a BattOr power trace into a specified model.
+ * @constructor
+ */
+ function BattorImporter(model, events) {
+ this.importPriority = 3; // runs after the linux_perf importer
+ this.model_ = model;
+
+ // The list of power samples contained within the trace.
+ this.samples_ = [];
+ // The clock sync markers contained within the trace.
+ this.syncTimestampsById_ = new Map();
+
+ this.parseTrace_(events);
+ }
+
+ const battorDataLineRE = new RegExp(
+ '^(-?\\d+\\.\\d+)\\s+(-?\\d+\\.\\d+)\\s+(-?\\d+\\.\\d+)' +
+ '(?:\\s+<(\\S+)>)?$'
+ );
+ const battorHeaderLineRE = /^# BattOr/;
+
+ /**
+ * Guesses whether the provided events is a BattOr string.
+ * Looks for the magic string "# BattOr" at the start of the file,
+ *
+ * @return {boolean} True when events is a BattOr array.
+ */
+ BattorImporter.canImport = function(events) {
+ if (!(typeof(events) === 'string' || events instanceof String)) {
+ return false;
+ }
+
+ return battorHeaderLineRE.test(events);
+ };
+
+ BattorImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'BattorImporter';
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ /**
+ * Imports clock sync markers from the trace into into this.model_.
+ */
+ importClockSyncMarkers() {
+ for (const [syncId, ts] of this.syncTimestampsById_) {
+ this.model_.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.BATTOR, syncId, ts);
+ }
+ },
+
+ /**
+ * Imports the events from the trace into this.model_.
+ */
+ importEvents() {
+ if (this.model_.device.powerSeries) {
+ this.model_.importWarning({
+ type: 'import_error',
+ message: 'Power counter exists, can not import BattOr power trace.'
+ });
+ return;
+ }
+
+ const modelTimeTransformer =
+ this.model_.clockSyncManager.getModelTimeTransformer(
+ tr.model.ClockDomainId.BATTOR);
+
+ const powerSeries = this.model_.device.powerSeries =
+ new tr.model.PowerSeries(this.model_.device);
+ for (let i = 0; i < this.samples_.length; i++) {
+ const sample = this.samples_[i];
+ powerSeries.addPowerSample(
+ modelTimeTransformer(sample.ts), sample.powerInW);
+ }
+ },
+
+ /**
+ * Given the BattOr trace as a string, parse it and store the results in
+ * this.samples_ and this.syncTimestampsById_.
+ */
+ parseTrace_(trace) {
+ const lines = trace.split('\n');
+
+ for (let line of lines) {
+ line = line.trim();
+
+ if (line.length === 0) continue;
+
+ if (line.startsWith('#')) continue;
+
+ // Parse power sample.
+ const groups = battorDataLineRE.exec(line);
+ if (!groups) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Unrecognized line in BattOr trace: ' + line
+ });
+ continue;
+ }
+
+ const ts = parseFloat(groups[1]);
+ const voltageInV = tr.b.convertUnit(parseFloat(groups[2]),
+ tr.b.UnitPrefixScale.METRIC.MILLI,
+ tr.b.UnitPrefixScale.METRIC.NONE);
+ const currentInA = tr.b.convertUnit(parseFloat(groups[3]),
+ tr.b.UnitPrefixScale.METRIC.MILLI,
+ tr.b.UnitPrefixScale.METRIC.NONE);
+ const syncId = groups[4];
+
+ if (syncId) {
+ this.syncTimestampsById_.set(syncId, ts);
+ }
+
+ if (voltageInV < 0 || currentInA < 0) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'The following line in the BattOr trace has a negative ' +
+ 'voltage or current, neither of which are allowed: ' + line +
+ '. A common cause of this is that the device is charging ' +
+ 'while the trace is being recorded.'
+ });
+ continue;
+ }
+
+ this.samples_.push(new Sample(ts, voltageInV, currentInA));
+ }
+ }
+ };
+
+ /**
+ * A sample recorded by a BattOr.
+ *
+ * @param {number} ts The timestamp (in milliseconds) of the sample.
+ * @param {number} voltage The voltage (in volts) at the specified time.
+ * @param {number} current The current (in amps) at the specified time.
+ *
+ * @constructor
+ */
+ function Sample(ts, voltageInV, currentInA) {
+ this.ts = ts;
+ this.voltageInV = voltageInV;
+ this.currentInA = currentInA;
+ }
+
+ Sample.prototype = {
+ /** Returns the instantaneous power consumption (in Watts). */
+ get powerInW() { return this.voltageInV * this.currentInA; }
+ };
+
+ tr.importer.Importer.register(BattorImporter);
+
+ return {
+ BattorImporter,
+ };
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer_test.html
new file mode 100644
index 00000000000..ba9e091716d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/battor_importer_test.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/battor_importer.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const CHROMIUM_EVENTS = [
+ {
+ name: 'a', args: {}, pid: 52, ts: 0,
+ cat: 'foo', tid: 53, ph: 'B'
+ },
+ {
+ pid: 94936, tid: 5643, ts: 15000,
+ ph: 'c', cat: '__metadata', name: 'clock_sync',
+ args: {sync_id: 'ABCDEF-01234-5678-0A1B2C3D', issue_ts: 10000},
+ tts: 16496444
+ }
+ ];
+
+ const BATTOR_LINES = [
+ '# BattOr',
+ '# voltage_range [0.0, 6144.0] mV',
+ '# current_range [0.0, 2275.5] mA',
+ '# sample_rate 2000 Hz, gain 5.0x',
+ '0.000000 0.000000 4000.000000',
+ '0.500000 0.000000 4000.000000',
+ '1.000000 0.000000 4000.000000',
+ '1.500000 0.000000 4000.000000',
+ '2.000000 1.000000 4000.000000'
+ ];
+
+ test('canImport', function() {
+ assert.isFalse(tr.e.importer.battor.BattorImporter.canImport('string'));
+ assert.isFalse(tr.e.importer.battor.BattorImporter.canImport([]));
+ assert.isTrue(tr.e.importer.battor.BattorImporter.canImport(
+ BATTOR_LINES.join('\n')));
+ });
+
+ test('importExplicitClockSync', function() {
+ // Add a BattOr sample with an explicit clock sync.
+ const battorLinesWithExplicitSync = BATTOR_LINES.slice();
+ battorLinesWithExplicitSync.push(
+ '2.500000 1.000000 4000.000000 <ABCDEF-01234-5678-0A1B2C3D>');
+
+ const m = tr.c.TestUtils.newModelWithEvents(
+ [CHROMIUM_EVENTS, battorLinesWithExplicitSync.join('\n')]);
+
+ // Check to see if power samples were imported successfully.
+ assert.isDefined(m.device.powerSeries);
+
+ assert.strictEqual(m.device.powerSeries.samples[0].start, 7.5);
+ assert.strictEqual(m.device.powerSeries.samples[5].start, 10.0);
+ });
+
+ test('importExplicitClockSync_syncOnNegativeVoltageLine', function() {
+ // Add a BattOr sample with an explicit clock sync that occurs on a line
+ // with a negative voltage measurement. The sample should be ignored, but
+ // the clock sync should be counted.
+ const battorLinesWithExplicitSync = BATTOR_LINES.slice();
+ battorLinesWithExplicitSync.push(
+ '2.500000 -1.000000 4000.000000 <ABCDEF-01234-5678-0A1B2C3D>');
+
+ const m = tr.c.TestUtils.newModelWithEvents(
+ [CHROMIUM_EVENTS, battorLinesWithExplicitSync.join('\n')]);
+
+ // Check to see if power samples were imported successfully.
+ assert.isDefined(m.device.powerSeries);
+
+ assert.lengthOf(m.device.powerSeries.samples, 5);
+ assert.strictEqual(m.device.powerSeries.samples[0].start, 7.5);
+ });
+
+ test('explicitClockSyncWithoutSyncMarkers', function() {
+ // Create an empty model.
+ let m = new tr.Model();
+ const io = new tr.importer.ImportOptions();
+ io.showImportWarnings = false;
+ m.importOptions = io;
+
+ // Add a BattOr sample with an explicit clock sync.
+ const battorLinesWithExplicitSync = BATTOR_LINES.slice();
+ battorLinesWithExplicitSync.push(
+ '2.500000 1.000000 4000.000000 <ABCDEF-01234-5678-0A1B2C3D>');
+
+ m = tr.c.TestUtils.newModelWithEvents(
+ [battorLinesWithExplicitSync.join('\n')]);
+
+ assert.lengthOf(m.device.powerSeries.samples, 6);
+ assert.strictEqual(m.device.powerSeries.samples[0].start, 0);
+ assert.strictEqual(m.device.powerSeries.samples[5].start, 2.5);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer.html
new file mode 100644
index 00000000000..73ffc41b4c1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+/**
+ * @fileoverview Blah.
+ */
+'use strict';
+
+tr.exportTo('tr.e.importer.ddms', function() {
+ const kPid = 0;
+ const kCategory = 'java';
+ const kMethodLutEndMarker = '\n*end\n';
+ const kThreadsStart = '\n*threads\n';
+ const kMethodsStart = '\n*methods\n';
+
+ const kTraceMethodEnter = 0x00; // method entry
+ const kTraceMethodExit = 0x01; // method exit
+ const kTraceUnroll = 0x02; // method exited by exception unrolling
+ // 0x03 currently unused
+ const kTraceMethodActionMask = 0x03; // two bits
+
+ const kTraceHeaderLength = 32;
+ const kTraceMagicValue = 0x574f4c53;
+ const kTraceVersionSingleClock = 2;
+ const kTraceVersionDualClock = 3;
+ const kTraceRecordSizeSingleClock = 10; // using v2
+ const kTraceRecordSizeDualClock = 14; // using v3 with two timestamps
+
+ function Reader(stringPayload) {
+ this.position_ = 0;
+ this.data_ = new Uint8Array(stringPayload.length);
+ for (let i = 0; i < stringPayload.length; ++i) {
+ this.data_[i] = stringPayload.charCodeAt(i);
+ }
+ }
+
+ Reader.prototype = {
+ __proto__: Object.prototype,
+
+ uint8() {
+ const result = this.data_[this.position_];
+ this.position_ += 1;
+ return result;
+ },
+
+ uint16() {
+ let result = 0;
+ result += this.uint8();
+ result += this.uint8() << 8;
+ return result;
+ },
+
+ uint32() {
+ let result = 0;
+ result += this.uint8();
+ result += this.uint8() << 8;
+ result += this.uint8() << 16;
+ result += this.uint8() << 24;
+ return result;
+ },
+
+ uint64() {
+ // Javascript isn't able to manage 64-bit numeric values.
+ const low = this.uint32();
+ const high = this.uint32();
+ const lowStr = ('0000000' + low.toString(16)).substr(-8);
+ const highStr = ('0000000' + high.toString(16)).substr(-8);
+ const result = highStr + lowStr;
+ return result;
+ },
+
+ seekTo(position) {
+ this.position_ = position;
+ },
+
+ hasMore() {
+ return this.position_ < this.data_.length;
+ }
+ };
+
+ /**
+ * Imports DDMS method tracing events into a specified model.
+ * @constructor
+ */
+ function DdmsImporter(model, data) {
+ this.importPriority = 3;
+ this.model_ = model;
+ this.data_ = data;
+ }
+
+ /**
+ * Guesses whether the provided events is from a DDMS method trace.
+ * @return {boolean} True when events is a DDMS method trace.
+ */
+ DdmsImporter.canImport = function(data) {
+ if (typeof(data) === 'string' || data instanceof String) {
+ const header = data.slice(0, 1000);
+ return header.startsWith('*version\n') &&
+ header.indexOf('\nvm=') >= 0 &&
+ header.indexOf(kThreadsStart) >= 0;
+ }
+ /* key bit */
+ return false;
+ };
+
+ DdmsImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'DdmsImporter';
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ /**
+ * Imports the data in this.data_ into this.model_.
+ */
+ importEvents() {
+ const divider = this.data_.indexOf(kMethodLutEndMarker) +
+ kMethodLutEndMarker.length;
+ this.metadata_ = this.data_.slice(0, divider);
+ this.methods_ = {};
+ this.parseThreads();
+ this.parseMethods();
+
+ const traceReader = new Reader(this.data_.slice(divider));
+ const magic = traceReader.uint32();
+ if (magic !== kTraceMagicValue) {
+ throw Error('Failed to match magic value');
+ }
+ this.version_ = traceReader.uint16();
+ if (this.version_ !== kTraceVersionDualClock) {
+ throw Error('Unknown version');
+ }
+ const dataOffest = traceReader.uint16();
+ const startDateTime = traceReader.uint64();
+ const recordSize = traceReader.uint16();
+
+ traceReader.seekTo(dataOffest);
+
+ while (traceReader.hasMore()) {
+ this.parseTraceEntry(traceReader);
+ }
+ },
+
+ parseTraceEntry(reader) {
+ const tid = reader.uint16();
+ const methodPacked = reader.uint32();
+ const cpuSinceStart = reader.uint32();
+ const wallClockSinceStart = reader.uint32();
+ let method = methodPacked & ~kTraceMethodActionMask;
+ const action = methodPacked & kTraceMethodActionMask;
+ const thread = this.getTid(tid);
+ method = this.getMethodName(method);
+ if (action === kTraceMethodEnter) {
+ thread.sliceGroup.beginSlice(kCategory, method, wallClockSinceStart,
+ undefined, cpuSinceStart);
+ } else if (thread.sliceGroup.openSliceCount) {
+ thread.sliceGroup.endSlice(wallClockSinceStart, cpuSinceStart);
+ }
+ },
+
+ parseThreads() {
+ let threads = this.metadata_.slice(this.metadata_.indexOf(kThreadsStart) +
+ kThreadsStart.length);
+ threads = threads.slice(0, threads.indexOf('\n*'));
+ threads = threads.split('\n');
+ threads.forEach(this.parseThread.bind(this));
+ },
+
+ parseThread(threadLine) {
+ const tid = threadLine.slice(0, threadLine.indexOf('\t'));
+ const thread = this.getTid(parseInt(tid));
+ thread.name = threadLine.slice(threadLine.indexOf('\t') + 1);
+ },
+
+ getTid(tid) {
+ return this.model_.getOrCreateProcess(kPid)
+ .getOrCreateThread(tid);
+ },
+
+ parseMethods() {
+ let methods = this.metadata_.slice(this.metadata_.indexOf(kMethodsStart) +
+ kMethodsStart.length);
+ methods = methods.slice(0, methods.indexOf('\n*'));
+ methods = methods.split('\n');
+ methods.forEach(this.parseMethod.bind(this));
+ },
+
+ parseMethod(methodLine) {
+ const data = methodLine.split('\t');
+ const methodId = parseInt(data[0]);
+ const methodName = data[1] + '.' + data[2] + data[3];
+ this.addMethod(methodId, methodName);
+ },
+
+ addMethod(methodId, methodName) {
+ this.methods_[methodId] = methodName;
+ },
+
+ getMethodName(methodId) {
+ return this.methods_[methodId];
+ }
+ };
+
+ // Register the DdmsImporter to the Importer.
+ tr.importer.Importer.register(DdmsImporter);
+
+ return {
+ DdmsImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer_test.html
new file mode 100644
index 00000000000..a516751e47b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/ddms_importer_test.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/ddms_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+
+ test('canImport', function() {
+ assert.isFalse(tr.e.importer.ddms.DdmsImporter.canImport('string'));
+ assert.isFalse(tr.e.importer.ddms.DdmsImporter.canImport([]));
+ assert.isTrue(tr.e.importer.ddms.DdmsImporter.canImport(TEST_DATA));
+ });
+
+ test('parseThreads', function() {
+ const m = tr.c.TestUtils.newModelWithEvents(TEST_DATA, {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ let threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 2);
+ threads = m.findAllThreadsNamed('main');
+ assert.strictEqual(threads.length, 1);
+ const thread = threads[0];
+ assert.strictEqual(thread.tid, 2703);
+ });
+
+ test('parseMethods', function() {
+ const m = tr.c.TestUtils.newModelWithEvents(TEST_DATA, {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.findAllThreadsNamed('Binder_1');
+ assert.strictEqual(threads.length, 1);
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.length, 22);
+ assert.strictEqual('android.os.Binder.execTransact(IJJI)Z',
+ thread.sliceGroup.slices[0].title);
+ });
+
+ const TEST_DATA = Base64.atob(
+ 'KnZlcnNpb24KMwpkYXRhLWZpbGUtb3ZlcmZsb3c9ZmFsc2UKY2' +
+ 'xvY2s9ZHVhbAplbGFwc2VkLXRpbWUtdXNlYz02MzMwNzc5Cm51' +
+ 'bS1tZXRob2QtY2FsbHM9NzYKY2xvY2stY2FsbC1vdmVyaGVhZC' +
+ '1uc2VjPTMzNDMKdm09YXJ0Cip0aHJlYWRzCjI3MDMJbWFpbgoy' +
+ 'NzEwCUhlYXAgdGhyZWFkIHBvb2wgd29ya2VyIHRocmVhZCAxCj' +
+ 'I3MDkJSGVhcCB0aHJlYWQgcG9vbCB3b3JrZXIgdGhyZWFkIDAK' +
+ 'MjcxMQlIZWFwIHRocmVhZCBwb29sIHdvcmtlciB0aHJlYWQgMg' +
+ 'oyNzEyCVNpZ25hbCBDYXRjaGVyCjI3MTMJSkRXUAoyNzE0CVJl' +
+ 'ZmVyZW5jZVF1ZXVlRGFlbW9uCjI3MTUJRmluYWxpemVyRGFlbW' +
+ '9uCjI3MTYJRmluYWxpemVyV2F0Y2hkb2dEYWVtb24KMjcxNwlI' +
+ 'ZWFwVHJpbW1lckRhZW1vbgoyNzE4CUdDRGFlbW9uCjI3MTkJQm' +
+ 'luZGVyXzEKMjcyMAlCaW5kZXJfMgoyNzI3CVJlbmRlclRocmVh' +
+ 'ZAoyNzI4CUFzeW5jVGFzayAjMQoyNzI5CUFzeW5jVGFzayAjMg' +
+ 'oyNzMwCUJpbmRlcl8zCjExNTk4CWh3dWlUYXNrMQoxMTU5OQlo' +
+ 'd3VpVGFzazIKMTE2MDAJQXN5bmNUYXNrICMzCjExNjAxCUFzeW' +
+ '5jVGFzayAjNAoxMTY3MwlBc3luY1Rhc2sgIzUKKm1ldGhvZHMK' +
+ 'MHg3MGZiNzc1OAlkYWx2aWsuc3lzdGVtLkNsb3NlR3VhcmQJY2' +
+ 'xvc2UJKClWCUNsb3NlR3VhcmQuamF2YQoweDcwZmI4NDE4CWRh' +
+ 'bHZpay5zeXN0ZW0uVk1EZWJ1ZwlzdGFydE1ldGhvZFRyYWNpbm' +
+ 'cJKExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2lvL0ZpbGVEZXNj' +
+ 'cmlwdG9yO0lJWkkpVglWTURlYnVnLmphdmEKMHg3MGZlYTcyOA' +
+ 'lqYXZhLnV0aWwuQXJyYXlMaXN0CXNpemUJKClJCUFycmF5TGlz' +
+ 'dC5qYXZhCjB4NzEwMTNmYTgJbGliY29yZS5pby5CbG9ja0d1YX' +
+ 'JkT3MJY2xvc2UJKExqYXZhL2lvL0ZpbGVEZXNjcmlwdG9yOylW' +
+ 'CUJsb2NrR3VhcmRPcy5qYXZhCjB4NzEwNTc3NDgJYW5kcm9pZC' +
+ '5vcy5CaW5kZXIJZXhlY1RyYW5zYWN0CShJSkpJKVoJQmluZGVy' +
+ 'LmphdmEKMHg3MTA3MTdlOAlhbmRyb2lkLmFwcC5BY3Rpdml0eV' +
+ 'RocmVhZCRBcHBsaWNhdGlvblRocmVhZAlwcm9maWxlckNvbnRy' +
+ 'b2wJKFpMYW5kcm9pZC9hcHAvUHJvZmlsZXJJbmZvO0kpVglBY3' +
+ 'Rpdml0eVRocmVhZC5qYXZhCjB4NzEwNzJhNDgJYW5kcm9pZC5h' +
+ 'cHAuQXBwbGljYXRpb25UaHJlYWROYXRpdmUJb25UcmFuc2FjdA' +
+ 'koSUxhbmRyb2lkL29zL1BhcmNlbDtMYW5kcm9pZC9vcy9QYXJj' +
+ 'ZWw7SSlaCUFwcGxpY2F0aW9uVGhyZWFkTmF0aXZlLmphdmEKMH' +
+ 'g3MTA3MmYyOAlhbmRyb2lkLm9zLkhhbmRsZXIJZW5xdWV1ZU1l' +
+ 'c3NhZ2UJKExhbmRyb2lkL29zL01lc3NhZ2VRdWV1ZTtMYW5kcm' +
+ '9pZC9vcy9NZXNzYWdlO0opWglIYW5kbGVyLmphdmEKMHg3MTA3' +
+ 'MmZlOAlhbmRyb2lkLm9zLkhhbmRsZXIJZGlzcGF0Y2hNZXNzYW' +
+ 'dlCShMYW5kcm9pZC9vcy9NZXNzYWdlOylWCUhhbmRsZXIuamF2' +
+ 'YQoweDcxMDczNTI4CWFuZHJvaWQub3MuSGFuZGxlcglzZW5kTW' +
+ 'Vzc2FnZQkoTGFuZHJvaWQvb3MvTWVzc2FnZTspWglIYW5kbGVy' +
+ 'LmphdmEKMHg3MTA3MzU4OAlhbmRyb2lkLm9zLkhhbmRsZXIJc2' +
+ 'VuZE1lc3NhZ2VBdFRpbWUJKExhbmRyb2lkL29zL01lc3NhZ2U7' +
+ 'SilaCUhhbmRsZXIuamF2YQoweDcxMDczNWI4CWFuZHJvaWQub3' +
+ 'MuSGFuZGxlcglzZW5kTWVzc2FnZURlbGF5ZWQJKExhbmRyb2lk' +
+ 'L29zL01lc3NhZ2U7SilaCUhhbmRsZXIuamF2YQoweDcxMDczNj' +
+ 'Q4CWFuZHJvaWQuYXBwLkFjdGl2aXR5VGhyZWFkJEgJaGFuZGxl' +
+ 'TWVzc2FnZQkoTGFuZHJvaWQvb3MvTWVzc2FnZTspVglBY3Rpdm' +
+ 'l0eVRocmVhZC5qYXZhCjB4NzEwNzM3NjgJYW5kcm9pZC5hcHAu' +
+ 'QWN0aXZpdHlUaHJlYWQkUHJvZmlsZXIJc3RhcnRQcm9maWxpbm' +
+ 'cJKClWCUFjdGl2aXR5VGhyZWFkLmphdmEKMHg3MTA3Mzc5OAlh' +
+ 'bmRyb2lkLmFwcC5BY3Rpdml0eVRocmVhZCRQcm9maWxlcglzdG' +
+ '9wUHJvZmlsaW5nCSgpVglBY3Rpdml0eVRocmVhZC5qYXZhCjB4' +
+ 'NzEwYzQ2ZDgJYW5kcm9pZC5vcy5NZXNzYWdlCW9idGFpbgkoKU' +
+ 'xhbmRyb2lkL29zL01lc3NhZ2U7CU1lc3NhZ2UuamF2YQoweDcx' +
+ 'MGM0YTM4CWFuZHJvaWQub3MuTWVzc2FnZQlpc0luVXNlCSgpWg' +
+ 'lNZXNzYWdlLmphdmEKMHg3MTBjNGE2OAlhbmRyb2lkLm9zLk1l' +
+ 'c3NhZ2UJbWFya0luVXNlCSgpVglNZXNzYWdlLmphdmEKMHg3MT' +
+ 'BjNGFmOAlhbmRyb2lkLm9zLk1lc3NhZ2UJcmVjeWNsZVVuY2hl' +
+ 'Y2tlZAkoKVYJTWVzc2FnZS5qYXZhCjB4NzEwYzRmMTgJYW5kcm' +
+ '9pZC5vcy5QYXJjZWwJaW5pdAkoSilWCVBhcmNlbC5qYXZhCjB4' +
+ 'NzEwYzRmYTgJYW5kcm9pZC5vcy5QYXJjZWwJb2J0YWluCShKKU' +
+ 'xhbmRyb2lkL29zL1BhcmNlbDsJUGFyY2VsLmphdmEKMHg3MTBj' +
+ 'NTQyOAlhbmRyb2lkLm9zLlBhcmNlbAllbmZvcmNlSW50ZXJmYW' +
+ 'NlCShMamF2YS9sYW5nL1N0cmluZzspVglQYXJjZWwuamF2YQow' +
+ 'eDcxMGM1OWY4CWFuZHJvaWQub3MuUGFyY2VsCXJlYWRJbnQJKC' +
+ 'lJCVBhcmNlbC5qYXZhCjB4NzEwYzZhNDgJYW5kcm9pZC5vcy5Q' +
+ 'YXJjZWxGaWxlRGVzY3JpcHRvcgljbG9zZVdpdGhTdGF0dXMJKE' +
+ 'lMamF2YS9sYW5nL1N0cmluZzspVglQYXJjZWxGaWxlRGVzY3Jp' +
+ 'cHRvci5qYXZhCjB4NzEwYzZkNzgJYW5kcm9pZC5vcy5QYXJjZW' +
+ 'xGaWxlRGVzY3JpcHRvcgl3cml0ZUNvbW1TdGF0dXNBbmRDbG9z' +
+ 'ZQkoSUxqYXZhL2xhbmcvU3RyaW5nOylWCVBhcmNlbEZpbGVEZX' +
+ 'NjcmlwdG9yLmphdmEKMHg3MTBjNmUwOAlhbmRyb2lkLm9zLlBh' +
+ 'cmNlbEZpbGVEZXNjcmlwdG9yCWNsb3NlCSgpVglQYXJjZWxGaW' +
+ 'xlRGVzY3JpcHRvci5qYXZhCjB4NzEwYzZmZTgJYW5kcm9pZC5v' +
+ 'cy5QYXJjZWxGaWxlRGVzY3JpcHRvcglyZWxlYXNlUmVzb3VyY2' +
+ 'VzCSgpVglQYXJjZWxGaWxlRGVzY3JpcHRvci5qYXZhCjB4NzEx' +
+ 'NWIwZjgJYW5kcm9pZC5vcy5NZXNzYWdlUXVldWUJZW5xdWV1ZU' +
+ '1lc3NhZ2UJKExhbmRyb2lkL29zL01lc3NhZ2U7SilaCU1lc3Nh' +
+ 'Z2VRdWV1ZS5qYXZhCjB4NzExNWIyMTgJYW5kcm9pZC5vcy5NZX' +
+ 'NzYWdlUXVldWUJbmV4dAkoKUxhbmRyb2lkL29zL01lc3NhZ2U7' +
+ 'CU1lc3NhZ2VRdWV1ZS5qYXZhCjB4NzE3ZTM2MzAJZGFsdmlrLn' +
+ 'N5c3RlbS5WTURlYnVnCXN0YXJ0TWV0aG9kVHJhY2luZ0ZkCShM' +
+ 'amF2YS9sYW5nL1N0cmluZztMamF2YS9pby9GaWxlRGVzY3JpcH' +
+ 'RvcjtJSVpJKVYJVk1EZWJ1Zy5qYXZhCjB4NzE4MTBhMjAJamF2' +
+ 'YS5pby5GaWxlRGVzY3JpcHRvcglpc1NvY2tldAkoKVoJRmlsZU' +
+ 'Rlc2NyaXB0b3IuamF2YQoweDcxODEwYWUwCWphdmEuaW8uRmls' +
+ 'ZURlc2NyaXB0b3IJdmFsaWQJKClaCUZpbGVEZXNjcmlwdG9yLm' +
+ 'phdmEKMHg3MTgxYmI4MAlsaWJjb3JlLmlvLklvVXRpbHMJY2xv' +
+ 'c2UJKExqYXZhL2lvL0ZpbGVEZXNjcmlwdG9yOylWCUlvVXRpbH' +
+ 'MuamF2YQoweDcxODFiYmIwCWxpYmNvcmUuaW8uSW9VdGlscwlj' +
+ 'bG9zZVF1aWV0bHkJKExqYXZhL2lvL0ZpbGVEZXNjcmlwdG9yOy' +
+ 'lWCUlvVXRpbHMuamF2YQoweDcxODIxZWUwCWFuZHJvaWQuYXBw' +
+ 'LkFjdGl2aXR5VGhyZWFkCWFjY2VzcyQzMDAJKExhbmRyb2lkL2' +
+ 'FwcC9BY3Rpdml0eVRocmVhZDtJTGphdmEvbGFuZy9PYmplY3Q7' +
+ 'SUkpVglBY3Rpdml0eVRocmVhZC5qYXZhCjB4NzE4MjJhZTAJYW' +
+ '5kcm9pZC5hcHAuQWN0aXZpdHlUaHJlYWQJc2VuZE1lc3NhZ2UJ' +
+ 'KElMamF2YS9sYW5nL09iamVjdDtJSSlWCUFjdGl2aXR5VGhyZW' +
+ 'FkLmphdmEKMHg3MTgyMmIxMAlhbmRyb2lkLmFwcC5BY3Rpdml0' +
+ 'eVRocmVhZAlzZW5kTWVzc2FnZQkoSUxqYXZhL2xhbmcvT2JqZW' +
+ 'N0O0lJWilWCUFjdGl2aXR5VGhyZWFkLmphdmEKMHg3MTgyMzIz' +
+ 'MAlhbmRyb2lkLmFwcC5BY3Rpdml0eVRocmVhZAloYW5kbGVQcm' +
+ '9maWxlckNvbnRyb2wJKFpMYW5kcm9pZC9hcHAvUHJvZmlsZXJJ' +
+ 'bmZvO0kpVglBY3Rpdml0eVRocmVhZC5qYXZhCjB4NzE4MzY3MD' +
+ 'AJYW5kcm9pZC5vcy5EZWJ1ZwlzdG9wTWV0aG9kVHJhY2luZwko' +
+ 'KVYJRGVidWcuamF2YQoqZW5kClNMT1cDACAASxycoWkAAAAOAA' +
+ 'AAAAAAAAAAAAAAAAAAjwoxNn5xxtotAKISAgCPChmE+3CT4y0A' +
+ '/hsCAI8KaTcHcbbkLQAtNQIAjwoIbgxx0eQtAEc1AgCPCkhqDH' +
+ 'Hk5C0AWjUCAI8KWHf7cPLkLQBoNQIAjwpZd/tw/eQtAHM1AgCP' +
+ 'CnhtDHED5S0AeTUCAI8KeW0McQjlLQB+NQIAjwqwu4FxFOUtAI' +
+ 'o1AgCPCoC7gXEa5S0AkDUCAI8K4AqBcSPlLQCaNQIAjwrhCoFx' +
+ 'KuUtAKE1AgCPCqg/AXE65S0AsTUCAI8KIAqBcUDlLQC2NQIAjw' +
+ 'ohCoFxTuUtAMU1AgCPCqk/AXFn5S0A3jUCAI8KgbuBcWzlLQDi' +
+ 'NQIAjwqxu4Fxb+UtAOU1AgCPCuhvDHF15S0A6zUCAI8K6W8McX' +
+ 'nlLQDvNQIAjwpJagxxfOUtAPI1AgCPCgluDHGB5S0A9zUCAI8K' +
+ 'MTKCcYXlLQD6NQIAjwpJNgdxjOUtAAM2AgCPCukvB3GS5S0ACT' +
+ 'YCAI8K+EoMcaTlLQAbNgIAjwr5SgxxwOUtADY2AgCPChiyFXHH' +
+ '5S0APTYCAI8KKKf+cPHlLQBnNgIAjwopp/5w++UtAHE2AgCfCk' +
+ 'h3BXHy2wAAu5RgAJ8KqE8McSvcAADwlGAAnwoYTwxxaNwAACyV' +
+ 'YACfChlPDHGD3AAARpVgAJ8KqU8McZbcAABZlWAAnwqoTwxxrN' +
+ 'wAAG6VYACfChhPDHHL3AAAjpVgAJ8KGU8MceDcAACjlWAAnwqp' +
+ 'Twxx8twAALSVYACfCkgqB3Ea3QAA3ZVgAJ8KKFQMcUHdAAAFlm' +
+ 'AAnwopVAxxjN0AAFGWYACfCvhZDHHD3QAAiJZgAJ8K+VkMcejd' +
+ 'AACslmAAnwr4WQxxAN4AAMOWYACfCvlZDHEa3gAA3ZZgAJ8K+F' +
+ 'kMcS/eAADylmAAnwr5WQxxR94AAAqXYACfCugXB3Fv3gAAM5dg' +
+ 'AJ8K4B6CcYveAABPl2AAnwrgKoJxrN4AAHCXYACfChArgnHQ3g' +
+ 'AAkpdgAJ8K2EYMceTeAACml2AAnwrZRgxxDd8AANCXYACfCig1' +
+ 'B3Er3wAA7pdgAJ8KuDUHcT7fAAAAmGAAnwqINQdxa98AAC+YYA' +
+ 'CfCigvB3GD3wAASJhgAJ8K+LAVcaDfAABjmGAAnwo4Sgxxtt8A' +
+ 'AHiYYACfCjlKDHHL3wAAkJhgAJ8KaEoMce3fAACxmGAAnwppSg' +
+ 'xxAuAAAMWYYACfCvmwFXFz4AAAOJlgAI8KGbIVcU3mLQBAmWAA' +
+ 'jwroLwdxVuYtAEmZYACfCikvB3GG4AAASZlgAI8KSDYHcVzmLQ' +
+ 'BPmWAAjwowMoJxZuYtAFmZYACfCok1B3GW4AAAWJlgAI8KmDcH' +
+ 'cXTmLQBnmWAAnwq5NQdxr+AAAHGZYACPCgBng3GC5i0AdplgAJ' +
+ '8KKTUHcb3gAAB/mWAAnwoRK4JxzOAAAI6ZYAA=');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer.html
new file mode 100644
index 00000000000..c0e928ebdc4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer.html
@@ -0,0 +1,483 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/importer/etw/eventtrace_parser.html">
+<link rel="import" href="/tracing/extras/importer/etw/process_parser.html">
+<link rel="import" href="/tracing/extras/importer/etw/thread_parser.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+/**
+ * @fileoverview Imports JSON file with the raw payloads from a Windows event
+ * trace into the Model. This format is outputted by Chrome running
+ * on a Windows system.
+ *
+ * This importer assumes the events arrived as a JSON file and the payloads are
+ * undecoded sequence of bytes in hex format string. The unit tests provide
+ * examples of the trace format.
+ *
+ * The format of the system trace is
+ * {
+ * name: 'ETW',
+ * content: [ <events> ]
+ * }
+ *
+ * where the <events> are dictionary values with fields.
+ *
+ * {
+ * guid: "1234-...", // The unique GUID for the event.
+ * op: 12, // The opcode of the event.
+ * ver: 1, // The encoding version of the event.
+ * cpu: 0, // The cpu id on which the event was captured.
+ * ts: 1092, // The thread id on which the event was captured.
+ * payload: "aaaa" // A base64 encoded string of the raw payload.
+ * }
+ *
+ * The payload is an undecoded version of the raw event sent by ETW.
+ * This importer uses specific parsers to decode recognized events.
+ * A parser need to register the recognized event by calling
+ * registerEventHandler(guid, opcode, handler). The parser is responsible to
+ * decode the payload and update the Model.
+ *
+ * The payload formats are described there:
+ * http://msdn.microsoft.com/en-us/library/windows/desktop/aa364085(v=vs.85).aspx
+ *
+ */
+'use strict';
+
+tr.exportTo('tr.e.importer.etw', function() {
+ // GUID and opcode of a Thread DCStart event, as defined at the link above.
+ const kThreadGuid = '3D6FA8D1-FE05-11D0-9DDA-00C04FD7BA7C';
+ const kThreadDCStartOpcode = 3;
+
+ /**
+ * Represents the raw bytes payload decoder.
+ * @constructor
+ */
+ function Decoder() {
+ this.payload_ = new DataView(new ArrayBuffer(256));
+ }
+
+ Decoder.prototype = {
+ __proto__: Object.prototype,
+
+ reset(base64Payload) {
+ const decodedSize = tr.b.Base64.getDecodedBufferLength(base64Payload);
+ if (decodedSize > this.payload_.byteLength) {
+ this.payload_ = new DataView(new ArrayBuffer(decodedSize));
+ }
+
+ tr.b.Base64.DecodeToTypedArray(base64Payload, this.payload_);
+ this.position_ = 0;
+ },
+
+ skip(length) {
+ this.position_ += length;
+ },
+
+ decodeUInt8() {
+ const result = this.payload_.getUint8(this.position_, true);
+ this.position_ += 1;
+ return result;
+ },
+
+ decodeUInt16() {
+ const result = this.payload_.getUint16(this.position_, true);
+ this.position_ += 2;
+ return result;
+ },
+
+ decodeUInt32() {
+ const result = this.payload_.getUint32(this.position_, true);
+ this.position_ += 4;
+ return result;
+ },
+
+ decodeUInt64ToString() {
+ // Javascript isn't able to manage 64-bit numeric values.
+ const low = this.decodeUInt32();
+ const high = this.decodeUInt32();
+ const lowStr = ('0000000' + low.toString(16)).substr(-8);
+ const highStr = ('0000000' + high.toString(16)).substr(-8);
+ const result = highStr + lowStr;
+ return result;
+ },
+
+ decodeInt8() {
+ const result = this.payload_.getInt8(this.position_, true);
+ this.position_ += 1;
+ return result;
+ },
+
+ decodeInt16() {
+ const result = this.payload_.getInt16(this.position_, true);
+ this.position_ += 2;
+ return result;
+ },
+
+ decodeInt32() {
+ const result = this.payload_.getInt32(this.position_, true);
+ this.position_ += 4;
+ return result;
+ },
+
+ decodeInt64ToString() {
+ // Javascript isn't able to manage 64-bit numeric values.
+ // Fallback to unsigned 64-bit hexa value.
+ return this.decodeUInt64ToString();
+ },
+
+ decodeUInteger(is64) {
+ if (is64) {
+ return this.decodeUInt64ToString();
+ }
+ return this.decodeUInt32();
+ },
+
+ decodeString() {
+ let str = '';
+ while (true) {
+ const c = this.decodeUInt8();
+ if (!c) return str;
+ str = str + String.fromCharCode(c);
+ }
+ },
+
+ decodeW16String() {
+ let str = '';
+ while (true) {
+ const c = this.decodeUInt16();
+ if (!c) return str;
+ str = str + String.fromCharCode(c);
+ }
+ },
+
+ decodeFixedW16String(length) {
+ const oldPosition = this.position_;
+ let str = '';
+ for (let i = 0; i < length; i++) {
+ const c = this.decodeUInt16();
+ if (!c) break;
+ str = str + String.fromCharCode(c);
+ }
+
+ // Move the position after the fixed buffer (i.e. wchar[length]).
+ this.position_ = oldPosition + 2 * length;
+ return str;
+ },
+
+ decodeBytes(length) {
+ const bytes = [];
+ for (let i = 0; i < length; ++i) {
+ const c = this.decodeUInt8();
+ bytes.push(c);
+ }
+ return bytes;
+ },
+
+ decodeSID(is64) {
+ // Decode the TOKEN_USER structure.
+ const pSid = this.decodeUInteger(is64);
+ const attributes = this.decodeUInt32();
+
+ // Skip padding.
+ if (is64) {
+ this.decodeUInt32();
+ }
+
+ // Decode the SID structure.
+ const revision = this.decodeUInt8();
+ const subAuthorityCount = this.decodeUInt8();
+ this.decodeUInt16();
+ this.decodeUInt32();
+
+ if (revision !== 1) {
+ throw new Error(
+ 'Invalid SID revision: could not decode the SID structure.');
+ }
+
+ const sid = this.decodeBytes(4 * subAuthorityCount);
+
+ return {
+ pSid,
+ attributes,
+ sid
+ };
+ },
+
+ decodeSystemTime() {
+ // Decode the SystemTime structure.
+ const wYear = this.decodeInt16();
+ const wMonth = this.decodeInt16();
+ const wDayOfWeek = this.decodeInt16();
+ const wDay = this.decodeInt16();
+ const wHour = this.decodeInt16();
+ const wMinute = this.decodeInt16();
+ const wSecond = this.decodeInt16();
+ const wMilliseconds = this.decodeInt16();
+ return {
+ wYear,
+ wMonth,
+ wDayOfWeek,
+ wDay,
+ wHour,
+ wMinute,
+ wSecond,
+ wMilliseconds
+ };
+ },
+
+ decodeTimeZoneInformation() {
+ // Decode the TimeZoneInformation structure.
+ const bias = this.decodeUInt32();
+ const standardName = this.decodeFixedW16String(32);
+ const standardDate = this.decodeSystemTime();
+ const standardBias = this.decodeUInt32();
+ const daylightName = this.decodeFixedW16String(32);
+ const daylightDate = this.decodeSystemTime();
+ const daylightBias = this.decodeUInt32();
+ return {
+ bias,
+ standardName,
+ standardDate,
+ standardBias,
+ daylightName,
+ daylightDate,
+ daylightBias
+ };
+ }
+
+ };
+
+ /**
+ * Imports Windows ETW kernel events into a specified model.
+ * @constructor
+ */
+ function EtwImporter(model, events) {
+ this.importPriority = 3;
+ this.model_ = model;
+ this.events_ = events;
+ this.handlers_ = {};
+ this.decoder_ = new Decoder();
+ this.walltime_ = undefined;
+ this.ticks_ = undefined;
+ this.is64bit_ = undefined;
+
+ // A map of tids to their process pid. On Windows, the tid is global to
+ // the system and doesn't need to belong to a process. As many events
+ // only provide tid, this map allows to retrieve the parent process.
+ this.tidsToPid_ = {};
+
+ // Instantiate the parsers; this will register handlers for known events.
+ const allTypeInfos = tr.e.importer.etw.Parser.getAllRegisteredTypeInfos();
+ this.parsers_ = allTypeInfos.map(
+ function(typeInfo) {
+ return new typeInfo.constructor(this);
+ }, this);
+ }
+
+ /**
+ * Guesses whether the provided events is from a Windows ETW trace.
+ * The object must has a property named 'name' with the value 'ETW' and
+ * a property 'content' with all the undecoded events.
+ *
+ * @return {boolean} True when events is a Windows ETW array.
+ */
+ 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) {
+ // Kernel threads are not defined.
+ 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);
+ },
+
+ /*
+ * Retrieve the Cpu for a given cpuNumber.
+ * @return {Cpu} A Cpu corresponding to the given cpuNumber.
+ */
+ getOrCreateCpu(cpuNumber) {
+ const cpu = this.model_.kernel.getOrCreateCpu(cpuNumber);
+ return cpu;
+ },
+
+ /**
+ * Imports the data in this.events_ into this.model_.
+ */
+ 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.');
+ }
+
+ this.events_.content.forEach(this.parseEvent.bind(this));
+ },
+
+ importTimestamp(timestamp) {
+ const ts = parseInt(timestamp, 16);
+ return (ts - this.walltime_ + this.ticks_) / 1000.;
+ },
+
+ parseInfo(event) {
+ // Retrieve clock sync information.
+ 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);
+ }
+
+ // Retrieve pointer size information from a Thread.DCStart event.
+ 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;
+ }
+
+ const timestamp = this.importTimestamp(event.ts);
+
+ // Create the event header.
+ const header = {
+ guid: event.guid,
+ opcode: event.op,
+ version: event.ver,
+ cpu: event.cpu,
+ timestamp,
+ is64: this.is64bit_
+ };
+
+ // Set the payload to decode.
+ const decoder = this.decoder_;
+ decoder.reset(event.payload);
+
+ // Retrieve the handler to decode the payload.
+ const handler = this.getEventHandler(header.guid, header.opcode);
+ if (!handler) return false;
+
+ if (!handler(header, decoder)) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Malformed ' + header.guid + ' event (' + event.payload + ')'
+ });
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Registers a windows ETW event handler used by parseEvent().
+ */
+ registerEventHandler(guid, opcode, handler) {
+ if (this.handlers_[guid] === undefined) {
+ this.handlers_[guid] = [];
+ }
+ this.handlers_[guid][opcode] = handler;
+ },
+
+ /**
+ * Retrieves a registered event handler.
+ */
+ getEventHandler(guid, opcode) {
+ if (this.handlers_[guid] === undefined) {
+ return undefined;
+ }
+ return this.handlers_[guid][opcode];
+ }
+
+ };
+
+ // Register the EtwImporter to the Importer.
+ tr.importer.Importer.register(EtwImporter);
+
+ return {
+ EtwImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer_test.html
new file mode 100644
index 00000000000..c071b13ff5b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/etw_importer_test.html
@@ -0,0 +1,293 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/etw/etw_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+
+ test('canImport', function() {
+ assert.isFalse(tr.e.importer.etw.EtwImporter.canImport('string'));
+ assert.isFalse(tr.e.importer.etw.EtwImporter.canImport([]));
+
+ // Must not parse an invalid name.
+ const dummy = { name: 'dummy', content: [] };
+ assert.isFalse(tr.e.importer.etw.EtwImporter.canImport(dummy));
+
+ // Must parse an empty valid trace.
+ const valid = { name: 'ETW', content: [] };
+ assert.isTrue(tr.e.importer.etw.EtwImporter.canImport(valid));
+ });
+
+ test('getModel', function() {
+ const model = 'dummy';
+ const events = [];
+ const importer = new tr.e.importer.etw.EtwImporter(model, events);
+ assert.strictEqual(importer.model, model);
+ });
+
+ test('registerEventHandler', function() {
+ // Create a dummy EtwImporter.
+ const model = 'dummy';
+ const events = ['events'];
+ const importer = new tr.e.importer.etw.EtwImporter(model, events);
+ const dummyHandler = function() {};
+
+ // The handler must not exists.
+ assert.isUndefined(importer.getEventHandler('ABCDEF', 2));
+
+ // Register an event handler for guid: ABCDEF and opcode: 2.
+ importer.registerEventHandler('ABCDEF', 2, dummyHandler);
+
+ // The handler exists now, must find it.
+ assert.isDefined(importer.getEventHandler('ABCDEF', 2));
+
+ // Must be able to manage an invalid handler.
+ assert.isUndefined(importer.getEventHandler('zzzzzz', 2));
+ });
+
+ test('parseEvent', function() {
+ const model = 'dummy';
+ const events = [];
+ const importer = new tr.e.importer.etw.EtwImporter(model, events);
+ let handlerCalled = false;
+ const dummyHandler = function() { handlerCalled = true; return true; };
+
+ // Register a valid handler.
+ importer.registerEventHandler('aaaa', 42, dummyHandler);
+
+ // Try to parse an invalid event with missing fields.
+ const incompleteEvent = { 'guid': 'aaaa', 'op': 42, 'ver': 0 };
+ assert.isFalse(importer.parseEvent(incompleteEvent));
+ assert.isFalse(handlerCalled);
+
+ // Try to parse a valid event.
+ const validEvent = {
+ 'guid': 'aaaa', 'op': 42, 'ver': 0, 'cpu': 0, 'ts': 0,
+ 'payload': Base64.btoa('0')
+ };
+ assert.isTrue(importer.parseEvent(validEvent));
+ assert.isTrue(handlerCalled);
+ });
+
+ test('resetTooSmall', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+
+ const oldByteLength = decoder.payload_.byteLength;
+ // Decode a payload too big for the actual buffer.
+ decoder.reset('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==');
+ const newByteLength = decoder.payload_.byteLength;
+
+ // Validate the buffer has been resized.
+ assert.isBelow(oldByteLength, newByteLength);
+ });
+
+ test('decode', function() {
+ const model = 'dummy';
+ const events = [];
+ const importer = new tr.e.importer.etw.EtwImporter(model, events);
+
+ const decoder = importer.decoder_;
+
+ decoder.reset('YQBiYw==');
+ assert.strictEqual(decoder.decodeInt32(), 0x63620061);
+
+ // Decode unsigned numbers.
+ decoder.reset('AQ==');
+ assert.strictEqual(decoder.decodeUInt8(), 0x01);
+
+ decoder.reset('AQI=');
+ assert.strictEqual(decoder.decodeUInt16(), 0x0201);
+
+ decoder.reset('AQIDBA==');
+ assert.strictEqual(decoder.decodeUInt32(), 0x04030201);
+
+ decoder.reset('AQIDBAUGBwg=');
+ assert.strictEqual(decoder.decodeUInt64ToString(), '0807060504030201');
+
+ // Decode signed numbers.
+ decoder.reset('AQ==');
+ assert.strictEqual(decoder.decodeInt8(), 0x01);
+
+ decoder.reset('AQI=');
+ assert.strictEqual(decoder.decodeInt16(), 0x0201);
+
+ decoder.reset('AQIDBA==');
+ assert.strictEqual(decoder.decodeInt32(), 0x04030201);
+
+ decoder.reset('AQIDBAUGBwg=');
+ assert.strictEqual(decoder.decodeInt64ToString(), '0807060504030201');
+
+ // Last value before being a signed number.
+ decoder.reset('fw==');
+ assert.strictEqual(decoder.decodeInt8(), 127);
+
+ // Decode negative numbers.
+ decoder.reset('1g==');
+ assert.strictEqual(decoder.decodeInt8(), -42);
+
+ decoder.reset('gA==');
+ assert.strictEqual(decoder.decodeInt8(), -128);
+
+ decoder.reset('hYI=');
+ assert.strictEqual(decoder.decodeInt16(), -32123);
+
+ decoder.reset('hYL//w==');
+ assert.strictEqual(decoder.decodeInt32(), -32123);
+
+ decoder.reset('Lv1ptv////8=');
+ assert.strictEqual(decoder.decodeInt32(), -1234567890);
+
+ // Decode number with zero (nul) in the middle of the string.
+ decoder.reset('YQBiYw==');
+ assert.strictEqual(decoder.decodeInt32(), 0x63620061);
+ });
+
+ test('decodeUInteger', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+
+ decoder.reset('AQIDBAUGBwg=');
+ assert.strictEqual(decoder.decodeUInteger(false), 0x04030201);
+
+ decoder.reset('AQIDBAUGBwg=');
+ assert.strictEqual(decoder.decodeUInteger(true), '0807060504030201');
+ });
+
+ test('decodeString', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+
+ decoder.reset('dGVzdAA=');
+ assert.strictEqual(decoder.decodeString(), 'test');
+
+ decoder.reset('VGhpcyBpcyBhIHRlc3Qu');
+ assert.strictEqual(decoder.decodeString(), 'This is a test.');
+ });
+
+ test('decodeW16String', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ decoder.reset('dABlAHMAdAAAAA==');
+ assert.strictEqual(decoder.decodeW16String(), 'test');
+ });
+
+ test('decodeFixedW16String', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ decoder.reset('dABlAHMAdAAAAA==');
+ assert.strictEqual(decoder.decodeFixedW16String(32), 'test');
+ assert.strictEqual(decoder.position_, 64);
+
+ decoder.reset('dABlAHMAdAAAAA==');
+ assert.strictEqual(decoder.decodeFixedW16String(1), 't');
+ assert.strictEqual(decoder.position_, 2);
+ });
+
+ test('decodeBytes', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ decoder.reset('AAECAwQFBgc=');
+ const bytes = decoder.decodeBytes(8);
+ for (let i = 0; i < bytes.length; ++i) {
+ assert.strictEqual(bytes[i], i);
+ }
+ });
+
+ test('decodeSID', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+
+ // Decode a SID structure with 64-bit pointer.
+ decoder.reset(
+ 'AQIDBAECAwQFBAMCAAAAAAEFAAAAAAAFFQAAAAECAwQFBgcICQoLDA0DAAA=');
+ const sid = decoder.decodeSID(true);
+
+ assert.strictEqual(sid.pSid, '0403020104030201');
+ assert.strictEqual(sid.attributes, 0x02030405);
+ assert.strictEqual(sid.sid.length, 20);
+ });
+
+ test('decodeSystemTime', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+
+ // Decode a SystemTime structure.
+ decoder.reset('AQACAAMABAAFAAYABwAIAA==');
+ const time = decoder.decodeSystemTime();
+ assert.strictEqual(time.wYear, 1);
+ assert.strictEqual(time.wMonth, 2);
+ assert.strictEqual(time.wDayOfWeek, 3);
+ assert.strictEqual(time.wDay, 4);
+ assert.strictEqual(time.wHour, 5);
+ assert.strictEqual(time.wMinute, 6);
+ assert.strictEqual(time.wSecond, 7);
+ assert.strictEqual(time.wMilliseconds, 8);
+ });
+
+ test('decodeTimeZoneInformation', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+
+ // Decode a TimeZoneInformation structure.
+ decoder.reset('AQIDBGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIAAwAEAAUABgAHAAgABA' +
+ 'MCAWIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIAAwAEAAUABgAHAAgACAgI' +
+ 'CA==');
+ const time = decoder.decodeTimeZoneInformation();
+
+ assert.strictEqual(time.bias, 0x04030201);
+ assert.strictEqual(time.standardBias, 0x01020304);
+ assert.strictEqual(time.daylightBias, 0x08080808);
+ assert.strictEqual(time.standardName, 'a');
+ assert.strictEqual(time.daylightName, 'b');
+ });
+
+ test('manageThreads', function() {
+ const events = [];
+ const model = 'dummy';
+ const importer = new tr.e.importer.etw.EtwImporter(model, events);
+
+ // After initialisation, no threads must exists.
+ assert.strictEqual(
+ Object.getOwnPropertyNames(importer.tidsToPid_).length, 0);
+
+ // Add some threads.
+ const thread10 = importer.createThreadIfNeeded(1, 10);
+ const thread11 = importer.createThreadIfNeeded(1, 11);
+ const thread20 = importer.createThreadIfNeeded(2, 20);
+
+ assert.strictEqual(
+ Object.getOwnPropertyNames(importer.tidsToPid_).length, 3);
+ assert.isTrue(importer.tidsToPid_.hasOwnProperty(10));
+ assert.isTrue(importer.tidsToPid_.hasOwnProperty(11));
+ assert.isTrue(importer.tidsToPid_.hasOwnProperty(20));
+
+ // Retrieve existing threads and processes.
+ const pid10 = importer.getPidFromWindowsTid(10);
+ const pid11 = importer.getPidFromWindowsTid(11);
+ const pid20 = importer.getPidFromWindowsTid(20);
+
+ assert.strictEqual(pid10, 1);
+ assert.strictEqual(pid11, 1);
+ assert.strictEqual(pid20, 2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser.html
new file mode 100644
index 00000000000..5dd1bcec174
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/etw/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses EventTrace events in the Windows event trace format.
+ */
+
+tr.exportTo('tr.e.importer.etw', function() {
+ const Parser = tr.e.importer.etw.Parser;
+
+ // Constants for EventTrace events.
+ const guid = '68FDD900-4A3E-11D1-84F4-0000F80464E3';
+ const kEventTraceHeaderOpcode = 0;
+
+ /**
+ * Parses Windows EventTrace trace events.
+ * @constructor
+ */
+ function EventTraceParser(importer) {
+ Parser.call(this, importer);
+
+ // Register handlers.
+ importer.registerEventHandler(guid, kEventTraceHeaderOpcode,
+ EventTraceParser.prototype.decodeHeader.bind(this));
+ }
+
+ EventTraceParser.prototype = {
+ __proto__: Parser.prototype,
+
+ decodeFields(header, decoder) {
+ if (header.version !== 2) {
+ throw new Error('Incompatible EventTrace event version.');
+ }
+
+ const bufferSize = decoder.decodeUInt32();
+ const version = decoder.decodeUInt32();
+ const providerVersion = decoder.decodeUInt32();
+ const numberOfProcessors = decoder.decodeUInt32();
+ const endTime = decoder.decodeUInt64ToString();
+ const timerResolution = decoder.decodeUInt32();
+ const maxFileSize = decoder.decodeUInt32();
+ const logFileMode = decoder.decodeUInt32();
+ const buffersWritten = decoder.decodeUInt32();
+ const startBuffers = decoder.decodeUInt32();
+ const pointerSize = decoder.decodeUInt32();
+ const eventsLost = decoder.decodeUInt32();
+ const cpuSpeed = decoder.decodeUInt32();
+ const loggerName = decoder.decodeUInteger(header.is64);
+ const logFileName = decoder.decodeUInteger(header.is64);
+ const timeZoneInformation = decoder.decodeTimeZoneInformation();
+ const padding = decoder.decodeUInt32();
+ const bootTime = decoder.decodeUInt64ToString();
+ const perfFreq = decoder.decodeUInt64ToString();
+ const startTime = decoder.decodeUInt64ToString();
+ const reservedFlags = decoder.decodeUInt32();
+ const buffersLost = decoder.decodeUInt32();
+ const sessionNameString = decoder.decodeW16String();
+ const logFileNameString = decoder.decodeW16String();
+
+ return {
+ bufferSize,
+ version,
+ providerVersion,
+ numberOfProcessors,
+ endTime,
+ timerResolution,
+ maxFileSize,
+ logFileMode,
+ buffersWritten,
+ startBuffers,
+ pointerSize,
+ eventsLost,
+ cpuSpeed,
+ loggerName,
+ logFileName,
+ timeZoneInformation,
+ bootTime,
+ perfFreq,
+ startTime,
+ reservedFlags,
+ buffersLost,
+ sessionNameString,
+ logFileNameString
+ };
+ },
+
+ decodeHeader(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ // TODO(etienneb): Update the Model with |fields|.
+ return true;
+ }
+
+ };
+
+ Parser.register(EventTraceParser);
+
+ return {
+ EventTraceParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser_test.html
new file mode 100644
index 00000000000..bfa2caed994
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/eventtrace_parser_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/tracing/extras/importer/etw/eventtrace_parser.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // Constants for EventTrace events.
+ const guid = '68FDD900-4A3E-11D1-84F4-0000F80464E3';
+ const kEventTraceHeaderOpcode = 0;
+
+ const kEventTraceHeaderPayload32bitV2 =
+ 'AAABAAYBAQWwHQAAEAAAABEs1WHICMwBYWECAGQAAAABAAAAAwAAAAEAAAAEAAAAAAAAA' +
+ 'FoJAAAFAAAABgAAACwBAABAAHQAegByAGUAcwAuAGQAbABsACwALQAxADEAMgAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAQACAAAAAAAAAAAAAABAAHQ' +
+ 'AegByAGUAcwAuAGQAbABsACwALQAxADEAMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAADAAAAAgACAAAAAAAAAMT///8AAAAAf0Ob368FzAGdrCMAAAAAACw0o' +
+ '2DICMwBAQAAAAAAAABNAGEAawBlACAAVABlAHMAdAAgAEQAYQB0AGEAIABTAGUAcwBzAG' +
+ 'kAbwBuAAAAYwA6AFwAcwByAGMAXABzAGEAdwBiAHUAYwBrAFwAdAByAHUAbgBrAFwAcwB' +
+ 'yAGMAXABzAGEAdwBiAHUAYwBrAFwAbABvAGcAXwBsAGkAYgBcAHQAZQBzAHQAXwBkAGEA' +
+ 'dABhAFwAaQBtAGEAZwBlAF8AZABhAHQAYQBfADMAMgBfAHYAMAAuAGUAdABsAAAA';
+
+ const kEventTraceHeaderPayload64bitV2 =
+ 'AAABAAYBAQWxHQAABAAAADsuzRRYLM8BYWECAAAAAAABAAEAtgEAAAEAAAAIAAAAHwAAA' +
+ 'KAGAAAAAAAAAAAAAAAAAAAAAAAALAEAAEAAdAB6AHIAZQBzAC4AZABsAGwALAAtADEAMQ' +
+ 'AyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAA' +
+ 'AAAAAAEAAdAB6AHIAZQBzAC4AZABsAGwALAAtADEAMQAxAAAAAAAAAAAAAAAAAAAAAAAA' +
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///wAAAABZQyWiwCvPAX1GG' +
+ 'QAAAAAALWSZBFgszwEBAAAAAAAAAFIAZQBsAG8AZwBnAGUAcgAAAEMAOgBcAGsAZQByAG' +
+ '4AZQBsAC4AZQB0AGwAAAA=';
+
+ test('DecodeFields', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ const parser = new tr.e.importer.etw.EventTraceParser(importer);
+ let header;
+ let fields;
+
+ // Validate a version 2 32-bit payload.
+ header = {
+ guid, opcode: kEventTraceHeaderOpcode, version: 2, is64: 0
+ };
+ decoder.reset(kEventTraceHeaderPayload32bitV2);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.bufferSize, 65536);
+ assert.strictEqual(fields.version, 83951878);
+ assert.strictEqual(fields.providerVersion, 7600);
+ assert.strictEqual(fields.numberOfProcessors, 16);
+ assert.strictEqual(fields.endTime, '01cc08c861d52c11');
+ assert.strictEqual(fields.timerResolution, 156001);
+ assert.strictEqual(fields.maxFileSize, 100);
+ assert.strictEqual(fields.logFileMode, 1);
+ assert.strictEqual(fields.buffersWritten, 3);
+ assert.strictEqual(fields.startBuffers, 1);
+ assert.strictEqual(fields.pointerSize, 4);
+ assert.strictEqual(fields.eventsLost, 0);
+ assert.strictEqual(fields.cpuSpeed, 2394);
+ assert.strictEqual(fields.loggerName, 5);
+ assert.strictEqual(fields.logFileName, 6);
+ assert.strictEqual(fields.timeZoneInformation.standardName,
+ '@tzres.dll,-112');
+ assert.strictEqual(fields.timeZoneInformation.daylightName,
+ '@tzres.dll,-111');
+ assert.strictEqual(fields.bootTime, '01cc05afdf9b437f');
+ assert.strictEqual(fields.perfFreq, '000000000023ac9d');
+ assert.strictEqual(fields.startTime, '01cc08c860a3342c');
+ assert.strictEqual(fields.reservedFlags, 1);
+ assert.strictEqual(fields.buffersLost, 0);
+ assert.strictEqual(fields.sessionNameString, 'Make Test Data Session');
+ assert.strictEqual(fields.logFileNameString,
+ 'c:\\src\\sawbuck\\trunk\\src\\sawbuck\\log_lib\\' +
+ 'test_data\\image_data_32_v0.etl');
+
+ // Validate a version 2 64-bit payload.
+ header = {
+ guid, opcode: kEventTraceHeaderOpcode, version: 2, is64: 1
+ };
+ decoder.reset(kEventTraceHeaderPayload64bitV2);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.bufferSize, 65536);
+ assert.strictEqual(fields.version, 83951878);
+ assert.strictEqual(fields.providerVersion, 7601);
+ assert.strictEqual(fields.numberOfProcessors, 4);
+ assert.strictEqual(fields.endTime, '01cf2c5814cd2e3b');
+ assert.strictEqual(fields.timerResolution, 156001);
+ assert.strictEqual(fields.maxFileSize, 0);
+ assert.strictEqual(fields.logFileMode, 0x10001);
+ assert.strictEqual(fields.buffersWritten, 438);
+ assert.strictEqual(fields.startBuffers, 1);
+ assert.strictEqual(fields.pointerSize, 8);
+ assert.strictEqual(fields.eventsLost, 31);
+ assert.strictEqual(fields.cpuSpeed, 1696);
+ assert.strictEqual(fields.loggerName, '0000000000000000');
+ assert.strictEqual(fields.logFileName, '0000000000000000');
+ assert.strictEqual(fields.timeZoneInformation.standardName,
+ '@tzres.dll,-112');
+ assert.strictEqual(fields.timeZoneInformation.daylightName,
+ '@tzres.dll,-111');
+ assert.strictEqual(fields.bootTime, '01cf2bc0a2254359');
+ assert.strictEqual(fields.perfFreq, '000000000019467d');
+ assert.strictEqual(fields.startTime, '01cf2c580499642d');
+ assert.strictEqual(fields.reservedFlags, 1);
+ assert.strictEqual(fields.buffersLost, 0);
+ assert.strictEqual(fields.sessionNameString, 'Relogger');
+ assert.strictEqual(fields.logFileNameString, 'C:\\kernel.etl');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/parser.html
new file mode 100644
index 00000000000..95f9d079e23
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/parser.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for Windows ETW event parsers.
+ *
+ * The ETW trace event importer depends on subclasses of
+ * Parser to parse event data. Each subclass corresponds
+ * to a group of trace events; e.g. Thread and Process implements
+ * decoding of scheduling events. Parser subclasses must
+ * call Parser.register to arrange to be instantiated
+ * and their constructor must register their event handlers with the
+ * importer. For example,
+ *
+ * var Parser = tr.e.importer.etw.Parser;
+ *
+ * function ThreadParser(importer) {
+ * Parser.call(this, importer);
+ *
+ * importer.registerEventHandler(guid, kThreadStartOpcode,
+ * ThreadParser.prototype.decodeStart.bind(this));
+ * importer.registerEventHandler(guid, kThreadEndOpcode,
+ * ThreadParser.prototype.decodeEnd.bind(this));
+ * }
+ *
+ * Parser.register(ThreadParser);
+ *
+ * When a registered event is found, the associated event handler is invoked:
+ *
+ * decodeStart: function(header, decoder) {
+ * [...]
+ * return true;
+ * },
+ *
+ * If the routine returns false the caller will generate an import error
+ * saying there was a problem parsing it. Handlers can also emit import
+ * messages using this.importer.model.importWarning. If this is done in lieu of
+ * the generic import error it may be desirable for the handler to return
+ * true.
+ *
+ */
+tr.exportTo('tr.e.importer.etw', function() {
+ /**
+ * Parses Windows ETW events.
+ * @constructor
+ */
+ function Parser(importer) {
+ this.importer = importer;
+ this.model = importer.model;
+ }
+
+ Parser.prototype = {
+ __proto__: Object.prototype
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.mandatoryBaseClass = Parser;
+ tr.b.decorateExtensionRegistry(Parser, options);
+
+
+ return {
+ Parser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser.html
new file mode 100644
index 00000000000..bb857a9a33d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/etw/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses processes events in the Windows event trace format.
+ *
+ * The Windows process events are:
+ *
+ * - DCStart: Describes a process that was already running when the trace
+ * started. ETW automatically generates these events for all running
+ * processes at the beginning of the trace.
+ * - Start: Describes a process launched during the tracing session.
+ * - End: Describes a process that ended during the tracing session.
+ * - DCEnd: Describes a process that was still running when the trace ended.
+ *
+ * See http://msdn.microsoft.com/library/windows/desktop/aa364092.aspx
+ */
+tr.exportTo('tr.e.importer.etw', function() {
+ const Parser = tr.e.importer.etw.Parser;
+
+ // Constants for Process events.
+ const guid = '3D6FA8D0-FE05-11D0-9DDA-00C04FD7BA7C';
+ const kProcessStartOpcode = 1;
+ const kProcessEndOpcode = 2;
+ const kProcessDCStartOpcode = 3;
+ const kProcessDCEndOpcode = 4;
+ const kProcessDefunctOpcode = 39;
+
+ /**
+ * Parses Windows process trace events.
+ * @constructor
+ */
+ function ProcessParser(importer) {
+ Parser.call(this, importer);
+
+ // Register handlers.
+ importer.registerEventHandler(guid, kProcessStartOpcode,
+ ProcessParser.prototype.decodeStart.bind(this));
+ importer.registerEventHandler(guid, kProcessEndOpcode,
+ ProcessParser.prototype.decodeEnd.bind(this));
+ importer.registerEventHandler(guid, kProcessDCStartOpcode,
+ ProcessParser.prototype.decodeDCStart.bind(this));
+ importer.registerEventHandler(guid, kProcessDCEndOpcode,
+ ProcessParser.prototype.decodeDCEnd.bind(this));
+ importer.registerEventHandler(guid, kProcessDefunctOpcode,
+ ProcessParser.prototype.decodeDefunct.bind(this));
+ }
+
+ ProcessParser.prototype = {
+ __proto__: Parser.prototype,
+
+ decodeFields(header, decoder) {
+ if (header.version > 5) {
+ throw new Error('Incompatible Process event version.');
+ }
+
+ let pageDirectoryBase;
+ if (header.version === 1) {
+ pageDirectoryBase = decoder.decodeUInteger(header.is64);
+ }
+
+ let uniqueProcessKey;
+ if (header.version >= 2) {
+ uniqueProcessKey = decoder.decodeUInteger(header.is64);
+ }
+
+ const processId = decoder.decodeUInt32();
+ const parentId = decoder.decodeUInt32();
+
+ let sessionId;
+ let exitStatus;
+ if (header.version >= 1) {
+ sessionId = decoder.decodeUInt32();
+ exitStatus = decoder.decodeInt32();
+ }
+
+ let directoryTableBase;
+ if (header.version >= 3) {
+ directoryTableBase = decoder.decodeUInteger(header.is64);
+ }
+
+ let flags;
+ if (header.version >= 4) {
+ flags = decoder.decodeUInt32();
+ }
+
+ const userSID = decoder.decodeSID(header.is64);
+
+ let imageFileName;
+ if (header.version >= 1) {
+ imageFileName = decoder.decodeString();
+ }
+
+ let commandLine;
+ if (header.version >= 2) {
+ commandLine = decoder.decodeW16String();
+ }
+
+ let packageFullName;
+ let applicationId;
+ if (header.version >= 4) {
+ packageFullName = decoder.decodeW16String();
+ applicationId = decoder.decodeW16String();
+ }
+
+ let exitTime;
+ if (header.version === 5 && header.opcode === kProcessDefunctOpcode) {
+ exitTime = decoder.decodeUInt64ToString();
+ }
+
+ return {
+ pageDirectoryBase,
+ uniqueProcessKey,
+ processId,
+ parentId,
+ sessionId,
+ exitStatus,
+ directoryTableBase,
+ flags,
+ userSID,
+ imageFileName,
+ commandLine,
+ packageFullName,
+ applicationId,
+ exitTime
+ };
+ },
+
+ decodeStart(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ const process = this.model.getOrCreateProcess(fields.processId);
+ if (process.hasOwnProperty('has_ended')) {
+ // On Windows, a process ID used by a process could be reused as soon as
+ // the process ends (there is no pid cycling like on Linux). However, in
+ // a short trace, this is unlikely to happen.
+ throw new Error('Process clash detected.');
+ }
+ process.name = fields.imageFileName;
+ return true;
+ },
+
+ decodeEnd(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ const process = this.model.getOrCreateProcess(fields.processId);
+ process.has_ended = true;
+ return true;
+ },
+
+ decodeDCStart(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ const process = this.model.getOrCreateProcess(fields.processId);
+ if (process.hasOwnProperty('has_ended')) {
+ throw new Error('Process clash detected.');
+ }
+ process.name = fields.imageFileName;
+ return true;
+ },
+
+ decodeDCEnd(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ const process = this.model.getOrCreateProcess(fields.processId);
+ process.has_ended = true;
+ return true;
+ },
+
+ decodeDefunct(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ // TODO(etienneb): Update the Model with |fields|.
+ return true;
+ }
+
+ };
+
+ Parser.register(ProcessParser);
+
+ return {
+ ProcessParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser_test.html
new file mode 100644
index 00000000000..aeeafc15dd2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/process_parser_test.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/tracing/extras/importer/etw/process_parser.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // Constants for Process events.
+ const guid = '3D6FA8D0-FE05-11D0-9DDA-00C04FD7BA7C';
+ const kProcessStartOpcode = 1;
+ const kProcessDefunctOpcode = 39;
+
+ const kProcessStartPayload32bitV1 =
+ 'AAAAAPAGAADcAwAAAQAAAAMBAAAAAAAAAAAAAAEFAAAAAAAFFQAAAJYs7Cxo/TEG8dyk0' +
+ '+gDAABub3RlcGFkLmV4ZQA=';
+
+ const kProcessStartPayload32bitV2 =
+ 'AAAAAPAGAADcAwAAAQAAAAMBAAAAAAAAAAAAAAEFAAAAAAAFFQAAAJYs7Cxo/TEG8dyk0' +
+ '+gDAABub3RlcGFkLmV4ZQAiAEMAOgBcAFcAaQBuAGQAbwB3AHMAXABzAHkAcwB0AGUAbQ' +
+ 'AzADIAXABuAG8AdABlAHAAYQBkAC4AZQB4AGUAIgAgAAAA';
+
+ const kProcessStartPayload32bitV3 =
+ 'AAAAAPAGAADcAwAAAQAAAAMBAAAAAAAAAAAAAAAAAAABBQAAAAAABRUAAACWLOwsaP0xB' +
+ 'vHcpNPoAwAAbm90ZXBhZC5leGUAIgBDADoAXABXAGkAbgBkAG8AdwBzAFwAcwB5AHMAdA' +
+ 'BlAG0AMwAyAFwAbgBvAHQAZQBwAGEAZAAuAGUAeABlACIAIAAAAA==';
+
+ const kProcessStartPayload64bitV3 =
+ 'YIBiD4D6//8AGgAAoBwAAAEAAAADAQAAAPBDHQEAAAAwVlMVoPj//wAAAACg+P//AQUAA' +
+ 'AAAAAUVAAAAAgMBAgMEBQYHCAkKCwwAAHhwZXJmLmV4ZQB4AHAAZQByAGYAIAAgAC0AZA' +
+ 'AgAG8AdQB0AC4AZQB0AGwAAAA=';
+
+ const kProcessStartPayload64bitV4 =
+ 'gED8GgDg//+MCgAACBcAAAUAAAADAQAAALCiowAAAAAAAAAAkPBXBADA//8AAAAAAAAAA' +
+ 'AEFAAAAAAAFFQAAAAECAwQFBgcICQoLBukDAAB4cGVyZi5leGUAeABwAGUAcgBmACAAIA' +
+ 'AtAHMAdABvAHAAAAAAAAAA';
+
+ const kProcessDefunctPayload64bitV5 =
+ 'wMXyBgDg//9IGQAAEAgAAAEAAAAAAAAAAGDLTwAAAAAAAAAA8OU7AwDA//8AAAAAAAAMA' +
+ 'AEFAAAAAAAFFQAAAMDBwsPExcbH0NHS09QDAABjaHJvbWUuZXhlAAAAAAAAAI1Jovns+s' +
+ '4B';
+
+ test('DecodeFields', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ const parser = new tr.e.importer.etw.ProcessParser(importer);
+ let header;
+ let fields;
+
+ // Validate a version 1 32-bit payload.
+ header = { guid, opcode: kProcessStartOpcode, version: 1, is64: 0 };
+ decoder.reset(kProcessStartPayload32bitV1);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.pageDirectoryBase, 0);
+ assert.strictEqual(fields.processId, 1776);
+ assert.strictEqual(fields.parentId, 988);
+ assert.strictEqual(fields.sessionId, 1);
+ assert.strictEqual(fields.exitStatus, 259);
+ assert.strictEqual(fields.imageFileName, 'notepad.exe');
+
+ // Validate a version 2 32-bit payload.
+ header = { guid, opcode: kProcessStartOpcode, version: 2, is64: 0 };
+ decoder.reset(kProcessStartPayload32bitV2);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.uniqueProcessKey, 0);
+ assert.strictEqual(fields.processId, 1776);
+ assert.strictEqual(fields.parentId, 988);
+ assert.strictEqual(fields.sessionId, 1);
+ assert.strictEqual(fields.exitStatus, 259);
+ assert.strictEqual(fields.imageFileName, 'notepad.exe');
+ assert.strictEqual(fields.commandLine,
+ '\"C:\\Windows\\system32\\notepad.exe\" ');
+
+ // Validate a version 3 32-bit payload.
+ header = { guid, opcode: kProcessStartOpcode, version: 3, is64: 0 };
+ decoder.reset(kProcessStartPayload32bitV3);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.uniqueProcessKey, 0);
+ assert.strictEqual(fields.processId, 1776);
+ assert.strictEqual(fields.parentId, 988);
+ assert.strictEqual(fields.sessionId, 1);
+ assert.strictEqual(fields.exitStatus, 259);
+ assert.strictEqual(fields.directoryTableBase, 0);
+ assert.strictEqual(fields.imageFileName, 'notepad.exe');
+ assert.strictEqual(fields.commandLine,
+ '\"C:\\Windows\\system32\\notepad.exe\" ');
+
+ // Validate a version 3 64-bit payload.
+ header = { guid, opcode: kProcessStartOpcode, version: 3, is64: 1 };
+ decoder.reset(kProcessStartPayload64bitV3);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.uniqueProcessKey, 'fffffa800f628060');
+ assert.strictEqual(fields.processId, 6656);
+ assert.strictEqual(fields.parentId, 7328);
+ assert.strictEqual(fields.sessionId, 1);
+ assert.strictEqual(fields.exitStatus, 259);
+ assert.strictEqual(fields.directoryTableBase, '000000011d43f000');
+ assert.strictEqual(fields.imageFileName, 'xperf.exe');
+ assert.strictEqual(fields.commandLine, 'xperf -d out.etl');
+
+ // Validate a version 4 64-bit payload.
+ header = { guid, opcode: kProcessStartOpcode, version: 4, is64: 1 };
+ decoder.reset(kProcessStartPayload64bitV4);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.uniqueProcessKey, 'ffffe0001afc4080');
+ assert.strictEqual(fields.processId, 2700);
+ assert.strictEqual(fields.parentId, 5896);
+ assert.strictEqual(fields.sessionId, 5);
+ assert.strictEqual(fields.exitStatus, 259);
+ assert.strictEqual(fields.directoryTableBase, '00000000a3a2b000');
+ assert.strictEqual(fields.flags, 0);
+ assert.strictEqual(fields.imageFileName, 'xperf.exe');
+ assert.strictEqual(fields.commandLine, 'xperf -stop');
+ assert.strictEqual(fields.packageFullName, '');
+ assert.strictEqual(fields.applicationId, '');
+
+ // Validate a version 5 64-bit payload.
+ header = { guid, opcode: kProcessDefunctOpcode, version: 5, is64: 1 };
+ decoder.reset(kProcessDefunctPayload64bitV5);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.uniqueProcessKey, 'ffffe00006f2c5c0');
+ assert.strictEqual(fields.processId, 6472);
+ assert.strictEqual(fields.parentId, 2064);
+ assert.strictEqual(fields.sessionId, 1);
+ assert.strictEqual(fields.exitStatus, 0);
+ assert.strictEqual(fields.directoryTableBase, '000000004fcb6000');
+ assert.strictEqual(fields.flags, 0);
+ assert.strictEqual(fields.imageFileName, 'chrome.exe');
+ assert.strictEqual(fields.commandLine, '');
+ assert.strictEqual(fields.packageFullName, '');
+ assert.strictEqual(fields.applicationId, '');
+ assert.strictEqual(fields.exitTime, '01cefaecf9a2498d');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser.html
new file mode 100644
index 00000000000..5a28e9f8165
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser.html
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/etw/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses threads events in the Windows event trace format.
+ *
+ * The Windows thread events are:
+ *
+ * - DCStart: Describes a thread that was already running when the trace
+ * started. ETW automatically generates these events for all running
+ * threads at the beginning of the trace.
+ * - Start: Describes a thread that started during the tracing session.
+ * - End: Describes a thread that ended during the tracing session.
+ * - DCEnd: Describes a thread that was still alive when the trace ended.
+ *
+ * See http://msdn.microsoft.com/library/windows/desktop/aa364132.aspx
+ */
+tr.exportTo('tr.e.importer.etw', function() {
+ const Parser = tr.e.importer.etw.Parser;
+
+ // Constants for Thread events.
+ const guid = '3D6FA8D1-FE05-11D0-9DDA-00C04FD7BA7C';
+ const kThreadStartOpcode = 1;
+ const kThreadEndOpcode = 2;
+ const kThreadDCStartOpcode = 3;
+ const kThreadDCEndOpcode = 4;
+ const kThreadCSwitchOpcode = 36;
+
+ /**
+ * Parses Windows threads trace events.
+ * @constructor
+ */
+ function ThreadParser(importer) {
+ Parser.call(this, importer);
+
+ // Register handlers.
+ importer.registerEventHandler(guid, kThreadStartOpcode,
+ ThreadParser.prototype.decodeStart.bind(this));
+ importer.registerEventHandler(guid, kThreadEndOpcode,
+ ThreadParser.prototype.decodeEnd.bind(this));
+ importer.registerEventHandler(guid, kThreadDCStartOpcode,
+ ThreadParser.prototype.decodeDCStart.bind(this));
+ importer.registerEventHandler(guid, kThreadDCEndOpcode,
+ ThreadParser.prototype.decodeDCEnd.bind(this));
+ importer.registerEventHandler(guid, kThreadCSwitchOpcode,
+ ThreadParser.prototype.decodeCSwitch.bind(this));
+ }
+
+ ThreadParser.prototype = {
+ __proto__: Parser.prototype,
+
+ decodeFields(header, decoder) {
+ if (header.version > 3) {
+ throw new Error('Incompatible Thread event version ' +
+ header.version + '.');
+ }
+
+ // Common fields to all Thread events.
+ const processId = decoder.decodeUInt32();
+ const threadId = decoder.decodeUInt32();
+
+ // Extended fields.
+ let stackBase;
+ let stackLimit;
+ let userStackBase;
+ let userStackLimit;
+ let affinity;
+ let startAddr;
+ let win32StartAddr;
+ let tebBase;
+ let subProcessTag;
+ let basePriority;
+ let pagePriority;
+ let ioPriority;
+ let threadFlags;
+ let waitMode;
+
+ if (header.version === 1) {
+ // On version 1, only start events have extended information.
+ if (header.opcode === kThreadStartOpcode ||
+ header.opcode === kThreadDCStartOpcode) {
+ stackBase = decoder.decodeUInteger(header.is64);
+ stackLimit = decoder.decodeUInteger(header.is64);
+ userStackBase = decoder.decodeUInteger(header.is64);
+ userStackLimit = decoder.decodeUInteger(header.is64);
+ startAddr = decoder.decodeUInteger(header.is64);
+ win32StartAddr = decoder.decodeUInteger(header.is64);
+ waitMode = decoder.decodeInt8();
+ decoder.skip(3);
+ }
+ } else {
+ stackBase = decoder.decodeUInteger(header.is64);
+ stackLimit = decoder.decodeUInteger(header.is64);
+ userStackBase = decoder.decodeUInteger(header.is64);
+ userStackLimit = decoder.decodeUInteger(header.is64);
+
+ // Version 2 produces a field named 'startAddr'.
+ if (header.version === 2) {
+ startAddr = decoder.decodeUInteger(header.is64);
+ } else {
+ affinity = decoder.decodeUInteger(header.is64);
+ }
+
+ win32StartAddr = decoder.decodeUInteger(header.is64);
+ tebBase = decoder.decodeUInteger(header.is64);
+ subProcessTag = decoder.decodeUInt32();
+
+ if (header.version === 3) {
+ basePriority = decoder.decodeUInt8();
+ pagePriority = decoder.decodeUInt8();
+ ioPriority = decoder.decodeUInt8();
+ threadFlags = decoder.decodeUInt8();
+ }
+ }
+
+ return {
+ processId,
+ threadId,
+ stackBase,
+ stackLimit,
+ userStackBase,
+ userStackLimit,
+ affinity,
+ startAddr,
+ win32StartAddr,
+ tebBase,
+ subProcessTag,
+ waitMode,
+ basePriority,
+ pagePriority,
+ ioPriority,
+ threadFlags
+ };
+ },
+
+ decodeCSwitchFields(header, decoder) {
+ // The only difference in version 3 is that oldThreadWaitMode is renamed
+ // to ThreadFlags. There is no difference between version 3 and version 4.
+ // Therefore we can safely handle all of these versions with the same code
+ // since we don't interpret oldThreadWaitMode at all.
+ // To see the layout of the CSwitch_V4 class you can run wbemtest
+ // elevated, "Connect...", change root\cimv2 to root\WMI, "Connect",
+ // "Open Class...", "CSwitch_V4", "Show MOF".
+ if (header.version < 2 || header.version > 4) {
+ throw new Error('Incompatible cswitch event version ' +
+ header.version + '.');
+ }
+
+ // Decode CSwitch payload.
+ const newThreadId = decoder.decodeUInt32();
+ const oldThreadId = decoder.decodeUInt32();
+ const newThreadPriority = decoder.decodeInt8();
+ const oldThreadPriority = decoder.decodeInt8();
+ const previousCState = decoder.decodeUInt8();
+ const spareByte = decoder.decodeInt8();
+ const oldThreadWaitReason = decoder.decodeInt8();
+ // oldThreadWaitMode is called ThreadFlags in V3 and V4.
+ const oldThreadWaitMode = decoder.decodeInt8();
+ const oldThreadState = decoder.decodeInt8();
+ const oldThreadWaitIdealProcessor = decoder.decodeInt8();
+ const newThreadWaitTime = decoder.decodeUInt32();
+ const reserved = decoder.decodeUInt32();
+
+ return {
+ newThreadId,
+ oldThreadId,
+ newThreadPriority,
+ oldThreadPriority,
+ previousCState,
+ spareByte,
+ oldThreadWaitReason,
+ oldThreadWaitMode,
+ oldThreadState,
+ oldThreadWaitIdealProcessor,
+ newThreadWaitTime,
+ reserved
+ };
+ },
+
+ decodeStart(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ this.importer.createThreadIfNeeded(fields.processId, fields.threadId);
+ return true;
+ },
+
+ decodeEnd(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ this.importer.removeThreadIfPresent(fields.threadId);
+ return true;
+ },
+
+ decodeDCStart(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ this.importer.createThreadIfNeeded(fields.processId, fields.threadId);
+ return true;
+ },
+
+ decodeDCEnd(header, decoder) {
+ const fields = this.decodeFields(header, decoder);
+ this.importer.removeThreadIfPresent(fields.threadId);
+ return true;
+ },
+
+ decodeCSwitch(header, decoder) {
+ const fields = this.decodeCSwitchFields(header, decoder);
+ const cpu = this.importer.getOrCreateCpu(header.cpu);
+ const newThread =
+ this.importer.getThreadFromWindowsTid(fields.newThreadId);
+
+ // Generate the new thread name. If some events were lost, it's possible
+ // that information about the new thread or process is not available.
+ let newThreadName;
+ if (newThread && newThread.userFriendlyName) {
+ newThreadName = newThread.userFriendlyName;
+ } else {
+ const newProcessId = this.importer.getPidFromWindowsTid(
+ fields.newThreadId);
+ const newProcess = this.model.getProcess(newProcessId);
+ let newProcessName;
+ if (newProcess) {
+ newProcessName = newProcess.name;
+ } else {
+ newProcessName = 'Unknown process';
+ }
+
+ newThreadName =
+ newProcessName + ' (tid ' + fields.newThreadId + ')';
+ }
+
+ cpu.switchActiveThread(
+ header.timestamp,
+ {},
+ fields.newThreadId,
+ newThreadName,
+ fields);
+ return true;
+ }
+
+ };
+
+ Parser.register(ThreadParser);
+
+ return {
+ ThreadParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser_test.html
new file mode 100644
index 00000000000..a15e3235a25
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/etw/thread_parser_test.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/tracing/extras/importer/etw/thread_parser.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // Constants for Thread events.
+ const guid = '3D6FA8D1-FE05-11D0-9DDA-00C04FD7BA7C';
+ const kThreadStartOpcode = 1;
+ const kThreadEndOpcode = 2;
+ const kThreadDCStartOpcode = 3;
+ const kThreadCSwitchOpcode = 36;
+
+ const kThreadStartPayload32bitV1 =
+ 'BAAAAEwHAAAAYLfzADC38wAAAAAAAAAAhdse9wAAAAD/AAAA';
+
+ const kThreadEndPayload32bitV1 = 'BAAAALQAAAA=';
+
+
+ const kThreadDCStartPayload64bitV2 =
+ 'AAAAAAAAAAAAYPUCAPj//wAA9QIA+P//AAAAAAAAAAAAAAAAAAAAAIAlxwEA+P//gCXHA' +
+ 'QD4//8AAAAAAAAAAAAAAAA=';
+
+ const kThreadStartPayload32bitV3 =
+ 'LAIAACwTAAAAUJixACCYsQAA1QAAwNQAAwAAAOkDq3cA4P1/AAAAAAkFAgA=';
+
+ const kThreadStartPayload64bitV3 =
+ 'eCEAAJQUAAAAMA4nAND//wDQDScA0P//MP0LBgAAAAAAgAsGAAAAAP8AAAAAAAAALP1YX' +
+ 'AAAAAAAwBL/AAAAAAAAAAAIBQIA';
+
+ const kThreadCSwitchPayload32bitV2 = 'AAAAACwRAAAACQAAFwABABIAAAAmSAAA';
+ const kThreadCSwitchPayload64bitV2 = 'zAgAAAAAAAAIAAEAAAACBAEAAACHbYg0';
+
+ test('DecodeFields', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ const parser = new tr.e.importer.etw.ThreadParser(importer);
+ let header;
+ let fields;
+
+ // Validate a version 1 32-bit payload.
+ header = { guid, opcode: kThreadStartOpcode, version: 1, is64: 0 };
+ decoder.reset(kThreadStartPayload32bitV1);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.processId, 4);
+ assert.strictEqual(fields.threadId, 1868);
+ assert.strictEqual(fields.stackBase, 4088881152);
+ assert.strictEqual(fields.stackLimit, 4088868864);
+ assert.strictEqual(fields.userStackBase, 0);
+ assert.strictEqual(fields.userStackLimit, 0);
+ assert.strictEqual(fields.startAddr, 4145994629);
+ assert.strictEqual(fields.win32StartAddr, 0);
+ assert.strictEqual(fields.waitMode, -1);
+
+ // Validate an End version 1 32-bit payload.
+ header = { guid, opcode: kThreadEndOpcode, version: 1, is64: 0 };
+ decoder.reset(kThreadStartPayload32bitV1);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.processId, 4);
+ assert.strictEqual(fields.threadId, 1868);
+
+ // Validate a version 2 64-bit payload.
+ header = { guid, opcode: kThreadDCStartOpcode, version: 2, is64: 1 };
+ decoder.reset(kThreadDCStartPayload64bitV2);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.processId, 0);
+ assert.strictEqual(fields.threadId, 0);
+ assert.strictEqual(fields.stackBase, 'fffff80002f56000');
+ assert.strictEqual(fields.stackLimit, 'fffff80002f50000');
+ assert.strictEqual(fields.userStackBase, '0000000000000000');
+ assert.strictEqual(fields.userStackLimit, '0000000000000000');
+ assert.strictEqual(fields.startAddr, 'fffff80001c72580');
+ assert.strictEqual(fields.win32StartAddr, 'fffff80001c72580');
+ assert.strictEqual(fields.tebBase, '0000000000000000');
+ assert.strictEqual(fields.subProcessTag, 0);
+
+ // Validate a version 3 32-bit payload.
+ header = { guid, opcode: kThreadStartOpcode, version: 3, is64: 0 };
+ decoder.reset(kThreadStartPayload32bitV3);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.processId, 556);
+ assert.strictEqual(fields.threadId, 4908);
+ assert.strictEqual(fields.stackBase, 2979549184);
+ assert.strictEqual(fields.stackLimit, 2979536896);
+ assert.strictEqual(fields.userStackBase, 13959168);
+ assert.strictEqual(fields.userStackLimit, 13942784);
+ assert.strictEqual(fields.affinity, 3);
+ assert.strictEqual(fields.win32StartAddr, 2007696361);
+ assert.strictEqual(fields.tebBase, 2147344384);
+ assert.strictEqual(fields.subProcessTag, 0);
+ assert.strictEqual(fields.basePriority, 9);
+ assert.strictEqual(fields.pagePriority, 5);
+ assert.strictEqual(fields.ioPriority, 2);
+ assert.strictEqual(fields.threadFlags, 0);
+
+ // Validate a version 3 64-bit payload.
+ header = { guid, opcode: kThreadStartOpcode, version: 3, is64: 1 };
+ decoder.reset(kThreadStartPayload64bitV3);
+ fields = parser.decodeFields(header, decoder);
+
+ assert.strictEqual(fields.processId, 8568);
+ assert.strictEqual(fields.threadId, 5268);
+ assert.strictEqual(fields.stackBase, 'ffffd000270e3000');
+ assert.strictEqual(fields.stackLimit, 'ffffd000270dd000');
+ assert.strictEqual(fields.userStackBase, '00000000060bfd30');
+ assert.strictEqual(fields.userStackLimit, '00000000060b8000');
+ assert.strictEqual(fields.affinity, '00000000000000ff');
+ assert.strictEqual(fields.win32StartAddr, '000000005c58fd2c');
+ assert.strictEqual(fields.tebBase, '00000000ff12c000');
+ assert.strictEqual(fields.subProcessTag, 0);
+ assert.strictEqual(fields.basePriority, 8);
+ assert.strictEqual(fields.pagePriority, 5);
+ assert.strictEqual(fields.ioPriority, 2);
+ assert.strictEqual(fields.threadFlags, 0);
+ });
+
+ test('DecodeCSwitchFields', function() {
+ const importer = new tr.e.importer.etw.EtwImporter('dummy', []);
+ const decoder = importer.decoder_;
+ const parser = new tr.e.importer.etw.ThreadParser(importer);
+ let header;
+ let fields;
+
+
+ // Validate a version 2 CSwitch 32-bit payload.
+ header = { guid, opcode: kThreadCSwitchOpcode, version: 2, is64: 0 };
+ decoder.reset(kThreadCSwitchPayload32bitV2);
+ fields = parser.decodeCSwitchFields(header, decoder);
+
+ assert.strictEqual(fields.newThreadId, 0);
+ assert.strictEqual(fields.oldThreadId, 4396);
+ assert.strictEqual(fields.newThreadPriority, 0);
+ assert.strictEqual(fields.oldThreadPriority, 9);
+ assert.strictEqual(fields.previousCState, 0);
+ assert.strictEqual(fields.spareByte, 0);
+ assert.strictEqual(fields.oldThreadWaitReason, 23);
+ assert.strictEqual(fields.oldThreadWaitMode, 0);
+ assert.strictEqual(fields.oldThreadState, 1);
+ assert.strictEqual(fields.oldThreadWaitIdealProcessor, 0);
+ assert.strictEqual(fields.newThreadWaitTime, 18);
+ assert.strictEqual(fields.reserved, 18470);
+
+ // Validate a version 2 CSwitch 64-bit payload.
+ header = { guid, opcode: kThreadCSwitchOpcode, version: 2, is64: 1 };
+ decoder.reset(kThreadCSwitchPayload64bitV2);
+ fields = parser.decodeCSwitchFields(header, decoder);
+
+ assert.strictEqual(fields.newThreadId, 2252);
+ assert.strictEqual(fields.oldThreadId, 0);
+ assert.strictEqual(fields.newThreadPriority, 8);
+ assert.strictEqual(fields.oldThreadPriority, 0);
+ assert.strictEqual(fields.previousCState, 1);
+ assert.strictEqual(fields.spareByte, 0);
+ assert.strictEqual(fields.oldThreadWaitReason, 0);
+ assert.strictEqual(fields.oldThreadWaitMode, 0);
+ assert.strictEqual(fields.oldThreadState, 2);
+ assert.strictEqual(fields.oldThreadWaitIdealProcessor, 4);
+ assert.strictEqual(fields.newThreadWaitTime, 1);
+ assert.strictEqual(fields.reserved, 881356167);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer.html
new file mode 100644
index 00000000000..8fd2fcbf870
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/trace_stream.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.importer.fuchsia', function() {
+ const IMPORT_PRIORITY = 0;
+ const IDLE_THREAD_THRESHOLD = 6444000000;
+
+ // Zircon thread state constants from:
+ // https://fuchsia.googlesource.com/zircon/+/master/docs/syscalls/object_get_info.md
+ const ZX_THREAD_STATE_NEW = 0;
+ const ZX_THREAD_STATE_RUNNING = 1;
+ const ZX_THREAD_STATE_SUSPENDED = 2;
+ const ZX_THREAD_STATE_BLOCKED = 3;
+ const ZX_THREAD_STATE_DYING = 4;
+ const ZX_THREAD_STATE_DEAD = 5;
+
+ class FuchsiaImporter extends tr.importer.Importer {
+ constructor(model, eventData) {
+ super(model, eventData);
+ this.importPriority = IMPORT_PRIORITY;
+ this.model_ = model;
+ this.events_ = eventData.events;
+ this.parsers_ = [];
+ this.threadInfo_ = new Map();
+ this.processNames_ = new Map();
+ this.threadStates_ = new Map();
+ }
+
+ static canImport(eventData) {
+ if (eventData instanceof tr.b.TraceStream) {
+ if (eventData.isBinary) return false;
+ eventData = eventData.header;
+ }
+
+ if (eventData instanceof Object && eventData.type === 'fuchsia') {
+ return true;
+ }
+
+ return false;
+ }
+
+ get importerName() {
+ return 'FuchsiaImporter';
+ }
+
+ get model() {
+ return this.model_;
+ }
+
+ importClockSyncMarkers() {
+ }
+
+ finalizeImport() {
+ }
+
+ isIdleThread(prio, tid) {
+ if (prio === undefined) {
+ // If the "prio" field is not available (if we were, for example,
+ // using an old trace), then fall back to the legacy heuristic of
+ // assuming that large numbered threads are idle ones.
+ return tid > IDLE_THREAD_THRESHOLD;
+ }
+ // A thread is idle iff its priority is set to 0.
+ return prio === 0;
+ }
+
+ recordThreadState_(tid, timestamp, state, prio) {
+ if (this.isIdleThread(prio, tid)) {
+ return;
+ }
+ const states =
+ this.threadStates_.has(tid) ? this.threadStates_.get(tid) : [];
+ states.push({'ts': timestamp, state});
+ this.threadStates_.set(tid, states);
+ }
+
+ // Context switch events take the form:
+ // {
+ // "ph": "k",
+ // "ts": 151981130.88743783,
+ // "cpu": 1,
+ // "out": {
+ // "pid": 25977,
+ // "tid": 28909,
+ // "state": 3,
+ // "prio": 20
+ // },
+ // "in": {
+ // "pid": 0,
+ // "tid": 6444931392,
+ // "prio": 0
+ // }
+ // },
+ processContextSwitchEvent_(event) {
+ let tid = event.in.tid;
+ let threadName = tid.toString();
+ let procName = '';
+ const prio = event.in.prio;
+
+ if (this.threadInfo_.has(tid)) {
+ const threadInfo = this.threadInfo_.get(tid);
+ threadName = threadInfo.name;
+ const pid = threadInfo.pid;
+ if (this.processNames_.has(pid)) {
+ procName = this.processNames_.get(pid) + ':';
+ }
+ }
+
+ const name = procName + threadName;
+
+ if (this.isIdleThread(prio, tid)) {
+ tid = undefined; // Fake kernel idle task
+ }
+
+ const cpu = this.model_.kernel.getOrCreateCpu(event.cpu);
+ const timestamp = tr.b.Unit.timestampFromUs(event.ts);
+ cpu.switchActiveThread(timestamp, {}, tid, name, tid);
+
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ this.recordThreadState_(tid, timestamp, SCHEDULING_STATE.RUNNING, prio);
+
+ let outState = SCHEDULING_STATE.UNKNOWN;
+
+ switch (event.out.state) {
+ case ZX_THREAD_STATE_NEW:
+ outState = SCHEDULING_STATE.RUNNABLE;
+ break;
+
+ case ZX_THREAD_STATE_RUNNING:
+ outState = SCHEDULING_STATE.RUNNABLE;
+ break;
+
+ case ZX_THREAD_STATE_BLOCKED:
+ outState = SCHEDULING_STATE.SLEEPING;
+ break;
+
+ case ZX_THREAD_STATE_SUSPENDED:
+ outState = SCHEDULING_STATE.STOPPED;
+ break;
+
+ case ZX_THREAD_STATE_DEAD:
+ outState = SCHEDULING_STATE.TASK_DEAD;
+ break;
+ }
+ this.recordThreadState_(event.out.tid, timestamp, outState,
+ event.out.prio);
+ }
+
+ processProcessInfoEvent_(event) {
+ const process = this.model_.getOrCreateProcess(event.pid);
+ process.name = event.name;
+ this.processNames_.set(event.pid, event.name);
+
+ if ('sort_index' in event) {
+ process.sortIndex = event.sort_index;
+ }
+ }
+
+ processThreadInfoEvent_(event) {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ thread.name = event.name;
+ this.threadInfo_.set(event.tid, {'name': event.name, 'pid': event.pid});
+
+ if ('sort_index' in event) {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ thread.sortIndex = event.sort_index;
+ }
+ }
+
+ processEvent_(event) {
+ switch (event.ph) {
+ case 'k':
+ this.processContextSwitchEvent_(event);
+ break;
+ case 'p':
+ this.processProcessInfoEvent_(event);
+ break;
+ case 't':
+ this.processThreadInfoEvent_(event);
+ break;
+ }
+ }
+
+ postProcessStates_() {
+ for (const [tid, states] of this.threadStates_) {
+ if (!this.threadInfo_.has(tid)) {
+ continue;
+ }
+ const pid = this.threadInfo_.get(tid).pid;
+ const thread = this.model_.getOrCreateProcess(
+ pid).getOrCreateThread(tid);
+ const slices = [];
+ for (let i = 0; i < states.length - 1; i++) {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, states[i].state, '',
+ states[i].ts, {}, states[i + 1].ts - states[i].ts));
+ }
+
+ thread.timeSlices = slices;
+ }
+ }
+
+ /**
+ * Imports the data in this.events_ into model_.
+ */
+ importEvents() {
+ for (const event of this.events_) {
+ this.processEvent_(event);
+ }
+ this.postProcessStates_();
+ }
+ }
+
+ tr.importer.Importer.register(FuchsiaImporter);
+
+ return {
+ FuchsiaImporter,
+ IMPORT_PRIORITY,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer_test.html
new file mode 100644
index 00000000000..e7494f0da82
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/fuchsia_importer_test.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/fuchsia_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ // Test that fuchsia process and thread info events properly annotate
+ // thread and process names.
+ test('processAndThreadInfo', function() {
+ const events = `{
+ "displayTimeUnit": "ns",
+ "traceEvents": [
+ {
+ "cat": "cat0",
+ "name": "name0",
+ "ts": 1.0,
+ "pid": 1,
+ "tid": 2,
+ "ph": "B"
+ },
+ {
+ "cat": "cat0",
+ "name": "name0",
+ "ts": 2.0,
+ "pid": 1,
+ "tid": 2,
+ "ph": "E"
+ }
+ ],
+ "systemTraceEvents": {
+ "type": "fuchsia",
+ "events": [
+ {
+ "ph": "p",
+ "pid": 1,
+ "name": "proc1"
+ },
+ {
+ "ph": "t",
+ "pid": 1,
+ "tid": 2,
+ "name": "thread2"
+ }
+ ]
+ }
+ }`;
+ const m = tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ const process = m.getProcess(1);
+ assert.isDefined(process);
+ assert.strictEqual(process.name, 'proc1');
+
+ const thread = process.getThread(2);
+ assert.isDefined(thread);
+ assert.strictEqual(thread.name, 'thread2');
+ });
+
+ // Test that context switch events create CPU slices.
+ test('cpuSlices', function() {
+ const events = `{
+ "displayTimeUnit": "ns",
+ "traceEvents": [
+ ],
+ "systemTraceEvents": {
+ "type": "fuchsia",
+ "events": [
+ {
+ "ph": "p",
+ "pid": 1,
+ "name": "proc1"
+ },
+ {
+ "ph": "t",
+ "pid": 1,
+ "tid": 2,
+ "name": "thread2"
+ },
+ {
+ "ph": "t",
+ "pid": 1,
+ "tid": 3,
+ "name": "thread3"
+ },
+ {
+ "ph": "k",
+ "ts": 1.0,
+ "cpu": 1,
+ "out": {
+ "pid": 1,
+ "tid": 2,
+ "state": 1
+ },
+ "in": {
+ "pid": 1,
+ "tid": 3
+ }
+ },
+ {
+ "ph": "k",
+ "ts": 2.0,
+ "cpu": 1,
+ "out": {
+ "pid": 1,
+ "tid": 3,
+ "state": 3
+ },
+ "in": {
+ "pid": 1,
+ "tid": 2
+ }
+ },
+ {
+ "ph": "k",
+ "ts": 3.0,
+ "cpu": 1,
+ "out": {
+ "pid": 1,
+ "tid": 2,
+ "state": 1
+ },
+ "in": {
+ "pid": 1,
+ "tid": 3
+ }
+ }
+ ]
+ }
+ }`;
+ const m = tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+ assert.strictEqual(cpu.slices.length, 2);
+
+ assert.strictEqual(cpu.slices[0].title, 'proc1:thread3');
+ assert.closeTo(cpu.slices[0].duration, 0.001, 1e-5);
+
+ assert.strictEqual(cpu.slices[1].title, 'proc1:thread2');
+ assert.closeTo(cpu.slices[1].duration, 0.001, 1e-5);
+
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+
+ const thread2Slices =
+ m.getOrCreateProcess(1).getOrCreateThread(2).timeSlices;
+ assert.strictEqual(thread2Slices[0].schedulingState,
+ SCHEDULING_STATE.RUNNABLE);
+ assert.closeTo(thread2Slices[0].start, 0.001, 1e-5);
+ assert.closeTo(thread2Slices[0].duration, 0.001, 1e-5);
+ assert.strictEqual(thread2Slices[1].schedulingState,
+ SCHEDULING_STATE.RUNNING);
+ assert.closeTo(thread2Slices[1].start, 0.002, 1e-5);
+ assert.closeTo(thread2Slices[1].duration, 0.001, 1e-5);
+
+ const thread3Slices =
+ m.getOrCreateProcess(1).getOrCreateThread(3).timeSlices;
+ assert.strictEqual(thread3Slices[0].schedulingState,
+ SCHEDULING_STATE.RUNNING);
+ assert.closeTo(thread3Slices[0].start, 0.001, 1e-5);
+ assert.closeTo(thread3Slices[0].duration, 0.001, 1e-5);
+ assert.strictEqual(thread3Slices[1].schedulingState,
+ SCHEDULING_STATE.SLEEPING);
+ assert.closeTo(thread3Slices[1].start, 0.002, 1e-5);
+ assert.closeTo(thread3Slices[1].duration, 0.001, 1e-5);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html
new file mode 100644
index 00000000000..bd3ce854503
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/importer/importer.html">
+
+<script>
+
+'use strict';
+
+/**
+ * @fileoverview GcloudTraceImporter imports JSON data from Google Cloud Trace.
+ */
+tr.exportTo('tr.e.importer.gcloud_trace', function() {
+ function GcloudTraceImporter(model, eventData) {
+ this.importPriority = 2;
+ this.eventData_ = eventData;
+ }
+
+ /**
+ * @return {boolean} Whether obj looks like the JSON output from Cloud Trace.
+ */
+ GcloudTraceImporter.canImport = function(eventData) {
+ if (typeof(eventData) !== 'string' && !(eventData instanceof String)) {
+ return false;
+ }
+
+ // Slice the data so we don't potentially do a replace on a gigantic string.
+ const normalizedEventData = eventData.slice(0, 20).replace(/\s/g, '');
+ if (normalizedEventData.length < 14) return false;
+
+ return normalizedEventData.slice(0, 14) === '{"projectId":"';
+ };
+
+ GcloudTraceImporter.prototype = {
+
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'GcloudTraceImporter';
+ },
+
+ /**
+ * Called by the Model to extract subtraces from the event data. The
+ * subtraces are passed on to other importers that can recognize them.
+ */
+ extractSubtraces() {
+ const traceEvents = this.createEventsForTrace();
+ return traceEvents ? [traceEvents] : [];
+ },
+
+ createEventsForTrace() {
+ const events = [];
+ const trace = JSON.parse(this.eventData_);
+ const spanLength = trace.spans.length;
+ for (let i = 0; i < spanLength; i++) {
+ events.push(this.createEventForSpan(trace.traceId, trace.spans[i]));
+ }
+ return {
+ 'traceEvents': events
+ };
+ },
+
+ createEventForSpan(traceId, span) {
+ let newArgs = {};
+ if (span.labels) {
+ newArgs = JSON.parse(JSON.stringify(span.labels));
+ }
+ newArgs['Span ID'] = span.spanId;
+ newArgs['Start Time'] = span.startTime;
+ newArgs['End Time'] = span.endTime;
+ if (span.parentSpanId) {
+ newArgs['Parent Span ID'] = span.parentSpanId;
+ }
+ // The timestamps are ISO-standard strings, which are parsed to millis,
+ // then converted to the micros that the trace viewer expects.
+ return {
+ name: span.name,
+ args: newArgs,
+ pid: traceId,
+ ts: Date.parse(span.startTime) * 1000,
+ dur: (Date.parse(span.endTime) - Date.parse(span.startTime)) * 1000,
+ cat: 'tracespan',
+ tid: traceId,
+ ph: 'X'
+ };
+ }
+ };
+
+ tr.importer.Importer.register(GcloudTraceImporter);
+
+ return {
+ GcloudTraceImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer_test.html
new file mode 100644
index 00000000000..b902ff5fce9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/gcloud_trace/gcloud_trace_importer_test.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const GcloudTraceImporter = tr.e.importer.gcloud_trace.GcloudTraceImporter;
+
+ test('noSpans', function() {
+ const trace = {projectId: 'My Project', traceId: '123', spans: []};
+
+ const model = new tr.Model();
+ const importer = new tr.e.importer.gcloud_trace.GcloudTraceImporter(model,
+ JSON.stringify(trace));
+ const subtraces = importer.extractSubtraces();
+ assert.strictEqual(1, subtraces.length);
+ // Note there are, in fact, use cases for traces with no events (spans).
+ assert.deepEqual([], subtraces[0].traceEvents);
+ });
+
+ test('typicalTrace', function() {
+ const span1 = {
+ 'spanId': '1',
+ 'kind': 'RPC_CLIENT',
+ 'name': '/first',
+ 'startTime': '2015-09-03T16:40:00.841654024Z',
+ 'endTime': '2015-09-03T16:40:00.856599389Z',
+ 'labels': {
+ 'key1': 'value1',
+ 'key2': 'value2'
+ }
+ };
+ const span2 = {
+ 'spanId': '2',
+ 'kind': 'RPC_SERVER',
+ 'name': '/second',
+ 'startTime': '2015-09-03T16:40:00.842880028Z',
+ 'endTime': '2015-09-03T16:40:00.851729538Z',
+ 'parentSpanId': '1',
+ 'labels': {
+ 'key1': 'value3',
+ 'key2': 'value4'
+ }
+ };
+ const trace = {projectId: 'My Project', traceId: '123',
+ spans: [span1, span2]};
+
+ const model = new tr.Model();
+ const importer = new tr.e.importer.gcloud_trace.GcloudTraceImporter(model,
+ JSON.stringify(trace));
+ const subtraces = importer.extractSubtraces();
+ assert.strictEqual(1, subtraces.length);
+ assert.strictEqual(2, subtraces[0].traceEvents.length);
+
+ const span1Event = subtraces[0].traceEvents[0];
+ assert.strictEqual('tracespan', span1Event.cat);
+ assert.strictEqual('/first', span1Event.name);
+ assert.strictEqual(1441298400841000, span1Event.ts);
+ assert.strictEqual(15000, span1Event.dur);
+ assert.strictEqual('123', span1Event.pid);
+ assert.strictEqual('123', span1Event.tid);
+ assert.strictEqual(
+ '2015-09-03T16:40:00.856599389Z', span1Event.args['End Time']);
+ assert.strictEqual('1', span1Event.args['Span ID']);
+ assert.strictEqual('value1', span1Event.args.key1);
+ assert.strictEqual('value2', span1Event.args.key2);
+ assert.strictEqual(
+ '2015-09-03T16:40:00.856599389Z', span1Event.args['End Time']);
+
+ const span2Event = subtraces[0].traceEvents[1];
+ assert.strictEqual('tracespan', span2Event.cat);
+ assert.strictEqual('/second', span2Event.name);
+ assert.strictEqual(1441298400842000, span2Event.ts);
+ assert.strictEqual(9000, span2Event.dur);
+ assert.strictEqual('123', span2Event.pid);
+ assert.strictEqual('123', span2Event.tid);
+ });
+
+ test('canImport', function() {
+ assert.isTrue(GcloudTraceImporter.canImport(
+ JSON.stringify({projectId: 'My Project'})));
+ assert.isTrue(GcloudTraceImporter.canImport(
+ JSON.stringify({projectId: '56', traceId: '34'})));
+ assert.isFalse(GcloudTraceImporter.canImport(
+ JSON.stringify({wrongjson: '33'})));
+ assert.isFalse(GcloudTraceImporter.canImport(''));
+ assert.isFalse(GcloudTraceImporter.canImport([]));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer.html
new file mode 100644
index 00000000000..88b6edba0d6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer.html
@@ -0,0 +1,252 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
+<link rel="import" href="/tracing/extras/importer/pako.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview GzipImporter inflates gzip compressed data and passes it along
+ * to an actual importer.
+ */
+tr.exportTo('tr.e.importer', function() {
+ const GZIP_MEMBER_HEADER_ID_SIZE = 3;
+
+ const GZIP_HEADER_ID1 = 0x1f;
+ const GZIP_HEADER_ID2 = 0x8b;
+ const GZIP_DEFLATE_COMPRESSION = 8;
+
+ function _stringToUInt8Array(str) {
+ const array = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; ++i) {
+ array[i] = str.charCodeAt(i);
+ }
+ return array;
+ }
+
+ function GzipImporter(model, eventData) {
+ // Normalize the data into an Uint8Array.
+ this.inflateAsTraceStream = false;
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ eventData = _stringToUInt8Array(eventData);
+ } else if (eventData instanceof ArrayBuffer) {
+ eventData = new Uint8Array(eventData);
+ } else if (eventData instanceof tr.b.InMemoryTraceStream) {
+ // This importer does not support processing general TraceStreams, only
+ // InMemoryTraceStreams for now.
+ eventData = eventData.data;
+ this.inflateAsTraceStream_ = true;
+ } else {
+ throw new Error('Unknown gzip data format');
+ }
+ this.model_ = model;
+ this.gzipData_ = eventData;
+ }
+
+ /**
+ * @param {eventData} Possibly gzip compressed data as a string or an
+ * ArrayBuffer.
+ * @return {boolean} Whether obj looks like gzip compressed data.
+ */
+ GzipImporter.canImport = function(eventData) {
+ // This importer does not support processing general TraceStreams, only
+ // InMemoryTraceStreams for now.
+ if (eventData instanceof tr.b.InMemoryTraceStream) {
+ eventData = eventData.header;
+ }
+
+ let header;
+ if (eventData instanceof ArrayBuffer) {
+ header = new Uint8Array(eventData.slice(0, GZIP_MEMBER_HEADER_ID_SIZE));
+ } else if (typeof(eventData) === 'string' || eventData instanceof String) {
+ header = eventData.substring(0, GZIP_MEMBER_HEADER_ID_SIZE);
+ header = _stringToUInt8Array(header);
+ } else {
+ return false;
+ }
+ return header[0] === GZIP_HEADER_ID1 &&
+ header[1] === GZIP_HEADER_ID2 &&
+ header[2] === GZIP_DEFLATE_COMPRESSION;
+ };
+
+ /**
+ * Inflates (decompresses) the data stored in the given gzip bitstream.
+ * @return {string} Inflated data.
+ */
+ GzipImporter.inflateGzipData_ = function(data) {
+ let position = 0;
+
+ function getByte() {
+ if (position >= data.length) {
+ throw new Error('Unexpected end of gzip data');
+ }
+ return data[position++];
+ }
+
+ function getWord() {
+ const low = getByte();
+ const high = getByte();
+ return (high << 8) + low;
+ }
+
+ function skipBytes(amount) {
+ position += amount;
+ }
+
+ function skipZeroTerminatedString() {
+ while (getByte() !== 0) {}
+ }
+
+ const id1 = getByte();
+ const id2 = getByte();
+ if (id1 !== GZIP_HEADER_ID1 || id2 !== GZIP_HEADER_ID2) {
+ throw new Error('Not gzip data');
+ }
+ const compressionMethod = getByte();
+ if (compressionMethod !== GZIP_DEFLATE_COMPRESSION) {
+ throw new Error('Unsupported compression method: ' + compressionMethod);
+ }
+ const flags = getByte();
+ const haveHeaderCrc = flags & (1 << 1);
+ const haveExtraFields = flags & (1 << 2);
+ const haveFileName = flags & (1 << 3);
+ const haveComment = flags & (1 << 4);
+
+ // Skip modification time, extra flags and OS.
+ skipBytes(4 + 1 + 1);
+
+ // Skip remaining fields before compressed data.
+ if (haveExtraFields) {
+ const bytesToSkip = getWord();
+ skipBytes(bytesToSkip);
+ }
+ if (haveFileName) skipZeroTerminatedString();
+ if (haveComment) skipZeroTerminatedString();
+ if (haveHeaderCrc) getWord();
+
+ // Inflate the data using pako.
+ const inflatedData = pako.inflateRaw(data.subarray(position));
+
+ if (this.inflateAsTraceStream_) {
+ return GzipImporter.transformToStream(inflatedData);
+ }
+
+ let string;
+ try {
+ string = GzipImporter.transformToString(inflatedData);
+ } catch (err) {
+ // It may be the case that inflated data does not fit into a V8 string. In
+ // that case, try to transform to a trace stream.
+ return GzipImporter.transformToStream(inflatedData);
+ }
+
+ if (inflatedData.length > 0 && string.length === 0) {
+ // Try transforming to a trace stream.
+ return GzipImporter.transformToStream(inflatedData);
+ }
+
+ return string;
+ };
+
+ GzipImporter.transformToStream = function(data) {
+ if (data instanceof Uint8Array) {
+ return new tr.b.InMemoryTraceStream(data, false);
+ }
+
+ throw new Error(`Cannot transform ${type} to TraceStream.`);
+ };
+
+ /**
+ * Transforms an array-like object to a string.
+ *
+ * Note that the following two expressions yield identical results:
+ *
+ * GzipImporter.transformToString_(data)
+ * JSZip.utils.transformTo('string', data)
+ *
+ * We use a custom static method because it is faster and, more importantly,
+ * avoids OOMing on large traces. See
+ * https://github.com/catapult-project/catapult/issues/2051.
+ */
+ GzipImporter.transformToString = function(data) {
+ if (typeof(data) === 'string') return data; // We already have a string.
+
+ // Fall back to manual conversion if TextDecoder is not available.
+ if (typeof TextDecoder === 'undefined') {
+ if (data instanceof ArrayBuffer) {
+ data = new Uint8Array(data);
+ }
+
+ // Based on JSZip.Utils.stringToArrayLike
+ const result = [];
+ let chunk = 65536;
+ let k = 0;
+ const len = data.length;
+
+ while (k < len && chunk > 1) {
+ try {
+ const chunklen = Math.min(k + chunk, len);
+ let dataslice;
+ if (data instanceof Array) {
+ dataslice = data.slice(k, chunklen);
+ } else {
+ dataslice = data.subarray(k, chunklen);
+ }
+ result.push(String.fromCharCode.apply(null, dataslice));
+ k += chunk;
+ } catch (e) {
+ chunk = Math.floor(chunk / 2);
+ }
+ }
+
+ return result.join('');
+ }
+
+ if (data instanceof Array) {
+ // TextDecoder requires an ArrayBuffer or an ArrayBufferView.
+ data = new Uint8Array(data);
+ }
+
+ return new TextDecoder('utf-8').decode(data);
+ };
+
+ GzipImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'GzipImporter';
+ },
+
+ /**
+ * Called by the Model to check whether the importer just encapsulates
+ * the actual trace data which needs to be imported by another importer.
+ */
+ isTraceDataContainer() {
+ return true;
+ },
+
+ /**
+ * Called by the Model to extract subtraces from the event data. The
+ * subtraces are passed on to other importers that can recognize them.
+ */
+ extractSubtraces() {
+ const eventData = GzipImporter.inflateGzipData_(this.gzipData_);
+ return eventData ? [eventData] : [];
+ }
+ };
+
+ tr.importer.Importer.register(GzipImporter);
+
+ return {
+ GzipImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer_test.html
new file mode 100644
index 00000000000..32fd72be45a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/gzip_importer_test.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/base/trace_stream.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/gzip_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+ const findSliceNamed = tr.c.TestUtils.findSliceNamed;
+ const ORIGINAL_DATA =
+ '[{"name":"a","args":{},"pid":52,"ts":520,"cat":"foo","tid":53,' +
+ '"ph":"B"},{"name":"a","args":{},"pid":52,"ts":520,"cat":"foo",' +
+ '"tid":53,"ph":"E"}]\n';
+ const GZIP_DATA_BASE_64 =
+ 'H4sICHr4HVIAA3RyYWNlAIuuVspLzE1VslJKVNJRSixKL1ayqq7VUSrITFGy' +
+ 'MjXSUSopBtEGOkrJiSVAVWn5+UB1JWBZY6CyDKCYk1KtDhWMcVWqjeUCALak' +
+ 'EH+QAAAA';
+
+ test('failImportEmpty', function() {
+ assert.isFalse(tr.e.importer.GzipImporter.canImport([]));
+ assert.isFalse(tr.e.importer.GzipImporter.canImport(''));
+ });
+
+ test('inflateString', function() {
+ // Test inflating the data from a string.
+ const gzipData = Base64.atob(GZIP_DATA_BASE_64);
+ const importer = new tr.e.importer.GzipImporter(null, gzipData);
+ assert.isTrue(tr.e.importer.GzipImporter.canImport(gzipData));
+ assert.strictEqual(
+ importer.extractSubtraces()[0].toString(), ORIGINAL_DATA);
+ });
+
+ test('inflateArrayBuffer', function() {
+ // Test inflating the data from an ArrayBuffer.
+ const gzipData = Base64.atob(GZIP_DATA_BASE_64);
+ const buffer = new ArrayBuffer(gzipData.length);
+ const view = new Uint8Array(buffer);
+ for (let i = 0; i < gzipData.length; i++) {
+ view[i] = gzipData.charCodeAt(i);
+ }
+ const importer = new tr.e.importer.GzipImporter(null, buffer);
+ assert.isTrue(tr.e.importer.GzipImporter.canImport(buffer));
+ assert.strictEqual(
+ importer.extractSubtraces()[0].toString(), ORIGINAL_DATA);
+ });
+
+ test('import', function() {
+ const gzipData = Base64.atob(GZIP_DATA_BASE_64);
+ assert.isTrue(tr.e.importer.GzipImporter.canImport(gzipData));
+
+ const model = tr.c.TestUtils.newModelWithEvents(gzipData);
+ const threads = model.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const slice = findSliceNamed(threads[0].sliceGroup, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ });
+
+ test('transformToString', function() {
+ function checkTransform(data, expectedString) {
+ assert.strictEqual(tr.e.importer.GzipImporter.transformToString(data),
+ expectedString);
+ }
+
+ function createArrayBuffer(values) {
+ const buffer = new ArrayBuffer(values.length);
+ const view = new Uint8Array(buffer);
+ view.set(values);
+ return buffer;
+ }
+
+ // If the browser supports TextDecoder, this will test our custom
+ // implementation. Otherwise, the jszip fallback will be tested.
+ checkTransform('abc012', 'abc012');
+ checkTransform([100, 101, 102, 51, 52, 53], 'def345');
+ checkTransform(createArrayBuffer([103, 104, 105, 54, 55, 56]), 'ghi678');
+ checkTransform(new Uint8Array([106, 107, 108, 57, 58, 59]), 'jkl9:;');
+
+ if (typeof TextDecoder === 'undefined') {
+ // The browser doesn't support TextDecoder, so we have already checked
+ // the jszip fallback.
+ return;
+ }
+
+ // The browser supports TextDecoder, so we now check the jszip fallback.
+ const oldTextDecoder = TextDecoder;
+ TextDecoder = undefined;
+ try {
+ checkTransform('abc012', 'abc012');
+ checkTransform([100, 101, 102, 51, 52, 53], 'def345');
+ checkTransform(createArrayBuffer([103, 104, 105, 54, 55, 56]), 'ghi678');
+ checkTransform(new Uint8Array([106, 107, 108, 57, 58, 59]), 'jkl9:;');
+ } finally {
+ TextDecoder = oldTextDecoder;
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/heap_dump_trace_event_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/heap_dump_trace_event_importer.html
new file mode 100644
index 00000000000..e22671b6359
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/heap_dump_trace_event_importer.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/model/heap_dump.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Logic for importing a Heap Dump.
+ */
+tr.exportTo('tr.e.importer', function() {
+ /**
+ * @constructor
+ */
+ function HeapDumpTraceEventImporter(
+ heapProfileExpander,
+ stackFrames,
+ processMemoryDump,
+ idPrefix,
+ model) {
+ this.expander = heapProfileExpander;
+ this.stackFrames = stackFrames;
+ this.processMemoryDump = processMemoryDump;
+ this.idPrefix = idPrefix;
+ this.model = model;
+ }
+
+ HeapDumpTraceEventImporter.prototype = {
+
+ getLeafStackFrame(stackFrameId) {
+ // Root.
+ if (stackFrameId === '') return undefined;
+ const parentId = this.idPrefix + stackFrameId;
+ const id = parentId + ':self';
+
+ // In the new format all values are 'self' values,
+ // we distingiush these from the totals in the UI via
+ // artificial '<self>' stack frames.
+ if (!this.stackFrames[id]) {
+ const parentStackFrame = this.stackFrames[parentId];
+ const stackFrame = new tr.model.StackFrame(
+ parentStackFrame, id, '<self>',
+ undefined /* colorId */);
+ this.model.addStackFrame(stackFrame);
+ }
+ return this.stackFrames[id];
+ },
+
+ parseEntry(entry, heapDump) {
+ const size = entry.size;
+ const count = entry.count;
+ const leafStackFrame = this.getLeafStackFrame(entry.node.id);
+ const objectTypeName = entry.type.name;
+ const valuesAreTotals = false;
+ if (objectTypeName === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Missing object type name (ID ' + typeId + ')',
+ });
+ }
+ heapDump.addEntry(
+ leafStackFrame, objectTypeName, size, count, valuesAreTotals);
+ },
+
+ parse() {
+ const heapDumps = {};
+ const inflated = this.expander.inflated;
+ for (const [allocatorName, entries] of Object.entries(inflated)) {
+ const heapDump =
+ new tr.model.HeapDump(this.processMemoryDump, allocatorName);
+ for (const entry of entries) {
+ this.parseEntry(entry, heapDump);
+ }
+ heapDump.isComplete = true;
+ heapDumps[allocatorName] = heapDump;
+ }
+ return heapDumps;
+ },
+
+ };
+
+ return {
+ HeapDumpTraceEventImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/jszip.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/jszip.html
new file mode 100644
index 00000000000..5db6c4bbfc3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/jszip.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+// Vinn workaround for JSzip requiring window.
+if (tr.isVinn) {
+ /**
+ * Hack.
+ */
+ global.window = {};
+}
+</script>
+<script src="/jszip.min.js"></script>
+<script>
+'use strict';
+// Vinn workaround for JSzip requiring window.
+if (tr.isVinn) {
+ /**
+ * Hack.
+ */
+ global.JSZip = global.window.JSZip;
+ global.window = undefined;
+} else if (tr.isNode) {
+ const jsZipAbsPath = HTMLImportsLoader.hrefToAbsolutePath(
+ '/jszip.min.js');
+ const jsZipModule = require(jsZipAbsPath);
+ global.JSZip = jsZipModule;
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/legacy_heap_dump_trace_event_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/legacy_heap_dump_trace_event_importer.html
new file mode 100644
index 00000000000..ee0097b903c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/legacy_heap_dump_trace_event_importer.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/heap_dump.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Logic for importing a Heap Dump.
+ */
+tr.exportTo('tr.e.importer', function() {
+ /**
+ * @constructor
+ * @param {!tr.model.Model} model The model we are currently building.
+ * @param {!tr.model.ProcessMemoryDump} processMemoryDump
+ * The parent memory dump for this heap dump.
+ * @param {!Map|undefined} processObjectTypeNameMap
+ * A map from raw heap dump 'type' ids to human-readable names.
+ * @param {!string} idPrefix Process-specific prefix to prepend to a stack
+ * trace id before looking it up in the model.
+ * @param {!string} dumpId
+ * Raw heap dump id, used only for nice error messages.
+ * @param {!Object} rawHeapDumps
+ * Raw heap dump.
+ */
+ function LegacyHeapDumpTraceEventImporter(
+ model,
+ processMemoryDump,
+ processObjectTypeNameMap,
+ idPrefix,
+ dumpId,
+ rawHeapDumps) {
+ this.model_ = model;
+ this.processObjectTypeNameMap_ = processObjectTypeNameMap;
+ this.idPrefix_ = idPrefix;
+ this.processMemoryDump_ = processMemoryDump;
+ this.pid_ = this.processMemoryDump_.process.pid;
+ this.dumpId_ = dumpId;
+ this.rawHeapDumps_ = rawHeapDumps;
+ }
+
+ LegacyHeapDumpTraceEventImporter.prototype = {
+ /**
+ * Parse rawHeapDump and add entries to heapDump.
+ *
+ * @param {!{!entries:(!Array<!Object>|undefined)}} rawHeapDump
+ * The data we're going to parse.
+ * @param {!string} allocatorName e.g. malloc.
+ * @return {!tr.model.HeapDump} on success or undefined on an error.
+ */
+ parseRawHeapDump(rawHeapDump, allocatorName) {
+ const model = this.model_;
+ const processMemoryDump = this.processMemoryDump_;
+ const heapDump = new tr.model.HeapDump(processMemoryDump, allocatorName);
+
+ const entries = rawHeapDump.entries;
+ if (entries === undefined || entries.length === 0) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'No heap entries in a ' + allocatorName +
+ ' heap dump for PID=' + this.pid_ +
+ ' and dump ID=' + this.dumpId_ + '.'
+ });
+
+ return undefined;
+ }
+
+ // The old format always starts with a {size: <total>} entry.
+ // See https://goo.gl/WYStil
+ // TODO(petrcermak): Remove support for the old format once the new
+ // format has been around long enough.
+ const isOldFormat = entries[0].bt === undefined;
+ if (!isOldFormat && this.processObjectTypeNameMap_ === undefined) {
+ // Mapping from object type IDs to names must be provided in the new
+ // format.
+ return undefined;
+ }
+
+ for (let i = 0; i < entries.length; i++) {
+ const entry = entries[i];
+ const size = parseInt(entry.size, 16);
+ const leafStackFrameIndex = entry.bt;
+ let leafStackFrame;
+
+ // There are two possible mappings from leaf stack frame indices
+ // (provided in the trace) to the corresponding stack frames
+ // depending on the format.
+ if (isOldFormat) {
+ // Old format:
+ // Undefined index -> / (root)
+ // Defined index for /A/B -> /A/B/<self>
+ if (leafStackFrameIndex === undefined) {
+ leafStackFrame = undefined; /* root */
+ } else {
+ // Get the leaf stack frame corresponding to the provided index.
+ let leafStackFrameId = this.idPrefix_ + leafStackFrameIndex;
+ if (leafStackFrameIndex === '') {
+ leafStackFrame = undefined; /* root */
+ } else {
+ leafStackFrame = model.stackFrames[leafStackFrameId];
+ if (leafStackFrame === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Missing leaf stack frame (ID ' +
+ leafStackFrameId + ') of heap entry ' + i + ' (size ' +
+ size + ') in a ' + allocatorName +
+ ' heap dump for PID=' + this.pid_ + '.'
+ });
+ continue;
+ }
+ }
+
+ // Inject an artificial <self> leaf stack frame.
+ leafStackFrameId += ':self';
+ if (model.stackFrames[leafStackFrameId] !== undefined) {
+ // The frame might already exist if there are multiple process
+ // memory dumps (for the same process) in the trace.
+ leafStackFrame = model.stackFrames[leafStackFrameId];
+ } else {
+ leafStackFrame = new tr.model.StackFrame(
+ leafStackFrame, leafStackFrameId, '<self>',
+ undefined /* colorId */);
+ model.addStackFrame(leafStackFrame);
+ }
+ }
+ } else {
+ // New format:
+ // Undefined index -> (invalid value)
+ // Defined index for /A/B -> /A/B
+ if (leafStackFrameIndex === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Missing stack frame ID of heap entry ' + i +
+ ' (size ' + size + ') in a ' + allocatorName +
+ ' heap dump for PID=' + this.pid_ + '.'
+ });
+ continue;
+ }
+
+ // Get the leaf stack frame corresponding to the provided index.
+ const leafStackFrameId = this.idPrefix_ + leafStackFrameIndex;
+ if (leafStackFrameIndex === '') {
+ leafStackFrame = undefined; /* root */
+ } else {
+ leafStackFrame = model.stackFrames[leafStackFrameId];
+ if (leafStackFrame === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Missing leaf stack frame (ID ' + leafStackFrameId +
+ ') of heap entry ' + i + ' (size ' + size + ') in a ' +
+ allocatorName + ' heap dump for PID=' + this.pid_ + '.'
+ });
+ continue;
+ }
+ }
+ }
+
+ const objectTypeId = entry.type;
+ let objectTypeName;
+ if (objectTypeId === undefined) {
+ objectTypeName = undefined; /* total over all types */
+ } else if (this.processObjectTypeNameMap_ === undefined) {
+ // This can only happen when the old format is used.
+ continue;
+ } else {
+ objectTypeName = this.processObjectTypeNameMap_[objectTypeId];
+ if (objectTypeName === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Missing object type name (ID ' + objectTypeId +
+ ') of heap entry ' + i + ' (size ' + size + ') in a ' +
+ allocatorName + ' heap dump for PID=' + this.pid_ + '.'
+ });
+ continue;
+ }
+ }
+
+ const count = entry.count === undefined ? undefined :
+ parseInt(entry.count, 16);
+ heapDump.addEntry(leafStackFrame, objectTypeName, size, count);
+ }
+
+ return heapDump;
+ },
+
+ parse() {
+ const heapDumps = {};
+ for (const allocatorName in this.rawHeapDumps_) {
+ const rawHeapDump = this.rawHeapDumps_[allocatorName];
+ const heapDump = this.parseRawHeapDump(rawHeapDump, allocatorName);
+
+ // Throw away heap dumps with no entries. This can happen if all raw
+ // entries in the trace are skipped for some reason (e.g. invalid leaf
+ // stack frame ID).
+ if (heapDump !== undefined && heapDump.entries.length > 0) {
+ heapDumps[allocatorName] = heapDump;
+ }
+ }
+ return heapDumps;
+ },
+
+ };
+
+ return {
+ LegacyHeapDumpTraceEventImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser.html
new file mode 100644
index 00000000000..bf534205db6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser.html
@@ -0,0 +1,239 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses trace_marker events that were inserted in the trace by
+ * userland.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux trace mark events that were inserted in the trace by userland.
+ * @constructor
+ */
+ function AndroidParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('tracing_mark_write:android',
+ AndroidParser.prototype.traceMarkWriteAndroidEvent.bind(this));
+ importer.registerEventHandler('0:android',
+ AndroidParser.prototype.traceMarkWriteAndroidEvent.bind(this));
+
+ this.model_ = importer.model_;
+ this.ppids_ = {};
+ }
+
+ function parseArgs(argsString) {
+ const args = {};
+ if (argsString) {
+ const argsArray = argsString.split(';');
+ for (let i = 0; i < argsArray.length; ++i) {
+ const parts = argsArray[i].split('=');
+ if (parts[0]) {
+ args[parts.shift()] = parts.join('=');
+ }
+ }
+ }
+ return args;
+ }
+
+ AndroidParser.prototype = {
+ __proto__: Parser.prototype,
+
+ openAsyncSlice(thread, category, name, cookie, ts, args) {
+ const asyncSliceConstructor =
+ tr.model.AsyncSlice.subTypes.getConstructor(
+ category, name);
+ const slice = new asyncSliceConstructor(
+ category, name,
+ ColorScheme.getColorIdForGeneralPurposeString(name), ts, args);
+ const key = category + ':' + name + ':' + cookie;
+ slice.id = cookie;
+ slice.startThread = thread;
+
+ if (!this.openAsyncSlices) {
+ this.openAsyncSlices = { };
+ }
+ this.openAsyncSlices[key] = slice;
+ },
+
+ closeAsyncSlice(thread, category, name, cookie, ts, args) {
+ if (!this.openAsyncSlices) {
+ // No async slices have been started.
+ return;
+ }
+
+ const key = category + ':' + name + ':' + cookie;
+ const slice = this.openAsyncSlices[key];
+ if (!slice) {
+ // No async slices w/ this key have been started.
+ return;
+ }
+
+ for (const arg in args) {
+ if (slice.args[arg] !== undefined) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Both the S and F events of ' + slice.title +
+ ' provided values for argument ' + arg + '.' +
+ ' The value of the F event will be used.'
+ });
+ }
+ slice.args[arg] = args[arg];
+ }
+
+ slice.endThread = thread;
+ slice.duration = ts - slice.start;
+ slice.startThread.asyncSliceGroup.push(slice);
+ delete this.openAsyncSlices[key];
+ },
+
+ traceMarkWriteAndroidEvent(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ const eventData = eventBase.details.split('|');
+ switch (eventData[0]) {
+ case 'B': {
+ const ppid = parseInt(eventData[1]);
+ const title = eventData[2];
+ const args = parseArgs(eventData[3]);
+ let category = eventData[4];
+ if (category === undefined) category = 'android';
+
+ const thread = this.model_.getOrCreateProcess(ppid)
+ .getOrCreateThread(pid);
+ thread.name = eventBase.threadName;
+ if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(ts)) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Timestamps are moving backward.'
+ });
+ return false;
+ }
+
+ this.ppids_[pid] = ppid;
+ thread.sliceGroup.beginSlice(category, title, ts, args);
+
+ break;
+ }
+
+ case 'E': {
+ const ppid = this.ppids_[pid];
+ if (ppid === undefined) {
+ // Silently ignore unmatched E events.
+ break;
+ }
+
+ const thread = this.model_.getOrCreateProcess(ppid)
+ .getOrCreateThread(pid);
+ if (!thread.sliceGroup.openSliceCount) {
+ // Silently ignore unmatched E events.
+ break;
+ }
+
+ const slice = thread.sliceGroup.endSlice(ts);
+
+ const args = parseArgs(eventData[3]);
+ for (const arg in args) {
+ if (slice.args[arg] !== undefined) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Both the B and E events of ' + slice.title +
+ ' provided values for argument ' + arg + '.' +
+ ' The value of the E event will be used.'
+ });
+ }
+ slice.args[arg] = args[arg];
+ }
+
+ break;
+ }
+
+ case 'C': {
+ const ppid = parseInt(eventData[1]);
+ const name = eventData[2];
+ const value = parseInt(eventData[3]);
+ let category = eventData[4];
+ if (category === undefined) category = 'android';
+
+ const ctr = this.model_.getOrCreateProcess(ppid)
+ .getOrCreateCounter(category, name);
+ // Initialize the counter's series fields if needed.
+ if (ctr.numSeries === 0) {
+ ctr.addSeries(new tr.model.CounterSeries(value,
+ ColorScheme.getColorIdForGeneralPurposeString(
+ ctr.name + '.' + 'value')));
+ }
+
+ ctr.series.forEach(function(series) {
+ series.addCounterSample(ts, value);
+ });
+
+ break;
+ }
+
+ case 'S': {
+ const ppid = parseInt(eventData[1]);
+ const name = eventData[2];
+ const cookie = parseInt(eventData[3]);
+ const args = parseArgs(eventData[4]);
+ let category = eventData[5];
+ if (category === undefined) category = 'android';
+
+ const thread = this.model_.getOrCreateProcess(ppid)
+ .getOrCreateThread(pid);
+ thread.name = eventBase.threadName;
+
+ this.ppids_[pid] = ppid;
+ this.openAsyncSlice(thread, category, name, cookie, ts, args);
+
+ break;
+ }
+
+ case 'F': {
+ // Note: An async slice may end on a different thread from the one
+ // that started it so this thread may not have been seen yet.
+ const ppid = parseInt(eventData[1]);
+
+ const name = eventData[2];
+ const cookie = parseInt(eventData[3]);
+ const args = parseArgs(eventData[4]);
+ let category = eventData[5];
+ if (category === undefined) category = 'android';
+
+ const thread = this.model_.getOrCreateProcess(ppid)
+ .getOrCreateThread(pid);
+ thread.name = eventBase.threadName;
+
+ this.ppids_[pid] = ppid;
+ this.closeAsyncSlice(thread, category, name, cookie, ts, args);
+
+ break;
+ }
+
+ default:
+ return false;
+ }
+
+ return true;
+ }
+ };
+
+ Parser.register(AndroidParser);
+
+ return {
+ AndroidParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser_test.html
new file mode 100644
index 00000000000..0e53e7e5e65
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/android_parser_test.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('androidUserlandImport', function() {
+ const lines = [
+ 'SurfaceFlinger-4831 [001] ...1 80909.598554: tracing_mark_write: B|4829|onMessageReceived', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598572: tracing_mark_write: B|4829|handleMessageInvalidate', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598590: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598604: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598627: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598651: tracing_mark_write: B|4829|updateTexImage', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598675: tracing_mark_write: B|4829|acquireBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598695: tracing_mark_write: B|4829|' + // @suppress longLineCheck
+ 'com.android.launcher/com.android.launcher2.Launcher: 0',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598709: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598733: tracing_mark_write: C|4829|' + // @suppress longLineCheck
+ 'com.android.launcher/com.android.launcher2.Launcher|0',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598746: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598844: tracing_mark_write: B|4829|releaseBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598862: tracing_mark_write: B|4829|' + // @suppress longLineCheck
+ 'com.android.launcher/com.android.launcher2.Launcher: 2',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598876: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598892: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598925: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598955: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598988: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.599001: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599021: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.599036: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599068: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599087: tracing_mark_write: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599104: tracing_mark_write: E'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 4829);
+ assert.strictEqual(thread.tid, 4831);
+ assert.strictEqual(thread.name, 'SurfaceFlinger');
+ assert.strictEqual(thread.sliceGroup.length, 11);
+ });
+
+ test('androidUserlandImportWithSpacesInThreadName', function() {
+ const lines = [
+ 'Surface Flinger -4831 [001] ...1 80909.598590: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+ 'Surface Flinger -4831 [001] ...1 80909.598604: tracing_mark_write: E' // @suppress longLineCheck
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 4829);
+ assert.strictEqual(thread.tid, 4831);
+ assert.strictEqual(thread.name, 'Surface Flinger ');
+ assert.strictEqual(thread.sliceGroup.length, 1);
+ });
+
+ test('androidAsyncUserlandImport', function() {
+ const lines = [
+ 'ndroid.launcher-9649 ( 9649) [000] ...1 1990280.663276: ' +
+ 'tracing_mark_write: S|9649|animator:childrenOutlineAlpha|' +
+ '1113053968',
+ 'ndroid.launcher-9649 ( 9649) [000] ...1 1990280.781445: ' +
+ 'tracing_mark_write: F|9649|animator:childrenOutlineAlpha|' +
+ '1113053968'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 9649);
+ assert.strictEqual(thread.tid, 9649);
+ assert.strictEqual(thread.name, 'ndroid.launcher');
+ assert.strictEqual(thread.sliceGroup.length, 0);
+ assert.strictEqual(thread.asyncSliceGroup.length, 1);
+
+ const slice = thread.asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'animator:childrenOutlineAlpha');
+ assert.closeTo(118.169, slice.duration, 1e-5);
+
+ // no subslice events created
+ assert.strictEqual(slice.subSlices.length, 0);
+ });
+
+ test('androidUserlandLegacyKernelImport', function() {
+ const lines = [
+ 'SurfaceFlinger-4831 [001] ...1 80909.598554: 0: B|4829|onMessageReceived', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598572: 0: B|4829|handleMessageInvalidate', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598590: 0: B|4829|latchBuffer',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598604: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598627: 0: B|4829|latchBuffer',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598651: 0: B|4829|updateTexImage', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598675: 0: B|4829|acquireBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598695: 0: B|4829|' +
+ 'com.android.launcher/com.android.launcher2.Launcher: 0',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598709: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598733: 0: C|4829|' +
+ 'com.android.launcher/com.android.launcher2.Launcher|0',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598746: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598844: 0: B|4829|releaseBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.598862: 0: B|4829|' +
+ 'com.android.launcher/com.android.launcher2.Launcher: 2',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598876: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598892: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598925: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598955: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.598988: 0: B|4829|latchBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.599001: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599021: 0: B|4829|latchBuffer', // @suppress longLineCheck
+ 'SurfaceFlinger-4831 [001] ...1 80909.599036: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599068: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599087: 0: E',
+ 'SurfaceFlinger-4831 [001] ...1 80909.599104: 0: E'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 4829);
+ assert.strictEqual(thread.tid, 4831);
+ assert.strictEqual(thread.name, 'SurfaceFlinger');
+ assert.strictEqual(thread.sliceGroup.length, 11);
+ });
+
+ test('androidUserlandChromiumImport', function() {
+ const lines = [
+ 'SandboxedProces-2894 [001] ...1 253.780659: tracing_mark_write: B|2867|DoWorkLoop|arg1=1|cat1', // @suppress longLineCheck
+ 'SandboxedProces-2894 [001] ...1 253.780671: tracing_mark_write: B|2867|DeferOrRunPendingTask|source=test=test;task=xyz|cat2', // @suppress longLineCheck
+ 'SandboxedProces-2894 [001] ...1 253.780671: tracing_mark_write: E|2867|DeferOrRunPendingTask||cat1', // @suppress longLineCheck
+ 'SandboxedProces-2894 [001] ...1 253.780686: tracing_mark_write: B|2867|MessageLoop::RunTask|source=ipc/ipc_sync_message_filter.cc:Send|cat2', // @suppress longLineCheck
+ 'SandboxedProces-2894 [001] ...1 253.780700: tracing_mark_write: E|2867|MessageLoop::RunTask||cat1', // @suppress longLineCheck
+ 'SandboxedProces-2894 [001] ...1 253.780750: tracing_mark_write: C|2867|counter1|10|cat1', // @suppress longLineCheck
+ 'SandboxedProces-2894 [001] ...1 253.780859: tracing_mark_write: E|2867|DoWorkLoop|arg2=2|cat2', // @suppress longLineCheck
+ 'SandboxedProces-2894 [000] ...1 255.663276: tracing_mark_write: S|2867|async|1113053968|arg1=1;arg2=2|cat1', // @suppress longLineCheck
+ 'SandboxedProces-2894 [000] ...1 255.663276: tracing_mark_write: F|2867|async|1113053968|arg3=3|cat1', // @suppress longLineCheck
+ 'SandboxedProces-2894 [000] ...1 255.663276: tracing_mark_write: trace_event_clock_sync: parent_ts=128' // @suppress longLineCheck
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 2867);
+ assert.strictEqual(thread.tid, 2894);
+ assert.strictEqual(thread.name, 'SandboxedProces');
+ assert.strictEqual(thread.sliceGroup.length, 3);
+
+ assert.strictEqual(thread.sliceGroup.slices[0].args.arg1, '1');
+ assert.strictEqual(thread.sliceGroup.slices[0].args.arg2, '2');
+
+ assert.strictEqual(thread.sliceGroup.slices[1].args.source, 'test=test');
+ assert.strictEqual(thread.sliceGroup.slices[1].category, 'cat2');
+ assert.strictEqual('DeferOrRunPendingTask',
+ thread.sliceGroup.slices[1].title);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.task, 'xyz');
+
+ assert.strictEqual('ipc/ipc_sync_message_filter.cc:Send',
+ thread.sliceGroup.slices[2].args.source);
+
+ assert.strictEqual(thread.asyncSliceGroup.length, 1);
+ assert.strictEqual(thread.asyncSliceGroup.slices[0].args.arg1, '1');
+ assert.strictEqual(thread.asyncSliceGroup.slices[0].args.arg2, '2');
+ assert.strictEqual(thread.asyncSliceGroup.slices[0].args.arg3, '3');
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 1);
+ assert.strictEqual(counters[0].category, 'cat1');
+ assert.strictEqual(counters[0].name, 'counter1');
+
+ assert.strictEqual(counters[0].numSamples, 1);
+ assert.strictEqual(counters[0].getSeries(0).getSample(0).value, 10);
+
+ assert.strictEqual(Math.round((253.780659 - (255.663276 - 128)) * 1000),
+ Math.round(thread.sliceGroup.slices[0].start));
+ });
+
+ test('androidNeedReschedImport', function() {
+ const lines = [
+ 'RenderThread-3894 [001] ...1 253.780659: tracing_mark_write: B|3586|DrawFrame', // @suppress longLineCheck
+ 'RenderThread-3894 [001] ...1 253.780671: tracing_mark_write: B|3586|waitForTask', // @suppress longLineCheck
+ 'RenderThread-3894 [001] .N.1 253.780671: tracing_mark_write: E', // @suppress longLineCheck
+ 'RenderThread-3894 [001] ...1 253.780671: tracing_mark_write: B|3586|waitForTask', // @suppress longLineCheck
+ 'RenderThread-3894 [001] .n.1 253.780671: tracing_mark_write: E', // @suppress longLineCheck
+ 'RenderThread-3894 [001] ...1 253.780671: tracing_mark_write: B|3586|waitForTask', // @suppress longLineCheck
+ 'RenderThread-3894 [001] .p.1 253.780671: tracing_mark_write: E', // @suppress longLineCheck
+ 'RenderThread-3894 [001] ...1 253.780686: tracing_mark_write: E' // @suppress longLineCheck
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings, 'hasImportWarnings');
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 3586);
+ assert.strictEqual(thread.tid, 3894);
+ assert.strictEqual(thread.name, 'RenderThread');
+ assert.strictEqual(thread.sliceGroup.length, 4);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser.html
new file mode 100644
index 00000000000..690d2dd6786
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser.html
@@ -0,0 +1,732 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses Binder events
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ // Matches binder transactions:
+ // transaction=%d dest_node=%d dest_proc=%d dest_thread=%d reply=%d flags=0x%x
+ // code=0x%x
+ const binderTransRE = new RegExp('transaction=(\\d+) dest_node=(\\d+) ' +
+ 'dest_proc=(\\d+) dest_thread=(\\d+) ' +
+ 'reply=(\\d+) flags=(0x[0-9a-fA-F]+) ' +
+ 'code=(0x[0-9a-fA-F]+)');
+ // binder_transaction_alloc_buf: transaction=%d data_size=%d offsets_size=%d
+ const binderAllocRE = new RegExp('transaction=(\\d+) data_size=(\\d+) ' +
+ 'offsets_size=(\\d+)');
+
+ const binderTransReceivedRE = /transaction=(\d+)/;
+
+ function isBinderThread(name) {
+ return (name.indexOf('Binder') > -1);
+ }
+
+ // Taken from kernel source: include/uapi/linux/android/binder.h.
+ const TF_ONE_WAY = 0x01;
+ const TF_ROOT_OBJECT = 0x04;
+ const TF_STATUS_CODE = 0x08;
+ const TF_ACCEPT_FDS = 0x10;
+ const NO_FLAGS = 0;
+
+ function binderFlagsToHuman(num) {
+ const flag = parseInt(num, 16);
+ let str = '';
+
+ if (flag & TF_ONE_WAY) {
+ str += 'this is a one-way call: async, no return; ';
+ }
+ if (flag & TF_ROOT_OBJECT) {
+ str += 'contents are the components root object; ';
+ }
+ if (flag & TF_STATUS_CODE) {
+ str += 'contents are a 32-bit status code; ';
+ }
+ if (flag & TF_ACCEPT_FDS) {
+ str += 'allow replies with file descriptors; ';
+ }
+ if (flag === NO_FLAGS) {
+ str += 'No Flags Set';
+ }
+
+ return str;
+ }
+
+ function isReplyToOrigin(calling, called) {
+ return (called.dest_proc === calling.calling_pid ||
+ called.dest_thread === calling.calling_pid);
+ }
+
+ function binderCodeToHuman(code) {
+ return 'Java Layer Dependent';
+ }
+
+ function doInternalSlice(trans, slice, ts) {
+ if (slice.subSlices.length !== 0) {
+ /* We want to make sure we keep moving the small slice to the end of
+ the big slice or else the arrows will not point to the end.
+ */
+ slice.subSlices[0].start = ts;
+ return slice.subSlices[0];
+ }
+ const kthread = trans.calling_kthread.thread;
+ const internalSlice = kthread.sliceGroup.pushCompleteSlice('binder',
+ slice.title,
+ ts, .001, 0, 0,
+ slice.args);
+
+ internalSlice.title = slice.title;
+ internalSlice.id = slice.id;
+ internalSlice.colorId = slice.colorId;
+ slice.subSlices.push(internalSlice);
+ return internalSlice;
+ }
+
+ function generateBinderArgsForSlice(trans, cThreadName) {
+ return {
+ 'Transaction Id': trans.transaction_key,
+ 'Destination Node': trans.dest_node,
+ 'Destination Process': trans.dest_proc,
+ 'Destination Thread': trans.dest_thread,
+ 'Destination Name': cThreadName,
+ 'Reply transaction?': trans.is_reply_transaction,
+ 'Flags': trans.flags + ' ' +
+ binderFlagsToHuman(trans.flags),
+
+ 'Code': trans.code + ' ' +
+ binderCodeToHuman(trans.code),
+
+ 'Calling PID': trans.calling_pid,
+ 'Calling tgid': trans.calling_kthread.thread.parent.pid
+ };
+ }
+
+ /** @constructor */
+ function BinderTransaction(events, callingPid, callingTs, callingKthread) {
+ this.transaction_key = parseInt(events[1]);
+ this.dest_node = parseInt(events[2]);
+ this.dest_proc = parseInt(events[3]);
+ this.dest_thread = parseInt(events[4]);
+ this.is_reply_transaction = parseInt(events[5]) === 1 ? true : false;
+ this.expect_reply = ((this.is_reply_transaction === false) &&
+ (parseInt(events[6], 16) & TF_ONE_WAY) === 0);
+
+ this.flags = events[6];
+ this.code = events[7];
+ this.calling_pid = callingPid;
+ this.calling_ts = callingTs;
+ this.calling_kthread = callingKthread;
+ }
+
+
+ /** @constructor */
+ function BinderParser(importer) {
+ Parser.call(this, importer);
+ importer.registerEventHandler('binder_locked',
+ BinderParser.prototype.
+ binderLocked.bind(this));
+ importer.registerEventHandler('binder_unlock',
+ BinderParser.prototype.
+ binderUnlock.bind(this));
+ importer.registerEventHandler('binder_lock',
+ BinderParser.prototype.binderLock.bind(this));
+ importer.registerEventHandler('binder_transaction',
+ BinderParser.prototype.
+ binderTransaction.bind(this));
+ importer.registerEventHandler('binder_transaction_received',
+ BinderParser.prototype.
+ binderTransactionReceived.bind(this));
+ importer.registerEventHandler('binder_transaction_alloc_buf',
+ BinderParser.prototype.
+ binderTransactionAllocBuf.bind(this));
+
+ this.model_ = importer.model;
+ this.kthreadlookup = {};
+ this.importer_ = importer;
+ this.transWaitingRecv = {};
+ this.syncTransWaitingCompletion = {};
+ this.recursiveSyncTransWaitingCompletion_ByPID = {};
+ this.receivedTransWaitingConversion = {};
+ }
+
+ BinderParser.prototype = {
+ __proto__: Parser.prototype,
+
+ binderLock(eventName, cpuNumber, pid, ts, eventBase) {
+ const tgid = parseInt(eventBase.tgid);
+ if (isNaN(tgid)) return false;
+
+ this.doNameMappings(pid, tgid, eventName.threadName);
+
+ const kthread = this.importer_.
+ getOrCreateBinderKernelThread(eventBase.threadName, tgid, pid);
+
+ kthread.binderAttemptLockTS = ts;
+ kthread.binderOpenTsA = ts;
+ return true;
+ },
+
+ binderLocked(eventName, cpuNumber, pid, ts, eventBase) {
+ const tgid = parseInt(eventBase.tgid);
+ if (isNaN(tgid)) return false;
+
+ const binderThread = isBinderThread(eventBase.threadName);
+ const name = eventBase.threadName;
+
+ const kthread = this.importer_.
+ getOrCreateBinderKernelThread(eventBase.threadName, tgid, pid);
+
+ this.doNameMappings(pid, tgid, name);
+
+ const rthread = kthread.thread;
+ kthread.binderLockAquiredTS = ts;
+
+ if (kthread.binderAttemptLockTS === undefined) return false;
+
+ const args = this.generateArgsForSlice(tgid, pid, name, kthread);
+ rthread.sliceGroup.pushCompleteSlice('binder', 'binder lock waiting',
+ kthread.binderAttemptLockTS,
+ ts - kthread.binderAttemptLockTS,
+ 0, 0, args);
+
+ kthread.binderAttemptLockTS = undefined;
+ return true;
+ },
+
+ binderUnlock(eventName, cpuNumber, pid, ts, eventBase) {
+ const tgid = parseInt(eventBase.tgid);
+ if (isNaN(tgid)) return false;
+
+ const kthread = this.importer_.
+ getOrCreateBinderKernelThread(
+ eventBase.threadName, tgid, pid);
+
+ if (kthread.binderLockAquiredTS === undefined) return false;
+
+ const args = this.generateArgsForSlice(tgid, pid, eventBase.threadName,
+ kthread);
+ kthread.thread.sliceGroup.pushCompleteSlice(
+ 'binder',
+ 'binder lock held',
+ kthread.binderLockAquiredTS,
+ ts - kthread.binderLockAquiredTS,
+ 0, 0, args);
+
+ kthread.binderLockAquiredTS = undefined;
+ return true;
+ },
+
+ /** There are a few transaction status changes that signify
+ * progress through a binder transaction:
+ *
+ * Case One: Sync transaction.
+ * Thread A calls a blocking function on Thread B. We receive a
+ * binder_transaction msg From thread A stating that it is going to Call
+ * thread B. We create a slice and a binder object for this transaction and
+ * add it to addTransactionWaitingForRecv(transaction key, binder object)
+ * This notifies thread B and passes the slice, binder object and time
+ * stamp.
+ *
+ * Case Two: Async transaction.
+ * Thread A calls an async function on Thread B. Like above we receive a
+ * binder_transaction message, but the flags differ from above. The
+ * TF_ONEWAY flags are set so we know that when Thread B gets the
+ * binder_transaction_received with the same transaciton key the total
+ * transaction is complete.
+ *
+ * Case Three: 'Prior_receive'
+ * Prior_receive occurs when the thread being called (think A calls B),
+ * receives a binder_transaction_received message, but cannot correlate it
+ * to any current outstanding recursive transactions. That means the
+ * message it just received is the start of some communication, not some
+ * ongoing communication.
+ * Once the execution flow has been passed to thread B, from A:
+ * Thread B enters binder_transaction_received() we see that Thread A
+ * had notified us that it sent us a message by calling
+ * getTransactionWaitingForRecv(transaction key);
+ * What can happen now is either this was a simple Call reply,
+ * or this is a call -> recursion -> reply. We call modelPriorReceive()
+ * which sets up the slices accordingly.
+ * If this is a call -> recursion -> reply
+ * we will go to case 4 by calling addRecursiveSyncTransNeedingCompletion()
+ * The function takes B's PID, the binder object from A and the current
+ * binder object from B. This function adds outstanding non-complete
+ * transactions to a stack on thread B.
+ *
+ * Case Four: 'recursiveTrans'
+ * This case follows Like above:
+ * A sent binder_transaction
+ * B got binder_transaction_received
+ * B instead of replying to A can Call C or call 'into' A, ie recursion
+ * Case four also deals with setting up a large slice to 'contain'
+ * all the recursive transactions that happen until B finally replies to
+ * A.
+ *
+ *
+ * An example: A-> B-> C-> B-> A
+ *
+ * (1) A starts a synchronous transaction to B.
+ * (2) A enters binderTransaction() event handler, hits the else statement
+ * (3) A calls addTransactionWaitingForRecv(trans key, object) to notify
+ * Thread B.
+ * (4) B Enters binderTransactionReceived().
+ * (5) B hits the second if after calling
+ * getTransactionWaitingForRecv(trans key)
+ * This function returns us the object set up in step (3).
+ * (6) This is not an async transaction, B calls
+ * setCurrentReceiveOnPID(B's PID, [ts for (4), object from (3)]).
+ *
+ * (7) B enters binderTransaction() event handler, first if triggers after
+ * calling getPriorReceiveOnPID(B's PID) the tuple from (6) is returned.
+ *
+ * (8) Execution enters modelPriorReceive().
+ * (8a) A slice is setup for this binder transaction in B's timeline.
+ * (9) This is not a reply to thread A, B is going to call Thread C.
+ * (10) else statement is hit.
+ * (11) We set the tile from (8a) to be binder_reply this is the
+ * 'containg slice' for the recursion
+ * (12) We create a new slice 'binder_transaction' this slice is the
+ * recursive slice that will have arrows to Thread C's slice.
+ * (13) addRecursiveSyncTransNeedingCompletion(B's PID,
+ * [obj from (3), obj from 7])
+ * this sets up notification that B's pid has outstanding recursive
+ * transactions that need to be completed.
+ * (14) B notifies C that a transaction is waiting for it by calling
+ * addTransactionWaitingForRecv like in step (3).
+ * (15) C enters binderTransactionReceived() step 5 6 7 8 8a happen, but in
+ * the context of Thread C.
+ * (16) C is in modelPriorReceive(), it hits the first if statement,
+ * this transaction _IS_ a reply, and it is a reply to B.
+ * (17) C calls addSyncTransNeedingCompletion(trans key,
+ * [object from(3), object from 15-5])
+ * (18) B enters binderTransactionReceived() hits the first if after calling
+ * getSyncTransNeedingCompletion(trans key from (17)) the tuple from
+ * (17) gets returned.
+ *
+ * (19) B scales up the slice created in (12) and sets up flows from 15-8a
+ * slice.
+ * (20) B enters BinderTransaction() event handler and the second if is hit
+ * after calling getRecursiveTransactionNeedingCompletion(B's pid).
+ * (21) modelRecursiveTransactions() gets called, first if executes.
+ * (22) slice durations are fixed up.
+ * (23) B notifies A via
+ * addSyncTransNeedingCompletion(trans key, binder obj from 8a).
+ * (24) B deletes the outstanding asynctrans via
+ ( removeRecursiveTransaction(B's pid).
+ * (25) A enters binderTransactionReceived() event handler and finishes up
+ * some flows, and slices.
+ */
+ binderTransaction(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = binderTransRE.exec(eventBase.details);
+ if (event === undefined) return false;
+
+ const tgid = parseInt(eventBase.tgid);
+ if (isNaN(tgid)) return false;
+
+ this.doNameMappings(pid, tgid, eventBase.threadName);
+
+ const kthread = this.importer_.
+ getOrCreateBinderKernelThread(eventBase.threadName, tgid, pid);
+
+ const trans = new BinderTransaction(event, pid, ts, kthread);
+ const args = generateBinderArgsForSlice(trans, eventBase.threadName);
+ /**
+ * This thread previously ack'd the transaction with a
+ * transaction_received. That means someone sent us a message we processed
+ * it and are now sending a transaction.
+ * The transaction could be a response, or it could be recursive.
+ */
+ const priorReceive = this.getPriorReceiveOnPID(pid);
+
+ if (priorReceive !== false) {
+ return this.modelPriorReceive(priorReceive, ts, pid, tgid, kthread,
+ trans, args, event);
+ }
+ /**
+ * This Thread has an already established recursive slice. We will now
+ * either complete the entire transaction, OR do more recursive calls.
+ */
+ const recursiveTrans = this.getRecursiveTransactionNeedingCompletion(pid);
+
+ if (recursiveTrans !== false) {
+ return this.modelRecursiveTransactions(recursiveTrans, ts, pid,
+ kthread, trans, args);
+ }
+
+ /**
+ * Start of a Transaction. This thread is the initiator of either a call
+ * response, an async call -> ack, or a call -> recursion -> response.
+ * Note, we put a fake duration into this slice and patch it up later.
+ */
+ const slice = kthread.thread.sliceGroup.pushCompleteSlice('binder',
+ '', ts, .03, 0, 0, args);
+
+ slice.colorId = ColorScheme.getColorIdForGeneralPurposeString(
+ ts.toString());
+ trans.slice = slice;
+
+ if (trans.expect_reply) {
+ slice.title = 'binder transaction';
+ } else {
+ slice.title = 'binder transaction async';
+ }
+
+ this.addTransactionWaitingForRecv(trans.transaction_key, trans);
+
+ return true;
+ },
+
+ binderTransactionReceived(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ const event = binderTransReceivedRE.exec(eventBase.details);
+ if (event === undefined) return false;
+ const tgid = parseInt(eventBase.tgid);
+ if (isNaN(tgid)) return false;
+
+ const transactionkey = parseInt(event[1]);
+ const kthread = this.importer_.
+ getOrCreateBinderKernelThread(eventBase.threadName, tgid, pid);
+
+ const syncComplete = this.getSyncTransNeedsCompletion(transactionkey);
+
+ if (syncComplete !== false) {
+ /* This recv is the completion of a synchronous transaction.
+ * We need to scale the slice up to the current ts and finish
+ * creating some flows.
+ */
+ const syncTrans = syncComplete[0];
+ const syncSlice = syncTrans.slice;
+ const responseTrans = syncComplete[1];
+ const responseSlice = responseTrans.slice;
+
+ syncSlice.duration = ts - syncSlice.start;
+ /** These calls are a little hack that places a very small slice at
+ * the end of the sync slice and the response slice. This allows us
+ * to hook flow events (arrows) from the start to the end of the
+ * slices.
+ */
+ const syncInternal = doInternalSlice(syncTrans, syncSlice, ts);
+ const responseTs = responseSlice.start + responseSlice.duration;
+ const responseInternal = doInternalSlice(responseTrans,
+ responseSlice, responseTs);
+
+ if (responseSlice.outFlowEvents.length === 0 ||
+ syncSlice.inFlowEvents.length === 0) {
+ const flow = this.generateFlow(responseInternal, syncInternal,
+ responseTrans, syncTrans);
+
+ syncSlice.inFlowEvents.push(flow);
+ responseSlice.outFlowEvents.push(flow);
+ this.model_.flowEvents.push(flow);
+ }
+ // Move flow arrows -- but not the first one.
+ for (let i = 1; i < syncSlice.inFlowEvents.length; i++) {
+ syncSlice.inFlowEvents[i].duration =
+ ts - syncSlice.inFlowEvents[i].start;
+ }
+ return true;
+ }
+
+ const trForRecv = this.getTransactionWaitingForRecv(transactionkey);
+
+ if (trForRecv !== false) {
+ if (!trForRecv.expect_reply) {
+ // This is an async call place an Async slice.
+ const args = generateBinderArgsForSlice(trForRecv,
+ eventBase.threadName);
+ const slice = kthread.thread.sliceGroup.
+ pushCompleteSlice('binder',
+ 'binder Async recv',
+ ts, .03, 0, 0,
+ args);
+
+ const fakeEvent = [0, 0, 0, 0, 0, 0, 0];
+ const fakeTrans = new BinderTransaction(fakeEvent, pid, ts, kthread);
+ const flow = this.generateFlow(trForRecv.slice, slice,
+ trForRecv, fakeTrans);
+
+ this.model_.flowEvents.push(flow);
+ trForRecv.slice.title = 'binder transaction async';
+ trForRecv.slice.duration = .03;
+ return true;
+ }
+ // Setup prior receive.
+ trForRecv.slice.title = 'binder transaction';
+ this.setCurrentReceiveOnPID(pid, [ts, trForRecv]);
+ return true;
+ }
+ /** This case is when we received an ack for a transaction we have
+ * never seen before. This usually happens at the start of a trace.
+ * We will get incomplete transactions that started before started
+ * tracing. Just discard them.
+ */
+ return false;
+ },
+
+ binderTransactionAllocBuf(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ const event = binderAllocRE.exec(eventBase.details);
+ if (event === null) return false;
+ const tgid = parseInt(eventBase.tgid);
+ if (isNaN(tgid)) return false;
+
+ const transactionkey = parseInt(event[1]);
+ const kthread = this.importer_.
+ getOrCreateBinderKernelThread(eventBase.threadName, tgid, pid);
+
+ const trans = this.peekTransactionWaitingForRecv(transactionkey);
+ if (trans && trans.slice) {
+ trans.slice.args['Data Size'] = parseInt(event[2]);
+ trans.slice.args['Offsets Size'] = parseInt(event[3]);
+ return true;
+ }
+ return false;
+ },
+
+
+ // helper functions
+ modelRecursiveTransactions(recursiveTrans, ts, pid, kthread,
+ trans, args) {
+ const recursiveSlice = recursiveTrans[1].slice;
+ const origSlice = recursiveTrans[0].slice;
+ recursiveSlice.duration = ts - recursiveSlice.start;
+ recursiveSlice.args = args;
+ trans.slice = recursiveSlice;
+
+ if (trans.is_reply_transaction) {
+ /* Case one:
+ * This transaction is finally the reply of the recursion.
+ */
+ origSlice.duration = ts - origSlice.start;
+ this.addSyncTransNeedingCompletion(trans.transaction_key,
+ recursiveTrans);
+
+ if (isReplyToOrigin(recursiveTrans[0], trans)) {
+ this.removeRecursiveTransaction(pid);
+ }
+ } else {
+ /**
+ * Case two:
+ * This transaction is more recursive calls.
+ * This is a nested call within an already started transaction,
+ * it can either be a async or a normal sync transaction.
+ */
+ const slice = kthread.thread.sliceGroup.pushCompleteSlice('binder',
+ '', ts, .03, 0,
+ 0, args);
+
+ trans.slice = slice;
+ this.addTransactionWaitingForRecv(trans.transaction_key, trans);
+ }
+ return true;
+ },
+
+ modelPriorReceive(priorReceive, ts, pid, tgid, kthread, trans,
+ args, event) {
+ const calleeSlice = priorReceive[1].slice;
+ const calleeTrans = priorReceive[1];
+ const recvTs = priorReceive[0];
+ let slice = kthread.thread.sliceGroup.pushCompleteSlice('binder',
+ '', recvTs, ts - recvTs, 0, 0);
+
+ const flow = this.generateFlow(calleeSlice, slice, calleeTrans, trans);
+ this.model_.flowEvents.push(flow);
+ trans.slice = slice;
+
+ if (trans.is_reply_transaction) {
+ /* This is a response to a synchronous or a recursive sync
+ * transaction.
+ */
+ slice.title = 'binder reply';
+ slice.args = args;
+ /* Notify this transaction key that when it recv's it is completing
+ * a sync transaction.
+ */
+ this.addSyncTransNeedingCompletion(trans.transaction_key,
+ [calleeTrans, trans]);
+ } else {
+ /**
+ * Recursive calls and or calls around, either way it's not
+ * going to complete a transaction.
+ */
+ slice.title = 'binder reply';
+ /* Since this is a recursive transaction we want to create the main
+ * large slice which will contain all these recursive transactions.
+ * For that we created the main slice above and this is a recursive
+ * transaction that will be placed right below it. Note, that this
+ * is only for the first recursive transaction. If more come they will
+ * be handled below in the getRecursiveTransactionNeedingCompletion
+ */
+ const trans1 = new BinderTransaction(event, pid, ts, kthread);
+
+ slice = kthread.thread.sliceGroup.
+ pushCompleteSlice('binder',
+ 'binder transaction',
+ recvTs,
+ (ts - recvTs), 0,
+ 0, args);
+
+ /* could be a async trans if so set the length to be a small one */
+ if (!trans.expect_reply) {
+ slice.title = 'binder transaction async';
+ slice.duration = .03;
+ } else {
+ /* stupid hack to stop merging of AIDL slices and
+ * this slice. This is currently disabled, if AIDL tracing is on we
+ * will see merging of this slice and the AIDL slice. Once upstream
+ * has a solution for flow events to be placed in the middle of
+ * slices this part can be fixed.
+ *
+ * This is commented out because AIDL tracing doesn't exit yet.
+ */
+ // slice.start += .15;
+ }
+ trans1.slice = slice;
+ this.addRecursiveSyncTransNeedingCompletion(pid,
+ [calleeTrans, trans]);
+ this.addTransactionWaitingForRecv(trans.transaction_key, trans1);
+ }
+ return true;
+ },
+
+ getRecursiveTransactionNeedingCompletion(pid) {
+ if (this.recursiveSyncTransWaitingCompletion_ByPID[pid] === undefined) {
+ return false;
+ }
+
+ const len = this.recursiveSyncTransWaitingCompletion_ByPID[pid].length;
+ if (len === 0) return false;
+
+ return this.recursiveSyncTransWaitingCompletion_ByPID[pid][len - 1];
+ },
+
+ addRecursiveSyncTransNeedingCompletion(pid, tuple) {
+ if (this.recursiveSyncTransWaitingCompletion_ByPID[pid] === undefined) {
+ this.recursiveSyncTransWaitingCompletion_ByPID[pid] = [];
+ }
+
+ this.recursiveSyncTransWaitingCompletion_ByPID[pid].push(tuple);
+ },
+
+ removeRecursiveTransaction(pid) {
+ const len = this.recursiveSyncTransWaitingCompletion_ByPID[pid].length;
+ if (len === 0) {
+ delete this.recursiveSyncTransWaitingCompletion_ByPID[pid];
+ return;
+ }
+
+ this.recursiveSyncTransWaitingCompletion_ByPID[pid].splice(len - 1, 1);
+ },
+
+ setCurrentReceiveOnPID(pid, tuple) {
+ if (this.receivedTransWaitingConversion[pid] === undefined) {
+ this.receivedTransWaitingConversion[pid] = [];
+ }
+ this.receivedTransWaitingConversion[pid].push(tuple);
+ },
+
+ getPriorReceiveOnPID(pid) {
+ if (this.receivedTransWaitingConversion[pid] === undefined) {
+ return false;
+ }
+
+ const len = this.receivedTransWaitingConversion[pid].length;
+ if (len === 0) return false;
+
+ return this.receivedTransWaitingConversion[pid].splice(len - 1, 1)[0];
+ },
+
+ addSyncTransNeedingCompletion(transactionkey, tuple) {
+ const dict = this.syncTransWaitingCompletion;
+ dict[transactionkey] = tuple;
+ },
+
+ getSyncTransNeedsCompletion(transactionkey) {
+ const ret = this.syncTransWaitingCompletion[transactionkey];
+ if (ret === undefined) return false;
+
+ delete this.syncTransWaitingCompletion[transactionkey];
+ return ret;
+ },
+
+ getTransactionWaitingForRecv(transactionkey) {
+ const ret = this.transWaitingRecv[transactionkey];
+ if (ret === undefined) return false;
+
+ delete this.transWaitingRecv[transactionkey];
+ return ret;
+ },
+
+ peekTransactionWaitingForRecv(transactionkey) {
+ const ret = this.transWaitingRecv[transactionkey];
+ if (ret === undefined) return false;
+ return ret;
+ },
+
+ addTransactionWaitingForRecv(transactionkey, transaction) {
+ this.transWaitingRecv[transactionkey] = transaction;
+ },
+
+ generateFlow(from, to, fromTrans, toTrans) {
+ const title = 'Transaction from : ' +
+ this.pid2name(fromTrans.calling_pid) +
+ ' From PID: ' + fromTrans.calling_pid + ' to pid: ' +
+ toTrans.calling_pid +
+ ' Thread Name: ' + this.pid2name(toTrans.calling_pid);
+
+ const ts = from.start;
+ const flow = new tr.model.FlowEvent('binder', 'binder',
+ title, 1, ts, []);
+ flow.startSlice = from;
+ flow.endSlice = to;
+ flow.start = from.start;
+ flow.duration = to.start - ts;
+
+ from.outFlowEvents.push(flow);
+ to.inFlowEvents.push(flow);
+
+ return flow;
+ },
+
+ generateArgsForSlice(tgid, pid, name, kthread) {
+ return {
+ 'Thread Name': name,
+ pid,
+ 'gid': tgid
+ };
+ },
+
+ pid2name(pid) {
+ return this.kthreadlookup[pid];
+ },
+
+ doNameMappings(pid, tgid, name) {
+ this.registerPidName(pid, name);
+ this.registerPidName(tgid, name);
+ },
+
+ registerPidName(pid, name) {
+ if (this.pid2name(pid) === undefined) {
+ this.kthreadlookup[pid] = name;
+ }
+ }
+ };
+
+ Parser.register(BinderParser);
+ return {
+ BinderParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser_test.html
new file mode 100644
index 00000000000..07a619b07cf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/binder_parser_test.html
@@ -0,0 +1,273 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('binderParserImport', function() {
+ const lines = [
+ 'binderLibTest-10619 (10619) [001] ...1 25191.335749: ' +
+ 'binder_transaction: transaction=489053 dest_node=489048 ' +
+ 'dest_proc=10670 dest_thread=0 reply=0 flags=0x10 code=0xd',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.335996:' +
+ ' binder_transaction_received: transaction=489053',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.336167:' +
+ ' binder_transaction: transaction=489056 dest_node=161277 ' +
+ 'dest_proc=3454 dest_thread=0 reply=0 flags=0x10 code=0x2',
+ '/system/bin/servicemanager-3454 ( 3454) [000] ...1 25191.336199:' +
+ ' binder_transaction_received: transaction=489056',
+ '/system/bin/servicemanager-3454 ( 3454) [000] ...1 25191.336302:' +
+ ' binder_transaction: transaction=489057 dest_node=0 dest_proc=10670' +
+ ' dest_thread=10670 reply=1 flags=0x0 code=0x0',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.336334:' +
+ ' binder_transaction_received: transaction=489057',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.336655:' +
+ ' binder_transaction: transaction=489059 dest_node=488755' +
+ ' dest_proc=10622 dest_thread=0 reply=0 flags=0x10 code=0x1',
+ 'Binder_4-10645 (10622) [001] ...1 25191.336693:' +
+ ' binder_transaction_received: transaction=489059',
+ 'Binder_4-10645 (10622) [001] ...1 25191.336754: binder_transaction:' +
+ ' transaction=489060 dest_node=0 dest_proc=10670 dest_thread=10670' +
+ ' reply=1 flags=0x0 code=0x0',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.337003:' +
+ ' binder_transaction_received: transaction=489060',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.337052:' +
+ ' binder_transaction: transaction=489061 dest_node=0 dest_proc=10619' +
+ ' dest_thread=10619 reply=1 flags=0x8 code=0x0',
+ 'binderLibTest-10619 (10619) [001] ...1 25191.337085:' +
+ ' binder_transaction_received: transaction=489061',
+ 'atrace-10618 (10618) [000] ...1 25196.059954: tracing_mark_write:' +
+ ' trace_event_clock_sync: parent_ts=25196.050781'
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shfitWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 4);
+ });
+
+ test('binderRecursiveTest', function() {
+ const lines = [
+ 'binderLibTest-10619 (10619) [001] ...1 25191.335749: ' +
+ 'binder_transaction: transaction=489053 dest_node=489048 ' +
+ 'dest_proc=10670 dest_thread=0 reply=0 flags=0x10 code=0xd',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.335996:' +
+ ' binder_transaction_received: transaction=489053',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.336167:' +
+ ' binder_transaction: transaction=489056 dest_node=161277 ' +
+ 'dest_proc=3454 dest_thread=0 reply=0 flags=0x10 code=0x2',
+ '/system/bin/servicemanager-3454 ( 3454) [000] ...1 25191.336199:' +
+ ' binder_transaction_received: transaction=489056',
+ '/system/bin/servicemanager-3454 ( 3454) [000] ...1 25191.336302:' +
+ ' binder_transaction: transaction=489057 dest_node=0 dest_proc=10670' +
+ ' dest_thread=10670 reply=1 flags=0x0 code=0x0',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.336334:' +
+ ' binder_transaction_received: transaction=489057',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.336655:' +
+ ' binder_transaction: transaction=489059 dest_node=488755' +
+ ' dest_proc=10622 dest_thread=0 reply=0 flags=0x10 code=0x1',
+ 'Binder_4-10645 (10622) [001] ...1 25191.336693:' +
+ ' binder_transaction_received: transaction=489059',
+ 'Binder_4-10645 (10622) [001] ...1 25191.336754: binder_transaction:' +
+ ' transaction=489060 dest_node=0 dest_proc=10670 dest_thread=10670' +
+ ' reply=1 flags=0x0 code=0x0',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.337003:' +
+ ' binder_transaction_received: transaction=489060',
+ 'binderLibTest-10670 (10670) [000] ...1 25191.337052:' +
+ ' binder_transaction: transaction=489061 dest_node=0 dest_proc=10619' +
+ ' dest_thread=10619 reply=1 flags=0x8 code=0x0',
+ 'binderLibTest-10619 (10619) [001] ...1 25191.337085:' +
+ ' binder_transaction_received: transaction=489061',
+ 'atrace-10618 (10618) [000] ...1 25196.059954: tracing_mark_write:' +
+ ' trace_event_clock_sync: parent_ts=25196.050781'
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shfitWorldToZero: false
+ });
+ const threads = m.getAllThreads();
+
+ let thread = threads[0];
+ assert.strictEqual(thread.parent.pid, 3454);
+ assert.strictEqual(thread.tid, 3454);
+ assert.strictEqual(thread.name, '/system/bin/servicemanager');
+ /* one main slice and one 'internal slice' for the end flow event' */
+ assert.strictEqual(thread.sliceGroup.length, 2);
+
+ /* check flow events for service manager */
+ let mainSlice = thread.sliceGroup.slices[0];
+ assert.strictEqual(mainSlice.inFlowEvents.length, 1);
+ assert.strictEqual(mainSlice.outFlowEvents.length, 1);
+ let internalSlice = thread.sliceGroup.slices[1];
+ assert.strictEqual(internalSlice.outFlowEvents.length, 1);
+
+ /* check name of slice */
+ assert.strictEqual(mainSlice.title, 'binder reply');
+ assert.strictEqual(mainSlice.args['Destination Process'], 10670);
+ assert.strictEqual(mainSlice.args['Destination Thread'], 10670);
+ assert.strictEqual(mainSlice.args['Reply transaction?'], true);
+ assert.strictEqual(mainSlice.args['Calling PID'], 3454);
+ assert.strictEqual(mainSlice.args['Calling tgid'], 3454);
+
+ /* check binderLibTest */
+ thread = threads[1];
+ assert.strictEqual(thread.parent.pid, 10619);
+ assert.strictEqual(thread.tid, 10619);
+ assert.strictEqual(thread.name, 'binderLibTest');
+
+ assert.strictEqual(2, thread.sliceGroup.length);
+
+ mainSlice = thread.sliceGroup.slices[0];
+ assert.strictEqual(mainSlice.inFlowEvents.length, 1);
+ assert.strictEqual(mainSlice.outFlowEvents.length, 1);
+ internalSlice = thread.sliceGroup.slices[1];
+ assert.strictEqual(internalSlice.inFlowEvents.length, 1);
+
+ assert.strictEqual(mainSlice.title, 'binder transaction');
+ assert.strictEqual(mainSlice.args['Destination Process'], 10670);
+ assert.strictEqual(mainSlice.args['Reply transaction?'], false);
+ assert.strictEqual(mainSlice.args['Calling PID'], 10619);
+ assert.strictEqual(mainSlice.args['Calling tgid'], 10619);
+
+ /* check Binder_4 */
+ thread = threads[2];
+ assert.strictEqual(thread.parent.pid, 10622);
+ assert.strictEqual(thread.tid, 10645);
+ assert.strictEqual(thread.name, 'Binder_4');
+ assert.strictEqual(2, thread.sliceGroup.length);
+
+ mainSlice = thread.sliceGroup.slices[0];
+ assert.strictEqual(mainSlice.inFlowEvents.length, 1);
+ assert.strictEqual(mainSlice.outFlowEvents.length, 1);
+ internalSlice = thread.sliceGroup.slices[1];
+ assert.strictEqual(internalSlice.outFlowEvents.length, 1);
+
+ assert.strictEqual(mainSlice.title, 'binder reply');
+ assert.strictEqual(mainSlice.args['Destination Process'], 10670);
+ assert.strictEqual(mainSlice.args['Destination Thread'], 10670);
+ assert.strictEqual(mainSlice.args['Reply transaction?'], true);
+ assert.strictEqual(mainSlice.args['Calling PID'], 10645);
+ assert.strictEqual(mainSlice.args['Calling tgid'], 10622);
+
+ /* check last binderLibTest with recursive slices */
+ thread = threads[3];
+ assert.strictEqual(thread.parent.pid, 10670);
+ assert.strictEqual(thread.tid, 10670);
+ assert.strictEqual(thread.name, 'binderLibTest');
+ assert.strictEqual(6, thread.sliceGroup.length);
+
+ mainSlice = thread.sliceGroup.slices[0];
+ assert.strictEqual(mainSlice.inFlowEvents.length, 1);
+ assert.strictEqual(mainSlice.outFlowEvents.length, 1);
+ internalSlice = thread.sliceGroup.slices[5];
+ assert.strictEqual(internalSlice.outFlowEvents.length, 1);
+
+ assert.strictEqual(mainSlice.title, 'binder reply');
+ assert.strictEqual(mainSlice.args['Destination Process'], 10619);
+ assert.strictEqual(mainSlice.args['Destination Thread'], 10619);
+ assert.strictEqual(mainSlice.args['Reply transaction?'], true);
+ assert.strictEqual(mainSlice.args['Calling PID'], 10670);
+ assert.strictEqual(mainSlice.args['Calling tgid'], 10670);
+
+ let recursive = thread.sliceGroup.slices[1];
+ let recursiveInternal = thread.sliceGroup.slices[2];
+ assert.strictEqual(recursive.inFlowEvents.length, 1);
+ assert.strictEqual(recursive.outFlowEvents.length, 1);
+ assert.strictEqual(recursiveInternal.inFlowEvents.length, 1);
+
+ assert.strictEqual(recursive.title, 'binder transaction');
+ assert.strictEqual(recursive.args['Destination Process'], 3454);
+ assert.strictEqual(recursive.args['Reply transaction?'], false);
+ assert.strictEqual(recursive.args['Calling PID'], 10670);
+ assert.strictEqual(recursive.args['Calling tgid'], 10670);
+
+ /* check second recursive slice and internal */
+
+ recursive = thread.sliceGroup.slices[3];
+ recursiveInternal = thread.sliceGroup.slices[4];
+ assert.strictEqual(recursive.inFlowEvents.length, 1);
+ assert.strictEqual(recursive.outFlowEvents.length, 1);
+ assert.strictEqual(recursiveInternal.inFlowEvents.length, 1);
+
+ assert.strictEqual(recursive.title, 'binder transaction');
+ assert.strictEqual(recursive.args['Destination Process'], 10622);
+ assert.strictEqual(recursive.args['Reply transaction?'], false);
+ assert.strictEqual(recursive.args['Calling PID'], 10670);
+ assert.strictEqual(recursive.args['Calling tgid'], 10670);
+ });
+
+ test('binderSimpleAsync', function() {
+ const lines = [
+ '/system/bin/surfaceflinger-3462 ( 3462) [000] ...1 108286.109437:' +
+ ' binder_transaction: transaction=923419 dest_node=175950' +
+ ' dest_proc=4044 dest_thread=0 reply=0 flags=0x11 code=0x1',
+ 'Binder_5-12869 ( 4044) [000] ...1 108286.109835:' +
+ ' binder_transaction_received: transaction=923419'
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shfitWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads(0);
+ assert.strictEqual(threads.length, 2);
+
+ let thread = threads[0];
+ assert.strictEqual(thread.tid, 3462);
+ assert.strictEqual(thread.name, '/system/bin/surfaceflinger');
+ assert.strictEqual(thread.sliceGroup.length, 1);
+
+ let slice = thread.sliceGroup.slices[0];
+ assert.strictEqual(slice.outFlowEvents.length, 1);
+ assert.strictEqual(slice.inFlowEvents.length, 0);
+ assert.strictEqual(slice.title, 'binder transaction async');
+
+ thread = threads[1];
+ assert.strictEqual(thread.tid, 12869);
+ assert.strictEqual(thread.name, 'Binder_5');
+ assert.strictEqual(thread.sliceGroup.length, 1);
+
+ slice = thread.sliceGroup.slices[0];
+ assert.strictEqual(slice.inFlowEvents.length, 1);
+ assert.strictEqual(slice.outFlowEvents.length, 0);
+ assert.strictEqual(slice.title, 'binder Async recv');
+ });
+
+ test('binderAllocBuf', function() {
+ const lines = [
+ 'binderLibTest-10619 (10619) [001] ...1 25191.335749: ' +
+ 'binder_transaction: transaction=489053 dest_node=489048 ' +
+ 'dest_proc=10670 dest_thread=0 reply=0 flags=0x10 code=0xd',
+ 'binderLibTest-10619 (10619) [001] ...1 25191.335750: ' +
+ 'binder_transaction_alloc_buf: transaction=489053 ' +
+ 'data_size=1001 offsets_size=1002',
+ 'binderLibTest-10670 (10620) [000] ...1 25191.335751: ' +
+ 'binder_transaction_received: transaction=489053',
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shfitWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+ const threads = m.getAllThreads(0);
+ assert.strictEqual(threads.length, 1);
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.length, 1);
+ const slice = thread.sliceGroup.slices[0];
+ assert.strictEqual(slice.args['Data Size'], 1001);
+ assert.strictEqual(slice.args['Offsets Size'], 1002);
+ });
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser.html
new file mode 100644
index 00000000000..501fa83aea0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses trace_marker events that were inserted in the trace by
+ * userland.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux trace mark events that were inserted in the trace by userland.
+ * @constructor
+ */
+ function BusParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('memory_bus_usage',
+ BusParser.prototype.traceMarkWriteBusEvent.bind(this));
+
+ this.model_ = importer.model_;
+ this.ppids_ = {};
+ }
+
+ BusParser.prototype = {
+ __proto__: Parser.prototype,
+
+ traceMarkWriteBusEvent(eventName, cpuNumber, pid, ts,
+ eventBase, threadName) {
+ const re = new RegExp('bus=(\\S+) rw_bytes=(\\d+) r_bytes=(\\d+) ' +
+ 'w_bytes=(\\d+) cycles=(\\d+) ns=(\\d+)');
+ const event = re.exec(eventBase.details);
+
+ const name = event[1];
+ const rwBytes = parseInt(event[2]);
+ const rBytes = parseInt(event[3]);
+ const wBytes = parseInt(event[4]);
+ const cycles = parseInt(event[5]);
+ const ns = parseInt(event[6]);
+
+ // BW in MiB/s.
+ const sec = tr.b.convertUnit(ns, tr.b.UnitPrefixScale.METRIC.NANO,
+ tr.b.UnitPrefixScale.METRIC.NONE);
+ const readBandwidthInBps = rBytes / sec;
+ const readBandwidthInMiBps = tr.b.convertUnit(readBandwidthInBps,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI);
+ const writeBandwidthInBps = wBytes / sec;
+ const writeBandwidthInMiBps = tr.b.convertUnit(writeBandwidthInBps,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI);
+
+ let ctr = this.model_.kernel
+ .getOrCreateCounter(null, 'bus ' + name + ' read');
+ if (ctr.numSeries === 0) {
+ ctr.addSeries(new tr.model.CounterSeries('value',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ ctr.name + '.' + 'value')));
+ }
+ ctr.series.forEach(function(series) {
+ series.addCounterSample(ts, readBandwidthInMiBps);
+ });
+
+ ctr = this.model_.kernel
+ .getOrCreateCounter(null, 'bus ' + name + ' write');
+ if (ctr.numSeries === 0) {
+ ctr.addSeries(new tr.model.CounterSeries('value',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ ctr.name + '.' + 'value')));
+ }
+ ctr.series.forEach(function(series) {
+ series.addCounterSample(ts, writeBandwidthInMiBps);
+ });
+
+ return true;
+ }
+ };
+
+ Parser.register(BusParser);
+
+ return {
+ BusParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser_test.html
new file mode 100644
index 00000000000..d2bfa35b1a5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/bus_parser_test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('exynos5Bus', function() {
+ const lines = [
+ 's3c-fb-vsync-85 [001] d..2 8116.730115: memory_bus_usage: ' +
+ 'bus=RIGHT rw_bytes=0 r_bytes=0 w_bytes=0 cycles=2681746 ns=16760792',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.730118: memory_bus_usage: ' +
+ 'bus=CPU rw_bytes=2756608 r_bytes=2267328 w_bytes=491328 ' +
+ 'cycles=6705198 ns=16763375',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.746788: memory_bus_usage: ' +
+ 'bus=DDR_C rw_bytes=2736128 r_bytes=2260864 w_bytes=479248 ' +
+ 'cycles=6670677 ns=16676375',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.746790: memory_bus_usage: ' +
+ 'bus=DDR_R1 rw_bytes=31457280 r_bytes=31460912 w_bytes=0 ' +
+ 'cycles=6670521 ns=16676500',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.746792: memory_bus_usage: ' +
+ 'bus=DDR_L rw_bytes=16953344 r_bytes=16731088 w_bytes=223664 ' +
+ 'cycles=6669885 ns=16674833',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.746793: memory_bus_usage: ' +
+ 'bus=RIGHT rw_bytes=0 r_bytes=0 w_bytes=0 cycles=2667378 ns=16671250',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.746798: memory_bus_usage: ' +
+ 'bus=CPU rw_bytes=2797568 r_bytes=2309424 w_bytes=491968 ' +
+ 'cycles=6672156 ns=16680458',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.763521: memory_bus_usage: ' +
+ 'bus=DDR_C rw_bytes=2408448 r_bytes=1968448 w_bytes=441456 ' +
+ 'cycles=6689562 ns=16723458',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.763523: memory_bus_usage: ' +
+ 'bus=DDR_R1 rw_bytes=31490048 r_bytes=31493360 w_bytes=0 ' +
+ 'cycles=6690012 ns=16725083',
+
+ 's3c-fb-vsync-85 [001] d..2 8116.763525: memory_bus_usage: ' +
+ 'bus=DDR_L rw_bytes=16941056 r_bytes=16719136 w_bytes=223472 ' +
+ 'cycles=6690156 ns=16725375'
+
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 10);
+
+ assert.strictEqual(counters[0].series[0].samples.length, 2);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser.html
new file mode 100644
index 00000000000..eb290cccd96
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses trace_marker events that were inserted in the trace by
+ * userland.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux trace mark events that were inserted in the trace by userland.
+ * @constructor
+ */
+ function ClockParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('clock_set_rate',
+ ClockParser.prototype.traceMarkWriteClockEvent.bind(this));
+ importer.registerEventHandler('clk_set_rate',
+ ClockParser.prototype.traceMarkWriteClkEvent.bind(this));
+ importer.registerEventHandler('clock_enable',
+ ClockParser.prototype.traceMarkWriteClockOnOffEvent.bind(this));
+ importer.registerEventHandler('clock_disable',
+ ClockParser.prototype.traceMarkWriteClockOnOffEvent.bind(this));
+ importer.registerEventHandler('clk_enable',
+ ClockParser.prototype.traceMarkWriteClkOnEvent.bind(this));
+ importer.registerEventHandler('clk_disable',
+ ClockParser.prototype.traceMarkWriteClkOffEvent.bind(this));
+ this.model_ = importer.model_;
+ this.ppids_ = {};
+ }
+
+ ClockParser.prototype = {
+ __proto__: Parser.prototype,
+
+ clockMark(name, subName, value, ts) {
+ const ctr = this.model_.kernel
+ .getOrCreateCounter(null, name + ' ' + subName);
+ // Initialize the counter's series fields if needed.
+ if (ctr.numSeries === 0) {
+ ctr.addSeries(new tr.model.CounterSeries('value',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ ctr.name + '.' + 'value')));
+ }
+ ctr.series.forEach(function(series) {
+ series.addCounterSample(ts, value);
+ });
+ },
+
+ traceMarkWriteClockEvent(eventName, cpuNumber, pid, ts,
+ eventBase, threadName) {
+ const event = /(\S+) state=(\d+)/.exec(eventBase.details);
+ const name = event[1];
+ const rate = parseInt(event[2]);
+ this.clockMark(name, 'Frequency', rate, ts);
+ return true;
+ },
+
+ traceMarkWriteClkEvent(eventName, cpuNumber, pid, ts,
+ eventBase, threadName) {
+ const event = /(\S+) (\d+)/.exec(eventBase.details);
+ const name = event[1];
+ const rate = parseInt(event[2]);
+ this.clockMark(name, 'Frequency', rate, ts);
+ return true;
+ },
+
+ traceMarkWriteClockOnOffEvent(eventName, cpuNumber, pid, ts,
+ eventBase, threadName) {
+ const event = /(\S+) state=(\d+)/.exec(eventBase.details);
+ const name = event[1];
+ const state = parseInt(event[2]);
+ this.clockMark(name, 'State', state, ts);
+ return true;
+ },
+
+ traceMarkWriteClkOnEvent(eventName, cpuNumber, pid, ts,
+ eventBase, threadName) {
+ const event = /\S+/.exec(eventBase.details);
+ const name = event[0];
+ this.clockMark(name, 'State', 1, ts);
+ return true;
+ },
+
+ traceMarkWriteClkOffEvent(eventName, cpuNumber, pid, ts,
+ eventBase, threadName) {
+ const event = /\S+/.exec(eventBase.details);
+ const name = event[0];
+ this.clockMark(name, 'State', 0, ts);
+ return true;
+ }
+ };
+
+ Parser.register(ClockParser);
+
+ return {
+ ClockParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser_test.html
new file mode 100644
index 00000000000..3ce688c796b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/clock_parser_test.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('clock', function() {
+ const lines = [
+ 'cfinteractive-23 [000] d..2 8113.233768: clock_set_rate: ' +
+ 'fout_apll state=500000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.249509: clock_set_rate: ' +
+ 'fout_apll state=300000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.289796: clock_set_rate: ' +
+ 'fout_apll state=400000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.294568: clock_set_rate: ' +
+ 'fout_apll state=500000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.309509: clock_set_rate: ' +
+ 'fout_apll state=800000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.388732: clock_set_rate: ' +
+ 'fout_apll state=200000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.410182: clock_set_rate: ' +
+ 'fout_apll state=300000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.414872: clock_set_rate: ' +
+ 'fout_apll state=600000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.494455: clock_set_rate: ' +
+ 'fout_apll state=200000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.515254: clock_set_rate: ' +
+ 'fout_apll state=500000000 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.694455: clk_set_rate: ' +
+ 'fout_apll 200000000',
+
+ 'cfinteractive-23 [000] d..2 8113.715254: clk_set_rate: ' +
+ 'fout_apll 500000000'
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 1);
+
+ assert.strictEqual(counters[0].series[0].samples.length, 12);
+ });
+
+ test('clock state', function() {
+ const lines = [
+ 'cfinteractive-23 [000] d..2 8113.233768: clock_enable: ' +
+ 'fout_apll state=1 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.249509: clock_disable: ' +
+ 'fout_apll state=0 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.289796: clock_enable: ' +
+ 'fout_apll state=1 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.294568: clock_disable: ' +
+ 'fout_apll state=0 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.309509: clock_enable: ' +
+ 'fout_apll state=1 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.388732: clock_disable: ' +
+ 'fout_apll state=0 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.410182: clock_enable: ' +
+ 'fout_apll state=1 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.414872: clock_disable: ' +
+ 'fout_apll state=0 cpu_id=0',
+
+ 'cfinteractive-23 [000] d..2 8113.694455: clk_enable: ' +
+ 'fout_apll',
+
+ 'cfinteractive-23 [000] d..2 8113.715254: clk_disable: ' +
+ 'fout_apll'
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 1);
+
+ assert.strictEqual(counters[0].series[0].samples.length, 10);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser.html
new file mode 100644
index 00000000000..da112cb0e31
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses cpufreq events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux cpufreq trace events.
+ * @constructor
+ */
+ function CpufreqParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('cpufreq_interactive_up',
+ CpufreqParser.prototype.cpufreqUpDownEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_down',
+ CpufreqParser.prototype.cpufreqUpDownEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_already',
+ CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_notyet',
+ CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_setspeed',
+ CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_target',
+ CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_boost',
+ CpufreqParser.prototype.cpufreqBoostUnboostEvent.bind(this));
+ importer.registerEventHandler('cpufreq_interactive_unboost',
+ CpufreqParser.prototype.cpufreqBoostUnboostEvent.bind(this));
+ }
+
+ function splitData(input) {
+ // TODO(sleffler) split by cpu
+ const data = {};
+ const args = input.split(/\s+/);
+ const len = args.length;
+ for (let i = 0; i < len; i++) {
+ const item = args[i].split('=');
+ data[item[0]] = parseInt(item[1]);
+ }
+ return data;
+ }
+
+ CpufreqParser.prototype = {
+ __proto__: Parser.prototype,
+
+ cpufreqSlice(ts, eventName, cpu, args) {
+ // TODO(sleffler) should be per-cpu
+ const kthread = this.importer.getOrCreatePseudoThread('cpufreq');
+ kthread.openSlice = eventName;
+ const slice = new tr.model.ThreadSlice('', kthread.openSlice,
+ ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),
+ ts, args, 0);
+
+ kthread.thread.sliceGroup.pushSlice(slice);
+ },
+
+ cpufreqBoostSlice(ts, eventName, args) {
+ const kthread = this.importer.getOrCreatePseudoThread('cpufreq_boost');
+ kthread.openSlice = eventName;
+ const slice = new tr.model.ThreadSlice('', kthread.openSlice,
+ ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),
+ ts, args, 0);
+
+ kthread.thread.sliceGroup.pushSlice(slice);
+ },
+
+ /**
+ * Parses cpufreq events and sets up state in the importer.
+ */
+ cpufreqUpDownEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const data = splitData(eventBase.details);
+ this.cpufreqSlice(ts, eventName, data.cpu, data);
+ return true;
+ },
+
+ cpufreqTargetEvent(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ const data = splitData(eventBase.details);
+ this.cpufreqSlice(ts, eventName, data.cpu, data);
+ return true;
+ },
+
+ cpufreqBoostUnboostEvent(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ this.cpufreqBoostSlice(ts, eventName,
+ {
+ type: eventBase.details
+ });
+ return true;
+ }
+ };
+
+ Parser.register(CpufreqParser);
+
+ return {
+ CpufreqParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser_test.html
new file mode 100644
index 00000000000..9c324a8346d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/cpufreq_parser_test.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('cpuFreqTargetImport', function() {
+ const lines = [
+ '<idle>-0 [000] ..s3 1043.718825: cpufreq_interactive_target: ' +
+ 'cpu=0 load=2 cur=2000000 targ=300000\n',
+ '<idle>-0 [000] ..s3 1043.718825: cpufreq_interactive_target: ' +
+ 'cpu=0 load=12 cur=1000000 actual=1000000 targ=200000\n'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cpu, 0);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.load, 2);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cur, 2000000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.targ, 300000);
+
+ assert.strictEqual(thread.sliceGroup.slices[1].args.cpu, 0);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.load, 12);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.cur, 1000000);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.actual, 1000000);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.targ, 200000);
+ });
+
+ test('cpuFreqNotYetImport', function() {
+ const lines = [
+ '<idle>-0 [001] ..s3 1043.718832: cpufreq_interactive_notyet: ' +
+ 'cpu=1 load=10 cur=700000 targ=200000\n',
+ '<idle>-0 [001] ..s3 1043.718832: cpufreq_interactive_notyet: ' +
+ 'cpu=1 load=10 cur=700000 actual=1000000 targ=200000\n'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cpu, 1);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.load, 10);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cur, 700000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.targ, 200000);
+
+ assert.strictEqual(thread.sliceGroup.slices[1].args.cpu, 1);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.load, 10);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.cur, 700000);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.actual, 1000000);
+ assert.strictEqual(thread.sliceGroup.slices[1].args.targ, 200000);
+ });
+
+ test('cpuFreqSetSpeedImport', function() {
+ const lines = [
+ 'cfinteractive-23 [001] ...1 1043.719688: ' +
+ 'cpufreq_interactive_setspeed: cpu=0 targ=200000 actual=700000\n'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cpu, 0);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.targ, 200000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.actual, 700000);
+ });
+
+ test('cpuFreqAlreadyImport', function() {
+ const lines = [
+ '<idle>-0 [000] ..s3 1043.738822: cpufreq_interactive_already: cpu=0 load=18 cur=200000 actual=700000 targ=200000\n' // @suppress longLineCheck
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cpu, 0);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.load, 18);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cur, 200000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.actual, 700000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.targ, 200000);
+ });
+
+ test('cpuFreqBoostImport', function() {
+ const lines = [
+ 'InputDispatcher-465 [001] ...1 1044.213948: ' +
+ 'cpufreq_interactive_boost: pulse\n'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.type, 'pulse');
+ });
+
+ test('cpuFreqUnBoostImport', function() {
+ const lines = [
+ 'InputDispatcher-465 [001] ...1 1044.213948: ' +
+ 'cpufreq_interactive_unboost: pulse\n'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.type, 'pulse');
+ });
+
+ test('cpuFreqUpImport', function() {
+ const lines = [
+ 'kinteractive-69 [003] .... 414324.164432: ' +
+ 'cpufreq_interactive_up: cpu=1 targ=1400000 actual=800000'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cpu, 1);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.targ, 1400000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.actual, 800000);
+ });
+
+ test('cpuFreqDownImport', function() {
+ const lines = [
+ 'kinteractive-69 [003] .... 414365.834193: ' +
+ 'cpufreq_interactive_down: cpu=3 targ=800000 actual=1000000'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+ assert.strictEqual(thread.sliceGroup.slices[0].args.cpu, 3);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.targ, 800000);
+ assert.strictEqual(thread.sliceGroup.slices[0].args.actual, 1000000);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser.html
new file mode 100644
index 00000000000..e44e29eb8cf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses filesystem and block device events in the Linux event
+ * trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux filesystem and block device trace events.
+ * @constructor
+ */
+ function DiskParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('f2fs_write_begin',
+ DiskParser.prototype.f2fsWriteBeginEvent.bind(this));
+ importer.registerEventHandler('f2fs_write_end',
+ DiskParser.prototype.f2fsWriteEndEvent.bind(this));
+ importer.registerEventHandler('f2fs_sync_file_enter',
+ DiskParser.prototype.f2fsSyncFileEnterEvent.bind(this));
+ importer.registerEventHandler('f2fs_sync_file_exit',
+ DiskParser.prototype.f2fsSyncFileExitEvent.bind(this));
+ importer.registerEventHandler('ext4_sync_file_enter',
+ DiskParser.prototype.ext4SyncFileEnterEvent.bind(this));
+ importer.registerEventHandler('ext4_sync_file_exit',
+ DiskParser.prototype.ext4SyncFileExitEvent.bind(this));
+ importer.registerEventHandler('ext4_da_write_begin',
+ DiskParser.prototype.ext4WriteBeginEvent.bind(this));
+ importer.registerEventHandler('ext4_da_write_end',
+ DiskParser.prototype.ext4WriteEndEvent.bind(this));
+ importer.registerEventHandler('block_rq_issue',
+ DiskParser.prototype.blockRqIssueEvent.bind(this));
+ importer.registerEventHandler('block_rq_complete',
+ DiskParser.prototype.blockRqCompleteEvent.bind(this));
+ }
+
+ DiskParser.prototype = {
+ __proto__: Parser.prototype,
+
+ openAsyncSlice(ts, category, threadName, pid, key, name) {
+ const kthread = this.importer.getOrCreateKernelThread(
+ category + ':' + threadName, pid);
+ const asyncSliceConstructor =
+ tr.model.AsyncSlice.subTypes.getConstructor(
+ category, name);
+ const slice = new asyncSliceConstructor(
+ category, name,
+ ColorScheme.getColorIdForGeneralPurposeString(name),
+ ts);
+ slice.startThread = kthread.thread;
+
+ if (!kthread.openAsyncSlices) {
+ kthread.openAsyncSlices = { };
+ }
+ kthread.openAsyncSlices[key] = slice;
+ },
+
+ closeAsyncSlice(ts, category, threadName, pid, key, args) {
+ const kthread = this.importer.getOrCreateKernelThread(
+ category + ':' + threadName, pid);
+ if (kthread.openAsyncSlices) {
+ const slice = kthread.openAsyncSlices[key];
+ if (slice) {
+ slice.duration = ts - slice.start;
+ slice.args = args;
+ slice.endThread = kthread.thread;
+ slice.subSlices = [
+ new tr.model.AsyncSlice(category, slice.title,
+ slice.colorId, slice.start, slice.args, slice.duration)
+ ];
+ kthread.thread.asyncSliceGroup.push(slice);
+ delete kthread.openAsyncSlices[key];
+ }
+ }
+ },
+
+ /**
+ * Parses events and sets up state in the importer.
+ */
+ f2fsWriteBeginEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /dev = \((\d+,\d+)\), ino = (\d+), pos = (\d+), len = (\d+), flags = (\d+)/. // @suppress longLineCheck
+ exec(eventBase.details);
+ if (!event) return false;
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const pos = parseInt(event[3]);
+ const len = parseInt(event[4]);
+ const key = device + '-' + inode + '-' + pos + '-' + len;
+ this.openAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+ key, 'f2fs_write');
+ return true;
+ },
+
+ f2fsWriteEndEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /dev = \((\d+,\d+)\), ino = (\d+), pos = (\d+), len = (\d+), copied = (\d+)/. // @suppress longLineCheck
+ exec(eventBase.details);
+ if (!event) return false;
+
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const pos = parseInt(event[3]);
+ const len = parseInt(event[4]);
+ const error = parseInt(event[5]) !== len;
+ const key = device + '-' + inode + '-' + pos + '-' + len;
+ this.closeAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+ key, {
+ device,
+ inode,
+ error
+ });
+ return true;
+ },
+
+ ext4WriteBeginEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /dev (\d+,\d+) ino (\d+) pos (\d+) len (\d+) flags (\d+)/.
+ exec(eventBase.details);
+ if (!event) return false;
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const pos = parseInt(event[3]);
+ const len = parseInt(event[4]);
+ const key = device + '-' + inode + '-' + pos + '-' + len;
+ this.openAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+ key, 'ext4_write');
+ return true;
+ },
+
+ ext4WriteEndEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /dev (\d+,\d+) ino (\d+) pos (\d+) len (\d+) copied (\d+)/.
+ exec(eventBase.details);
+ if (!event) return false;
+
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const pos = parseInt(event[3]);
+ const len = parseInt(event[4]);
+ const error = parseInt(event[5]) !== len;
+ const key = device + '-' + inode + '-' + pos + '-' + len;
+ this.closeAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+ key, {
+ device,
+ inode,
+ error
+ });
+ return true;
+ },
+
+ f2fsSyncFileEnterEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = new RegExp(
+ 'dev = \\((\\d+,\\d+)\\), ino = (\\d+), pino = (\\d+), i_mode = (\\S+), ' + // @suppress longLineCheck
+ 'i_size = (\\d+), i_nlink = (\\d+), i_blocks = (\\d+), i_advise = (\\d+)'). // @suppress longLineCheck
+ exec(eventBase.details);
+ if (!event) return false;
+
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const key = device + '-' + inode;
+ this.openAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+ key, 'fsync');
+ return true;
+ },
+
+ f2fsSyncFileExitEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = new RegExp('dev = \\((\\d+,\\d+)\\), ino = (\\d+), checkpoint is (\\S+), ' + // @suppress longLineCheck
+ 'datasync = (\\d+), ret = (\\d+)').
+ exec(eventBase.details.replace('not needed', 'not_needed'));
+ if (!event) return false;
+
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const error = parseInt(event[5]);
+ const key = device + '-' + inode;
+ this.closeAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+ key, {
+ device,
+ inode,
+ error
+ });
+ return true;
+ },
+
+ ext4SyncFileEnterEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /dev (\d+,\d+) ino (\d+) parent (\d+) datasync (\d+)/.
+ exec(eventBase.details);
+ if (!event) return false;
+
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const datasync = (event[4] === '1') || (event[4] === 1);
+ const key = device + '-' + inode;
+ const action = datasync ? 'fdatasync' : 'fsync';
+ this.openAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+ key, action);
+ return true;
+ },
+
+ ext4SyncFileExitEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /dev (\d+,\d+) ino (\d+) ret (\d+)/.exec(eventBase.details);
+ if (!event) return false;
+
+ const device = event[1];
+ const inode = parseInt(event[2]);
+ const error = parseInt(event[3]);
+ const key = device + '-' + inode;
+ this.closeAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+ key, {
+ device,
+ inode,
+ error
+ });
+ return true;
+ },
+
+ blockRqIssueEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = new RegExp('(\\d+,\\d+) (F)?([DWRN])(F)?(A)?(S)?(M)? ' +
+ '\\d+ \\(.*\\) (\\d+) \\+ (\\d+) \\[.*\\]').exec(eventBase.details);
+ if (!event) return false;
+
+ let action;
+ switch (event[3]) {
+ case 'D':
+ action = 'discard';
+ break;
+ case 'W':
+ action = 'write';
+ break;
+ case 'R':
+ action = 'read';
+ break;
+ case 'N':
+ action = 'none';
+ break;
+ default:
+ action = 'unknown';
+ break;
+ }
+
+ if (event[2]) {
+ action += ' flush';
+ }
+ if (event[4] === 'F') {
+ action += ' fua';
+ }
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser_test.html
new file mode 100644
index 00000000000..ac1795a7140
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/disk_parser_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('diskImport', function() {
+ const lines = [
+ // NB: spliced from different traces; mismatched timestamps don't matter
+ 'AsyncTask #2-18830 [000] ...1 154578.668286: ext4_sync_file_enter: ' +
+ 'dev 259,1 ino 81993 parent 81906 datasync 1',
+ 'Binder_A-3179 [001] ...1 1354.510088: f2fs_sync_file_enter: ' +
+ 'dev = (259,14), ino = 4882, pino = 313, i_mode = 0x81b0, i_size = ' +
+ '25136, i_nlink = 1, i_blocks = 8, i_advise = 0x0',
+ 'Binder_A-3179 [001] ...1 1354.514013: f2fs_sync_file_exit: ' +
+ 'dev = (259,14), ino = 4882, checkpoint is not needed, datasync = 1, ret = 0', // @suppress longLineCheck
+ 'mmcqd/0-81 [000] d..2 154578.668390: block_rq_issue: ' +
+ '179,0 WS 0 () 3427120 + 16 [mmcqd/0]',
+ 'mmcqd/0-81 [000] d..2 154578.669181: block_rq_complete: ' +
+ '179,0 WS () 3427120 + 16 [0]',
+ 'mmcqd/0-81 [001] d..2 154578.670853: block_rq_issue: ' +
+ '179,0 FWS 0 () 18446744073709551615 + 0 [mmcqd/0]',
+ 'mmcqd/0-81 [001] d..2 154578.670869: block_rq_complete: ' +
+ '179,0 FWS () 18446744073709551615 + 0 [0]',
+ 'AsyncTask #2-18830 [001] ...1 154578.670901: ext4_sync_file_exit: ' +
+ 'dev 259,1 ino 81993 ret 0',
+ 'mmcqd/0-81 [001] d..2 154578.877038: block_rq_issue: ' +
+ '179,0 R 0 () 3255256 + 8 [mmcqd/0]',
+ 'mmcqd/0-81 [001] d..2 154578.877110: block_rq_issue: ' +
+ '179,0 R 0 () 3255288 + 8 [mmcqd/0]',
+ 'mmcqd/0-81 [000] d..2 154578.877345: block_rq_complete: ' +
+ '179,0 R () 3255256 + 8 [0]',
+ 'mmcqd/0-81 [000] d..2 154578.877466: block_rq_complete: ' +
+ '179,0 R () 3255288 + 8 [0]',
+ 'ContactsProvide-1184 [000] ...1 66.613719: f2fs_write_begin: ' +
+ 'dev = (253,2), ino = 3342, pos = 0, len = 75, flags = 0',
+ 'ContactsProvide-1184 [000] ...1 66.613733: f2fs_write_end: ' +
+ 'dev = (253,2), ino = 3342, pos = 0, len = 75, copied = 75'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ let blockThread = undefined;
+ let ext4Thread = undefined;
+ let f2fsSyncThread = undefined;
+ let f2fsWriteThread = undefined;
+
+ m.getAllThreads().forEach(function(t) {
+ switch (t.name) {
+ case 'block:mmcqd/0':
+ blockThread = t;
+ break;
+ case 'ext4:AsyncTask #2':
+ ext4Thread = t;
+ break;
+ case 'f2fs:Binder_A':
+ f2fsSyncThread = t;
+ break;
+ case 'f2fs:ContactsProvide':
+ f2fsWriteThread = t;
+ break;
+ default:
+ assert.fail(t, undefined, 'Unexpected thread named ' + t.name);
+ }
+ });
+ assert.isDefined(blockThread);
+ assert.isDefined(ext4Thread);
+ assert.isDefined(f2fsSyncThread);
+ assert.isDefined(f2fsWriteThread);
+
+ assert.strictEqual(blockThread.asyncSliceGroup.length, 4);
+
+ let slice = blockThread.asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.category, 'block');
+ assert.strictEqual(slice.title, 'write sync');
+ assert.strictEqual(slice.args.device, '179,0');
+ assert.strictEqual(slice.args.error, 0);
+ assert.strictEqual(slice.args.numSectors, 16);
+ assert.strictEqual(slice.args.sector, 3427120);
+
+ assert.strictEqual(ext4Thread.asyncSliceGroup.length, 1);
+
+ slice = ext4Thread.asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.category, 'ext4');
+ assert.strictEqual(slice.title, 'fdatasync');
+ assert.strictEqual(slice.args.device, '259,1');
+ assert.strictEqual(slice.args.error, 0);
+ assert.strictEqual(slice.args.inode, 81993);
+
+ assert.strictEqual(f2fsSyncThread.asyncSliceGroup.length, 1);
+
+ slice = f2fsSyncThread.asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.category, 'f2fs');
+ assert.strictEqual(slice.title, 'fsync');
+ assert.strictEqual(slice.args.device, '259,14');
+ assert.strictEqual(slice.args.error, 0);
+ assert.strictEqual(slice.args.inode, 4882);
+
+ assert.strictEqual(f2fsWriteThread.asyncSliceGroup.length, 1);
+
+ slice = f2fsWriteThread.asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.category, 'f2fs');
+ assert.strictEqual(slice.title, 'f2fs_write');
+ assert.strictEqual(slice.args.device, '253,2');
+ assert.strictEqual(slice.args.inode, 3342);
+ assert.strictEqual(slice.args.error, false);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser.html
new file mode 100644
index 00000000000..a7f02f23e93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses drm driver events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux drm trace events.
+ * @constructor
+ */
+ 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);
+ },
+
+ /**
+ * Parses drm driver events and sets up state in the importer.
+ */
+ 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,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser_test.html
new file mode 100644
index 00000000000..1369fa5ca9a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/drm_parser_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('drmImport', function() {
+ const lines = [
+ ' chrome-2465 [000] 71.653157: drm_vblank_event: crtc=0, seq=4233',
+ ' <idle>-0 [000] 71.669851: drm_vblank_event: crtc=0, seq=4234'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const vblankThread = threads[0];
+ assert.strictEqual(vblankThread.name, 'drm_vblank');
+ assert.strictEqual(vblankThread.sliceGroup.length, 2);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser.html
new file mode 100644
index 00000000000..3f93d3bd6de
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses exynos events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux exynos trace events.
+ * @constructor
+ */
+ 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);
+ });
+ },
+
+ /**
+ * Parses exynos_busfreq_target_int events and sets up state.
+ */
+ 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;
+ },
+
+ /**
+ * Parses exynos_busfreq_target_mif events and sets up state.
+ */
+ 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;
+ },
+
+ /**
+ * Parses page_flip_state events and sets up state in the importer.
+ */
+ 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,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser_test.html
new file mode 100644
index 00000000000..74cd74a7b4c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/exynos_parser_test.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('exynosBusfreqImport', function() {
+ const lines = [
+ ' kworker/1:0-4177 [001] .... 2803.129806: ' +
+ 'exynos_busfreq_target_int: frequency=200000',
+ ' kworker/1:0-4177 [001] .... 2803.229207: ' +
+ 'exynos_busfreq_target_int: frequency=267000',
+ ' kworker/1:0-4177 [001] .... 2803.329031: ' +
+ 'exynos_busfreq_target_int: frequency=160000',
+ ' kworker/1:0-4177 [001] .... 2805.729039: ' +
+ 'exynos_busfreq_target_mif: frequency=200000'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c0 = m.kernel.cpus[0];
+ assert.strictEqual(c0.slices.length, 0);
+ assert.strictEqual(
+ c0.counters['.INT Frequency'].series[0].samples.length, 3);
+ assert.strictEqual(
+ c0.counters['.MIF Frequency'].series[0].samples.length, 1);
+ });
+
+ test('exynosPageFlipSlowRequestImport', function() {
+ const lines = [
+ ' <idle>-0 [000] d.h. 1000.000000: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_kds',
+ ' Chrome_IOThread-21603 [000] d.h. 1000.000001: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_apply',
+ ' kworker/0:1-25931 [000] .... 1000.000002: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_flip',
+ ' kworker/0:1-25931 [000] .... 1000.000003: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=flipped',
+ ' <idle>-0 [000] d.h. 1000.000004: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_kds',
+ ' Chrome_IOThread-21603 [000] d.h. 1000.000005: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_apply',
+ ' kworker/0:1-25931 [000] .... 1000.000006: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_flip',
+ ' kworker/0:1-25931 [000] .... 1000.000007: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=flipped'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ // there are 2 threads:
+ // (1) "exynos_flip_state (pipe:0, fb:25)"
+ // (2) "exynos_flip_state (pipe:0, fb:26)"
+ assert.strictEqual(threads.length, 2);
+
+ // in the test data, event of fb=26 occurs first, so it's thread[0]
+ const gfxFbId26Thread = threads[0]; // thread where fb === 26
+ const gfxFbId25Thread = threads[1]; // thread where fb === 25
+ assert.strictEqual(
+ gfxFbId25Thread.name, 'exynos_flip_state (pipe:0, fb:25)');
+ assert.strictEqual(
+ gfxFbId26Thread.name, 'exynos_flip_state (pipe:0, fb:26)');
+ // Every state (except for 'flipped') will start a new slice.
+ // The last event will not be closed, so it's not a slice
+ assert.strictEqual(gfxFbId25Thread.sliceGroup.length, 3);
+ assert.strictEqual(gfxFbId26Thread.sliceGroup.length, 3);
+ });
+
+ test('exynosPageFlipFastRequestImport', function() {
+ const lines = [
+ ' <idle>-0 [000] d.h. 1000.000000: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_kds',
+ ' Chrome_IOThread-21603 [000] d.h. 1000.000001: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_kds',
+ ' X-21385 [000] .... 1000.000002: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_apply',
+ ' kworker/0:1-25931 [000] .... 1000.000003: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_flip',
+ ' X-21385 [001] .... 1000.000004: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_apply',
+ ' kworker/0:1-25931 [000] .... 1000.000005: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=flipped',
+ ' <idle>-0 [000] d.h. 1000.000006: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_kds',
+ ' X-21385 [000] .... 1000.000007: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_flip',
+ ' kworker/0:1-25931 [000] .... 1000.000008: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=flipped',
+ ' kworker/0:1-25931 [000] .... 1000.000009: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_kds',
+ ' Chrome_IOThread-21603 [000] d.h. 1000.000010: ' +
+ 'exynos_page_flip_state: pipe=0, fb=25, state=wait_apply',
+ ' <idle>-0 [000] d.h. 1000.000011: ' +
+ 'exynos_page_flip_state: pipe=0, fb=26, state=wait_apply'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ // there are 2 threads:
+ // (1) "exynos_flip_state (pipe:0, fb:25)"
+ // (2) "exynos_flip_state (pipe:0, fb:26)"
+ assert.strictEqual(threads.length, 2);
+
+ // in the test data, event of fb=26 occurs first, so it's thread[0]
+ const gfxFbId26Thread = threads[0]; // thread where fb === 26
+ const gfxFbId25Thread = threads[1]; // thread where fb === 25
+ assert.strictEqual(
+ gfxFbId25Thread.name, 'exynos_flip_state (pipe:0, fb:25)');
+ assert.strictEqual(
+ gfxFbId26Thread.name, 'exynos_flip_state (pipe:0, fb:26)');
+ // Every state (except for 'flipped') will start a new slice.
+ // The last event will not be closed, so it's not a slice
+ assert.strictEqual(gfxFbId25Thread.sliceGroup.length, 4);
+ assert.strictEqual(gfxFbId26Thread.sliceGroup.length, 4);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser.html
new file mode 100644
index 00000000000..5745ff5e4a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses fence events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux fence trace events.
+ * @constructor
+ */
+ 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,
+
+ /**
+ * Parses fence events and sets up state in the importer.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser_test.html
new file mode 100644
index 00000000000..ae439e2c591
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/fence_parser_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('fenceEventImport', function() {
+ const lines = [
+ 'HwBinder:595_2-650 ( 595) [001] .... 584509.195956: ' +
+ 'fence_init: driver=sde_fence:crtc111:25602 timeline=crtc111 ' +
+ 'context=3 seqno=256022',
+ 'crtc_event:111-247 ( 247) [001] d..1 584509.213897: ' +
+ 'fence_signaled: driver=sde_fence:crtc111:25602 timeline=crtc111 ' +
+ 'context=3 seqno=256022'
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer.html
new file mode 100644
index 00000000000..cedff930314
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer.html
@@ -0,0 +1,941 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/trace_stream.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/android_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/binder_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/bus_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/clock_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/cpufreq_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/disk_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/drm_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/exynos_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/fence_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/gesture_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/i2c_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/i915_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ion_heap_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/irq_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/kfunc_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/mali_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/memreclaim_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/power_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/regulator_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/rss_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/sched_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/sync_parser.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/workqueue_parser.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/importer/simple_line_reader.html">
+<link rel="import" href="/tracing/model/clock_sync_manager.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+/**
+ * @fileoverview Imports text files in the Linux event trace format into the
+ * Model. This format is output both by sched_trace and by Linux's perf tool.
+ *
+ * This importer assumes the events arrive as a string. The unit tests provide
+ * examples of the trace format.
+ *
+ * Linux scheduler traces use a definition for 'pid' that is different than
+ * tracing uses. Whereas tracing uses pid to identify a specific process, a pid
+ * in a linux trace refers to a specific thread within a process. Within this
+ * file, we the definition used in Linux traces, as it improves the importing
+ * code's readability.
+ */
+'use strict';
+
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const MONOTONIC_TO_FTRACE_GLOBAL_SYNC_ID =
+ 'linux_clock_monotonic_to_ftrace_global';
+
+ const IMPORT_PRIORITY = 2;
+
+ /**
+ * Imports linux perf events into a specified model.
+ * @constructor
+ */
+ function FTraceImporter(model, events) {
+ this.importPriority = IMPORT_PRIORITY;
+ this.model_ = model;
+ this.events_ = events;
+ this.wakeups_ = [];
+ this.blockedReasons_ = [];
+ this.kernelThreadStates_ = {};
+ this.buildMapFromLinuxPidsToThreads_();
+ this.lines_ = [];
+ this.pseudoThreadCounter = 1;
+ this.parsers_ = [];
+ this.eventHandlers_ = {};
+ this.haveClockSyncedMonotonicToGlobal_ = false;
+ this.clockDomainId_ = tr.model.ClockDomainId.LINUX_FTRACE_GLOBAL;
+ }
+
+ const TestExports = {};
+
+ // Matches the trace record in 3.2 and later with the print-tgid option:
+ // <idle>-0 0 [001] d... 1.23: sched_switch
+ //
+ // A TGID (Thread Group ID) is basically what the Linux kernel calls what
+ // userland refers to as a process ID (as opposed to a Linux pid, which is
+ // what userland calls a thread ID).
+ const lineREWithTGID = new RegExp(
+ '^\\s*(.+)-(\\d+)\\s+\\(\\s*(\\d+|-+)\\)\\s\\[(\\d+)\\]' +
+ '\\s+[dX.][Nnp.][Hhs.][0-9a-f.]' +
+ '\\s+(\\d+\\.\\d+):\\s+(\\S+):\\s(.*)$');
+ const lineParserWithTGID = function(line) {
+ const groups = lineREWithTGID.exec(line);
+ if (!groups) return groups;
+
+ let tgid = groups[3];
+ if (tgid[0] === '-') tgid = undefined;
+
+ return {
+ threadName: groups[1],
+ pid: groups[2],
+ tgid,
+ cpuNumber: groups[4],
+ timestamp: groups[5],
+ eventName: groups[6],
+ details: groups[7]
+ };
+ };
+ TestExports.lineParserWithTGID = lineParserWithTGID;
+
+ // Matches the default trace record in 3.2 and later (includes irq-info):
+ // <idle>-0 [001] d... 1.23: sched_switch
+ const lineREWithIRQInfo = new RegExp(
+ '^\\s*(.+)-(\\d+)\\s+\\[(\\d+)\\]' +
+ '\\s+[dX.][Nnp.][Hhs.][0-9a-f.]' +
+ '\\s+(\\d+\\.\\d+):\\s+(\\S+):\\s(.*)$');
+ const lineParserWithIRQInfo = function(line) {
+ const groups = lineREWithIRQInfo.exec(line);
+ if (!groups) return groups;
+ return {
+ threadName: groups[1],
+ pid: groups[2],
+ cpuNumber: groups[3],
+ timestamp: groups[4],
+ eventName: groups[5],
+ details: groups[6]
+ };
+ };
+ TestExports.lineParserWithIRQInfo = lineParserWithIRQInfo;
+
+ // Matches the default trace record pre-3.2:
+ // <idle>-0 [001] 1.23: sched_switch
+ const lineREWithLegacyFmt =
+ /^\s*(.+)-(\d+)\s+\[(\d+)\]\s*(\d+\.\d+):\s+(\S+):\s(.*)$/;
+ const lineParserWithLegacyFmt = function(line) {
+ const groups = lineREWithLegacyFmt.exec(line);
+ if (!groups) {
+ return groups;
+ }
+ return {
+ threadName: groups[1],
+ pid: groups[2],
+ cpuNumber: groups[3],
+ timestamp: groups[4],
+ eventName: groups[5],
+ details: groups[6]
+ };
+ };
+ TestExports.lineParserWithLegacyFmt = lineParserWithLegacyFmt;
+
+ // Matches the trace_event_clock_sync marker:
+ // 0: trace_event_clock_sync: parent_ts=19581477508
+ const traceEventClockSyncRE = /trace_event_clock_sync: parent_ts=(\d+\.?\d*)/;
+ TestExports.traceEventClockSyncRE = traceEventClockSyncRE;
+
+ const realTimeClockSyncRE = /trace_event_clock_sync: realtime_ts=(\d+)/;
+ const genericClockSyncRE = /trace_event_clock_sync: name=([\w\-]+)/;
+
+ // Some kernel trace events are manually classified in slices and
+ // hand-assigned a pseudo PID.
+ const pseudoKernelPID = 0;
+
+ /**
+ * Deduce the format of trace data. Linux kernels prior to 3.3 used one
+ * format (by default); 3.4 and later used another. Additionally, newer
+ * kernels can optionally trace the TGID.
+ *
+ * @return {function} the function for parsing data when the format is
+ * recognized; otherwise undefined.
+ */
+ function autoDetectLineParser(line) {
+ if (line[0] === '{') return false;
+ if (lineREWithTGID.test(line)) return lineParserWithTGID;
+ if (lineREWithIRQInfo.test(line)) return lineParserWithIRQInfo;
+ if (lineREWithLegacyFmt.test(line)) return lineParserWithLegacyFmt;
+ return undefined;
+ }
+ TestExports.autoDetectLineParser = autoDetectLineParser;
+
+ /**
+ * Guesses whether the provided events is a Linux perf string.
+ * Looks for the magic string "# tracer" at the start of the file,
+ * or the typical task-pid-cpu-timestamp-function sequence of a typical
+ * trace's body.
+ *
+ * @return {boolean} True when events is a linux perf array.
+ */
+ FTraceImporter.canImport = function(events) {
+ if (events instanceof tr.b.TraceStream) events = events.header;
+
+ if (!(typeof(events) === 'string' || events instanceof String)) {
+ return false;
+ }
+
+ if (FTraceImporter._extractEventsFromSystraceHTML(events, false).ok) {
+ return true;
+ }
+
+ if (FTraceImporter._extractEventsFromSystraceMultiHTML(events, false).ok) {
+ return true;
+ }
+
+ if (/^# tracer:/.test(events)) return true;
+
+ const lineBreakIndex = events.indexOf('\n');
+ if (lineBreakIndex > -1) events = events.substring(0, lineBreakIndex);
+
+ if (autoDetectLineParser(events)) return true;
+
+ return false;
+ };
+
+ FTraceImporter._extractEventsFromSystraceHTML = function(
+ incomingEvents, produceResult) {
+ const failure = {ok: false};
+ if (produceResult === undefined) produceResult = true;
+
+ const header = incomingEvents instanceof tr.b.TraceStream ?
+ incomingEvents.header : incomingEvents;
+ if (!/^<!DOCTYPE html>/.test(header)) return failure;
+ const r = new tr.importer.SimpleLineReader(incomingEvents);
+
+ // Try to find the data...
+ if (!r.advanceToLineMatching(/^ <script>$/)) return failure;
+ if (!r.advanceToLineMatching(/^ var linuxPerfData = "\\$/)) return failure;
+
+ const eventsBeginAtLine = r.curLineNumber + 1;
+ r.beginSavingLines();
+ if (!r.advanceToLineMatching(/^ <\/script>$/)) return failure;
+
+ let rawEvents = r.endSavingLinesAndGetResult();
+
+ // Drop off first and last event as it contains the tag.
+ rawEvents = rawEvents.slice(1, rawEvents.length - 1);
+
+ if (!r.advanceToLineMatching(/^<\/body>$/)) return failure;
+ if (!r.advanceToLineMatching(/^<\/html>$/)) return failure;
+
+ function endsWith(str, suffix) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+ function stripSuffix(str, suffix) {
+ if (!endsWith(str, suffix)) return str;
+ return str.substring(str, str.length - suffix.length);
+ }
+
+ // Strip off escaping in the file needed to preserve linebreaks.
+ 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]];
+ }
+
+ // Last event ends differently. Strip that off too,
+ // treating absence of that trailing string as a failure.
+ 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);
+
+ // Try to find the Linux perf trace in any of the trace-data tags
+ 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();
+
+ // Drop off first and last event as it contains the tag.
+ 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_;
+ },
+
+ /**
+ * Imports clock sync markers into 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)) {
+ // TODO(charliea): Migrate this sync to ClockSyncManager.
+ // This entry syncs CLOCK_REALTIME with CLOCK_MONOTONIC. Store the
+ // offset between the two in the model so that importers parsing files
+ // with CLOCK_REALTIME timestamps can map back to CLOCK_MONOTONIC.
+ const match = realTimeClockSyncRE.exec(eventBase.details);
+ this.model_.realtime_to_monotonic_offset_ms = ts - match[1];
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Imports the data in this.events_ into model_.
+ */
+ importEvents() {
+ const modelTimeTransformer =
+ this.model_.clockSyncManager.getModelTimeTransformer(
+ this.clockDomainId_);
+
+ this.importCpuData_(modelTimeTransformer);
+ this.buildMapFromLinuxPidsToThreads_();
+ this.buildPerThreadCpuSlicesFromCpuState_();
+ },
+
+ /**
+ * Registers a linux perf event parser used by importCpuData_.
+ */
+ registerEventHandler(eventName, handler) {
+ // TODO(sleffler) how to handle conflicts?
+ this.eventHandlers_[eventName] = handler;
+ },
+
+ /**
+ * @return {Cpu} A Cpu corresponding to the given cpuNumber.
+ */
+ getOrCreateCpu(cpuNumber) {
+ return this.model_.kernel.getOrCreateCpu(cpuNumber);
+ },
+
+ /**
+ * @return {TimelineThread} A thread corresponding to the kernelThreadName.
+ */
+ 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];
+ },
+
+ /**
+ * Processes can have multiple binder threads.
+ * Binder thread names are not unique across processes we therefore need to
+ * keep more information in order to return the correct threads.
+ */
+ 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];
+ },
+
+ /**
+ * @return {TimelineThread} A pseudo thread corresponding to the
+ * threadName. Pseudo threads are for events that we want to break
+ * out to a separate timeline but would not otherwise happen.
+ * These threads are assigned to pseudoKernelPID and given a
+ * unique (incrementing) TID.
+ */
+ getOrCreatePseudoThread(threadName) {
+ let thread = this.kernelThreadStates_[threadName];
+ if (!thread) {
+ thread = this.getOrCreateKernelThread(threadName, pseudoKernelPID,
+ this.pseudoThreadCounter);
+ this.pseudoThreadCounter++;
+ }
+ return thread;
+ },
+
+ /**
+ * Records the fact that a pid has become runnable. This data will
+ * eventually get used to derive each thread's timeSlices array.
+ */
+ markPidRunnable(ts, pid, comm, prio, fromPid) {
+ // The the pids that get passed in to this function are Linux kernel
+ // pids, which identify threads. The rest of trace-viewer refers to
+ // these as tids, so the change of nomenclature happens in the following
+ // construction of the wakeup object.
+ this.wakeups_.push({ts, tid: pid, fromTid: fromPid});
+ },
+
+ /**
+ * Records the reason why a pid has gone into uninterruptible sleep.
+ */
+ addPidBlockedReason(ts, pid, iowait, caller) {
+ // The the pids that get passed in to this function are Linux kernel
+ // pids, which identify threads. The rest of trace-viewer refers to
+ // these as tids, so the change of nomenclature happens in the following
+ // construction of the wakeup object.
+ this.blockedReasons_.push({ts, tid: pid, iowait,
+ caller});
+ },
+
+ /**
+ * Precomputes a lookup table from linux pids back to existing
+ * Threads. This is used during importing to add information to each
+ * thread about whether it was running, descheduled, sleeping, et
+ * cetera.
+ */
+ buildMapFromLinuxPidsToThreads_() {
+ this.threadsByLinuxPid = {};
+ this.model_.getAllThreads().forEach(
+ function(thread) {
+ this.threadsByLinuxPid[thread.tid] = thread;
+ }.bind(this));
+ },
+
+ /**
+ * Builds the timeSlices array on each thread based on our knowledge of what
+ * each Cpu is doing. This is done only for Threads that are
+ * already in the model, on the assumption that not having any traced data
+ * on a thread means that it is not of interest to the user.
+ */
+ buildPerThreadCpuSlicesFromCpuState_() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+
+ // Push the cpu slices to the threads that they run on.
+ 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 = [];
+ }
+ thread.tempCpuSlices.push(cpuSlice);
+ }
+ }
+
+ for (const i in this.wakeups_) {
+ const wakeup = this.wakeups_[i];
+ const thread = this.threadsByLinuxPid[wakeup.tid];
+ if (!thread) continue;
+ thread.tempWakeups = thread.tempWakeups || [];
+ thread.tempWakeups.push(wakeup);
+ }
+ for (const i in this.blockedReasons_) {
+ const reason = this.blockedReasons_[i];
+ const thread = this.threadsByLinuxPid[reason.tid];
+ if (!thread) continue;
+ thread.tempBlockedReasons = thread.tempBlockedReasons || [];
+ thread.tempBlockedReasons.push(reason);
+ }
+
+ // Create slices for when the thread is not running.
+ this.model_.getAllThreads().forEach(function(thread) {
+ if (thread.tempCpuSlices === undefined) return;
+ const origSlices = thread.tempCpuSlices;
+ delete thread.tempCpuSlices;
+
+ origSlices.sort(function(x, y) {
+ return x.start - y.start;
+ });
+
+ const wakeups = thread.tempWakeups || [];
+ delete thread.tempWakeups;
+ wakeups.sort(function(x, y) {
+ return x.ts - y.ts;
+ });
+
+ const reasons = thread.tempBlockedReasons || [];
+ delete thread.tempBlockedReasons;
+ reasons.sort(function(x, y) {
+ return x.ts - y.ts;
+ });
+
+ // Walk the slice list and put slices between each original slice to
+ // show when the thread isn't running.
+ const slices = [];
+
+ if (origSlices.length) {
+ const slice = origSlices[0];
+
+ if (wakeups.length && wakeups[0].ts < slice.start) {
+ const wakeup = wakeups.shift();
+ const wakeupDuration = slice.start - wakeup.ts;
+ const args = {'wakeup from tid': wakeup.fromTid};
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.RUNNABLE, '',
+ wakeup.ts, args, wakeupDuration));
+ }
+
+ const runningSlice = new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.RUNNING, '',
+ slice.start, {}, slice.duration);
+ runningSlice.cpuOnWhichThreadWasRunning = slice.cpu;
+ slices.push(runningSlice);
+ }
+
+ for (let i = 1; i < origSlices.length; i++) {
+ let wakeup = undefined;
+ const prevSlice = origSlices[i - 1];
+ const nextSlice = origSlices[i];
+ let midDuration = nextSlice.start - prevSlice.end;
+ while (wakeups.length && wakeups[0].ts < nextSlice.start) {
+ const w = wakeups.shift();
+ if (wakeup === undefined && w.ts > prevSlice.end) {
+ wakeup = w;
+ }
+ }
+ let blockedReason = undefined;
+ while (reasons.length && reasons[0].ts < prevSlice.end) {
+ const r = reasons.shift();
+ }
+ if (wakeup !== undefined &&
+ reasons.length &&
+ reasons[0].ts < wakeup.ts) {
+ blockedReason = reasons.shift();
+ }
+
+ // Push a sleep slice onto the slices list, interrupting it with a
+ // wakeup if appropriate.
+ const pushSleep = function(state) {
+ if (wakeup !== undefined) {
+ midDuration = wakeup.ts - prevSlice.end;
+ }
+
+ if (blockedReason !== undefined) {
+ const args = {
+ 'kernel callsite when blocked:': blockedReason.caller
+ };
+ if (blockedReason.iowait) {
+ switch (state) {
+ case SCHEDULING_STATE.UNINTR_SLEEP:
+ state = SCHEDULING_STATE.UNINTR_SLEEP_IO;
+ break;
+ case SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL:
+ state = SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL_IO;
+ break;
+ case SCHEDULING_STATE.UNINTR_SLEEP_WAKING:
+ state = SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL_IO;
+ break;
+ default:
+ }
+ }
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread,
+ state, '', prevSlice.end, args, midDuration));
+ } else {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread,
+ state, '', prevSlice.end, {}, midDuration));
+ }
+ if (wakeup !== undefined) {
+ const wakeupDuration = nextSlice.start - wakeup.ts;
+ const args = {'wakeup from tid': wakeup.fromTid};
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.RUNNABLE, '',
+ wakeup.ts, args, wakeupDuration));
+ wakeup = undefined;
+ }
+ };
+
+ if (prevSlice.args.stateWhenDescheduled === 'S') {
+ pushSleep(SCHEDULING_STATE.SLEEPING);
+ } else if (prevSlice.args.stateWhenDescheduled === 'R' ||
+ prevSlice.args.stateWhenDescheduled === 'R+') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.RUNNABLE, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'D') {
+ pushSleep(SCHEDULING_STATE.UNINTR_SLEEP);
+ } else if (prevSlice.args.stateWhenDescheduled === 'T') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.STOPPED, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 't') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.DEBUG, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'Z') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.ZOMBIE, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'X') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.EXIT_DEAD, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'x') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.TASK_DEAD, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'K') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.WAKE_KILL, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'W') {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.WAKING, '',
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled === 'D|K') {
+ pushSleep(SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL);
+ } else if (prevSlice.args.stateWhenDescheduled === 'D|W') {
+ pushSleep(SCHEDULING_STATE.UNINTR_SLEEP_WAKING);
+ } else {
+ slices.push(new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.UNKNOWN, '',
+ prevSlice.end, {}, midDuration));
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Unrecognized sleep state: ' +
+ prevSlice.args.stateWhenDescheduled
+ });
+ }
+
+ const runningSlice = new tr.model.ThreadTimeSlice(
+ thread, SCHEDULING_STATE.RUNNING, '',
+ nextSlice.start, {}, nextSlice.duration);
+ runningSlice.cpuOnWhichThreadWasRunning = prevSlice.cpu;
+ slices.push(runningSlice);
+ }
+ thread.timeSlices = slices;
+ }, this);
+ },
+
+ /**
+ * Creates an instance of each registered linux perf event parser.
+ * This allows the parsers to register handlers for the events they
+ * understand. We also register our own special handlers (for the
+ * timestamp synchronization markers).
+ */
+ createParsers_() {
+ // Instantiate the parsers; this will register handlers for known events
+ const allTypeInfos = tr.e.importer.linux_perf.
+ Parser.getAllRegisteredTypeInfos();
+ const parsers = allTypeInfos.map(
+ function(typeInfo) {
+ return new typeInfo.constructor(this);
+ }, this);
+
+ return parsers;
+ },
+
+ registerDefaultHandlers_() {
+ this.registerEventHandler('tracing_mark_write',
+ FTraceImporter.prototype.traceMarkingWriteEvent_.bind(this));
+ // NB: old-style trace markers; deprecated
+ this.registerEventHandler('0',
+ FTraceImporter.prototype.traceMarkingWriteEvent_.bind(this));
+ // Register dummy clock sync handlers to avoid warnings in the log.
+ this.registerEventHandler('tracing_mark_write:trace_event_clock_sync',
+ function() { return true; });
+ this.registerEventHandler('0:trace_event_clock_sync',
+ function() { return true; });
+ },
+
+ /**
+ * Processes a trace_event_clock_sync event.
+ */
+ traceClockSyncEvent_(eventName, cpuNumber, pid, ts, eventBase) {
+ // Check to see if we have a normal clock sync marker that contains a
+ // sync ID and the current time according to the "ftrace global" clock.
+ let event = /name=(\w+?)\s(.+)/.exec(eventBase.details);
+ if (event) {
+ // TODO(alexandermont): This section of code seems to be broken. It
+ // creates an "args" variable, but doesn't seem to do anything with it.
+ const name = event[1];
+ const pieces = event[2].split(' ');
+ const args = {
+ perfTs: ts
+ };
+ for (let i = 0; i < pieces.length; i++) {
+ const parts = pieces[i].split('=');
+ if (parts.length !== 2) {
+ throw new Error('omgbbq');
+ }
+ args[parts[0]] = parts[1];
+ }
+
+ this.model_.clockSyncManager.addClockSyncMarker(
+ this.clockDomainId_, name, ts);
+ return true;
+ }
+
+ // Check to see if we have a "new style" clock sync marker that contains
+ // only a sync ID.
+ event = /name=([\w\-]+)/.exec(eventBase.details);
+ if (event) {
+ this.model_.clockSyncManager.addClockSyncMarker(
+ this.clockDomainId_, event[1], ts);
+ return true;
+ }
+
+ // Check to see if we have a special clock sync marker that contains both
+ // the current "ftrace global" time and the current CLOCK_MONOTONIC time.
+ event = /parent_ts=(\d+\.?\d*)/.exec(eventBase.details);
+ if (!event) return false;
+
+ let monotonicTs = event[1] * 1000;
+ // A monotonic timestamp of zero is used as a sentinel value to indicate
+ // that CLOCK_MONOTONIC and the ftrace global clock are identical.
+ if (monotonicTs === 0) monotonicTs = ts;
+
+ if (this.haveClockSyncedMonotonicToGlobal_) {
+ // ftrace sometimes includes multiple clock syncs between the monotonic
+ // and global clocks within a single trace. We protect against this by
+ // only taking the first one into account.
+ return true;
+ }
+
+ // We have a clock sync event that contains two timestamps: a timestamp
+ // according to the ftrace 'global' clock, and that same timestamp
+ // according to clock_gettime(CLOCK_MONOTONIC).
+ this.model_.clockSyncManager.addClockSyncMarker(
+ this.clockDomainId_,
+ MONOTONIC_TO_FTRACE_GLOBAL_SYNC_ID, ts);
+ this.model_.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.LINUX_CLOCK_MONOTONIC,
+ MONOTONIC_TO_FTRACE_GLOBAL_SYNC_ID, monotonicTs);
+
+ this.haveClockSyncedMonotonicToGlobal_ = true;
+ return true;
+ },
+
+ /**
+ * Processes a trace_marking_write event.
+ */
+ traceMarkingWriteEvent_(eventName, cpuNumber, pid, ts, eventBase,
+ threadName) {
+ // Some profiles end up with a \n\ on the end of each line. Strip it
+ // before we do the comparisons.
+ eventBase.details = eventBase.details.replace(/\\n.*$/, '');
+
+ const event = /^\s*(\w+):\s*(.*)$/.exec(eventBase.details);
+ if (!event) {
+ // Check if the event matches events traced by the Android framework
+ const tag = eventBase.details.substring(0, 2);
+ if (tag === 'B|' || tag === 'E' || tag === 'E|' || tag === 'X|' ||
+ tag === 'C|' || tag === 'S|' || tag === 'F|') {
+ eventBase.subEventName = 'android';
+ } else {
+ return false;
+ }
+ } else {
+ eventBase.subEventName = event[1];
+ eventBase.details = event[2];
+ }
+
+ const writeEventName = eventName + ':' + eventBase.subEventName;
+ const handler = this.eventHandlers_[writeEventName];
+ if (!handler) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Unknown trace_marking_write event ' + writeEventName
+ });
+ return true;
+ }
+ return handler(writeEventName, cpuNumber, pid, ts, eventBase, threadName);
+ },
+
+ /**
+ * Walks the this.events_ structure and creates Cpu objects.
+ */
+ importCpuData_(modelTimeTransformer) {
+ this.forEachLine_(function(text, eventBase, cpuNumber, pid, ts) {
+ const eventName = eventBase.eventName;
+ const handler = this.eventHandlers_[eventName];
+ if (!handler) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Unknown event ' + eventName + ' (' + text + ')'
+ });
+ return;
+ }
+ ts = modelTimeTransformer(ts);
+ if (!handler(eventName, cpuNumber, pid, ts, eventBase)) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Malformed ' + eventName + ' event (' + text + ')'
+ });
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Walks the this.events_ structure and populates this.lines_.
+ */
+ parseLines_() {
+ let extractResult = FTraceImporter._extractEventsFromSystraceHTML(
+ this.events_, true);
+ if (!extractResult.ok) {
+ extractResult = FTraceImporter._extractEventsFromSystraceMultiHTML(
+ this.events_, true);
+ }
+ let lineParser = undefined;
+ if (extractResult.ok) {
+ for (const line of extractResult.lines) {
+ lineParser = this.parseLine_(line, lineParser);
+ }
+ } else {
+ const r = new tr.importer.SimpleLineReader(this.events_);
+ for (const line of r) {
+ lineParser = this.parseLine_(line, lineParser);
+ }
+ }
+ },
+
+ parseLine_(line, lineParser) {
+ line = line.trim();
+ if (line.length === 0) return lineParser;
+ if (/^#/.test(line)) {
+ const clockType = /^# clock_type=([A-Z_]+)$/.exec(line);
+ // This allows the clock domain to be specified through a comment,
+ // Ex. "# clock_type=LINUX_CLOCK_MONOTONIC".
+ // This is used in the WALT trace agent.
+ if (clockType) {
+ this.clockDomainId_ = clockType[1];
+ }
+ return lineParser;
+ }
+
+ if (!lineParser) {
+ lineParser = autoDetectLineParser(line);
+ if (!lineParser) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Cannot parse line: ' + line
+ });
+ return lineParser;
+ }
+ }
+
+ const eventBase = lineParser(line);
+ if (!eventBase) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Unrecognized line: ' + line
+ });
+ return lineParser;
+ }
+
+ this.lines_.push([
+ line,
+ eventBase,
+ parseInt(eventBase.cpuNumber),
+ parseInt(eventBase.pid),
+ parseFloat(eventBase.timestamp) * 1000
+ ]);
+ return lineParser;
+ },
+
+ /**
+ * Calls |handler| for every parsed line.
+ */
+ forEachLine_(handler) {
+ for (let i = 0; i < this.lines_.length; ++i) {
+ const line = this.lines_[i];
+ handler.apply(this, line);
+ }
+ },
+
+ /**
+ * Initializes the ftrace importer. This initialization can't be done in the
+ * constructor because all trace event handlers may not have been registered
+ * by that point.
+ */
+ lazyInit_() {
+ this.parsers_ = this.createParsers_();
+ this.registerDefaultHandlers_();
+ this.parseLines_();
+ }
+ };
+
+ tr.importer.Importer.register(FTraceImporter);
+
+ return {
+ FTraceImporter,
+ _FTraceImporterTestExports: TestExports,
+ IMPORT_PRIORITY,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer_test.html
new file mode 100644
index 00000000000..ee9382ab13f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ftrace_importer_test.html
@@ -0,0 +1,686 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/clock_sync_manager.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ClockDomainId = tr.model.ClockDomainId;
+ const FTraceImporter = tr.e.importer.linux_perf.FTraceImporter;
+ const FTraceImporterTestExports =
+ tr.e.importer.linux_perf._FTraceImporterTestExports;
+
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('lineParserWithLegacyFmt', function() {
+ const p = FTraceImporterTestExports.lineParserWithLegacyFmt;
+ let x = p(' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, '<idle>');
+ assert.strictEqual(x.pid, '0');
+ assert.strictEqual(x.cpuNumber, '001');
+ assert.strictEqual(x.timestamp, '4467.843475');
+ assert.strictEqual(x.eventName, 'sched_switch');
+ assert.strictEqual(
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R' +
+ ' ==> next_comm=SurfaceFlinger next_pid=178 next_prio=112', x.details);
+
+ x = p('Binder-Thread #-647 [001] 260.464294: sched_switch: ' +
+ 'prev_comm=Binder Thread # prev_pid=647 prev_prio=120 prev_state=D ' +
+ ' ==> next_comm=.android.chrome next_pid=1562 next_prio=120');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'Binder-Thread #');
+ assert.strictEqual(x.pid, '647');
+ });
+
+ test('lineParserWithIRQInfo', function() {
+ const p = FTraceImporterTestExports.lineParserWithIRQInfo;
+ const x = p(' systrace.sh-5441 [001] d... 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'systrace.sh');
+ assert.strictEqual(x.pid, '5441');
+ assert.strictEqual(x.cpuNumber, '001');
+ assert.strictEqual(x.timestamp, '1031.091570');
+ assert.strictEqual(x.eventName, 'sched_wakeup');
+ assert.strictEqual(x.details, 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000'); // @suppress longLineCheck
+ });
+
+ test('lineParserWithIRQInfoNeedResched', function() {
+ const p = FTraceImporterTestExports.lineParserWithIRQInfo;
+ let x = p(' systrace.sh-5441 [001] .N.. 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'systrace.sh');
+ assert.strictEqual(x.pid, '5441');
+ assert.strictEqual(x.cpuNumber, '001');
+ assert.strictEqual(x.timestamp, '1031.091570');
+ assert.strictEqual(x.eventName, 'sched_wakeup');
+ assert.strictEqual(x.details, 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000'); // @suppress longLineCheck
+
+ x = p(' systrace.sh-5441 [001] .n.. 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'systrace.sh');
+
+ x = p(' systrace.sh-5441 [001] .p.. 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'systrace.sh');
+ });
+
+ test('lineParserWithTGID', function() {
+ const p = FTraceImporterTestExports.lineParserWithTGID;
+ let x = p(' systrace.sh-5441 (54321) [001] d... 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'systrace.sh');
+ assert.strictEqual(x.pid, '5441');
+ assert.strictEqual(x.tgid, '54321');
+ assert.strictEqual(x.cpuNumber, '001');
+ assert.strictEqual(x.timestamp, '1031.091570');
+ assert.strictEqual(x.eventName, 'sched_wakeup');
+ assert.strictEqual(x.details, 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000'); // @suppress longLineCheck
+
+ x = p(' systrace.sh-5441 ( 321) [001] d... 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.tgid, '321');
+
+ x = p(' systrace.sh-5441 (-----) [001] d... 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.isUndefined(x.tgid);
+ });
+
+ test('lineParserWithTGIDNeedResched', function() {
+ const p = FTraceImporterTestExports.lineParserWithTGID;
+ let x = p(' systrace.sh-5441 (54321) [001] .N.. 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.threadName, 'systrace.sh');
+ assert.strictEqual(x.pid, '5441');
+ assert.strictEqual(x.tgid, '54321');
+ assert.strictEqual(x.cpuNumber, '001');
+ assert.strictEqual(x.timestamp, '1031.091570');
+ assert.strictEqual(x.eventName, 'sched_wakeup');
+ assert.strictEqual(x.details, 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000'); // @suppress longLineCheck
+
+ x = p(' systrace.sh-5441 ( 321) [001] .n.. 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x.tgid, '321');
+
+ x = p(' systrace.sh-5441 (-----) [001] .p.. 1031.091570: ' +
+ 'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.isUndefined(x.tgid);
+ });
+
+ test('autodetectLineCornerCases', function() {
+ const detectParser = FTraceImporterTestExports.autoDetectLineParser;
+ const lineParserWithLegacyFmt =
+ FTraceImporterTestExports.lineParserWithLegacyFmt;
+ const lineParserWithIRQInfo =
+ FTraceImporterTestExports.lineParserWithIRQInfo;
+ const lineParserWithTGID = FTraceImporterTestExports.lineParserWithTGID;
+
+ const lineWithLegacyFmt =
+ 'systrace.sh-8170 [001] 15180.978813: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+ 'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+ 'next_prio=120';
+ let detected = detectParser(lineWithLegacyFmt);
+ assert.strictEqual(lineParserWithLegacyFmt, detected);
+
+ const lineWithIRQInfo =
+ 'systrace.sh-8170 [001] d... 15180.978813: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+ 'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+ 'next_prio=120';
+ detected = detectParser(lineWithIRQInfo);
+ assert.strictEqual(lineParserWithIRQInfo, detected);
+
+ const lineWithTGID =
+ 'systrace.sh-8170 (54321) [001] d... 15180.978813: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+ 'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+ 'next_prio=120';
+ detected = detectParser(lineWithTGID);
+ assert.strictEqual(lineParserWithTGID, detected);
+ });
+
+ test('traceEventClockSyncRE', function() {
+ const re = FTraceImporterTestExports.traceEventClockSyncRE;
+ let x = re.exec('trace_event_clock_sync: parent_ts=19581477508');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], '19581477508');
+
+ x = re.exec('trace_event_clock_sync: parent_ts=123.456');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], '123.456');
+ });
+
+ test('genericClockSync', function() {
+ const lines = [
+ '# tracer: nop',
+ '#',
+ '# TASK-PID CPU# TIMESTAMP FUNCTION',
+ '# | | | | |',
+ 'sh-26121 [000] ...1 15.050: tracing_mark_write: trace_event_clock_sync: name=battor regulator=8941_smbb_boost' // @suppress longLineCheck
+ ];
+
+ const io = new tr.importer.ImportOptions();
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m, io);
+
+ m.clockSyncManager.addClockSyncMarker(ClockDomainId.BATTOR, 'battor', 50);
+
+ i.importTraces([lines.join('\n')]);
+
+ assert.isFalse(m.hasImportWarnings);
+ // The clock sync happened at 15050 in the ftrace global domain and at 50
+ // in the BattOr domain. This means the ftrace global timestamps need 15000
+ // subtracted from them in order to be on the BattOr timeline.
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(ClockDomainId.BATTOR)(3),
+ 3);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.LINUX_FTRACE_GLOBAL)(15003),
+ 3);
+ });
+
+ test('clockTypeMarker', function() {
+ const lines = [
+ '# tracer: nop',
+ '#',
+ '# clock_type=LINUX_CLOCK_MONOTONIC',
+ '# TASK-PID CPU# TIMESTAMP FUNCTION',
+ '# | | | | |',
+ '<0>-0 (-----) [001] ...1 15.050: tracing_mark_write: ' +
+ 'trace_event_clock_sync: name=some_sync_id'
+ ];
+
+ const io = new tr.importer.ImportOptions();
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m, io);
+
+ m.clockSyncManager.addClockSyncMarker(
+ ClockDomainId.LINUX_FTRACE_GLOBAL, 'some_sync_id', 50);
+
+ i.importTraces([lines.join('\n')]);
+
+ assert.isFalse(m.hasImportWarnings);
+ // The clock sync happened at 15050 in the linux monotonic domain and at 50
+ // in the ftrace global domain. This means the ftrace global timestamps need
+ // 15000 added to them in order to be on the linux monotonic timeline.
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.LINUX_FTRACE_GLOBAL)(3),
+ 15003);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC)(3),
+ 3);
+ });
+
+ test('canImport', function() {
+ let lines = [
+ '# tracer: nop',
+ '#',
+ '# TASK-PID CPU# TIMESTAMP FUNCTION',
+ '# | | | | |',
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 prev_state=S ' +
+ '==> next_comm=kworker/u:2 next_pid=2844 next_prio=120',
+
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 prev_state=S ' +
+ '==> next_comm=swapper next_pid=0 next_prio=120',
+
+ ' <idle>-0 [001] 4467.844208: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=kworker/u:2 next_pid=2844 next_prio=120'
+ ];
+ assert.isTrue(FTraceImporter.canImport(lines.join('\n')));
+
+ lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112'
+ ];
+ assert.isTrue(FTraceImporter.canImport(lines.join('\n')));
+
+ lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120'
+ ];
+ assert.isTrue(FTraceImporter.canImport(lines.join('\n')));
+
+ lines = [
+ 'SomeRandomText',
+ 'More random text'
+ ];
+ assert.isFalse(FTraceImporter.canImport(lines.join('\n')));
+ });
+
+ test('canImport34AndLater', function() {
+ let lines = [
+ '# tracer: nop',
+ '#',
+ '# entries-in-buffer/entries-written: 55191/55191 #P:2',
+ '#',
+ '# _-----=> irqs-off',
+ '# / _----=> need-resched',
+ '# | / _---=> hardirq/softirq',
+ '# || / _--=> preempt-depth',
+ '# ||| / delay',
+ '# TASK-PID CPU# |||| TIMESTAMP FUNCTION',
+ '# | | | |||| | |',
+ ' systrace.sh-5441 [001] d... 1031.091570: sched_wakeup: ' +
+ 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000',
+ ' systrace.sh-5441 [001] d... 1031.091584: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=5441 prev_prio=120 prev_state=x ' +
+ '==> next_comm=chrome next_pid=5418 next_prio=120'
+ ];
+ assert.isTrue(FTraceImporter.canImport(lines.join('\n')));
+
+ lines = [
+ ' systrace.sh-5441 [001] d... 1031.091570: sched_wakeup: ' +
+ 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000',
+ ' systrace.sh-5441 [001] d... 1031.091584: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=5441 prev_prio=120 prev_state=x ' +
+ '==> next_comm=chrome next_pid=5418 next_prio=120'
+ ];
+ assert.isTrue(FTraceImporter.canImport(lines.join('\n')));
+ });
+
+ test('importOneSequence', function() {
+ const lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120',
+
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=swapper next_pid=0 next_prio=120'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c = m.kernel.cpus[1];
+ assert.strictEqual(c.slices.length, 2);
+
+ assert.strictEqual(c.slices[0].title, 'SurfaceFlinger');
+ assert.strictEqual(c.slices[0].start, 4467843.475);
+ assert.closeTo(.536 - .475, c.slices[0].duration, 1e-5);
+ });
+
+ test('importOneSequenceWithSpacyThreadName', function() {
+ const lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=Surface Flinger next_pid=178 next_prio=112',
+
+ 'Surface Flinger -178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=Surface Flinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120',
+
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=swapper next_pid=0 next_prio=120'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c = m.kernel.cpus[1];
+ assert.strictEqual(c.slices.length, 2);
+
+ assert.strictEqual(c.slices[0].title, 'Surface Flinger ');
+ assert.strictEqual(c.slices[0].start, 4467843.475);
+ assert.closeTo(.536 - .475, c.slices[0].duration, 1e-5);
+ });
+
+ test('importWithNewline', function() {
+ const lines = [
+ ''
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+ });
+
+ test('importSystraceHtml', function() {
+ const p = tr.b.getAsync(
+ '/test_data/trivial_systrace.html');
+ return p.then(function(data) {
+ const m = newModel(data);
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.isDefined(m.processes[124]);
+ assert.isDefined(m.processes[360]);
+
+ assert.isDefined(m.processes[124].counters['android.StatusBar']);
+ assert.strictEqual(
+ m.processes[124].counters['android.StatusBar'].numSamples, 1);
+ assert.isDefined(m.processes[124].counters['android.VSYNC']);
+ assert.strictEqual(
+ 2, m.processes[124].counters['android.VSYNC'].numSamples);
+ assert.isDefined(m.processes[360].counters['android.iq']);
+ assert.strictEqual(
+ 1, m.processes[360].counters['android.iq'].numSamples);
+ }, function(err) {
+ throw err;
+ });
+ });
+
+ test('importMultiTraceHtml', function() {
+ const lines = [
+ '<!DoCTYPE hTml>', // check must be case insensitive
+ '<body>',
+ ' <div class="view">',
+ ' <\/div>',
+ ' <script class="trace-data" type="application/text">',
+ 'test1',
+ 'test2',
+ ' <\/script>',
+ ' <script class="trace-data" type="application/text">',
+ '# tracer: nop',
+ '#',
+ '# TASK-PID CPU# TIMESTAMP FUNCTION',
+ '# | | | | |',
+ ' hwc_eventmon-336 [000] 50260.929925: 0: C|124|VSYNC|1',
+ ' Binder_1-340 [000] 50260.935656: 0: C|124|StatusBar|1',
+ ' hwc_eventmon-336 [000] 50260.946573: 0: C|124|VSYNC|0',
+ ' InputReader-419 [000] 50262.538578: 0: C|360|iq|1',
+ ' <\/script>',
+ '<\/body>',
+ '<\/html>'
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.isDefined(m.processes[124]);
+ assert.isDefined(m.processes[360]);
+
+ assert.isDefined(m.processes[124].counters['android.StatusBar']);
+ assert.strictEqual(
+ m.processes[124].counters['android.StatusBar'].numSamples, 1);
+ assert.isDefined(m.processes[124].counters['android.VSYNC']);
+ assert.strictEqual(
+ 2, m.processes[124].counters['android.VSYNC'].numSamples);
+ assert.isDefined(m.processes[360].counters['android.iq']);
+ assert.strictEqual(
+ 1, m.processes[360].counters['android.iq'].numSamples);
+ });
+
+ test('importTraceStream', function() {
+ const lines = [
+ '<!DoCTYPE hTml>', // check must be case insensitive
+ '<body>',
+ ' <div class="view">',
+ ' <\/div>',
+ ' <script class="trace-data" type="application/text">',
+ 'test1',
+ 'test2',
+ ' <\/script>',
+ ' <script class="trace-data" type="application/text">',
+ '# tracer: nop',
+ '#',
+ '# TASK-PID CPU# TIMESTAMP FUNCTION',
+ '# | | | | |',
+ ' hwc_eventmon-336 [000] 50260.929925: 0: C|124|VSYNC|1',
+ ' Binder_1-340 [000] 50260.935656: 0: C|124|StatusBar|1',
+ ' hwc_eventmon-336 [000] 50260.946573: 0: C|124|VSYNC|0',
+ ' InputReader-419 [000] 50262.538578: 0: C|360|iq|1',
+ ' <\/script>',
+ '<\/body>',
+ '<\/html>'
+ ];
+ const inputStr = lines.join('\n');
+ const buffer = new ArrayBuffer(inputStr.length);
+ const bufferView = new Uint8Array(buffer);
+ for (let i = 0; i < bufferView.length; i++) {
+ bufferView[i] = inputStr.charCodeAt(i);
+ }
+
+ const m = newModel(new tr.b.InMemoryTraceStream(bufferView));
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.isDefined(m.processes[124]);
+ assert.isDefined(m.processes[360]);
+
+ assert.isDefined(m.processes[124].counters['android.StatusBar']);
+ assert.strictEqual(
+ m.processes[124].counters['android.StatusBar'].numSamples, 1);
+ assert.isDefined(m.processes[124].counters['android.VSYNC']);
+ assert.strictEqual(
+ 2, m.processes[124].counters['android.VSYNC'].numSamples);
+ assert.isDefined(m.processes[360].counters['android.iq']);
+ assert.strictEqual(1, m.processes[360].counters['android.iq'].numSamples);
+ });
+
+ test('clockSync', function() {
+ const lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ' +
+ '==> next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120',
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=swapper next_pid=0 ' +
+ 'next_prio=120',
+ ' kworker/u:2-2844 [001] 4467.843000: 0: ' +
+ 'trace_event_clock_sync: parent_ts=0.1'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c = m.kernel.cpus[1];
+ assert.strictEqual(c.slices.length, 2);
+
+ assert.closeTo(
+ (467.843475 - (467.843 - 0.1)) * 1000,
+ c.slices[0].start,
+ 1e-5);
+ });
+
+ test('clockSyncMarkWrite', function() {
+ const lines = [
+ 'systrace.sh-8170 [001] 15180.978813: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+ 'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+ 'next_prio=120',
+ ' kworker/1:0-7873 [001] 15180.978836: sched_switch: ' +
+ 'prev_comm=kworker/1:0 prev_pid=7873 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=debugd next_pid=4404 next_prio=120',
+ ' debugd-4404 [001] 15180.979010: sched_switch: prev_comm=debugd ' +
+ 'prev_pid=4404 prev_prio=120 prev_state=S ==> ' +
+ 'next_comm=dbus-daemon next_pid=510 next_prio=120',
+ 'systrace.sh-8182 [000] 15186.203900: tracing_mark_write: ' +
+ 'trace_event_clock_sync: parent_ts=0'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c = m.kernel.cpus[1];
+ assert.strictEqual(c.slices.length, 2);
+
+ assert.closeTo((15180.978813 - 0) * 1000, c.slices[0].start, 1e-5);
+
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ tr.model.ClockDomainId.LINUX_FTRACE_GLOBAL)(100),
+ 100);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ tr.model.ClockDomainId.LINUX_CLOCK_MONOTONIC)(100),
+ 100);
+ });
+
+ test('clockSyncMarkWriteSecondIgnored', function() {
+ const lines = [
+ 'systrace.sh-8182 [000] 15186.203900: tracing_mark_write: ' +
+ 'trace_event_clock_sync: parent_ts=0',
+ 'systrace.sh-8182 [000] 15187.203900: tracing_mark_write: ' +
+ 'trace_event_clock_sync: parent_ts=0'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ tr.model.ClockDomainId.LINUX_FTRACE_GLOBAL)(100),
+ 100);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ tr.model.ClockDomainId.LINUX_CLOCK_MONOTONIC)(100),
+ 100);
+ });
+
+ test('tracingMarkWriteEOLCleanup', function() {
+ const lines = [
+ 'systrace.sh-8182 [001] ...1 2068001.677892: tracing_mark_write: ' +
+ 'B|9304|test\\n\\',
+ 'systrace.sh-8182 [002] ...1 2068991.686415: tracing_mark_write: E\\n\\'
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c = m.processes[9304].threads[8182].sliceGroup;
+ assert.strictEqual(c.slices.length, 1);
+
+ assert.closeTo((2068001.677892 - 0) * 1000, c.slices[0].start, 1e-5);
+ assert.closeTo(
+ (2068991.686415 - 2068001.677892) * 1000,
+ c.slices[0].duration,
+ 1e-5);
+ });
+
+ test('cpuCount', function() {
+ const lines = [
+ 'systrace.sh-8170 [001] 15180.978813: sched_switch: ' +
+ 'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+ 'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+ 'next_prio=120',
+ ' kworker/1:0-7873 [001] 15180.978836: sched_switch: ' +
+ 'prev_comm=kworker/1:0 prev_pid=7873 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=debugd next_pid=4404 next_prio=120',
+ ' debugd-4404 [000] 15180.979010: sched_switch: prev_comm=debugd ' +
+ 'prev_pid=4404 prev_prio=120 prev_state=S ==> ' +
+ 'next_comm=dbus-daemon next_pid=510 next_prio=120'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.lengthOf(Object.keys(m.kernel.cpus), 2);
+ assert.strictEqual(m.kernel.bestGuessAtCpuCount, 2);
+ });
+
+ test('noOverlap', function() {
+ const lines = [
+ '# tracer: nop',
+ '#',
+ '# entries-in-buffer/entries-written: 10/10 #P:1',
+ '#',
+ '# _-----=> irqs-off',
+ '# / _----=> need-resched',
+ '# | / _---=> hardirq/softirq',
+ '# || / _--=> preempt-depth',
+ '# ||| / delay',
+ '# TASK-PID CPU# |||| TIMESTAMP FUNCTION',
+ '# | | | |||| | |',
+ ' <idle>-0 [000] d..3 49.000000: sched_switch: ' +
+ 'prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=S ' +
+ '==> next_comm=Unity next_pid=29677 next_prio=120',
+ ' <...>-29677 [000] d..3 49.001000: sched_switch: ' +
+ 'prev_comm=Unity prev_pid=29677 prev_prio=120 prev_state=R+ ' +
+ '==> next_comm=swapper/3 next_pid=0 next_prio=120',
+ ' <...>-29678 [000] d..4 49.002000: sched_wakeup: ' +
+ 'comm=Unity pid=29677 prio=120 success=1 target_cpu=000',
+ ' <idle>-0 [000] d..3 49.003000: sched_switch: ' +
+ 'prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=R ' +
+ '==> next_comm=Unity next_pid=29677 next_prio=120',
+ ' <...>-29677 [000] ...1 49.004000: tracing_mark_write: ' +
+ 'B|7244|eglSwapBuffersWithDamageKHR',
+ ' <...>-29677 [000] d..3 49.005000: sched_switch: ' +
+ 'prev_comm=Unity prev_pid=29677 prev_prio=120 prev_state=S ' +
+ '==> next_comm=swapper/3 next_pid=0 next_prio=120',
+ ' <...>-2599 [000] d.h4 49.006000: sched_wakeup: ' +
+ 'comm=Unity pid=29677 prio=120 success=1 target_cpu=000',
+ ' <idle>-0 [000] d..3 49.007000: sched_switch: ' +
+ 'prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=R ' +
+ '==> next_comm=Unity next_pid=29677 next_prio=120',
+ ' <...>-29677 [000] ...1 49.008000: tracing_mark_write: ' +
+ 'B|7244|queueBuffer',
+ ' <...>-29677 [000] d..3 49.009000: sched_switch: ' +
+ 'prev_comm=Unity prev_pid=29677 prev_prio=120 prev_state=R ' +
+ '==> next_comm=swapper/3 next_pid=0 next_prio=120'
+ ];
+ const m = newModel(lines.join('\n'));
+ const thread = m.getAllThreads()[0];
+ let previousEnd = 0;
+ for (const slice of thread.timeSlices) {
+ assert.isTrue(slice.end >= slice.start);
+ assert.isTrue(slice.start >= previousEnd);
+ previousEnd = slice.end;
+ }
+ });
+
+ test('cpuSliceNaming', function() {
+ // Invoke the binder parser first on a thread(1002) in the process that is
+ // not the main thread(1001) and then use sched events to create a cpu
+ // slice.
+ const lines = [
+ 'bar-1002 (1001) [000] ...1 1.000000: binder_transaction_received: ' +
+ 'transaction=612345 ',
+ 'dont_care-2000 (2000) [001] d..3 2.000000: sched_switch: ' +
+ 'prev_comm=dont_care prev_pid=2000 prev_prio=100 prev_state=D ' +
+ '==> next_comm=foo next_pid=1001 next_prio=100',
+ 'foo-1001 (1001) [001] d..3 3.000000: sched_switch: ' +
+ 'prev_comm=foo prev_pid=1001 prev_prio=100 prev_state=S ' +
+ '==> next_comm=doesnt_matter next_pid=3000 next_prio=100'
+ ];
+ const m = newModel(lines.join('\n'));
+ const slice = m.kernel.getOrCreateCpu(1).slices[0];
+ assert.strictEqual(slice.title, 'foo');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser.html
new file mode 100644
index 00000000000..e06cfbe9ef4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses gesture events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses trace events generated by gesture library for touchpad.
+ * @constructor
+ */
+ 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,
+
+ /**
+ * Parse events generate by gesture library.
+ * gestureOpenSlice and gestureCloseSlice are two common
+ * functions to store the begin time and end time for all
+ * events in gesture library
+ */
+ 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);
+ }
+ }
+ },
+
+ /**
+ * For log events, events will come in pairs with a tag log:
+ * like this:
+ * tracing_mark_write: log: start: TimerLogOutputs
+ * tracing_mark_write: log: end: TimerLogOutputs
+ * which represent the start and the end time of certain log behavior
+ * Take these logs above for example, they are the start and end time
+ * of logging Output for HandleTimer function
+ */
+ 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);
+ }
+ return true;
+ },
+
+ /**
+ * For SyncInterpret events, events will come in pairs with
+ * a tag SyncInterpret:
+ * like this:
+ * tracing_mark_write: SyncInterpret: start: ClickWiggleFilterInterpreter
+ * tracing_mark_write: SyncInterpret: end: ClickWiggleFilterInterpreter
+ * which represent the start and the end time of SyncInterpret function
+ * inside the certain interpreter in the gesture library.
+ * Take the logs above for example, they are the start and end time
+ * of the SyncInterpret function inside ClickWiggleFilterInterpreter
+ */
+ syncEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const innerEvent = /^\s*(\w+):\s*(\w+)$/.exec(eventBase.details);
+ switch (innerEvent[1]) {
+ case 'start':
+ this.gestureOpenSlice('SyncInterpret', ts,
+ {interpreter: innerEvent[2]});
+ break;
+ case 'end':
+ this.gestureCloseSlice('SyncInterpret', ts);
+ }
+ return true;
+ },
+
+ /**
+ * For HandleTimer events, events will come in pairs with
+ * a tag HandleTimer:
+ * like this:
+ * tracing_mark_write: HandleTimer: start: LookaheadFilterInterpreter
+ * tracing_mark_write: HandleTimer: end: LookaheadFilterInterpreter
+ * which represent the start and the end time of HandleTimer function
+ * inside the certain interpreter in the gesture library.
+ * Take the logs above for example, they are the start and end time
+ * of the HandleTimer function inside LookaheadFilterInterpreter
+ */
+ timerEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const innerEvent = /^\s*(\w+):\s*(\w+)$/.exec(eventBase.details);
+ switch (innerEvent[1]) {
+ case 'start':
+ this.gestureOpenSlice('HandleTimer', ts,
+ {interpreter: innerEvent[2]});
+ break;
+ case 'end':
+ this.gestureCloseSlice('HandleTimer', ts);
+ }
+ return true;
+ }
+ };
+
+ Parser.register(GestureParser);
+
+ return {
+ GestureParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser_test.html
new file mode 100644
index 00000000000..95f5cb5a54b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/gesture_parser_test.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('gestureImport', function() {
+ const lines = [
+ '<...>-1837 [000] ...1 875292.741648: tracing_mark_write: ' +
+ 'log: start: TimerLogOutputs', // 0
+ '<...>-1837 [000] ...1 875292.741651: tracing_mark_write: ' +
+ 'log: end: TimerLogOutputs',
+ '<...>-1837 [000] ...1 875292.742796: tracing_mark_write: ' +
+ 'log: start: LogTimerCallback',
+ '<...>-1837 [000] ...1 875292.742802: tracing_mark_write: ' +
+ 'log: end: LogTimerCallback',
+ '<...>-1837 [000] ...1 875292.742805: tracing_mark_write: ' +
+ 'HandleTimer: start: LoggingFilterInterpreter', // 2
+ '<...>-1837 [000] ...1 875292.742809: tracing_mark_write: ' +
+ 'HandleTimer: start: AppleTrackpadFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742814: tracing_mark_write: ' +
+ 'HandleTimer: start: Cr48ProfileSensorFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742818: tracing_mark_write: ' +
+ 'HandleTimer: start: T5R2CorrectingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742822: tracing_mark_write: ' +
+ 'HandleTimer: start: StuckButtonInhibitorFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742825: tracing_mark_write: ' +
+ 'HandleTimer: start: IntegralGestureFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742829: tracing_mark_write: ' +
+ 'HandleTimer: start: ScalingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742833: tracing_mark_write: ' +
+ 'HandleTimer: start: SplitCorrectingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742836: tracing_mark_write: ' +
+ 'HandleTimer: start: AccelFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742840: tracing_mark_write: ' +
+ 'HandleTimer: start: SensorJumpFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742843: tracing_mark_write: ' +
+ 'HandleTimer: start: BoxFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742846: tracing_mark_write: ' +
+ 'HandleTimer: start: LookaheadFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742853: tracing_mark_write: ' +
+ 'SyncInterpret: start: IirFilterInterpreter', // 14
+ '<...>-1837 [000] ...1 875292.742861: tracing_mark_write: ' +
+ 'SyncInterpret: start: PalmClassifyingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742872: tracing_mark_write: ' +
+ 'SyncInterpret: start: ClickWiggleFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742881: tracing_mark_write: ' +
+ 'SyncInterpret: start: FlingStopFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742887: tracing_mark_write: ' +
+ 'SyncInterpret: start: ImmediateInterpreter',
+ '<...>-1837 [000] ...1 875292.742906: tracing_mark_write: ' +
+ 'SyncInterpret: end: ImmediateInterpreter',
+ '<...>-1837 [000] ...1 875292.742910: tracing_mark_write: ' +
+ 'SyncInterpret: end: FlingStopFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742914: tracing_mark_write: ' +
+ 'SyncInterpret: end: ClickWiggleFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742917: tracing_mark_write: ' +
+ 'SyncInterpret: end: PalmClassifyingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742921: tracing_mark_write: ' +
+ 'SyncInterpret: end: IirFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742926: tracing_mark_write: ' +
+ 'HandleTimer: end: LookaheadFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742929: tracing_mark_write: ' +
+ 'HandleTimer: end: BoxFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742932: tracing_mark_write: ' +
+ 'HandleTimer: end: SensorJumpFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742937: tracing_mark_write: ' +
+ 'HandleTimer: end: AccelFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742940: tracing_mark_write: ' +
+ 'HandleTimer: end: SplitCorrectingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742944: tracing_mark_write: ' +
+ 'HandleTimer: end: ScalingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742949: tracing_mark_write: ' +
+ 'HandleTimer: end: IntegralGestureFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742952: tracing_mark_write: ' +
+ 'HandleTimer: end: StuckButtonInhibitorFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742956: tracing_mark_write: ' +
+ 'HandleTimer: end: T5R2CorrectingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742959: tracing_mark_write: ' +
+ 'HandleTimer: end: Cr48ProfileSensorFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742962: tracing_mark_write: ' +
+ 'HandleTimer: end: AppleTrackpadFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742966: tracing_mark_write: ' +
+ 'HandleTimer: end: LoggingFilterInterpreter',
+ '<...>-1837 [000] ...1 875292.742969: tracing_mark_write: ' +
+ 'log: start: TimerLogOutputs',
+ '<...>-1837 [000] ...1 875292.742973: tracing_mark_write: ' +
+ 'log: end: TimerLogOutputs',
+ '<...>-1837 [000] ...1 875292.795219: tracing_mark_write: ' +
+ 'log: start: LogHardwareState',
+ '<...>-1837 [000] ...1 875292.795231: tracing_mark_write: ' +
+ 'log: end: LogHardwareState'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const gestureThread = threads[0];
+ assert.strictEqual(gestureThread.name, 'gesture');
+ assert.strictEqual(gestureThread.sliceGroup.length, 21);
+ assert.strictEqual('touchpad_gesture',
+ gestureThread.sliceGroup.slices[0].category);
+ assert.strictEqual('GestureLog',
+ gestureThread.sliceGroup.slices[0].title);
+ assert.strictEqual('touchpad_gesture',
+ gestureThread.sliceGroup.slices[2].category);
+ assert.strictEqual('HandleTimer',
+ gestureThread.sliceGroup.slices[2].title);
+ assert.strictEqual('touchpad_gesture',
+ gestureThread.sliceGroup.slices[14].category);
+ assert.strictEqual('SyncInterpret',
+ gestureThread.sliceGroup.slices[14].title);
+ });
+
+ test('unusualStart', function() {
+ const lines = [
+ 'X-30368 [000] ...1 1819362.481867: tracing_mark_write: ' +
+ 'SyncInterpret: start: IirFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481881: tracing_mark_write: ' +
+ 'SyncInterpret: start: PalmClassifyingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481894: tracing_mark_write: ' +
+ 'SyncInterpret: start: ClickWiggleFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481905: tracing_mark_write: ' +
+ 'SyncInterpret: start: FlingStopFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481912: tracing_mark_write: ' +
+ 'SyncInterpret: start: ImmediateInterpreter',
+ 'X-30368 [000] ...1 1819362.481933: tracing_mark_write: ' +
+ 'SyncInterpret: end: ImmediateInterpreter',
+ 'X-30368 [000] ...1 1819362.481938: tracing_mark_write: ' +
+ 'SyncInterpret: end: FlingStopFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481943: tracing_mark_write: ' +
+ 'SyncInterpret: end: ClickWiggleFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481947: tracing_mark_write: ' +
+ 'SyncInterpret: end: PalmClassifyingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481952: tracing_mark_write: ' +
+ 'SyncInterpret: end: IirFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481958: tracing_mark_write: ' +
+ 'HandleTimer: end: LookaheadFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481962: tracing_mark_write: ' +
+ 'HandleTimer: end: BoxFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481967: tracing_mark_write: ' +
+ 'HandleTimer: end: SensorJumpFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481973: tracing_mark_write: ' +
+ 'HandleTimer: end: AccelFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481977: tracing_mark_write: ' +
+ 'HandleTimer: end: SplitCorrectingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481982: tracing_mark_write: ' +
+ 'HandleTimer: end: ScalingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481988: tracing_mark_write: ' +
+ 'HandleTimer: end: IntegralGestureFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481993: tracing_mark_write: ' +
+ 'HandleTimer: end: StuckButtonInhibitorFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481998: tracing_mark_write: ' +
+ 'HandleTimer: end: T5R2CorrectingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.482033: tracing_mark_write: ' +
+ 'HandleTimer: end: Cr48ProfileSensorFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.482038: tracing_mark_write: ' +
+ 'HandleTimer: end: AppleTrackpadFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.482043: tracing_mark_write: ' +
+ 'HandleTimer: end: LoggingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.482047: tracing_mark_write: ' +
+ 'log: start: TimerLogOutputs',
+ 'X-30368 [000] ...1 1819362.482053: tracing_mark_write: ' +
+ 'log: end: TimerLogOutputs'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+ });
+
+ test('importError', function() {
+ const lines = [
+ 'X-30368 [000] ...1 1819362.481912: tracing_mark_write: ' +
+ 'SyncInterpret: start: ImmediateInterpreter',
+ 'X-30368 [000] ...1 1819362.481958: tracing_mark_write: ' +
+ 'HandleTimer: end: LookaheadFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481962: tracing_mark_write: ' +
+ 'HandleTimer: end: BoxFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481967: tracing_mark_write: ' +
+ 'HandleTimer: end: SensorJumpFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481973: tracing_mark_write: ' +
+ 'HandleTimer: end: AccelFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481977: tracing_mark_write: ' +
+ 'HandleTimer: end: SplitCorrectingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481982: tracing_mark_write: ' +
+ 'HandleTimer: end: ScalingFilterInterpreter',
+ 'X-30368 [000] ...1 1819362.481988: tracing_mark_write: ' +
+ 'HandleTimer: end: IntegralGestureFilterInterpreter'
+ ];
+ const m = newModel(lines.join('\n'));
+
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(m.importWarnings.length, 7);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser.html
new file mode 100644
index 00000000000..60769de3ad2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses i2c driver events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux i2c trace events.
+ * @constructor
+ */
+ function I2cParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('i2c_write',
+ I2cParser.prototype.i2cWriteEvent.bind(this));
+ importer.registerEventHandler('i2c_read',
+ I2cParser.prototype.i2cReadEvent.bind(this));
+ importer.registerEventHandler('i2c_reply',
+ I2cParser.prototype.i2cReplyEvent.bind(this));
+ importer.registerEventHandler('i2c_result',
+ I2cParser.prototype.i2cResultEvent.bind(this));
+ }
+
+ // Matches the i2c_write and i2c_reply records
+ const i2cWriteReplyRE = new RegExp(
+ 'i2c-(\\d+) #(\\d+) a=([\\da-fA-F]+) f=([\\da-fA-F]+) l=(\\d+) ' +
+ '(\\[[\\da-fA-F\\-]+\\])');
+ // Matches the i2c_read record
+ const i2cReadRE = /i2c-(\d+) #(\d+) a=([\da-fA-F]+) f=([\da-fA-F]+) l=(\d+)/;
+ // Matches the i2c_result record
+ const i2cResultRE = /i2c-(\d+) n=(\d+) ret=(\d+)/;
+
+ I2cParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses i2c events and sets up state in the importer.
+ */
+ i2cWriteEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = i2cWriteReplyRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const adapterNumber = parseInt(event[1]);
+ const messageNumber = event[2];
+ const address = event[3];
+ const flags = event[4];
+ const dataLength = event[5];
+ const data = event[6];
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'i2c adapter ' + adapterNumber);
+
+ pushLastSliceIfNeeded(thread, event[1], ts);
+
+ thread.lastEntryTitle = 'i2c write';
+ thread.lastEntryTs = ts;
+ thread.lastEntryArgs = {
+ 'Message number': messageNumber,
+ 'Address': address,
+ 'Flags': flags,
+ 'Data Length': dataLength,
+ 'Data': data
+ };
+
+ return true;
+ },
+
+ i2cReadEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = i2cReadRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const adapterNumber = parseInt(event[1]);
+ const messageNumber = event[2];
+ const address = event[3];
+ const flags = event[4];
+ const dataLength = event[5];
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'i2c adapter ' + adapterNumber);
+
+ pushLastSliceIfNeeded(thread, event[1], ts);
+
+ thread.lastEntryTitle = 'i2c read';
+ thread.lastEntryTs = ts;
+ thread.lastEntryArgs = {
+ 'Message number': messageNumber,
+ 'Address': address,
+ 'Flags': flags,
+ 'Data Length': dataLength
+ };
+
+ return true;
+ },
+
+ i2cReplyEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = i2cWriteReplyRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const adapterNumber = parseInt(event[1]);
+ const messageNumber = event[2];
+ const address = event[3];
+ const flags = event[4];
+ const dataLength = event[5];
+ const data = event[6];
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'i2c adapter ' + adapterNumber);
+
+ pushLastSliceIfNeeded(thread, event[1], ts);
+
+ thread.lastEntryTitle = 'i2c reply';
+ thread.lastEntryTs = ts;
+ thread.lastEntryArgs = {
+ 'Message number': messageNumber,
+ 'Address': address,
+ 'Flags': flags,
+ 'Data Length': dataLength,
+ 'Data': data
+ };
+
+ return true;
+ },
+
+ i2cResultEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = i2cResultRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const adapterNumber = parseInt(event[1]);
+ const numMessages = event[2];
+ const ret = event[3];
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'i2c adapter ' + adapterNumber);
+
+ const args = thread.lastEntryArgs;
+ if (args !== undefined) {
+ args['Number of messages'] = numMessages;
+ args.Return = ret;
+ }
+
+ pushLastSliceIfNeeded(thread, event[1], ts);
+
+ thread.lastEntryTitle = undefined;
+ thread.lastEntryTs = undefined;
+ thread.lastEntryArgs = undefined;
+
+ return true;
+ },
+ };
+
+ function pushLastSliceIfNeeded(thread, id, currentTs) {
+ if (thread.lastEntryTs !== undefined) {
+ const duration = currentTs - thread.lastEntryTs;
+ const slice = new tr.model.ThreadSlice(
+ '', thread.lastEntryTitle,
+ ColorScheme.getColorIdForGeneralPurposeString(id),
+ thread.lastEntryTs, thread.lastEntryArgs, duration);
+ thread.thread.sliceGroup.pushSlice(slice);
+ }
+ }
+
+ Parser.register(I2cParser);
+
+ return {
+ I2cParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser_test.html
new file mode 100644
index 00000000000..da031719eb1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i2c_parser_test.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('i2cImport', function() {
+ const lines = [
+
+ ' /system/bin/surfaceflinger-516 ( 516) [002] ...1 6481.291425: ' +
+ 'i2c_write: i2c-3 #0 a=020 f=0000 l=2 [15-00]',
+ ' /system/bin/surfaceflinger-516 ( 516) [002] ...1 6481.291602: ' +
+ 'i2c_result: i2c-3 n=1 ret=1',
+ ' kworker/u8:5-9309 ( 9309) [001] ...1 6481.365821: ' +
+ 'i2c_write: i2c-7 #0 a=01d f=0000 l=1 [00]',
+ ' kworker/u8:5-9309 ( 9309) [001] ...1 6481.365823: ' +
+ 'i2c_read: i2c-7 #1 a=01d f=0001 l=1',
+ ' kworker/u8:5-9309 ( 9309) [001] ...1 6481.366730: ' +
+ 'i2c_reply: i2c-7 #1 a=01d f=0001 l=1 [01]',
+ ' kworker/u8:5-9309 ( 9309) [001] ...1 6481.366732: ' +
+ 'i2c_result: i2c-7 n=2 ret=2'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings,
+ `Expected no import warnings but got ${m.importWarnings}`);
+
+ let threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 2);
+
+ threads = m.findAllThreadsNamed('i2c adapter 3');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+ let slice = threads[0].sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'i2c write');
+ assert.isDefined(slice.args);
+ assert.strictEqual(slice.args['Message number'], '0');
+ assert.strictEqual(slice.args.Address, '020');
+ assert.strictEqual(slice.args.Flags, '0000');
+ assert.strictEqual(slice.args['Data Length'], '2');
+ assert.strictEqual(slice.args.Data, '[15-00]');
+
+ threads = m.findAllThreadsNamed('i2c adapter 7');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 3);
+
+ slice = threads[0].sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'i2c write');
+ assert.strictEqual(slice.args['Message number'], '0');
+ assert.strictEqual(slice.args.Address, '01d');
+ assert.strictEqual(slice.args.Flags, '0000');
+ assert.strictEqual(slice.args['Data Length'], '1');
+ assert.strictEqual(slice.args.Data, '[00]');
+
+ slice = threads[0].sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'i2c read');
+ assert.strictEqual(slice.args['Message number'], '1');
+ assert.strictEqual(slice.args.Address, '01d');
+ assert.strictEqual(slice.args.Flags, '0001');
+ assert.strictEqual(slice.args['Data Length'], '1');
+
+ slice = threads[0].sliceGroup.slices[2];
+ assert.strictEqual(slice.title, 'i2c reply');
+ assert.strictEqual(slice.args['Message number'], '1');
+ assert.strictEqual(slice.args.Address, '01d');
+ assert.strictEqual(slice.args.Flags, '0001');
+ assert.strictEqual(slice.args['Data Length'], '1');
+ assert.strictEqual(slice.args.Data, '[01]');
+ assert.strictEqual(slice.args['Number of messages'], '2');
+ assert.strictEqual(slice.args.Return, '2');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser.html
new file mode 100644
index 00000000000..3862b351652
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser.html
@@ -0,0 +1,363 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses i915 driver events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux i915 trace events.
+ * @constructor
+ */
+ 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',
+ // NB: reuse destroy handler
+ 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) {
+ // use i915_flip_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);
+ },
+
+ /**
+ * Parses i915 driver events and sets up state in the importer.
+ */
+ 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) {
+ // TODO(sleffler) mappable
+ 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) {
+ // TODO(sleffler) writable
+ 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,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser_test.html
new file mode 100644
index 00000000000..e3564493b96
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/i915_parser_test.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('i915Import', function() {
+ const lines = [
+ // NB: spliced from different traces; mismatched timestamps don't matter
+ ' chrome-1223 [000] 2784.773556: i915_gem_object_pwrite: ' +
+ 'obj=ffff88013f13fc00, offset=0, len=2984',
+ ' chrome-1539 [000] 18420.677750: ' +
+ 'i915_gem_object_change_domain: ' +
+ 'obj=ffff8800a88d1400, read=44=>40, write=00=>40',
+ ' chrome-1539 [000] 18420.677759: i915_gem_object_fault: ' +
+ 'obj=ffff8800a88d1400, GTT index=0 , writable',
+ ' X-964 [000] 2784.774864: i915_flip_request: ' +
+ 'plane=0, obj=ffff88013f0b9a00',
+ ' <idle>-0 [000] 2784.788644: i915_flip_complete: ' +
+ 'plane=0, obj=ffff88013f0b9a00',
+ ' chrome-1539 [001] 18420.681687: i915_gem_request_retire: ' +
+ 'dev=0, ring=1, seqno=1178152',
+ ' chrome-1539 [000] 18422.955688: i915_gem_request_add: ' +
+ 'dev=0, ring=1, seqno=1178364',
+ ' cat-21833 [000] 18422.956832: i915_gem_request_complete: ' +
+ 'dev=0, ring=1, seqno=1178364',
+ ' X-1012 [001] 18420.682511: i915_gem_request_wait_begin: ' +
+ 'dev=0, ring=4, seqno=1178156',
+ ' X-1012 [000] 18422.765707: i915_gem_request_wait_end: ' +
+ 'dev=0, ring=4, seqno=1178359',
+ ' chrome-1539 [000] 18422.955655: i915_gem_ring_flush: ' +
+ 'dev=0, ring=1, invalidate=001e, flush=0040',
+ ' chrome-1539 [000] 18422.955660: i915_gem_ring_dispatch: ' +
+ 'dev=0, ring=1, seqno=1178364',
+ ' chrome-1539 [000] 18420.677772: i915_reg_rw: ' +
+ 'write reg=0x100030, len=8, val=(0xfca9001, 0xfce8007)',
+ ' kworker/u16:2-13998 [005] 1577664.436065: ' +
+ 'intel_gpu_freq_change: new_freq=350'
+
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ let i915GemThread = undefined;
+ let i915FlipThread = undefined;
+ let i915GemRingThread = undefined;
+ let i915RegThread = undefined;
+ let i915GpuFreqThread = undefined;
+ m.getAllThreads().forEach(function(t) {
+ switch (t.name) {
+ case 'i915_gem':
+ i915GemThread = t;
+ break;
+ case 'i915_flip':
+ i915FlipThread = t;
+ break;
+ case 'i915_gem_ring':
+ i915GemRingThread = t;
+ break;
+ case 'i915_reg':
+ i915RegThread = t;
+ break;
+ case 'i915_gpu_freq':
+ i915GpuFreqThread = t;
+ break;
+ default:
+ assert.fail(t, undefined, 'Unexpected thread named ' + t.name);
+ }
+ });
+ assert.isDefined(i915GemThread);
+ assert.isDefined(i915FlipThread);
+ assert.isDefined(i915GemRingThread);
+ assert.isDefined(i915RegThread);
+ assert.isDefined(i915GpuFreqThread);
+
+ assert.strictEqual(i915GemThread.sliceGroup.length, 3);
+
+ assert.strictEqual(i915FlipThread.sliceGroup.length, 1);
+
+ assert.closeTo(
+ 2784.774864 * 1000.0,
+ i915FlipThread.sliceGroup.slices[0].start,
+ 1e-5);
+ assert.closeTo(
+ (2784.788644 - 2784.774864) * 1000.0,
+ i915FlipThread.sliceGroup.slices[0].duration,
+ 1e-5);
+
+ assert.strictEqual(i915GemRingThread.sliceGroup.length, 7);
+ assert.strictEqual(i915RegThread.sliceGroup.length, 1);
+ assert.strictEqual(i915GpuFreqThread.sliceGroup.length, 1);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser.html
new file mode 100644
index 00000000000..bb08b7b4cc9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses ion heap events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses ion heap trace events.
+ * @constructor
+ */
+ 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 = {};
+
+ // Matches the ion_heap_shrink and ion_heap_grow records
+ const ionHeapRE = new RegExp(
+ 'heap_name=(\\S+), len=(\\d+), total_allocated=(\\d+)');
+
+ TestExports.ionHeapRE = ionHeapRE;
+
+
+ IonHeapParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses ion_heap_shrink events and sets up state in the importer.
+ */
+ 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;
+ },
+
+ /**
+ * Parses ion_heap_grow events and sets up state in the importer.
+ */
+ 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
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser_test.html
new file mode 100644
index 00000000000..3979f56870f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/ion_heap_parser_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('ionHeapRE', function() {
+ const re = tr.e.importer.linux_perf._IonHeapParserTestExports.ionHeapRE;
+ let x = re.exec('ion_heap_shrink: heap_name=system, len=20480, ' +
+ 'total_allocated=181116928');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], 'system');
+ assert.strictEqual(x[2], '20480');
+ assert.strictEqual(x[3], '181116928');
+
+ x = re.exec('ion_heap_grow: heap_name=system, len=17526784, ' +
+ 'total_allocated=181096448');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], 'system');
+ assert.strictEqual(x[2], '17526784');
+ assert.strictEqual(x[3], '181096448');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser.html
new file mode 100644
index 00000000000..0654c9d96be
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses drm driver events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux irq trace events.
+ * @constructor
+ */
+ 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));
+ }
+
+ // Matches the irq_handler_entry record
+ const irqHandlerEntryRE = /irq=(\d+) name=(.+)/;
+
+ // Matches the irq_handler_exit record
+ const irqHandlerExitRE = /irq=(\d+) ret=(.+)/;
+
+ // Matches the softirq_raise record
+ const softirqRE = /vec=(\d+) \[action=(.+)\]/;
+
+ // Matches the ipi_exit_
+ const ipiHandlerExitRE = /\((.+)\)/;
+
+ // Matches the preempt_disable/enable records
+ const preemptirqRE = /caller=(.+) parent=(.+)/;
+
+ IrqParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses irq events and sets up state in the mporter.
+ */
+ 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;
+ },
+ /**
+ * Parses ipi events and sets up state in the mporter.
+ */
+ 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);
+ }
+ thread.lastEntryTs = undefined;
+ return true;
+ },
+
+ /**
+ * Parses preempt disable/enable events and sets up state in the importer.
+ */
+ preemptStartEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = preemptirqRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'preempt cpu ' + cpuNumber);
+ thread.lastEntryTs = ts;
+
+ thread.preemptStartCaller = event[1];
+ thread.preemptStartParent = event[2];
+ return true;
+ },
+
+ preemptEndEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = preemptirqRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'preempt cpu ' + cpuNumber);
+
+ thread.preemptEndCaller = event[1];
+ thread.preemptEndParent = event[2];
+
+ if (thread.lastEntryTs !== undefined) {
+ const duration = ts - thread.lastEntryTs;
+ const slice = new tr.model.ThreadSlice(
+ '',
+ thread.preemptStartParent + ': ' + thread.preemptStartCaller,
+ ColorScheme.getColorIdForGeneralPurposeString(
+ thread.preemptEndCaller),
+ thread.lastEntryTs, {},
+ duration);
+ thread.thread.sliceGroup.pushSlice(slice);
+ }
+ thread.lastEntryTs = undefined;
+ return true;
+ },
+
+ /**
+ * Parses irqoff disable/enable events and sets up state in the importer.
+ */
+ irqoffStartEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = preemptirqRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'irqoff cpu ' + cpuNumber);
+ thread.lastEntryTs = ts;
+
+ thread.irqoffStartCaller = event[1];
+ thread.irqoffStartParent = event[2];
+ return true;
+ },
+
+ irqoffEndEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = preemptirqRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const thread = this.importer.getOrCreatePseudoThread(
+ 'irqoff cpu ' + cpuNumber);
+
+ thread.irqoffEndCaller = event[1];
+ thread.irqoffEndParent = event[2];
+
+ if (thread.lastEntryTs !== undefined) {
+ const duration = ts - thread.lastEntryTs;
+ const slice = new tr.model.ThreadSlice(
+ '',
+ thread.irqoffStartParent + ': ' + thread.irqoffStartCaller,
+ ColorScheme.getColorIdForGeneralPurposeString(
+ thread.irqoffEndCaller),
+ thread.lastEntryTs, {},
+ duration);
+ thread.thread.sliceGroup.pushSlice(slice);
+ }
+ thread.lastEntryTs = undefined;
+ return true;
+ }
+ };
+
+ Parser.register(IrqParser);
+
+ return {
+ IrqParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser_test.html
new file mode 100644
index 00000000000..84080971650
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/irq_parser_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('irqImport', function() {
+ const lines = [
+ ' kworker/u4:1-31907 (31907) [001] d.h3 14063.748288: ' +
+ 'irq_handler_entry: irq=27 name=arch_timer',
+ ' kworker/u4:1-31907 (31907) [001] dNh3 14063.748384: ' +
+ 'irq_handler_exit: irq=27 ret=handled',
+ ' kworker/u4:2-31908 (31908) [000] ..s3 14063.477231: ' +
+ 'softirq_entry: vec=9 [action=RCU]',
+ ' kworker/u4:2-31908 (31908) [000] ..s3 14063.477246: ' +
+ 'softirq_exit: vec=9 [action=RCU]',
+ ' RenderThread-2978 ( 2794) [002] dN.1 62828.421805: ' +
+ 'ipi_entry: (Rescheduling interrupts)',
+ ' RenderThread-2978 ( 2794) [002] dN.1 62828.421809: ' +
+ 'ipi_exit: (Rescheduling interrupts)',
+ ' RenderThread-2978 ( 2794) [002] dN.1 62829.421809: ' +
+ 'preempt_disable: caller=avc_lookup parent=avc_has_perm_noaudit',
+ ' RenderThread-2978 ( 2794) [002] dN.1 62830.421809: ' +
+ 'preempt_enable: caller=avc_lookup parent=avc_has_perm_noaudit',
+ ' kworker/u1:2-31908 (31908) [004] ..s3 14163.477231: ' +
+ 'irq_disable: caller=avc_lookup parent=avc_has_perm_noaudit',
+ ' kworker/u1:2-31908 (31908) [004] ..s3 14163.477246: ' +
+ 'irq_enable: caller=avc_lookup parent=avc_has_perm_noaudit'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ let threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 5);
+
+ threads = m.findAllThreadsNamed('irqs cpu 1');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+
+ threads = m.findAllThreadsNamed('softirq cpu 0');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+
+ threads = m.findAllThreadsNamed('irqs cpu 2');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+
+ threads = m.findAllThreadsNamed('preempt cpu 2');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+
+ threads = m.findAllThreadsNamed('irqoff cpu 4');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser.html
new file mode 100644
index 00000000000..b8f6f9aa915
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses graph_ent and graph_ret events that were inserted by
+ * the Linux kernel's function graph trace.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const LinuxPerfParser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses graph_ent and graph_ret events that were inserted by the Linux
+ * kernel's function graph trace.
+ * @constructor
+ */
+ function KernelFuncParser(importer) {
+ LinuxPerfParser.call(this, importer);
+
+ importer.registerEventHandler('graph_ent',
+ KernelFuncParser.prototype.traceKernelFuncEnterEvent.
+ bind(this));
+ importer.registerEventHandler('graph_ret',
+ KernelFuncParser.prototype.traceKernelFuncReturnEvent.
+ bind(this));
+
+ this.model_ = importer.model_;
+ this.ppids_ = {};
+ }
+
+ const TestExports = {};
+
+ const funcEnterRE = new RegExp('func=(.+)');
+ TestExports.funcEnterRE = funcEnterRE;
+
+ KernelFuncParser.prototype = {
+ __proto__: LinuxPerfParser.prototype,
+
+ traceKernelFuncEnterEvent(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ const eventData = funcEnterRE.exec(eventBase.details);
+ if (!eventData) return false;
+
+ if (eventBase.tgid === undefined) {
+ return false;
+ }
+
+ const tgid = parseInt(eventBase.tgid);
+ const name = eventData[1];
+ 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 slice = slices.beginSlice(null, name, ts, {});
+
+ return true;
+ },
+
+ traceKernelFuncReturnEvent(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ if (eventBase.tgid === undefined) {
+ 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;
+ }
+
+ if (slices.openSliceCount > 0) {
+ slices.endSlice(ts);
+ }
+
+ return true;
+ }
+ };
+
+ LinuxPerfParser.register(KernelFuncParser);
+
+ return {
+ KernelFuncParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser_test.html
new file mode 100644
index 00000000000..16f2a13c934
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/kfunc_parser_test.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('kernelFunctionParser', function() {
+ const lines = [
+ 'Binder_2-127 ( 127) [001] .... 3431.906759: graph_ent: func=sys_write',
+ 'Binder_2-127 ( 127) [001] .... 3431.906769: graph_ret: func=sys_write',
+ 'Binder_2-127 ( 127) [001] .... 3431.906785: graph_ent: func=sys_write',
+ 'Binder_2-127 ( 127) [001] ...1 3431.906798: tracing_mark_write: B|' +
+ '127|dequeueBuffer',
+ 'Binder_2-127 ( 127) [001] .... 3431.906802: graph_ret: func=sys_write',
+ 'Binder_2-127 ( 127) [001] .... 3431.906842: graph_ent: func=sys_write',
+ 'Binder_2-127 ( 127) [001] ...1 3431.906849: tracing_mark_write: E',
+ 'Binder_2-127 ( 127) [001] .... 3431.906853: graph_ret: func=sys_write',
+ 'Binder_2-127 ( 127) [001] .... 3431.906896: graph_ent: func=sys_write',
+ 'Binder_2-127 ( 127) [001] .... 3431.906906: graph_ret: func=sys_write'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ const process = m.processes[127];
+ assert.isDefined(process);
+
+ const thread = process.threads[127];
+ assert.isDefined(thread);
+
+ const slices = thread.sliceGroup.slices;
+ assert.strictEqual(thread.sliceGroup.length, 7);
+
+ // Slice 0 is an un-split sys_write
+ assert.strictEqual(slices[0].title, 'sys_write');
+
+ // Slices 1 & 3 are a split sys_write
+ assert.strictEqual(slices[1].title, 'sys_write');
+ assert.strictEqual(slices[2].title, 'dequeueBuffer');
+ assert.strictEqual(slices[3].title, 'sys_write (cont.)');
+
+ // Slices 4 & 5 are a split sys_write with the dequeueBuffer in between
+ assert.strictEqual(slices[4].title, 'sys_write');
+ assert.strictEqual(slices[5].title, 'sys_write (cont.)');
+
+ // Slice 6 is another un-split sys_write
+ assert.strictEqual(slices[6].title, 'sys_write');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser.html
new file mode 100644
index 00000000000..38c54d176fc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser.html
@@ -0,0 +1,659 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses Mali DDK/kernel events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses Mali DDK/kernel trace events.
+ * @constructor
+ */
+ function MaliParser(importer) {
+ Parser.call(this, importer);
+
+ // kernel DVFS events
+ importer.registerEventHandler('mali_dvfs_event',
+ MaliParser.prototype.dvfsEventEvent.bind(this));
+ importer.registerEventHandler('mali_dvfs_set_clock',
+ MaliParser.prototype.dvfsSetClockEvent.bind(this));
+ importer.registerEventHandler('mali_dvfs_set_voltage',
+ MaliParser.prototype.dvfsSetVoltageEvent.bind(this));
+
+ // kernel Mali hw counter events
+ this.addJMCounter('mali_hwc_MESSAGES_SENT', 'Messages Sent');
+ this.addJMCounter('mali_hwc_MESSAGES_RECEIVED', 'Messages Received');
+ this.addJMCycles('mali_hwc_GPU_ACTIVE', 'GPU Active');
+ this.addJMCycles('mali_hwc_IRQ_ACTIVE', 'IRQ Active');
+
+ for (let i = 0; i < 7; i++) {
+ const jobStr = 'JS' + i;
+ const jobHWCStr = 'mali_hwc_' + jobStr;
+ this.addJMCounter(jobHWCStr + '_JOBS', jobStr + ' Jobs');
+ this.addJMCounter(jobHWCStr + '_TASKS', jobStr + ' Tasks');
+ this.addJMCycles(jobHWCStr + '_ACTIVE', jobStr + ' Active');
+ this.addJMCycles(jobHWCStr + '_WAIT_READ', jobStr + ' Wait Read');
+ this.addJMCycles(jobHWCStr + '_WAIT_ISSUE', jobStr + ' Wait Issue');
+ this.addJMCycles(jobHWCStr + '_WAIT_DEPEND', jobStr + ' Wait Depend');
+ this.addJMCycles(jobHWCStr + '_WAIT_FINISH', jobStr + ' Wait Finish');
+ }
+
+ this.addTilerCounter('mali_hwc_TRIANGLES', 'Triangles');
+ this.addTilerCounter('mali_hwc_QUADS', 'Quads');
+ this.addTilerCounter('mali_hwc_POLYGONS', 'Polygons');
+ this.addTilerCounter('mali_hwc_POINTS', 'Points');
+ this.addTilerCounter('mali_hwc_LINES', 'Lines');
+ this.addTilerCounter('mali_hwc_VCACHE_HIT', 'VCache Hit');
+ this.addTilerCounter('mali_hwc_VCACHE_MISS', 'VCache Miss');
+ this.addTilerCounter('mali_hwc_FRONT_FACING', 'Front Facing');
+ this.addTilerCounter('mali_hwc_BACK_FACING', 'Back Facing');
+ this.addTilerCounter('mali_hwc_PRIM_VISIBLE', 'Prim Visible');
+ this.addTilerCounter('mali_hwc_PRIM_CULLED', 'Prim Culled');
+ this.addTilerCounter('mali_hwc_PRIM_CLIPPED', 'Prim Clipped');
+
+ this.addTilerCounter('mali_hwc_WRBUF_HIT', 'Wrbuf Hit');
+ this.addTilerCounter('mali_hwc_WRBUF_MISS', 'Wrbuf Miss');
+ this.addTilerCounter('mali_hwc_WRBUF_LINE', 'Wrbuf Line');
+ this.addTilerCounter('mali_hwc_WRBUF_PARTIAL', 'Wrbuf Partial');
+ this.addTilerCounter('mali_hwc_WRBUF_STALL', 'Wrbuf Stall');
+
+ this.addTilerCycles('mali_hwc_ACTIVE', 'Tiler Active');
+ this.addTilerCycles('mali_hwc_INDEX_WAIT', 'Index Wait');
+ this.addTilerCycles('mali_hwc_INDEX_RANGE_WAIT', 'Index Range Wait');
+ this.addTilerCycles('mali_hwc_VERTEX_WAIT', 'Vertex Wait');
+ this.addTilerCycles('mali_hwc_PCACHE_WAIT', 'Pcache Wait');
+ this.addTilerCycles('mali_hwc_WRBUF_WAIT', 'Wrbuf Wait');
+ this.addTilerCycles('mali_hwc_BUS_READ', 'Bus Read');
+ this.addTilerCycles('mali_hwc_BUS_WRITE', 'Bus Write');
+
+ this.addTilerCycles('mali_hwc_TILER_UTLB_STALL', 'Tiler UTLB Stall');
+ this.addTilerCycles('mali_hwc_TILER_UTLB_HIT', 'Tiler UTLB Hit');
+
+ this.addFragCycles('mali_hwc_FRAG_ACTIVE', 'Active');
+ /* NB: don't propagate spelling mistakes to labels */
+ this.addFragCounter('mali_hwc_FRAG_PRIMATIVES', 'Primitives');
+ this.addFragCounter('mali_hwc_FRAG_PRIMATIVES_DROPPED',
+ 'Primitives Dropped');
+ this.addFragCycles('mali_hwc_FRAG_CYCLE_DESC', 'Descriptor Processing');
+ this.addFragCycles('mali_hwc_FRAG_CYCLES_PLR', 'PLR Processing??');
+ this.addFragCycles('mali_hwc_FRAG_CYCLES_VERT', 'Vertex Processing');
+ this.addFragCycles('mali_hwc_FRAG_CYCLES_TRISETUP', 'Triangle Setup');
+ this.addFragCycles('mali_hwc_FRAG_CYCLES_RAST', 'Rasterization???');
+ this.addFragCounter('mali_hwc_FRAG_THREADS', 'Threads');
+ this.addFragCounter('mali_hwc_FRAG_DUMMY_THREADS', 'Dummy Threads');
+ this.addFragCounter('mali_hwc_FRAG_QUADS_RAST', 'Quads Rast');
+ this.addFragCounter('mali_hwc_FRAG_QUADS_EZS_TEST', 'Quads EZS Test');
+ this.addFragCounter('mali_hwc_FRAG_QUADS_EZS_KILLED', 'Quads EZS Killed');
+ this.addFragCounter('mali_hwc_FRAG_QUADS_LZS_TEST', 'Quads LZS Test');
+ this.addFragCounter('mali_hwc_FRAG_QUADS_LZS_KILLED', 'Quads LZS Killed');
+ this.addFragCycles('mali_hwc_FRAG_CYCLE_NO_TILE', 'No Tiles');
+ this.addFragCounter('mali_hwc_FRAG_NUM_TILES', 'Tiles');
+ this.addFragCounter('mali_hwc_FRAG_TRANS_ELIM', 'Transactions Eliminated');
+
+ this.addComputeCycles('mali_hwc_COMPUTE_ACTIVE', 'Active');
+ this.addComputeCounter('mali_hwc_COMPUTE_TASKS', 'Tasks');
+ this.addComputeCounter('mali_hwc_COMPUTE_THREADS', 'Threads Started');
+ this.addComputeCycles('mali_hwc_COMPUTE_CYCLES_DESC',
+ 'Waiting for Descriptors');
+
+ this.addTripipeCycles('mali_hwc_TRIPIPE_ACTIVE', 'Active');
+
+ this.addArithCounter('mali_hwc_ARITH_WORDS', 'Instructions (/Pipes)');
+ this.addArithCycles('mali_hwc_ARITH_CYCLES_REG',
+ 'Reg scheduling stalls (/Pipes)');
+ this.addArithCycles('mali_hwc_ARITH_CYCLES_L0',
+ 'L0 cache miss stalls (/Pipes)');
+ this.addArithCounter('mali_hwc_ARITH_FRAG_DEPEND',
+ 'Frag dep check failures (/Pipes)');
+
+ this.addLSCounter('mali_hwc_LS_WORDS', 'Instruction Words Completed');
+ this.addLSCounter('mali_hwc_LS_ISSUES', 'Full Pipeline Issues');
+ this.addLSCounter('mali_hwc_LS_RESTARTS', 'Restarts (unpairable insts)');
+ this.addLSCounter('mali_hwc_LS_REISSUES_MISS',
+ 'Pipeline reissue (cache miss/uTLB)');
+ this.addLSCounter('mali_hwc_LS_REISSUES_VD',
+ 'Pipeline reissue (varying data)');
+ /* TODO(sleffler) fix kernel event typo */
+ this.addLSCounter('mali_hwc_LS_REISSUE_ATTRIB_MISS',
+ 'Pipeline reissue (attribute cache miss)');
+ this.addLSCounter('mali_hwc_LS_REISSUE_NO_WB', 'Writeback not used');
+
+ this.addTexCounter('mali_hwc_TEX_WORDS', 'Words');
+ this.addTexCounter('mali_hwc_TEX_BUBBLES', 'Bubbles');
+ this.addTexCounter('mali_hwc_TEX_WORDS_L0', 'Words L0');
+ this.addTexCounter('mali_hwc_TEX_WORDS_DESC', 'Words Desc');
+ this.addTexCounter('mali_hwc_TEX_THREADS', 'Threads');
+ this.addTexCounter('mali_hwc_TEX_RECIRC_FMISS', 'Recirc due to Full Miss');
+ this.addTexCounter('mali_hwc_TEX_RECIRC_DESC', 'Recirc due to Desc Miss');
+ this.addTexCounter('mali_hwc_TEX_RECIRC_MULTI', 'Recirc due to Multipass');
+ this.addTexCounter('mali_hwc_TEX_RECIRC_PMISS',
+ 'Recirc due to Partial Cache Miss');
+ this.addTexCounter('mali_hwc_TEX_RECIRC_CONF',
+ 'Recirc due to Cache Conflict');
+
+ this.addLSCCounter('mali_hwc_LSC_READ_HITS', 'Read Hits');
+ this.addLSCCounter('mali_hwc_LSC_READ_MISSES', 'Read Misses');
+ this.addLSCCounter('mali_hwc_LSC_WRITE_HITS', 'Write Hits');
+ this.addLSCCounter('mali_hwc_LSC_WRITE_MISSES', 'Write Misses');
+ this.addLSCCounter('mali_hwc_LSC_ATOMIC_HITS', 'Atomic Hits');
+ this.addLSCCounter('mali_hwc_LSC_ATOMIC_MISSES', 'Atomic Misses');
+ this.addLSCCounter('mali_hwc_LSC_LINE_FETCHES', 'Line Fetches');
+ this.addLSCCounter('mali_hwc_LSC_DIRTY_LINE', 'Dirty Lines');
+ this.addLSCCounter('mali_hwc_LSC_SNOOPS', 'Snoops');
+
+ this.addAXICounter('mali_hwc_AXI_TLB_STALL', 'Address channel stall');
+ this.addAXICounter('mali_hwc_AXI_TLB_MISS', 'Cache Miss');
+ this.addAXICounter('mali_hwc_AXI_TLB_TRANSACTION', 'Transactions');
+ this.addAXICounter('mali_hwc_LS_TLB_MISS', 'LS Cache Miss');
+ this.addAXICounter('mali_hwc_LS_TLB_HIT', 'LS Cache Hit');
+ this.addAXICounter('mali_hwc_AXI_BEATS_READ', 'Read Beats');
+ this.addAXICounter('mali_hwc_AXI_BEATS_WRITE', 'Write Beats');
+
+ this.addMMUCounter('mali_hwc_MMU_TABLE_WALK', 'Page Table Walks');
+ this.addMMUCounter('mali_hwc_MMU_REPLAY_MISS',
+ 'Cache Miss from Replay Buffer');
+ this.addMMUCounter('mali_hwc_MMU_REPLAY_FULL', 'Replay Buffer Full');
+ this.addMMUCounter('mali_hwc_MMU_NEW_MISS', 'Cache Miss on New Request');
+ this.addMMUCounter('mali_hwc_MMU_HIT', 'Cache Hit');
+
+ this.addMMUCycles('mali_hwc_UTLB_STALL', 'UTLB Stalled');
+ this.addMMUCycles('mali_hwc_UTLB_REPLAY_MISS', 'UTLB Replay Miss');
+ this.addMMUCycles('mali_hwc_UTLB_REPLAY_FULL', 'UTLB Replay Full');
+ this.addMMUCycles('mali_hwc_UTLB_NEW_MISS', 'UTLB New Miss');
+ this.addMMUCycles('mali_hwc_UTLB_HIT', 'UTLB Hit');
+
+ this.addL2Counter('mali_hwc_L2_READ_BEATS', 'Read Beats');
+ this.addL2Counter('mali_hwc_L2_WRITE_BEATS', 'Write Beats');
+ this.addL2Counter('mali_hwc_L2_ANY_LOOKUP', 'Any Lookup');
+ this.addL2Counter('mali_hwc_L2_READ_LOOKUP', 'Read Lookup');
+ this.addL2Counter('mali_hwc_L2_SREAD_LOOKUP', 'Shareable Read Lookup');
+ this.addL2Counter('mali_hwc_L2_READ_REPLAY', 'Read Replayed');
+ this.addL2Counter('mali_hwc_L2_READ_SNOOP', 'Read Snoop');
+ this.addL2Counter('mali_hwc_L2_READ_HIT', 'Read Cache Hit');
+ this.addL2Counter('mali_hwc_L2_CLEAN_MISS', 'CleanUnique Miss');
+ this.addL2Counter('mali_hwc_L2_WRITE_LOOKUP', 'Write Lookup');
+ this.addL2Counter('mali_hwc_L2_SWRITE_LOOKUP', 'Shareable Write Lookup');
+ this.addL2Counter('mali_hwc_L2_WRITE_REPLAY', 'Write Replayed');
+ this.addL2Counter('mali_hwc_L2_WRITE_SNOOP', 'Write Snoop');
+ this.addL2Counter('mali_hwc_L2_WRITE_HIT', 'Write Cache Hit');
+ this.addL2Counter('mali_hwc_L2_EXT_READ_FULL', 'ExtRD with BIU Full');
+ this.addL2Counter('mali_hwc_L2_EXT_READ_HALF', 'ExtRD with BIU >1/2 Full');
+ this.addL2Counter('mali_hwc_L2_EXT_WRITE_FULL', 'ExtWR with BIU Full');
+ this.addL2Counter('mali_hwc_L2_EXT_WRITE_HALF', 'ExtWR with BIU >1/2 Full');
+
+ this.addL2Counter('mali_hwc_L2_EXT_READ', 'External Read (ExtRD)');
+ this.addL2Counter('mali_hwc_L2_EXT_READ_LINE', 'ExtRD (linefill)');
+ this.addL2Counter('mali_hwc_L2_EXT_WRITE', 'External Write (ExtWR)');
+ this.addL2Counter('mali_hwc_L2_EXT_WRITE_LINE', 'ExtWR (linefill)');
+ this.addL2Counter('mali_hwc_L2_EXT_WRITE_SMALL', 'ExtWR (burst size <64B)');
+ this.addL2Counter('mali_hwc_L2_EXT_BARRIER', 'External Barrier');
+ this.addL2Counter('mali_hwc_L2_EXT_AR_STALL', 'Address Read stalls');
+ this.addL2Counter('mali_hwc_L2_EXT_R_BUF_FULL',
+ 'Response Buffer full stalls');
+ this.addL2Counter('mali_hwc_L2_EXT_RD_BUF_FULL',
+ 'Read Data Buffer full stalls');
+ this.addL2Counter('mali_hwc_L2_EXT_R_RAW', 'RAW hazard stalls');
+ this.addL2Counter('mali_hwc_L2_EXT_W_STALL', 'Write Data stalls');
+ this.addL2Counter('mali_hwc_L2_EXT_W_BUF_FULL', 'Write Data Buffer full');
+ this.addL2Counter('mali_hwc_L2_EXT_R_W_HAZARD', 'WAW or WAR hazard stalls');
+ this.addL2Counter('mali_hwc_L2_TAG_HAZARD', 'Tag hazard replays');
+ this.addL2Cycles('mali_hwc_L2_SNOOP_FULL', 'Snoop buffer full');
+ this.addL2Cycles('mali_hwc_L2_REPLAY_FULL', 'Replay buffer full');
+
+ // DDK events (from X server)
+ importer.registerEventHandler('tracing_mark_write:mali_driver',
+ MaliParser.prototype.maliDDKEvent.bind(this));
+
+ // Mali job scheduler events from Exynos kernels
+ importer.registerEventHandler('mali_job_systrace_event_start',
+ MaliParser.prototype.maliJobEvent.bind(this));
+ importer.registerEventHandler('mali_job_systrace_event_stop',
+ MaliParser.prototype.maliJobEvent.bind(this));
+
+ this.model_ = importer.model_;
+
+ this.deferredJobs_ = {}; // <job type>: [<deferred jobs>]
+ }
+
+ MaliParser.prototype = {
+ __proto__: Parser.prototype,
+
+ maliDDKOpenSlice(pid, tid, ts, func, blockinfo) {
+ const thread = this.importer.model_.getOrCreateProcess(pid)
+ .getOrCreateThread(tid);
+ const funcArgs = /^([\w\d_]*)(?:\(\))?:?\s*(.*)$/.exec(func);
+ thread.sliceGroup.beginSlice('gpu-driver', funcArgs[1], ts,
+ { 'args': funcArgs[2],
+ blockinfo });
+ },
+
+ maliDDKCloseSlice(pid, tid, ts, args, blockinfo) {
+ const thread = this.importer.model_.getOrCreateProcess(pid)
+ .getOrCreateThread(tid);
+ if (!thread.sliceGroup.openSliceCount) {
+ // Discard unmatched ends.
+ return;
+ }
+ thread.sliceGroup.endSlice(ts);
+ },
+
+ /**
+ * Deduce the format of Mali perf events.
+ *
+ * @return {RegExp} the regular expression for parsing data when the format
+ * is recognized; otherwise null.
+ */
+ autoDetectLineRE(line) {
+ // Matches Mali perf events with thread info
+ const lineREWithThread =
+ /^\s*\(([\w\-]*)\)\s*(\w+):\s*([\w\\\/\.\-]*@\d*):?\s*(.*)$/;
+ if (lineREWithThread.test(line)) {
+ return lineREWithThread;
+ }
+
+ // Matches old-style Mali perf events
+ const lineRENoThread = /^s*()(\w+):\s*([\w\\\/.\-]*):?\s*(.*)$/;
+ if (lineRENoThread.test(line)) {
+ return lineRENoThread;
+ }
+ return null;
+ },
+
+ lineRE: null,
+
+ /**
+ * Parses maliDDK events and sets up state in the importer.
+ * events will come in pairs with a cros_trace_print_enter
+ * like this (line broken here for formatting):
+ *
+ * tracing_mark_write: mali_driver: (mali-012345) cros_trace_print_enter: \
+ * gles/src/texture/mali_gles_texture_slave.c@1505: gles2_texturep_upload
+ *
+ * and a cros_trace_print_exit like this:
+ *
+ * tracing_mark_write: mali_driver: (mali-012345) cros_trace_print_exit: \
+ * gles/src/texture/mali_gles_texture_slave.c@1505:
+ */
+ maliDDKEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ if (this.lineRE === null) {
+ this.lineRE = this.autoDetectLineRE(eventBase.details);
+ if (this.lineRE === null) return false;
+ }
+ const maliEvent = this.lineRE.exec(eventBase.details);
+ // Old-style Mali perf events have no thread id, so make one.
+ const tid = (maliEvent[1] === '' ? 'mali' : maliEvent[1]);
+ switch (maliEvent[2]) {
+ case 'cros_trace_print_enter':
+ this.maliDDKOpenSlice(pid, tid, ts, maliEvent[4],
+ maliEvent[3]);
+ break;
+ case 'cros_trace_print_exit':
+ this.maliDDKCloseSlice(pid, tid, ts, [], maliEvent[3]);
+ }
+ return true;
+ },
+
+ /**
+ * Parses Mali Job events and sets up state in the importer.
+ * Events come in pairs like this:
+ *
+ * tracing_mark_write: S|7229|vertex-job|62|0|0|0|0|f46aed08|722962164982...
+ * tracing_mark_write: F|7229|vertex-job|62|0|0|0|0|f46aed08|722962164982...
+ *
+ * The '|' separated fields are (numbers correspond to jobEventRE groups
+ * below):
+ * 1: 'S'tart / 'F'inish
+ * 2: tgid that submitted the job
+ * 3: job type
+ * 4: job id
+ * 5,6: dependency 0: job id, dependency type
+ * 7,8: dependency 1: job id, dependency type
+ * 9: ctx id
+ * 10: unique job submission id
+ */
+ maliJobEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const jobEventRE =
+ // eslint-disable-next-line max-len
+ /^.*tracing_mark_write: (S|F)\|(\d+)\|(\w+)-job\|(\d+)\|(\d+)\|(\d+)\|(\d+)\|(\d+)\|([a-z0-9]+)\|(\d+)$/;
+ const jobEvent = jobEventRE.exec(eventBase.details);
+ if (!jobEvent) {
+ this.model_.importWarning({
+ type: 'parse_error',
+ args: 'unexpected mali_job_systrace_event_* event syntax'
+ });
+ return;
+ }
+ const jobType = jobEvent[3];
+ const jobId = jobEvent[4];
+ const thread = this.importer.model_.getOrCreateProcess(0)
+ .getOrCreateThread('mali:' + jobType);
+ switch (jobEvent[1]) {
+ case 'S': {
+ /* The HW has a two-deep ("head", "next") job FIFO for each job
+ * type. Start events indicate when the job was pushed into the FIFO,
+ * *not* when the hardware actually starts executing the job. It
+ * either starts ~immediately if the FIFO was empty, or when the
+ * previous job finishes otherwise.
+ *
+ * So we can get events in the order S|0, S|1, F|0, F|1, but we want
+ * to turn that into a '0' slice from S|0 to F|0, and another '1'
+ * slice from F|0 to F|1. In other words, when there is already a
+ * slice open, we want to defer creating a slice for this job until we
+ * finish the previous slice, then immediately open the slice for this
+ * event using the same timestamp.
+ */
+ const args = {
+ ctx: jobEvent[9],
+ pid: parseInt(jobEvent[2], 10),
+ dep0: parseInt(jobEvent[5], 10),
+ dep1: parseInt(jobEvent[7], 10)
+ };
+ if (thread.sliceGroup.openSliceCount) {
+ if (!(jobType in this.deferredJobs_)) {
+ this.deferredJobs_[jobType] = [];
+ }
+ this.deferredJobs_[jobType].push({id: jobId, args});
+ } else {
+ thread.sliceGroup.beginSlice(null, jobId, ts, args);
+ }
+ } break;
+ case 'F': {
+ if (!thread.sliceGroup.openSliceCount) {
+ // Discard unmatched ends.
+ return;
+ }
+ if (thread.sliceGroup.mostRecentlyOpenedPartialSlice.title !==
+ jobId) {
+ this.model_.importWarning({
+ type: 'invalid event nesting',
+ message: 'non-sequential jobs in same mali job slot'
+ });
+ }
+ thread.sliceGroup.endSlice(ts);
+ const deferredJobs = this.deferredJobs_[jobType];
+ if (deferredJobs && deferredJobs.length) {
+ const job = deferredJobs.shift();
+ thread.sliceGroup.beginSlice(null, job.id, ts, job.args);
+ }
+ } break;
+ }
+ return true;
+ },
+
+ /*
+ * Kernel event support.
+ */
+
+ dvfsSample(counterName, seriesName, ts, s) {
+ const value = parseInt(s);
+ const counter = this.model_.kernel.
+ getOrCreateCounter('DVFS', counterName);
+ if (counter.numSeries === 0) {
+ counter.addSeries(new tr.model.CounterSeries(seriesName,
+ ColorScheme.getColorIdForGeneralPurposeString(counter.name)));
+ }
+ counter.series.forEach(function(series) {
+ series.addCounterSample(ts, value);
+ });
+ },
+
+ dvfsEventEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /utilization=(\d+)/.exec(eventBase.details);
+ if (!event) return false;
+
+ this.dvfsSample('DVFS Utilization', 'utilization', ts, event[1]);
+ return true;
+ },
+
+ dvfsSetClockEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /frequency=(\d+)/.exec(eventBase.details);
+ if (!event) return false;
+
+ this.dvfsSample('DVFS Frequency', 'frequency', ts, event[1]);
+ return true;
+ },
+
+ dvfsSetVoltageEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = /voltage=(\d+)/.exec(eventBase.details);
+ if (!event) return false;
+
+ this.dvfsSample('DVFS Voltage', 'voltage', ts, event[1]);
+ return true;
+ },
+
+ hwcSample(cat, counterName, seriesName, ts, eventBase) {
+ const event = /val=(\d+)/.exec(eventBase.details);
+ if (!event) return false;
+ const value = parseInt(event[1]);
+
+ const counter = this.model_.kernel.
+ getOrCreateCounter(cat, counterName);
+ if (counter.numSeries === 0) {
+ counter.addSeries(new tr.model.CounterSeries(seriesName,
+ ColorScheme.getColorIdForGeneralPurposeString(counter.name)));
+ }
+ counter.series.forEach(function(series) {
+ series.addCounterSample(ts, value);
+ });
+ return true;
+ },
+
+ /*
+ * Job Manager block counters.
+ */
+ jmSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:jm', 'JM: ' + ctrName, seriesName, ts,
+ eventBase);
+ },
+ addJMCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.jmSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addJMCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.jmSample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Tiler block counters.
+ */
+ tilerSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:tiler', 'Tiler: ' + ctrName, seriesName,
+ ts, eventBase);
+ },
+ addTilerCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.tilerSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addTilerCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.tilerSample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Fragment counters.
+ */
+ fragSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:fragment', 'Fragment: ' + ctrName,
+ seriesName, ts, eventBase);
+ },
+ addFragCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.fragSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addFragCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.fragSample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Compute counters.
+ */
+ computeSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:compute', 'Compute: ' + ctrName,
+ seriesName, ts, eventBase);
+ },
+ addComputeCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.computeSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addComputeCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.computeSample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Tripipe counters.
+ */
+ addTripipeCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.hwcSample('mali:shader', 'Tripipe: ' + hwcTitle, 'cycles',
+ ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Arith counters.
+ */
+ arithSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:arith', 'Arith: ' + ctrName, seriesName, ts,
+ eventBase);
+ },
+ addArithCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.arithSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addArithCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.arithSample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Load/Store counters.
+ */
+ addLSCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.hwcSample('mali:ls', 'LS: ' + hwcTitle, 'count', ts,
+ eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * Texture counters.
+ */
+ textureSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:texture', 'Texture: ' + ctrName,
+ seriesName, ts, eventBase);
+ },
+ addTexCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.textureSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * LSC counters.
+ */
+ addLSCCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.hwcSample('mali:lsc', 'LSC: ' + hwcTitle, 'count', ts,
+ eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * TLB counters.
+ */
+ addAXICounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.hwcSample('mali:axi', 'AXI: ' + hwcTitle, 'count', ts,
+ eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * MMU counters.
+ */
+ mmuSample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:mmu', 'MMU: ' + ctrName, seriesName, ts,
+ eventBase);
+ },
+ addMMUCounter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.mmuSample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addMMUCycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.mmuSample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+
+ /*
+ * L2 counters.
+ */
+ l2Sample(ctrName, seriesName, ts, eventBase) {
+ return this.hwcSample('mali:l2', 'L2: ' + ctrName, seriesName, ts,
+ eventBase);
+ },
+ addL2Counter(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.l2Sample(hwcTitle, 'count', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ },
+ addL2Cycles(hwcEventName, hwcTitle) {
+ function handler(eventName, cpuNumber, pid, ts, eventBase) {
+ return this.l2Sample(hwcTitle, 'cycles', ts, eventBase);
+ }
+ this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+ }
+ };
+
+ Parser.register(MaliParser);
+
+ return {
+ MaliParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser_test.html
new file mode 100644
index 00000000000..76cc2e73b9c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/mali_parser_test.html
@@ -0,0 +1,559 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('maliDDKImport', function() {
+ const linesNoThread = [
+ // Row 1 open
+ ' chrome-1780 [001] ...1 28.562633: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'gles/src/dispatch/mali_gles_dispatch_entrypoints.c992: ' +
+ 'glTexSubImage2D',
+ // Row 2 open
+ ' chrome-1780 [001] ...1 28.562655: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_api.c996: ' +
+ 'gles_texture_tex_sub_image_2d',
+ // Row 3 open
+ ' chrome-1780 [001] ...1 28.562671: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c295: ' +
+ 'gles_texturep_slave_map_master',
+ // Row 3 close
+ ' chrome-1780 [001] ...1 28.562684: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c295: ',
+ // Row 3 open
+ ' chrome-1780 [001] ...1 28.562700: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c1505: ' +
+ 'gles2_texturep_upload_2d',
+ // Row 4 open
+ ' chrome-1780 [001] ...1 28.562726: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c1612: ' +
+ 'gles2_texturep_upload_2d: pixel array: wait for dependencies',
+ // Row 5 open
+ ' chrome-1780 [001] ...1 28.562742: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1693: ' +
+ 'cobj_convert_pixels_to_surface',
+ // Row 6 open
+ ' chrome-1780 [001] ...1 28.562776: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1461: ' +
+ 'cobj_convert_pixels',
+ // Row 7 open
+ ' chrome-1780 [001] ...1 28.562791: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1505: ' +
+ 'cobj_convert_pixels: fast-path linear copy',
+ // Row 8 open
+ ' chrome-1780 [001] ...1 28.562808: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1511: ' +
+ 'cobj_convert_pixels: reorder-only',
+ // Row 8 close
+ ' chrome-1780 [001] ...1 28.563383: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1511',
+ // Row 7 close
+ ' chrome-1780 [001] ...1 28.563397: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1505',
+ // Row 6 close
+ ' chrome-1780 [001] ...1 28.563409: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1461',
+ // Row 5 close
+ ' chrome-1780 [001] ...1 28.563438: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c1693',
+ // Row 4 close
+ ' chrome-1780 [001] ...1 28.563451: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c1612',
+ // Row 3 close
+ ' chrome-1780 [001] ...1 28.563462: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c1505',
+ // Row 2 close
+ ' chrome-1780 [001] ...1 28.563475: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_api.c996',
+ // Row 1 close
+ ' chrome-1780 [001] ...1 28.563486: tracing_mark_write: ' +
+ 'mali_driver: cros_trace_print_exit: ' +
+ 'gles/src/dispatch/mali_gles_dispatch_entrypoints.c992'
+ ];
+
+ const linesWithThread = [
+ // Row 1 open
+ ' chrome-1780 [001] ...1 28.562633: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'gles/src/dispatch/mali_gles_dispatch_entrypoints.c@992: ' +
+ 'glTexSubImage2D',
+ // Row 2 open
+ ' chrome-1780 [001] ...1 28.562655: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_api.c@996: ' +
+ 'gles_texture_tex_sub_image_2d',
+ // Row 3 open
+ ' chrome-1780 [001] ...1 28.562671: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c@295: ' +
+ 'gles_texturep_slave_map_master',
+ // Row 3 close
+ ' chrome-1780 [001] ...1 28.562684: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c@295: ',
+ // Row 3 open
+ ' chrome-1780 [001] ...1 28.562700: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c@1505: ' +
+ 'gles2_texturep_upload_2d',
+ // Row 4 open
+ ' chrome-1780 [001] ...1 28.562726: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c@1612: ' +
+ 'gles2_texturep_upload_2d: pixel array: wait for dependencies',
+ // Row 5 open
+ ' chrome-1780 [001] ...1 28.562742: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1693: ' +
+ 'cobj_convert_pixels_to_surface',
+ // Row 6 open
+ ' chrome-1780 [001] ...1 28.562776: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1461: ' +
+ 'cobj_convert_pixels',
+ // Row 7 open
+ ' chrome-1780 [001] ...1 28.562791: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1505: ' +
+ 'cobj_convert_pixels: fast-path linear copy',
+ // Row 8 open
+ ' chrome-1780 [001] ...1 28.562808: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1511: ' +
+ 'cobj_convert_pixels: reorder-only',
+ // Row 8 close
+ ' chrome-1780 [001] ...1 28.563383: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1511',
+ // Row 7 close
+ ' chrome-1780 [001] ...1 28.563397: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1505',
+ // Row 6 close
+ ' chrome-1780 [001] ...1 28.563409: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1461',
+ // Row 5 close
+ ' chrome-1780 [001] ...1 28.563438: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'cobj/src/mali_cobj_surface_operations.c@1693',
+ // Row 4 close
+ ' chrome-1780 [001] ...1 28.563451: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c@1612',
+ // Row 3 close
+ ' chrome-1780 [001] ...1 28.563462: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_slave.c@1505',
+ // Row 2 close
+ ' chrome-1780 [001] ...1 28.563475: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'gles/src/texture/mali_gles_texture_api.c@996',
+ // Row 1 close
+ ' chrome-1780 [001] ...1 28.563486: tracing_mark_write: ' +
+ 'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+ 'gles/src/dispatch/mali_gles_dispatch_entrypoints.c@992'
+ ];
+ const traceNoThread = newModel(linesNoThread.join('\n'));
+ const traceWithThread = newModel(linesWithThread.join('\n'));
+ assert.isFalse(traceNoThread.hasImportWarnings);
+ assert.isFalse(traceWithThread.hasImportWarnings);
+
+ const threadsNoThread = traceNoThread.getAllThreads();
+ const threadsWithThread = traceWithThread.getAllThreads();
+ assert.strictEqual(threadsNoThread.length, 1);
+ assert.strictEqual(threadsWithThread.length, 1);
+
+ const maliThreadNoThread = threadsNoThread[0];
+ const maliThreadWithThread = threadsWithThread[0];
+ assert.strictEqual(maliThreadNoThread.tid, 'mali');
+ assert.strictEqual(maliThreadWithThread.tid, 'mali-1878934320');
+ assert.strictEqual(maliThreadNoThread.sliceGroup.length, 9);
+ assert.strictEqual(maliThreadWithThread.sliceGroup.length, 9);
+ });
+
+ test('DVFSFrequencyImport', function() {
+ const lines = [
+ ' kworker/u:0-5 [001] .... 1174.839552: mali_dvfs_set_clock: ' +
+ 'frequency=266',
+ ' kworker/u:0-5 [000] .... 1183.840486: mali_dvfs_set_clock: ' +
+ 'frequency=400'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 1);
+
+ const c0 = counters[0];
+ assert.strictEqual(c0.name, 'DVFS Frequency');
+ assert.strictEqual(c0.series[0].samples.length, 2);
+ });
+
+ test('DVFSVoltageImport', function() {
+ const lines = [
+ ' kworker/u:0-5 [001] .... 1174.839562: mali_dvfs_set_voltage: ' +
+ 'voltage=937500',
+ ' kworker/u:0-5 [000] .... 1183.840009: mali_dvfs_set_voltage: ' +
+ 'voltage=1100000'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 1);
+
+ const c0 = counters[0];
+ assert.strictEqual(c0.name, 'DVFS Voltage');
+ assert.strictEqual(c0.series[0].samples.length, 2);
+ });
+
+ test('DVFSUtilizationImport', function() {
+ const lines = [
+ ' kworker/u:0-5 [001] .... 1174.839552: mali_dvfs_event: ' +
+ 'utilization=7',
+ ' kworker/u:0-5 [000] .... 1183.840486: mali_dvfs_event: ' +
+ 'utilization=37'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 1);
+
+ const c0 = counters[0];
+ assert.strictEqual(c0.name, 'DVFS Utilization');
+ assert.strictEqual(c0.series[0].samples.length, 2);
+ });
+
+ test('maliHWCImport', function() {
+ const lines = [
+ ' kworker/u:0-5 [000] .... 78.896588: ' +
+ 'mali_hwc_ACTIVE: val=238',
+ ' kworker/u:0-5 [000] .... 79.046889: ' +
+ 'mali_hwc_ARITH_CYCLES_L0: val=1967',
+ ' kworker/u:0-5 [000] .... 79.046888: ' +
+ 'mali_hwc_ARITH_CYCLES_REG: val=136',
+ ' kworker/u:0-5 [000] .... 79.046890: ' +
+ 'mali_hwc_ARITH_FRAG_DEPEND: val=19676',
+ ' kworker/u:0-5 [000] .... 79.046886: ' +
+ 'mali_hwc_ARITH_WORDS: val=255543',
+ ' kworker/u:0-5 [000] .... 79.046920: ' +
+ 'mali_hwc_AXI_BEATS_READ: val=257053',
+ ' kworker/u:0-5 [000] .... 78.896594: ' +
+ 'mali_hwc_AXI_TLB_STALL: val=1',
+ ' kworker/u:0-5 [000] .... 78.946646: ' +
+ 'mali_hwc_AXI_TLB_TRANSACTION: val=4',
+ ' kworker/u:0-5 [000] .... 79.046853: ' +
+ 'mali_hwc_BACK_FACING: val=104',
+ ' kworker/u:0-5 [000] .... 79.046880: ' +
+ 'mali_hwc_COMPUTE_ACTIVE: val=17462',
+ ' kworker/u:0-5 [000] .... 79.046884: ' +
+ 'mali_hwc_COMPUTE_CYCLES_DESC: val=3933',
+ ' kworker/u:0-5 [000] .... 79.046881: ' +
+ 'mali_hwc_COMPUTE_TASKS: val=15',
+ ' kworker/u:0-5 [000] .... 79.046883: ' +
+ 'mali_hwc_COMPUTE_THREADS: val=60',
+ ' kworker/u:0-5 [000] .... 79.046860: ' +
+ 'mali_hwc_FRAG_ACTIVE: val=690986',
+ ' kworker/u:0-5 [000] .... 79.046864: ' +
+ 'mali_hwc_FRAG_CYCLE_DESC: val=13980',
+ ' kworker/u:0-5 [000] .... 79.046876: ' +
+ 'mali_hwc_FRAG_CYCLE_NO_TILE: val=3539',
+ ' kworker/u:0-5 [000] .... 79.046865: ' +
+ 'mali_hwc_FRAG_CYCLES_PLR: val=1499',
+ ' kworker/u:0-5 [000] .... 79.046869: ' +
+ 'mali_hwc_FRAG_CYCLES_RAST: val=1999',
+ ' kworker/u:0-5 [000] .... 79.046868: ' +
+ 'mali_hwc_FRAG_CYCLES_TRISETUP: val=22353',
+ ' kworker/u:0-5 [000] .... 79.046867: ' +
+ 'mali_hwc_FRAG_CYCLES_VERT: val=20763',
+ ' kworker/u:0-5 [000] .... 79.046872: ' +
+ 'mali_hwc_FRAG_DUMMY_THREADS: val=1968',
+ ' kworker/u:0-5 [000] .... 79.046877: ' +
+ 'mali_hwc_FRAG_NUM_TILES: val=1840',
+ ' kworker/u:0-5 [000] .... 79.046862: ' +
+ 'mali_hwc_FRAG_PRIMATIVES: val=3752',
+ ' kworker/u:0-5 [000] .... 79.046863: ' +
+ 'mali_hwc_FRAG_PRIMATIVES_DROPPED: val=18',
+ ' kworker/u:0-5 [000] .... 79.046874: ' +
+ 'mali_hwc_FRAG_QUADS_EZS_TEST: val=117925',
+ ' kworker/u:0-5 [000] .... 79.046873: ' +
+ 'mali_hwc_FRAG_QUADS_RAST: val=117889',
+ ' kworker/u:0-5 [000] .... 79.046870: ' +
+ 'mali_hwc_FRAG_THREADS: val=471507',
+ ' kworker/u:0-5 [000] .... 79.046879: ' +
+ 'mali_hwc_FRAG_TRANS_ELIM: val=687',
+ ' kworker/u:0-5 [000] .... 80.315162: ' +
+ 'mali_hwc_FRONT_FACING: val=56',
+ ' kworker/u:0-5 [000] .... 78.896582: ' +
+ 'mali_hwc_GPU_ACTIVE: val=1316',
+ ' kworker/u:0-5 [000] .... 78.896584: ' +
+ 'mali_hwc_IRQ_ACTIVE: val=17',
+ ' kworker/u:0-5 [000] .... 79.046834: ' +
+ 'mali_hwc_JS0_ACTIVE: val=709444',
+ ' kworker/u:0-5 [000] .... 79.046831: ' +
+ 'mali_hwc_JS0_JOBS: val=2',
+ ' kworker/u:0-5 [000] .... 79.046832: ' +
+ 'mali_hwc_JS0_TASKS: val=7263',
+ ' kworker/u:0-5 [000] .... 79.046836: ' +
+ 'mali_hwc_JS0_WAIT_DEPEND: val=665876',
+ ' kworker/u:0-5 [000] .... 79.046835: ' +
+ 'mali_hwc_JS0_WAIT_ISSUE: val=910',
+ ' kworker/u:0-5 [000] .... 79.046840: ' +
+ 'mali_hwc_JS1_ACTIVE: val=153980',
+ ' kworker/u:0-5 [000] .... 79.046838: ' +
+ 'mali_hwc_JS1_JOBS: val=133',
+ ' kworker/u:0-5 [000] .... 79.046839: ' +
+ 'mali_hwc_JS1_TASKS: val=128',
+ ' kworker/u:0-5 [000] .... 79.046843: ' +
+ 'mali_hwc_JS1_WAIT_FINISH: val=74404',
+ ' kworker/u:0-5 [000] .... 79.046842: ' +
+ 'mali_hwc_JS1_WAIT_ISSUE: val=10146',
+ ' kworker/u:0-5 [000] .... 78.896603: ' +
+ 'mali_hwc_L2_ANY_LOOKUP: val=22',
+ ' kworker/u:0-5 [000] .... 79.046942: ' +
+ 'mali_hwc_L2_CLEAN_MISS: val=116',
+ ' kworker/u:0-5 [000] .... 79.063515: ' +
+ 'mali_hwc_L2_EXT_AR_STALL: val=9',
+ ' kworker/u:0-5 [000] .... 78.963384: ' +
+ 'mali_hwc_L2_EXT_BARRIER: val=1',
+ ' kworker/u:0-5 [000] .... 79.063516: ' +
+ 'mali_hwc_L2_EXT_R_BUF_FULL: val=43',
+ ' kworker/u:0-5 [000] .... 78.896611: ' +
+ 'mali_hwc_L2_EXT_READ: val=4',
+ ' kworker/u:0-5 [000] .... 78.896612: ' +
+ 'mali_hwc_L2_EXT_READ_LINE: val=4',
+ ' kworker/u:0-5 [000] .... 79.046956: ' +
+ 'mali_hwc_L2_EXT_R_RAW: val=1',
+ ' kworker/u:0-5 [000] .... 79.063518: ' +
+ 'mali_hwc_L2_EXT_R_W_HAZARD: val=15',
+ ' kworker/u:0-5 [000] .... 78.963381: ' +
+ 'mali_hwc_L2_EXT_WRITE: val=25',
+ ' kworker/u:0-5 [000] .... 79.046952: ' +
+ 'mali_hwc_L2_EXT_WRITE_LINE: val=63278',
+ ' kworker/u:0-5 [000] .... 78.963382: ' +
+ 'mali_hwc_L2_EXT_WRITE_SMALL: val=1',
+ ' kworker/u:0-5 [000] .... 79.814532: ' +
+ 'mali_hwc_L2_EXT_W_STALL: val=9',
+ ' kworker/u:0-5 [000] .... 78.896602: ' +
+ 'mali_hwc_L2_READ_BEATS: val=16',
+ ' kworker/u:0-5 [000] .... 78.896607: ' +
+ 'mali_hwc_L2_READ_HIT: val=11',
+ ' kworker/u:0-5 [000] .... 78.896604: ' +
+ 'mali_hwc_L2_READ_LOOKUP: val=19',
+ ' kworker/u:0-5 [000] .... 78.896606: ' +
+ 'mali_hwc_L2_READ_REPLAY: val=2',
+ ' kworker/u:0-5 [000] .... 79.046940: ' +
+ 'mali_hwc_L2_READ_SNOOP: val=24',
+ ' kworker/u:0-5 [000] .... 79.046959: ' +
+ 'mali_hwc_L2_REPLAY_FULL: val=6629',
+ ' kworker/u:0-5 [000] .N.. 80.565684: ' +
+ 'mali_hwc_L2_SNOOP_FULL: val=5',
+ ' kworker/u:0-5 [000] .... 79.046937: ' +
+ 'mali_hwc_L2_SREAD_LOOKUP: val=241',
+ ' kworker/u:0-5 [000] .... 79.046944: ' +
+ 'mali_hwc_L2_SWRITE_LOOKUP: val=133',
+ ' kworker/u:0-5 [000] .... 78.896614: ' +
+ 'mali_hwc_L2_TAG_HAZARD: val=4',
+ ' kworker/u:0-5 [000] .... 78.963368: ' +
+ 'mali_hwc_L2_WRITE_BEATS: val=96',
+ ' kworker/u:0-5 [000] .... 79.046947: ' +
+ 'mali_hwc_L2_WRITE_HIT: val=78265',
+ ' kworker/u:0-5 [000] .... 78.896608: ' +
+ 'mali_hwc_L2_WRITE_LOOKUP: val=3',
+ ' kworker/u:0-5 [000] .... 79.046946: ' +
+ 'mali_hwc_L2_WRITE_REPLAY: val=15879',
+ ' kworker/u:0-5 [000] .... 79.046912: ' +
+ 'mali_hwc_LSC_LINE_FETCHES: val=15',
+ ' kworker/u:0-5 [000] .... 79.046909: ' +
+ 'mali_hwc_LSC_READ_HITS: val=2961',
+ ' kworker/u:0-5 [000] .... 79.046911: ' +
+ 'mali_hwc_LSC_READ_MISSES: val=22',
+ ' kworker/u:0-5 [000] .... 79.046914: ' +
+ 'mali_hwc_LSC_SNOOPS: val=10',
+ ' kworker/u:0-5 [000] .... 79.046893: ' +
+ 'mali_hwc_LS_ISSUES: val=524219',
+ ' kworker/u:0-5 [000] .... 79.046894: ' +
+ 'mali_hwc_LS_REISSUES_MISS: val=439',
+ ' kworker/u:0-5 [000] .... 79.046895: ' +
+ 'mali_hwc_LS_REISSUES_VD: val=52007',
+ ' kworker/u:0-5 [000] .... 79.046919: ' +
+ 'mali_hwc_LS_TLB_HIT: val=3043',
+ ' kworker/u:0-5 [000] .... 79.046918: ' +
+ 'mali_hwc_LS_TLB_MISS: val=5',
+ ' kworker/u:0-5 [000] .... 79.046891: ' +
+ 'mali_hwc_LS_WORDS: val=471514',
+ ' kworker/u:0-5 [000] .... 79.046925: ' +
+ 'mali_hwc_MMU_HIT: val=771',
+ ' kworker/u:0-5 [000] .... 79.046924: ' +
+ 'mali_hwc_MMU_NEW_MISS: val=494',
+ ' kworker/u:0-5 [000] .... 79.046922: ' +
+ 'mali_hwc_MMU_REPLAY_MISS: val=841',
+ ' kworker/u:0-5 [000] .... 79.046921: ' +
+ 'mali_hwc_MMU_TABLE_WALK: val=3119',
+ ' kworker/u:0-5 [000] .... 79.046848: ' +
+ 'mali_hwc_POINTS: val=5',
+ ' kworker/u:0-5 [000] .... 79.046856: ' +
+ 'mali_hwc_PRIM_CLIPPED: val=70',
+ ' kworker/u:0-5 [000] .... 79.046855: ' +
+ 'mali_hwc_PRIM_CULLED: val=26',
+ ' kworker/u:0-5 [000] .... 79.046854: ' +
+ 'mali_hwc_PRIM_VISIBLE: val=109',
+ ' kworker/u:0-5 [000] .... 79.046898: ' +
+ 'mali_hwc_TEX_BUBBLES: val=24874',
+ ' kworker/u:0-5 [000] .... 79.046905: ' +
+ 'mali_hwc_TEX_RECIRC_DESC: val=5937',
+ ' kworker/u:0-5 [000] .... 79.046904: ' +
+ 'mali_hwc_TEX_RECIRC_FMISS: val=209450',
+ ' kworker/u:0-5 [000] .... 78.896592: ' +
+ 'mali_hwc_TEX_RECIRC_MULTI: val=238',
+ ' kworker/u:0-5 [000] .... 79.046908: ' +
+ 'mali_hwc_TEX_RECIRC_PMISS: val=9672',
+ ' kworker/u:0-5 [000] .... 79.046903: ' +
+ 'mali_hwc_TEX_THREADS: val=660900',
+ ' kworker/u:0-5 [000] .... 79.046897: ' +
+ 'mali_hwc_TEX_WORDS: val=471193',
+ ' kworker/u:0-5 [000] .... 79.046901: ' +
+ 'mali_hwc_TEX_WORDS_DESC: val=707',
+ ' kworker/u:0-5 [000] .... 79.046900: ' +
+ 'mali_hwc_TEX_WORDS_L0: val=32',
+ ' kworker/u:0-5 [000] .... 79.046846: ' +
+ 'mali_hwc_TRIANGLES: val=130',
+ ' kworker/u:0-5 [000] .... 79.046885: ' +
+ 'mali_hwc_TRIPIPE_ACTIVE: val=691001',
+ ' kworker/u:0-5 [000] .... 78.896600: ' +
+ 'mali_hwc_UTLB_NEW_MISS: val=6',
+ ' kworker/u:0-5 [000] .... 78.896599: ' +
+ 'mali_hwc_UTLB_REPLAY_FULL: val=248',
+ ' kworker/u:0-5 [000] .... 78.896597: ' +
+ 'mali_hwc_UTLB_REPLAY_MISS: val=1',
+ ' kworker/u:0-5 [000] .... 78.896596: ' +
+ 'mali_hwc_UTLB_STALL: val=1',
+ ' kworker/u:0-5 [000] .... 79.046850: ' +
+ 'mali_hwc_VCACHE_HIT: val=311',
+ ' kworker/u:0-5 [000] .... 79.046851: ' +
+ 'mali_hwc_VCACHE_MISS: val=70'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const counters = m.getAllCounters();
+ assert.strictEqual(counters.length, 103);
+
+ // all counters should have 1 sample
+ for (let tI = 0; tI < counters.length; tI++) {
+ const counter = counters[tI];
+ assert.strictEqual(counter.series[0].samples.length, 1);
+ }
+ // TODO(sleffler) verify counter names? (not sure if it's worth the effort)
+ });
+
+ test('maliJobImport', function() {
+ const jobEvents = [
+ // vertex slice 0 open
+ ' surfaceflinger-6962 ( 6962) [000] d.h2 498269.966588: ' +
+ 'mali_job_systrace_event_start: tracing_mark_write: ' +
+ 'S|6962|vertex-job|85|84|1|0|0|920d408|69628513498269966584032',
+ // vertex slice 0 close
+ ' surfaceflinger-6962 ( 6962) [000] d.h2 498269.966702: ' +
+ 'mali_job_systrace_event_stop: tracing_mark_write: ' +
+ 'F|6962|vertex-job|85|84|1|0|0|920d408|69628513498269966584032',
+
+ // vertex slice 1 open
+ ' surfaceflinger-6962 ( 6962) [000] d..1 498269.966966: ' +
+ 'mali_job_systrace_event_start: tracing_mark_write: ' +
+ 'S|7541|vertex-job|225|0|0|0|0|f5e13b48|754122517498269966963802',
+ // vertex slice 1 close
+ ' surfaceflinger-6962 ( 6962) [000] d.h1 498269.967109: ' +
+ 'mali_job_systrace_event_stop: tracing_mark_write: ' +
+ 'F|7541|vertex-job|225|0|0|0|0|f5e13b48|754122517498269966963802',
+
+ // fragment slice 0 open
+ ' kworker/u17:4-6269 ( 6269) [001] d..1 498269.967667: ' +
+ 'mali_job_systrace_event_start: tracing_mark_write: ' +
+ 'S|6962|fragment-job|86|85|1|0|0|920d408|69628613498269967664417',
+ // fragment slice 1 open (deferred)
+ ' mali-cmar-backe-7634 ( 7541) [003] d..1 498269.968473: ' +
+ 'mali_job_systrace_event_start: tracing_mark_write: ' +
+ 'S|7541|fragment-job|223|225|1|0|0|f5e13b48|754122317498269968470186',
+ // fragment slice 0 close (adjusted open of slice 1)
+ ' HwBinder:6936_2-7001 ( 6936) [000] d.h3 498269.970336: ' +
+ 'mali_job_systrace_event_stop: tracing_mark_write: ' +
+ 'F|6962|fragment-job|86|85|1|0|0|920d408|69628613498269967664417',
+ // fragment slice 1 close
+ ' RenderThread-7619 ( 7541) [000] d.h1 498269.971688: ' +
+ 'mali_job_systrace_event_stop: tracing_mark_write: ' +
+ 'F|7541|fragment-job|223|225|1|0|0|f5e13b48|754122317498269970293032',
+ ];
+
+ const expectedThreads = [
+ {
+ tid: 'mali:vertex',
+ slices: [
+ {title: '85', start: '498269.966588', end: '498269.966702'},
+ {title: '225', start: '498269.966966', end: '498269.967109'}]},
+ {
+ tid: 'mali:fragment',
+ slices: [
+ {title: '86', start: '498269.967667', end: '498269.970336'},
+ {title: '223', start: '498269.970336', end: '498269.971688'}]},
+ ];
+
+ const trace = newModel(jobEvents.join('\n'));
+ assert.isFalse(trace.hasImportWarnings);
+
+ const threads = trace.getAllThreads();
+ assert.strictEqual(threads.length, expectedThreads.length);
+ for (let tidx = 0; tidx < threads.length; tidx++) {
+ const actualThread = threads[tidx];
+ const expectedThread = expectedThreads[tidx];
+ assert.strictEqual(actualThread.tid, expectedThreads[tidx].tid);
+ assert.strictEqual(actualThread.sliceGroup.length,
+ expectedThreads[tidx].slices.length);
+ for (let sidx = 0; sidx < actualThread.sliceGroup.length; sidx++) {
+ const actualSlice = actualThread.sliceGroup.slices[sidx];
+ const expectedSlice = expectedThread.slices[sidx];
+ assert.strictEqual(actualSlice.title, expectedSlice.title);
+ assert.strictEqual(actualSlice.start,
+ parseFloat(expectedSlice.start) * 1000);
+ assert.strictEqual(actualSlice.end,
+ parseFloat(expectedSlice.end) * 1000);
+ }
+ }
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser.html
new file mode 100644
index 00000000000..027554eacfd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses drm driver events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux vmscan trace events.
+ * @constructor
+ */
+ function MemReclaimParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('mm_vmscan_kswapd_wake',
+ MemReclaimParser.prototype.kswapdWake.bind(this));
+ importer.registerEventHandler('mm_vmscan_kswapd_sleep',
+ MemReclaimParser.prototype.kswapdSleep.bind(this));
+ importer.registerEventHandler('mm_vmscan_direct_reclaim_begin',
+ MemReclaimParser.prototype.reclaimBegin.bind(this));
+ importer.registerEventHandler('mm_vmscan_direct_reclaim_end',
+ MemReclaimParser.prototype.reclaimEnd.bind(this));
+ importer.registerEventHandler('lowmemory_kill',
+ MemReclaimParser.prototype.lowmemoryKill.bind(this));
+ }
+
+ // Matches the mm_vmscan_kswapd_wake record
+ // mm_vmscan_kswapd_wake: nid=%d order=%d
+ const kswapdWakeRE = /nid=(\d+) order=(\d+)/;
+
+ // Matches the mm_vmscan_kswapd_sleep record
+ // mm_vmscan_kswapd_sleep: order=%d
+ const kswapdSleepRE = /nid=(\d+)/;
+
+ // Matches the mm_vmscan_direct_reclaim_begin record
+ // mm_vmscan_direct_reclaim_begin: order=%d may_writepage=%d gfp_flags=%s
+ const reclaimBeginRE = /order=(\d+) may_writepage=\d+ gfp_flags=(.+)/;
+
+ // Matches the mm_vmscan_direct_reclaim_end record
+ // mm_vmscan_direct_reclaim_end: nr_reclaimed=%lu
+ const reclaimEndRE = /nr_reclaimed=(\d+)/;
+
+ // Matches the lowmemory_kill record
+ const lowmemoryRE =
+ /([^ ]+) \((\d+)\), page cache (\d+)kB \(limit (\d+)kB\), free (-?\d+)Kb/;
+
+ MemReclaimParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses memreclaim events and sets up state in the importer.
+ */
+ kswapdWake(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = kswapdWakeRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const tgid = parseInt(eventBase.tgid);
+
+ const nid = parseInt(event[1]);
+ const order = parseInt(event[2]);
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, tgid, pid);
+
+ if (kthread.openSliceTS) {
+ if (order > kthread.order) {
+ kthread.order = order;
+ }
+ } else {
+ kthread.openSliceTS = ts;
+ kthread.order = order;
+ }
+ return true;
+ },
+
+ kswapdSleep(eventName, cpuNumber, pid, ts, eventBase) {
+ const tgid = parseInt(eventBase.tgid);
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, tgid, pid);
+
+ if (kthread.openSliceTS) {
+ kthread.thread.sliceGroup.pushCompleteSlice(
+ 'memreclaim', eventBase.threadName, kthread.openSliceTS,
+ ts - kthread.openSliceTS, 0, 0,
+ {
+ order: kthread.order
+ });
+ }
+ kthread.openSliceTS = undefined;
+ kthread.order = undefined;
+ return true;
+ },
+
+ reclaimBegin(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = reclaimBeginRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const order = parseInt(event[1]);
+ const gfp = event[2];
+ const tgid = parseInt(eventBase.tgid);
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, tgid, pid);
+
+ kthread.openSliceTS = ts;
+ kthread.order = order;
+ kthread.gfp = gfp;
+ return true;
+ },
+
+ reclaimEnd(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = reclaimEndRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const nrReclaimed = parseInt(event[1]);
+ const tgid = parseInt(eventBase.tgid);
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, tgid, pid);
+
+ if (kthread.openSliceTS !== undefined) {
+ kthread.thread.sliceGroup.pushCompleteSlice('memreclaim',
+ 'direct reclaim', kthread.openSliceTS, ts - kthread.openSliceTS,
+ 0, 0,
+ {
+ order: kthread.order,
+ gfp: kthread.gfp,
+ nr_reclaimed: nrReclaimed
+ });
+ }
+ kthread.openSliceTS = undefined;
+ kthread.order = undefined;
+ kthread.gfp = undefined;
+ return true;
+ },
+
+ lowmemoryKill(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = lowmemoryRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const tgid = parseInt(eventBase.tgid);
+ const killedName = event[1];
+ const killedPid = parseInt(event[2]);
+ const cache = parseInt(event[3]);
+ const free = parseInt(event[5]);
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, tgid, pid);
+
+ kthread.thread.sliceGroup.pushCompleteSlice('lowmemory',
+ 'low memory kill', ts, 0,
+ 0, 0,
+ {
+ killed_name: killedName,
+ killed_pid: killedPid,
+ cache,
+ free
+ });
+ return true;
+ }
+ };
+
+ Parser.register(MemReclaimParser);
+
+ return {
+ MemReclaimParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser_test.html
new file mode 100644
index 00000000000..1396d6713eb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/memreclaim_parser_test.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('memreclaimImport', function() {
+ const lines = [
+ ' surfaceflinger-1155 ( 1155) [001] ...1 12839.528756: mm_vmscan_direct_reclaim_begin: order=0 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|GFP_ZERO|0x2', // @suppress longLineCheck
+ ' surfaceflinger-1155 ( 1155) [001] ...1 12839.531950: mm_vmscan_direct_reclaim_end: nr_reclaimed=66', // @suppress longLineCheck
+ ' kswapd0-33 ( 33) [001] ...1 12838.491407: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ ' kswapd0-33 ( 33) [001] ...1 12838.529770: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ ' kswapd0-33 ( 33) [001] ...1 12840.545737: mm_vmscan_kswapd_sleep: nid=0'// @suppress longLineCheck
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.strictEqual(
+ m.processes['1155'].threads['1155'].sliceGroup.length, 1);
+ assert.strictEqual(m.processes['33'].threads['33'].sliceGroup.length, 1);
+ });
+
+ test('memreclaimDirectReclaim', function() {
+ const lines = [
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.106078: mm_vmscan_direct_reclaim_begin: order=5 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|0x2', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.107619: mm_vmscan_direct_reclaim_end: nr_reclaimed=72', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.107738: mm_vmscan_direct_reclaim_begin: order=4 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|0x2', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.107844: mm_vmscan_direct_reclaim_end: nr_reclaimed=35', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.107891: mm_vmscan_direct_reclaim_begin: order=4 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|0x2', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.107945: mm_vmscan_direct_reclaim_end: nr_reclaimed=35', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.107990: mm_vmscan_direct_reclaim_begin: order=4 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|0x2', // @suppress longLineCheck
+ 'RenderThread-9844 ( 9786) [001] ...1 1734.108062: mm_vmscan_direct_reclaim_end: nr_reclaimed=34', // @suppress longLineCheck
+ 'Binder_8-1735 ( 1022) [001] ...1 1735.472240: mm_vmscan_direct_reclaim_begin: order=3 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|0x2', // @suppress longLineCheck
+ 'Binder_8-1735 ( 1022) [001] ...1 1735.472849: mm_vmscan_direct_reclaim_end: nr_reclaimed=47', // @suppress longLineCheck
+ 'Binder_8-1735 ( 1022) [001] ...1 1735.473002: mm_vmscan_direct_reclaim_begin: order=3 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|0x2', // @suppress longLineCheck
+ 'Binder_8-1735 ( 1022) [001] ...1 1735.474859: mm_vmscan_direct_reclaim_end: nr_reclaimed=48', // @suppress longLineCheck
+ 'touchFusion-88 ( 88) [000] ...1 1736.510656: mm_vmscan_direct_reclaim_begin: order=2 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|GFP_COMP|GFP_NOMEMALLOC|GFP_KMEMCG', // @suppress longLineCheck
+ 'touchFusion-88 ( 88) [000] ...1 1736.517616: mm_vmscan_direct_reclaim_end: nr_reclaimed=34', // @suppress longLineCheck
+ 'touchFusion-88 ( 88) [000] ...1 1736.527061: mm_vmscan_direct_reclaim_begin: order=2 may_writepage=1 gfp_flags=GFP_KERNEL|GFP_NOWARN|GFP_COMP|GFP_NOMEMALLOC|GFP_KMEMCG', // @suppress longLineCheck
+ 'touchFusion-88 ( 88) [000] ...1 1736.530857: mm_vmscan_direct_reclaim_end: nr_reclaimed=39'// @suppress longLineCheck
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+
+ assert.strictEqual(threads.length, 3);
+
+ const Binder8 = threads[0];
+ const touchFusion = threads[1];
+ const RenderThread = threads[2];
+
+ /* make sure there are the expected amount of slices per thread */
+ assert.strictEqual(Binder8.sliceGroup.length, 2);
+ assert.strictEqual(touchFusion.sliceGroup.length, 2);
+ assert.strictEqual(RenderThread.sliceGroup.length, 4);
+
+ /* make sure the slices have information to display to the
+ * user when selected
+ */
+
+ const iterateMe = [Binder8, touchFusion, RenderThread];
+ iterateMe.forEach(function(thread) {
+ for (let i = 0; i < thread.sliceGroup.length; i++) {
+ assert.isDefined(thread.sliceGroup.slices[i].args);
+ }
+ });
+ });
+
+ test('memreclaimKswapd', function() {
+ const lines = [
+ 'kswapd0-48 ( 48) [001] ...1 1734.210437: mm_vmscan_kswapd_wake: nid=0 order=5', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.227291: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.237585: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.258698: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.269642: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.319484: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.344839: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.428425: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.429593: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.599419: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1734.696606: mm_vmscan_kswapd_sleep: nid=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1735.465745: mm_vmscan_kswapd_wake: nid=0 order=3', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1735.563917: mm_vmscan_kswapd_wake: nid=0 order=5', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1735.570555: mm_vmscan_kswapd_wake: nid=0 order=4', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1735.666658: mm_vmscan_kswapd_sleep: nid=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1736.508069: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1736.529293: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1736.696725: mm_vmscan_kswapd_sleep: nid=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1737.945426: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1737.988642: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.057237: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.144630: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.207546: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.221963: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.316889: mm_vmscan_kswapd_sleep: nid=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.712804: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.751103: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.773175: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.785068: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.789545: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1738.873675: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1738.899117: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1738.939214: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1738.990366: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1739.028269: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1739.036765: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1739.077631: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.094731: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.096757: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.160536: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.256638: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.264972: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1739.360137: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.368759: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.387082: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.455657: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.489058: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.507561: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.570247: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [001] ...1 1739.582975: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.678148: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.762025: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.799245: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.821950: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.894130: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1739.919775: lowmemory_kill: d.process.media (10517), page cache 311740kB (limit 322560kB), free -1456Kb', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.026933: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.126608: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.150819: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.156101: mm_vmscan_kswapd_wake: nid=0 order=1', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.246626: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.357055: mm_vmscan_kswapd_sleep: nid=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.762705: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.772367: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.783509: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+ 'kswapd0-48 ( 48) [000] ...1 1740.876601: mm_vmscan_kswapd_sleep: nid=0'// @suppress longLineCheck
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 1);
+
+ const thread = threads[0];
+
+ assert.strictEqual(thread.sliceGroup.length, 7);
+
+ thread.sliceGroup.slices.forEach(function(slice) {
+ assert.strictEqual(slice.args !== undefined, true);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/parser.html
new file mode 100644
index 00000000000..42151aea319
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/parser.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for linux perf event parsers.
+ *
+ * The linux perf trace event importer depends on subclasses of
+ * Parser to parse event data. Each subclass corresponds
+ * to a group of trace events; e.g. SchedParser implements
+ * parsing of sched:* kernel trace events. Parser subclasses must
+ * call Parser.register to arrange to be instantiated
+ * and their constructor must register their event handlers with the
+ * importer. For example,
+ *
+ * var Parser = tr.e.importer.linux_perf.Parser;
+ *
+ * function WorkqueueParser(importer) {
+ * Parser.call(this, importer);
+ *
+ * importer.registerEventHandler('workqueue_execute_start',
+ * WorkqueueParser.prototype.executeStartEvent.bind(this));
+ * importer.registerEventHandler('workqueue_execute_end',
+ * WorkqueueParser.prototype.executeEndEvent.bind(this));
+ * }
+ *
+ * Parser.register(WorkqueueParser);
+ *
+ * When a registered event name is found in the data stream the associated
+ * event handler is invoked:
+ *
+ * executeStartEvent: function(eventName, cpuNumber, ts, eventBase)
+ *
+ * If the routine returns false the caller will generate an import error
+ * saying there was a problem parsing it. Handlers can also emit import
+ * messages using this.importer.model.importWarning. If this is done in lieu of
+ * the generic import error it may be desirable for the handler to return
+ * true.
+ *
+ * Trace events generated by writing to the trace_marker file are expected
+ * to have a leading text marker followed by a ':'; e.g. the trace clock
+ * synchronization event is:
+ *
+ * tracing_mark_write: trace_event_clock_sync: parent_ts=0
+ *
+ * To register an event handler for these events, prepend the marker with
+ * 'tracing_mark_write:'; e.g.
+ *
+ * this.registerEventHandler('tracing_mark_write:trace_event_clock_sync',
+ *
+ * All subclasses should depend on importer.linux_perf.parser, e.g.
+ *
+ * tr.defineModule('importer.linux_perf.workqueue_parser')
+ * .dependsOn('importer.linux_perf.parser')
+ * .exportsTo('tracing', function()
+ *
+ * and be listed in the dependsOn of FTraceImporter. Beware that after adding a
+ * new subclass you must run build/generate_about_tracing_contents.py to
+ * regenerate tr.ui.e.about_tracing.*.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ /**
+ * Parses linux perf events.
+ * @constructor
+ */
+ function Parser(importer) {
+ this.importer = importer;
+ this.model = importer.model;
+ }
+
+ Parser.prototype = {
+ __proto__: Object.prototype
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.mandatoryBaseClass = Parser;
+ tr.b.decorateExtensionRegistry(Parser, options);
+
+ return {
+ Parser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser.html
new file mode 100644
index 00000000000..90a668eb3d2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses power events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux power trace events.
+ * @constructor
+ */
+ function PowerParser(importer) {
+ Parser.call(this, importer);
+
+ // NB: old-style power events, deprecated
+ importer.registerEventHandler('power_start',
+ PowerParser.prototype.powerStartEvent.bind(this));
+ importer.registerEventHandler('power_frequency',
+ PowerParser.prototype.powerFrequencyEvent.bind(this));
+
+ importer.registerEventHandler('cpu_frequency',
+ PowerParser.prototype.cpuFrequencyEvent.bind(this));
+ importer.registerEventHandler('cpu_frequency_limits',
+ PowerParser.prototype.cpuFrequencyLimitsEvent.bind(this));
+ importer.registerEventHandler('cpu_idle',
+ PowerParser.prototype.cpuIdleEvent.bind(this));
+ }
+
+ PowerParser.prototype = {
+ __proto__: Parser.prototype,
+
+ cpuStateSlice(ts, targetCpuNumber, eventType, cpuState) {
+ const targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+ if (eventType !== '1') {
+ this.importer.model.importWarning({
+ type: 'parse_error',
+ message: 'Don\'t understand power_start events of ' +
+ 'type ' + eventType
+ });
+ return;
+ }
+ const powerCounter = targetCpu.getOrCreateCounter('', 'C-State');
+ if (powerCounter.numSeries === 0) {
+ powerCounter.addSeries(new tr.model.CounterSeries('state',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ powerCounter.name + '.' + 'state')));
+ }
+ powerCounter.series.forEach(function(series) {
+ series.addCounterSample(ts, cpuState);
+ });
+ },
+
+ cpuIdleSlice(ts, targetCpuNumber, cpuState) {
+ const targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+ const powerCounter = targetCpu.getOrCreateCounter('', 'C-State');
+ if (powerCounter.numSeries === 0) {
+ powerCounter.addSeries(new tr.model.CounterSeries('state',
+ ColorScheme.getColorIdForGeneralPurposeString(powerCounter.name)));
+ }
+ // NB: 4294967295/-1 means an exit from the current state
+ const val = (cpuState !== 4294967295 ? cpuState + 1 : 0);
+ powerCounter.series.forEach(function(series) {
+ series.addCounterSample(ts, val);
+ });
+ },
+
+ cpuFrequencySlice(ts, targetCpuNumber, powerState) {
+ const targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+ const powerCounter =
+ targetCpu.getOrCreateCounter('', 'Clock Frequency');
+ if (powerCounter.numSeries === 0) {
+ powerCounter.addSeries(new tr.model.CounterSeries('state',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ powerCounter.name + '.' + 'state')));
+ }
+ powerCounter.series.forEach(function(series) {
+ series.addCounterSample(ts, powerState);
+ });
+ },
+
+ cpuFrequencyLimitsSlice(ts, targetCpuNumber, minFreq, maxFreq) {
+ const targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+ const powerCounter =
+ targetCpu.getOrCreateCounter('', 'Clock Frequency Limits');
+ if (powerCounter.numSeries === 0) {
+ powerCounter.addSeries(new tr.model.CounterSeries('Min Frequency',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ powerCounter.name + '.' + 'Min Frequency')));
+ powerCounter.addSeries(new tr.model.CounterSeries('Max Frequency',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ powerCounter.name + '.' + 'Max Frequency')));
+ }
+ powerCounter.series.forEach(function(series) {
+ if (series.name === 'Min Frequency') {
+ series.addCounterSample(ts, minFreq);
+ }
+ if (series.name === 'Max Frequency') {
+ series.addCounterSample(ts, maxFreq);
+ }
+ });
+ },
+
+ /**
+ * Parses power events and sets up state in the importer.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser_test.html
new file mode 100644
index 00000000000..99335c464c8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/power_parser_test.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('powerFrequencyImport', function() {
+ const lines = [
+ ' kworker/0:3-6880 [000] 2784.783015: power_frequency: ' +
+ 'type=2 state=1000000 cpu_id=0',
+ ' kworker/1:2-7269 [001] 2784.788993: power_frequency: ' +
+ 'type=2 state=800000 cpu_id=1',
+ ' kworker/1:2-7269 [001] 2784.993120: power_frequency: ' +
+ 'type=2 state=1300000 cpu_id=1'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c0 = m.kernel.cpus[0];
+ assert.strictEqual(c0.slices.length, 0);
+ assert.strictEqual(
+ c0.counters['.Clock Frequency'].series[0].samples.length, 1);
+
+ const c1 = m.kernel.cpus[1];
+ assert.strictEqual(c1.slices.length, 0);
+ assert.strictEqual(
+ c1.counters['.Clock Frequency'].series[0].samples.length, 2);
+ });
+
+ test('cpuFrequencyImport', function() {
+ const lines = [
+ ' kworker/1:0-9665 [001] 15051.007301: cpu_frequency: ' +
+ 'state=800000 cpu_id=1',
+ ' kworker/1:0-9665 [001] 15051.010278: cpu_frequency: ' +
+ 'state=1300000 cpu_id=1',
+ ' kworker/0:2-7972 [000] 15051.010278: cpu_frequency: ' +
+ 'state=1000000 cpu_id=0',
+ ' kworker/0:2-7972 [000] 15051.020304: cpu_frequency: ' +
+ 'state=800000 cpu_id=0'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c0 = m.kernel.cpus[0];
+ assert.strictEqual(c0.slices.length, 0);
+ assert.strictEqual(
+ c0.counters['.Clock Frequency'].series[0].samples.length, 2);
+
+ const c1 = m.kernel.cpus[1];
+ assert.strictEqual(c1.slices.length, 0);
+ assert.strictEqual(
+ c1.counters['.Clock Frequency'].series[0].samples.length, 2);
+ });
+
+ test('cpuIdleImport', function() {
+ const lines = [
+ ' <idle>-0 [000] 15050.992883: cpu_idle: ' +
+ 'state=1 cpu_id=0',
+ ' <idle>-0 [000] 15050.993027: cpu_idle: ' +
+ 'state=4294967295 cpu_id=0',
+ ' <idle>-0 [001] 15050.993132: cpu_idle: ' +
+ 'state=1 cpu_id=1',
+ ' <idle>-0 [001] 15050.993276: cpu_idle: ' +
+ 'state=4294967295 cpu_id=1',
+ ' <idle>-0 [001] 15050.993279: cpu_idle: ' +
+ 'state=3 cpu_id=1',
+ ' <idle>-0 [001] 15050.993457: cpu_idle: ' +
+ 'state=4294967295 cpu_id=1'
+ ];
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const c0 = m.kernel.cpus[0];
+ assert.strictEqual(c0.slices.length, 0);
+ assert.strictEqual(c0.counters['.C-State'].series[0].samples.length, 2);
+
+ const c1 = m.kernel.cpus[1];
+ assert.strictEqual(c1.slices.length, 0);
+ assert.strictEqual(c1.counters['.C-State'].series[0].samples.length, 4);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser.html
new file mode 100644
index 00000000000..c92767eaba2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses regulator events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux regulator trace events.
+ * @constructor
+ */
+ 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_;
+ }
+
+ // Matches the regulator_enable record
+ const regulatorEnableRE = /name=(.+)/;
+
+ // Matches the regulator_disable record
+ const regulatorDisableRE = /name=(.+)/;
+
+ // Matches the regulator_set_voltage_complete record
+ const regulatorSetVoltageCompleteRE = /name=(\S+), val=(\d+)/;
+
+ RegulatorParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Get or create a counter with one series.
+ */
+ getCtr_(ctrName, valueName) {
+ const ctr = this.model_.kernel
+ .getOrCreateCounter(null, 'vreg ' + ctrName + ' ' + valueName);
+ // Initialize the counter's series fields if needed.
+ if (ctr.series[0] === undefined) {
+ ctr.addSeries(new tr.model.CounterSeries(valueName,
+ ColorScheme.getColorIdForGeneralPurposeString(
+ ctrName + '.' + valueName)));
+ }
+ return ctr;
+ },
+
+ /**
+ * Parses regulator events and sets up state in the importer.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser_test.html
new file mode 100644
index 00000000000..55ffc85c4aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/regulator_parser_test.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('regulatorImport', function() {
+ const lines = [
+ ' kworker/0:2H-14312 [000] ...1 143713.787749: ' +
+ 'regulator_set_voltage: name=krait0 (810000-1100000)',
+ ' kworker/0:2H-14312 [000] ...1 143713.787778: ' +
+ 'regulator_set_voltage_complete: name=krait0, val=810000',
+ ' kworker/0:2H-14312 [000] ...1 143714.037871: ' +
+ 'regulator_set_voltage: name=krait0 (800000-1100000)',
+ ' kworker/0:2H-14312 [000] ...1 143714.037895: ' +
+ 'regulator_set_voltage_complete: name=krait0, val=800000',
+ 'kworker/0:1-30321 [000] ...1 144568.624596: ' +
+ 'regulator_enable: name=8941_smbb_boost',
+ 'kworker/0:1-30321 [000] ...1 144568.624715: ' +
+ 'regulator_enable_delay: name=8941_smbb_boost',
+ 'kworker/0:1-30321 [000] ...1 144568.624723: ' +
+ 'regulator_enable_complete: name=8941_smbb_boost',
+ 'kworker/0:1-30321 [000] ...1 144568.653546: ' +
+ 'regulator_disable: name=8941_smbb_boost',
+ 'kworker/0:1-30321 [000] ...1 144568.654785: ' +
+ 'regulator_disable_complete: name=8941_smbb_boost'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.property(m.kernel.counters, 'null.vreg krait0 voltage');
+ assert.property(m.kernel.counters, 'null.vreg 8941_smbb_boost enabled');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser.html
new file mode 100644
index 00000000000..70192df1b5b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses rss stat events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses Rss stat trace events.
+ * @constructor
+ */
+ function RssParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('rss_stat',
+ RssParser.prototype.rssStat.bind(this));
+ }
+
+ const TestExports = {};
+
+ // Matches the rss_stat record
+ const rssStatRE = new RegExp('member=(\\d+) size=(\\d+)');
+
+ TestExports.rssStatRE = rssStatRE;
+
+ const unknownThreadName = '<...>';
+
+ RssParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses rss stat events and sets up state in the importer.
+ */
+ 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
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser_test.html
new file mode 100644
index 00000000000..7ac477eed50
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/rss_parser_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('rssStatRE', function() {
+ const re = tr.e.importer.linux_perf._RssParserTestExports.rssStatRE;
+ let x = re.exec('rss_stat: member=0 size=536576');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], '0');
+ assert.strictEqual(x[2], '536576');
+
+ x = re.exec('rss_stat: member=1 size=716800');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], '1');
+ assert.strictEqual(x[2], '716800');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser.html
new file mode 100644
index 00000000000..3004f577f1f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser.html
@@ -0,0 +1,161 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/tracing/model/counter.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses scheduler events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux sched trace events.
+ * @constructor
+ */
+ 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 = {};
+
+ // Matches the sched_switch record
+ 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+)');
+
+ // Matches sched_blocked_reason record
+ const schedBlockedRE = new RegExp('pid=(\\d+) iowait=(\\d) caller=(.+)');
+ TestExports.schedSwitchRE = schedSwitchRE;
+
+ // Matches the sched_wakeup record
+ // success=? is optional not all kernels report it, so don't include
+ // it in the capture groups
+ const schedWakeupRE =
+ /comm=(.+) pid=(\d+) prio=(\d+)(?: success=\d+)? target_cpu=(\d+)/;
+ TestExports.schedWakeupRE = schedWakeupRE;
+
+ const unknownThreadName = '<...>';
+
+ SchedParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses scheduler events and sets up state in the CPUs of the importer.
+ */
+ 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')));
+ }
+ powerCounter.series.forEach(function(series) {
+ if (series.name === 'State') {
+ series.addCounterSample(ts, state.localeCompare('offline') ? 0 : 1);
+ }
+ });
+ return true;
+ },
+
+ schedBlockedEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = schedBlockedRE.exec(eventBase.details);
+ if (!event) return false;
+
+ pid = parseInt(event[1]);
+ const iowait = parseInt(event[2]);
+ const caller = event[3];
+
+ this.importer.addPidBlockedReason(ts, pid, iowait, caller);
+ return true;
+ }
+ };
+
+ Parser.register(SchedParser);
+
+ return {
+ SchedParser,
+ _SchedParserTestExports: TestExports
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser_test.html
new file mode 100644
index 00000000000..998d7f4ab21
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sched_parser_test.html
@@ -0,0 +1,262 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('schedSwitchRE', function() {
+ const re = tr.e.importer.linux_perf._SchedParserTestExports.schedSwitchRE;
+ let x = re.exec('prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ' +
+ '==> next_comm=SurfaceFlinger next_pid=178 next_prio=112');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], 'swapper');
+ assert.strictEqual(x[2], '0');
+ assert.strictEqual(x[3], '120');
+ assert.strictEqual(x[4], 'R');
+ assert.strictEqual(x[5], 'SurfaceFlinger');
+ assert.strictEqual(x[6], '178');
+ assert.strictEqual(x[7], '112');
+
+ x = re.exec('prev_comm=.android.chrome prev_pid=1562 prev_prio=120 prev_state=R ==> next_comm=Binder Thread # next_pid=195 next_prio=120'); // @suppress longLineCheck
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], '.android.chrome');
+ assert.strictEqual(x[5], 'Binder Thread #');
+
+ x = re.exec('prev_comm=Binder Thread # prev_pid=1562 prev_prio=120 prev_state=R ==> next_comm=.android.chrome next_pid=195 next_prio=120'); // @suppress longLineCheck
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], 'Binder Thread #');
+ assert.strictEqual(x[5], '.android.chrome');
+
+ // explicit test for prev_state of D|W
+ x = re.exec('prev_comm=.android.chrome prev_pid=1562 prev_prio=120 ' +
+ 'prev_state=D|W ==> next_comm=Binder Thread # next_pid=195 ' +
+ 'next_prio=120');
+ assert.isNotNull(x);
+ assert.strictEqual(x[4], 'D|W');
+ });
+
+ test('schedWakeupRE', function() {
+ const re = tr.e.importer.linux_perf._SchedParserTestExports.schedWakeupRE;
+ const x = re.exec(
+ 'comm=SensorService pid=207 prio=112 success=1 target_cpu=000');
+ assert.isNotNull(x);
+ assert.strictEqual(x[1], 'SensorService');
+ assert.strictEqual(x[2], '207');
+ assert.strictEqual(x[3], '112');
+ assert.strictEqual(x[4], '000');
+ const y = re.exec(
+ 'comm=SensorService pid=207 prio=112 target_cpu=000');
+ assert.isNotNull(y);
+ assert.strictEqual(y[1], 'SensorService');
+ assert.strictEqual(y[2], '207');
+ assert.strictEqual(y[3], '112');
+ assert.strictEqual(y[4], '000');
+ });
+
+ test('importOneSequenceWithSchedWakeUp', function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const lines = [
+ 'ndroid.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=ndroid.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=ndroid.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'ndroid.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'ndroid.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=ndroid.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=ndroid.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+ const timeSlices = thread.timeSlices;
+ assert.strictEqual(timeSlices.length, 4);
+
+ const runningSlice = timeSlices[0];
+ assert.strictEqual(runningSlice.schedulingState, SCHEDULING_STATE.RUNNING);
+ assert.closeTo(12622506.890, runningSlice.start, 1e-5);
+ assert.closeTo(.918 - .890, runningSlice.duration, 1e-5);
+
+ const sleepSlice = timeSlices[1];
+ assert.strictEqual(
+ sleepSlice.schedulingState, SCHEDULING_STATE.UNINTR_SLEEP);
+ assert.closeTo(12622506.918, sleepSlice.start, 1e-5);
+ assert.closeTo(.936 - .918, sleepSlice.duration, 1e-5);
+
+ const wakeupSlice = timeSlices[2];
+ assert.strictEqual(
+ wakeupSlice.schedulingState, SCHEDULING_STATE.RUNNABLE);
+ assert.closeTo(12622506.936, wakeupSlice.start, 1e-5);
+ assert.closeTo(.950 - .936, wakeupSlice.duration, 1e-5);
+ assert.strictEqual(wakeupSlice.args['wakeup from tid'], 584);
+
+ const runningSlice2 = timeSlices[3];
+ assert.strictEqual(
+ runningSlice2.schedulingState, SCHEDULING_STATE.RUNNING);
+ assert.closeTo(12622506.950, runningSlice2.start, 1e-5);
+ assert.closeTo(7.253 - 6.950, runningSlice2.duration, 1e-5);
+ });
+
+ test('importWithUnknownSleepState', function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const lines = [
+ 'ndroid.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=ndroid.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=F|O ==> next_comm=ndroid.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'ndroid.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'ndroid.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=ndroid.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=F|O ==> next_comm=ndroid.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ let m;
+ assert.doesNotThrow(function() {
+ m = newModel(lines.join('\n'));
+ });
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(
+ m.importWarnings[0].message, 'Unrecognized sleep state: F|O');
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+ const timeSlices = thread.timeSlices;
+
+ assert.strictEqual(timeSlices[1].schedulingState, SCHEDULING_STATE.UNKNOWN);
+ });
+
+ test('importWithUninterruptibleSleep', function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const lines = [
+ 'ndroid.launcher-584 [001] d..3 12622.506890: sched_switch: ' +
+ 'prev_comm=ndroid.launcher prev_pid=584 ' +
+ 'prev_prio=120 prev_state=R+ ' +
+ '==> next_comm=Binder_1 next_pid=217 next_prio=120',
+
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: ' +
+ 'prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D|K ' +
+ '==> next_comm=ndroid.launcher next_pid=584 next_prio=120',
+
+ 'ndroid.launcher-584 [001] d..4 12622.506936: sched_wakeup: ' +
+ 'comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001',
+
+ 'ndroid.launcher-584 [001] d..3 12622.506950: sched_switch: ' +
+ 'prev_comm=ndroid.launcher prev_pid=584 ' +
+ 'prev_prio=120 prev_state=R+ ' +
+ '==> next_comm=Binder_1 next_pid=217 next_prio=120',
+
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: ' +
+ 'B|128|queueBuffer',
+
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: ' +
+ 'prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ' +
+ '==> next_comm=ndroid.launcher next_pid=584 next_prio=120'
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+ const timeSlices = thread.timeSlices;
+ assert.strictEqual(timeSlices.length, 4);
+
+ const wakeKillSlice = timeSlices[1];
+ assert.strictEqual(wakeKillSlice.schedulingState,
+ SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL);
+ assert.closeTo(12622506.918, wakeKillSlice.start, 1e-5);
+ assert.closeTo(.936 - .918, wakeKillSlice.duration, 1e-5);
+ });
+
+ test('importWithUninterruptibleSleepAndBlockedReason', function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const lines = [
+ 'ndroid.launcher-584 [001] d..3 12622.506890: sched_switch: ' +
+ 'prev_comm=ndroid.launcher prev_pid=584 ' +
+ 'prev_prio=120 prev_state=R+ ' +
+ '==> next_comm=Binder_1 next_pid=217 next_prio=120',
+
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: ' +
+ 'prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D|K ' +
+ '==> next_comm=ndroid.launcher next_pid=584 next_prio=120',
+
+ ' Binder_1-217 [001] d..3 12622.506930: sched_blocked_reason: ' +
+ 'pid=217 iowait=1 caller=sleep_on_page_killable+0x10/0x4c',
+
+ 'ndroid.launcher-584 [001] d..4 12622.506936: sched_wakeup: ' +
+ 'comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001',
+
+ 'ndroid.launcher-584 [001] d..3 12622.506950: sched_switch: ' +
+ 'prev_comm=ndroid.launcher prev_pid=584 ' +
+ 'prev_prio=120 prev_state=R+ ' +
+ '==> next_comm=Binder_1 next_pid=217 next_prio=120',
+
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: ' +
+ 'B|128|queueBuffer',
+
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: ' +
+ 'prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ' +
+ '==> next_comm=ndroid.launcher next_pid=584 next_prio=120'
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+ const timeSlices = thread.timeSlices;
+ assert.strictEqual(timeSlices.length, 4);
+
+ const wakeKillSlice = timeSlices[1];
+ assert.strictEqual(wakeKillSlice.schedulingState,
+ SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL_IO);
+ assert.closeTo(12622506.918, wakeKillSlice.start, 1e-5);
+ assert.closeTo(.936 - .918, wakeKillSlice.duration, 1e-5);
+ });
+
+ test('importWithTgids', function() {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const lines = [
+ 'kworker/u12:4-9393 ( 9393) [002] d..3 10386.798979: sched_switch: ' +
+ 'prev_comm=kworker/u12:4 prev_pid=9393 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120',
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const p = m.processes[9393];
+ assert.strictEqual(p.pid, 9393);
+ });
+
+ test('importWithMissingNames', function() {
+ const lines = [
+ ' Binder_1-217 ( 438) [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=F|O ==> next_comm=ndroid.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ '<...>-584 ( 954) [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'ndroid.launcher-584 ( 954) [001] d..3 12622.506950: sched_switch: prev_comm=ndroid.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ];
+
+ const m = newModel(lines.join('\n'));
+ assert.isFalse(m.hasImportWarnings);
+
+ const p = m.processes[954];
+ const t = p.getThread(584);
+ assert.strictEqual(t.name, 'ndroid.launcher');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser.html
new file mode 100644
index 00000000000..2e96b8714b6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses sync events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux sync trace events.
+ * @constructor
+ */
+ function SyncParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler(
+ 'sync_timeline',
+ SyncParser.prototype.timelineEvent.bind(this));
+ importer.registerEventHandler(
+ 'sync_wait',
+ SyncParser.prototype.syncWaitEvent.bind(this));
+ importer.registerEventHandler(
+ 'sync_pt',
+ SyncParser.prototype.syncPtEvent.bind(this));
+ this.model_ = importer.model_;
+ }
+
+ const syncTimelineRE = /name=(\S+) value=(\S*)/;
+ const syncWaitRE = /(\S+) name=(\S+) state=(\d+)/;
+ const syncPtRE = /name=(\S+) value=(\S*)/;
+
+ SyncParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses sync events and sets up state in the importer.
+ */
+ timelineEvent(eventName, cpuNumber, pid,
+ ts, eventBase) {
+ const event = syncTimelineRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const thread = this.importer.getOrCreatePseudoThread(event[1]);
+
+ if (thread.lastActiveTs !== undefined) {
+ const duration = ts - thread.lastActiveTs;
+ let value = thread.lastActiveValue;
+ if (value === undefined) value = ' ';
+ const slice = new tr.model.ThreadSlice(
+ '', value,
+ ColorScheme.getColorIdForGeneralPurposeString(value),
+ thread.lastActiveTs, {},
+ duration);
+ thread.thread.sliceGroup.pushSlice(slice);
+ }
+ thread.lastActiveTs = ts;
+ thread.lastActiveValue = event[2];
+ return true;
+ },
+
+ syncWaitEvent(eventName, cpuNumber, pid, ts,
+ eventBase) {
+ const event = syncWaitRE.exec(eventBase.details);
+ if (!event) return false;
+
+ if (eventBase.tgid === undefined) {
+ 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 = 'fence_wait("' + event[2] + '")';
+ if (event[1] === 'begin') {
+ const slice = slices.beginSlice(null, name, ts, {
+ 'Start state': event[3]
+ });
+ } else if (event[1] === 'end') {
+ if (slices.openSliceCount > 0) {
+ slices.endSlice(ts);
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ },
+
+ syncPtEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ return !!syncPtRE.exec(eventBase.details);
+ }
+ };
+
+ Parser.register(SyncParser);
+
+ return {
+ SyncParser,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser_test.html
new file mode 100644
index 00000000000..eaf824acad6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/sync_parser_test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('syncEventImport', function() {
+ const lines = [
+ 's3c-fb-92 ( 0) [000] ...1 7206.550061: sync_timeline: name=s3c-fb value=7094', // @suppress longLineCheck
+ 'TimedEventQueue-2700 ( 0) [001] ...1 7206.569027: sync_wait: begin name=SurfaceView:6 state=1', // @suppress longLineCheck
+ 'TimedEventQueue-2700 ( 0) [001] ...1 7206.569038: sync_pt: name=malitl_124_0x40b6406c value=7289', // @suppress longLineCheck
+ 'TimedEventQueue-2700 ( 0) [001] ...1 7206.569056: sync_pt: name=exynos-gsc.0-src value=25', // @suppress longLineCheck
+ 'TimedEventQueue-2700 ( 0) [001] ...1 7206.569068: sync_wait: end name=SurfaceView:6 state=1', // @suppress longLineCheck
+ 'irq/128-s5p-mfc-62 ( 0) [000] d..3 7206.572402: sync_timeline: name=vb2 value=37', // @suppress longLineCheck
+ 'irq/128-s5p-mfc-62 ( 0) [000] d..3 7206.572475: sync_timeline: name=vb2 value=33', // @suppress longLineCheck
+ 'SurfaceFlinger-225 ( 0) [001] ...1 7206.584769: sync_timeline: name=malitl_124_0x40b6406c value=7290', // @suppress longLineCheck
+ 'kworker/u:5-2269 ( 0) [000] ...1 7206.586745: sync_wait: begin name=display state=1', // @suppress longLineCheck
+ 'kworker/u:5-2269 ( 0) [000] ...1 7206.586750: sync_pt: name=s3c-fb value=7093', // @suppress longLineCheck
+ 'kworker/u:5-2269 ( 0) [000] ...1 7206.586760: sync_wait: end name=display state=1', // @suppress longLineCheck
+ 's3c-fb-92 ( 0) [000] ...1 7206.587193: sync_wait: begin name=vb2 state=0', // @suppress longLineCheck
+ 's3c-fb-92 ( 0) [000] ...1 7206.587198: sync_pt: name=exynos-gsc.0-dst value=27', // @suppress longLineCheck
+ '<idle>-0 ( 0) [000] d.h4 7206.591133: sync_timeline: name=exynos-gsc.0-src value=27', // @suppress longLineCheck
+ '<idle>-0 ( 0) [000] d.h4 7206.591152: sync_timeline: name=exynos-gsc.0-dst value=27', // @suppress longLineCheck
+ 's3c-fb-92 ( 0) [000] ...1 7206.591244: sync_wait: end name=vb2 state=1' // @suppress longLineCheck
+ ];
+
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ let threads = m.getAllThreads();
+ assert.strictEqual(threads.length, 4);
+
+ threads = m.findAllThreadsNamed('s3c-fb');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+
+ threads = m.findAllThreadsNamed('kworker/u:5');
+ assert.strictEqual(threads.length, 1);
+ assert.strictEqual(threads[0].sliceGroup.length, 1);
+ assert.strictEqual('fence_wait("display")',
+ threads[0].sliceGroup.slices[0].title);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser.html
new file mode 100644
index 00000000000..5e9ab9db1b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses workqueue events in the Linux event trace format.
+ */
+tr.exportTo('tr.e.importer.linux_perf', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Parser = tr.e.importer.linux_perf.Parser;
+
+ /**
+ * Parses linux workqueue trace events.
+ * @constructor
+ */
+ function WorkqueueParser(importer) {
+ Parser.call(this, importer);
+
+ importer.registerEventHandler('workqueue_execute_start',
+ WorkqueueParser.prototype.executeStartEvent.bind(this));
+ importer.registerEventHandler('workqueue_execute_end',
+ WorkqueueParser.prototype.executeEndEvent.bind(this));
+ importer.registerEventHandler('workqueue_queue_work',
+ WorkqueueParser.prototype.executeQueueWork.bind(this));
+ importer.registerEventHandler('workqueue_activate_work',
+ WorkqueueParser.prototype.executeActivateWork.bind(this));
+ }
+
+ // Matches the workqueue_execute_start record
+ // workqueue_execute_start: work struct c7a8a89c: function MISRWrapper
+ const workqueueExecuteStartRE = /work struct (.+): function (\S+)/;
+
+ // Matches the workqueue_execute_start record
+ // workqueue_execute_end: work struct c7a8a89c
+ const workqueueExecuteEndRE = /work struct (.+)/;
+
+ WorkqueueParser.prototype = {
+ __proto__: Parser.prototype,
+
+ /**
+ * Parses workqueue events and sets up state in the importer.
+ */
+ executeStartEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = workqueueExecuteStartRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, pid, pid);
+ kthread.openSliceTS = ts;
+ kthread.openSlice = event[2];
+ return true;
+ },
+
+ executeEndEvent(eventName, cpuNumber, pid, ts, eventBase) {
+ const event = workqueueExecuteEndRE.exec(eventBase.details);
+ if (!event) return false;
+
+ const kthread = this.importer.getOrCreateKernelThread(
+ eventBase.threadName, pid, pid);
+ if (kthread.openSlice) {
+ const slice = new tr.model.ThreadSlice('', kthread.openSlice,
+ ColorScheme.getColorIdForGeneralPurposeString(kthread.openSlice),
+ kthread.openSliceTS,
+ {},
+ ts - kthread.openSliceTS);
+
+ kthread.thread.sliceGroup.pushSlice(slice);
+ }
+ kthread.openSlice = undefined;
+ return true;
+ },
+
+ executeQueueWork(eventName, cpuNumber, pid, ts, eventBase) {
+ // TODO: Do something with this event?
+ return true;
+ },
+
+ executeActivateWork(eventName, cpuNumber, pid, ts, eventBase) {
+ // TODO: Do something with this event?
+ return true;
+ }
+
+ };
+
+ Parser.register(WorkqueueParser);
+
+ return {
+ WorkqueueParser,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser_test.html
new file mode 100644
index 00000000000..dfd6646d27f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/linux_perf/workqueue_parser_test.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('workQueueImport', function() {
+ const lines = [
+ ' kworker/0:3-6880 [000] 2784.771958: workqueue_execute_start: ' +
+ 'work struct ffff8800a5083a20: function intel_unpin_work_fn',
+ ' kworker/0:3-6880 [000] 2784.771966: workqueue_execute_end: ' +
+ 'work struct ffff8800a5083a20',
+ ' kworker/1:2-7269 [001] 2784.805966: workqueue_execute_start: ' +
+ 'work struct ffff88014fb0f158: function do_dbs_timer',
+ ' kworker/1:2-7269 [001] 2784.805975: workqueue_execute_end: ' +
+ 'work struct ffff88014fb0f158'
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.strictEqual(
+ m.processes['6880'].threads['6880'].sliceGroup.length, 1);
+ assert.strictEqual(
+ m.processes['7269'].threads['7269'].sliceGroup.length, 1);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/oboe.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/oboe.html
new file mode 100644
index 00000000000..e9ae74e5e08
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/oboe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+if (tr.isHeadless) {
+ // oboe requires window; there is no window object in headless mode.
+ // Temporarily make one.
+ global.window = {};
+}
+</script>
+
+<script id="oboe-script" src="/oboe/dist/oboe-browser.js"></script>
+
+<script>
+'use strict';
+if (tr.isVinn) {
+ global.oboe = global.window.oboe;
+ global.window = undefined;
+} else if (tr.isNode) {
+ // Use the node.js version instead.
+ global.window = undefined;
+ const path = HTMLImportsLoader.hrefToAbsolutePath('/oboe/dist/oboe-node.js');
+ global.oboe = require(path);
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/pako.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/pako.html
new file mode 100644
index 00000000000..691a55c0217
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/pako.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script src="/pako.min.js"></script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader.html
new file mode 100644
index 00000000000..ae8c51334e8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader.html
@@ -0,0 +1,315 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.importer', function() {
+ const STRING_ID_SUFFIX = '_sid';
+ const PLURAL_STRING_ID_SUFFIX = '_sids';
+
+ function isStringReference(s) {
+ return s.endsWith(STRING_ID_SUFFIX) || s.endsWith(PLURAL_STRING_ID_SUFFIX);
+ }
+
+ function getStringReferenceName(name) {
+ if (name.endsWith(PLURAL_STRING_ID_SUFFIX)) {
+ return name.slice(0, -PLURAL_STRING_ID_SUFFIX.length);
+ }
+ return name.slice(0, -STRING_ID_SUFFIX.length);
+ }
+
+ function deferenceStrings(idToString, o) {
+ const clone = Object.assign({}, o);
+ for (const [key, value] of Object.entries(clone)) {
+ if (isStringReference(key)) {
+ const name = getStringReferenceName(key);
+ clone[name] = idToString(value);
+ }
+ }
+ return clone;
+ }
+
+ function singularize(word) {
+ if (word.endsWith('s')) {
+ return word.slice(0, -1);
+ }
+ return word;
+ }
+
+ function getMetadataPairs(dataJson) {
+ const isMetadata = v => typeof v !== 'object' || Array.isArray(v);
+ const pairs = Object.entries(dataJson);
+ const metadataPairs = pairs.filter(([_, v]) => isMetadata(v));
+ return metadataPairs;
+ }
+
+ function getGroupPairs(dataJson) {
+ const pairs = Object.entries(dataJson);
+ const nonMapPairs = pairs.filter(([k, _]) => k !== 'maps');
+ const groupPairs = nonMapPairs.filter(([_, v]) => typeof v === 'object');
+ return groupPairs;
+ }
+
+ function createMap(mapJson) {
+ const map = new Map();
+ for (const entry of mapJson) {
+ if (entry.id === undefined) {
+ throw new Error('Missing required key "id" in streaming event.');
+ }
+ map.set(entry.id, entry);
+ }
+ return map;
+ }
+
+ function createMaps(mapsJson) {
+ const maps = new Map();
+ for (const [name, mapJson] of Object.entries(mapsJson)) {
+ maps.set(name, createMap(mapJson));
+ }
+ return maps;
+ }
+
+ function createGroup(groupJson, opt_startTime) {
+ const entries = [];
+ const n = Object.values(groupJson)[0].length;
+
+ for (let i = 0; i < n; i++) {
+ const entry = {};
+ for (const name in groupJson) {
+ entry[name] = groupJson[name][i];
+ }
+ entries.push(entry);
+ }
+
+ const timeDelta = groupJson.timeDelta;
+ if (opt_startTime === undefined && timeDelta !== undefined) {
+ throw new Error('Missing required key "startTime" in streaming event.');
+ }
+
+ if (opt_startTime) {
+ let delta = 0;
+ for (const entry of entries) {
+ delta += entry.timeDelta ? entry.timeDelta : 0;
+ entry.time = opt_startTime + delta;
+ }
+ }
+
+ return entries;
+ }
+
+ function createGroups(groupsJson, opt_startTime) {
+ const groups = new Map();
+ for (const [name, groupJson] of Object.entries(groupsJson)) {
+ groups.set(name, createGroup(groupJson, opt_startTime));
+ }
+
+ return groups;
+ }
+
+ function createMetadata(metadataPairs) {
+ const metadata = new Map();
+ for (const [name, value] of metadataPairs) {
+ metadata.set(name, value);
+ }
+ if (metadata.get('version') === undefined) {
+ throw new Error('Missing required key "version" in streaming event.');
+ }
+ return metadata;
+ }
+
+ /**
+ * Extracts data from a profiling dictionary. See goo.gl/R0Ae4f.
+ *
+ * A profiling dictionary is a compressed format that is good for recording
+ * sampling data. ProfilingDictionaryReader unpacks that data. To use the
+ * ProfilingDictionaryReader first create an 'empty' reader using .empty()
+ * then call #expandData(data) on your dictionary or the helper:
+ * #expandEvent(event) on a tracing event containing the profiling dictionary.
+ * ProfilingDictionaryReader is an immutable data structure so these methods
+ * don't modify the ProfilingDictionaryReader instead they return new
+ * ProfilingDictionaryReaders which wrap the data you passed. To access the
+ * unpacked data use the #inflated property and the #getMapValue() method.
+ *
+ * Usage example, given input like:
+ * $ let input = {
+ * version: 1,
+ * allocators: {
+ * books: {
+ * authors: [1, 1, 2],
+ * title_sid: [10, 11, 12],
+ * },
+ * },
+ * maps: {
+ * authors: [
+ * { id: 1, name_sid: 1 },
+ * { id: 2, name_sid: 2 },
+ * ],
+ * strings: [
+ * { id: 1, string: 'DFW' },
+ * { id: 2, string: 'C. Stross' },
+ * { id: 10, string: 'Book A' },
+ * { id: 11, string: 'Book B' },
+ * { id: 12, string: 'Book C' },
+ * ],
+ * }
+ * };
+ * We can create an empty reader:
+ * $ let reader = ProfilingDictionaryReader.empty();
+ * Then read in the input:
+ * $ reader = reader.expandData(input);
+ * Then view the expanded data:
+ * $ console.log(reader.inflated);
+ * {
+ * books: [
+ * { author: { id: 1, name: 'DFW' }, title: "Book A", },
+ * { author: { id: 2, name: 'C. Stross' }, title: "Book B", },
+ * { author: { id: 2, name: 'C. Stross' }, title: "Book C", },
+ * ],
+ * }
+ *
+ */
+ class ProfilingDictionaryReader {
+ constructor(opt_metadata, opt_maps, opt_groups, opt_parent) {
+ this.metadata = opt_metadata || new Map();
+ this.maps = opt_maps || new Map();
+ this.groups = opt_groups || new Map();
+ this.parent_ = opt_parent || undefined;
+ this.inflated_ = undefined;
+ this.raw_ = undefined;
+ this.boundGetString_ = this.getString.bind(this);
+ this.deferenceStrings_ = o => deferenceStrings(this.boundGetString_, o);
+ }
+
+ /**
+ * Creates an empty ProfilingDictionaryReader.
+ */
+ static empty() {
+ return new ProfilingDictionaryReader();
+ }
+
+ /**
+ * Returns the parent or null if this is the root ProfilingDictionaryReader.
+ */
+ get parent() {
+ return this.parent_;
+ }
+
+ get raw() {
+ if (this.raw_) return this.raw_;
+ this.raw_ = {};
+ for (const [name, group] of this.groups.entries()) {
+ this.raw_[name] = group;
+ }
+ return this.raw_;
+ }
+
+ get inflated() {
+ if (this.inflated_) return this.inflated_;
+ this.inflated_ = {};
+ for (const [name, group] of this.groups.entries()) {
+ this.inflated_[name] = this.inflateGroup(group);
+ }
+ return this.inflated_;
+ }
+
+ /**
+ * Get a map from the newest event by name.
+ * If no map with that name was present returns an empty Map.
+ */
+ getNewMap(name) {
+ return this.maps.get(name) || new Map();
+ }
+
+ /**
+ * Get a record with the id |id| from the map with name |mapName|.
+ * This method searches through the expanded events in reverse order of
+ * expansion until it finds a matching value. If no value matches returns
+ * undefined.
+ */
+ getMapValue(mapName, id) {
+ let value = this.getNewMap(mapName).get(id);
+ if (value === undefined && this.parent) {
+ value = this.parent.getMapValue(mapName, id);
+ }
+ return value;
+ }
+
+ /**
+ * Get the string with the id |id|.
+ * This method searches through the expanded events in reverse order of
+ * expansion until it finds a string with the matching id. If there is no
+ * matching string with returns undefined.
+ */
+ getString(id) {
+ const value = this.getMapValue('strings', id);
+ if (value === undefined) return undefined;
+ return value.string;
+ }
+
+ /**
+ * True iff this or any parent has a map with name |name|.
+ */
+ hasMap(name) {
+ if (this.maps.has(name)) return true;
+ if (this.parent === undefined) return false;
+ return this.parent.hasMap(name);
+ }
+
+ inflateGroup(group) {
+ return group.map(this.inflateEntry.bind(this));
+ }
+
+ inflateEntry(entry) {
+ const inflatedEntry = {};
+ for (const [name, value] of Object.entries(entry)) {
+ let inflatedValue;
+ if (this.hasMap(name)) {
+ const id = value;
+ inflatedValue = this.deferenceStrings_(this.getMapValue(name, id));
+ } else {
+ inflatedValue = value;
+ }
+ inflatedEntry[singularize(name)] = inflatedValue;
+ }
+ return this.deferenceStrings_(inflatedEntry);
+ }
+
+ /**
+ * Returns a new ProfilingDictionaryReader with this
+ * ProfilingDictionaryReader as its parent and the fields 'maps', 'groups'
+ * and 'metadata' filled in based on |data|.
+ */
+ expandData(data) {
+ const mapsJson = data.maps || {};
+ const groupsJson = data.allocators || {};
+ const metadataPairs = getMetadataPairs(data);
+ const metadata = createMetadata(metadataPairs);
+ const opt_startTime = metadata.get('startTime');
+ const maps = createMaps(mapsJson);
+ const groups = createGroups(groupsJson, opt_startTime);
+ return new ProfilingDictionaryReader(metadata, maps, groups, this);
+ }
+
+ /**
+ * Convenience method for this.expandData(event.args.data).
+ */
+ expandEvent(event) {
+ return this.expandData(event.args.data);
+ }
+ }
+
+ return {
+ ProfilingDictionaryReader,
+ singularize,
+ deferenceStringsForTest: deferenceStrings,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader_test.html
new file mode 100644
index 00000000000..fb622e1fac9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/profiling_dictionary_reader_test.html
@@ -0,0 +1,275 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/profiling_dictionary_reader.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function makeEvent(json) {
+ return {
+ args: {
+ data: json
+ },
+ };
+ }
+
+ test('deferenceStrings', function() {
+ const stringToId = id => '' + id;
+ const result = tr.e.importer.deferenceStringsForTest(stringToId, {
+ foo: 'foo',
+ one_bar_sid: 1,
+ many_baz_sids: 2,
+ });
+ assert.deepEqual(result, {
+ foo: 'foo',
+ one_bar_sid: 1,
+ many_baz_sids: 2,
+ one_bar: '1',
+ many_baz: '2',
+ });
+ });
+
+ test('singularize', function() {
+ assert.strictEqual('name', tr.e.importer.singularize('names'));
+ assert.strictEqual('aircraft', tr.e.importer.singularize('aircraft'));
+ });
+
+ test('throws_an_error_if_version_key_missing', function() {
+ const data = {};
+ const expander = new tr.e.importer.ProfilingDictionaryReader();
+ assert.throws(() => expander.expandData(data));
+ });
+
+ test('throws_an_error_if_map_entry_is_missing_an_id', function() {
+ const data = {
+ maps: {
+ version: 1,
+ price: [
+ { 'i_dont_have_an_id_key': 100 },
+ ],
+ },
+ };
+ const expander = new tr.e.importer.ProfilingDictionaryReader();
+ assert.throws(() => expander.expandData(data));
+ });
+
+ test('has_an_empty_constructor_to_create_an_empty_reader', function() {
+ const expander = tr.e.importer.ProfilingDictionaryReader.empty();
+ assert.strictEqual(expander.metadata.size, 0);
+ assert.deepEqual(expander.raw, {});
+ assert.deepEqual(expander.inflated, {});
+ });
+
+ test('reads_extra_metadata', function() {
+ const data = {
+ version: 1,
+ myStringMetadata: '1',
+ myNumberMetadata: 2,
+ myArrayMetadata: [3],
+ };
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandData(data);
+ assert.deepEqual(expander2.metadata.get('myStringMetadata'), '1');
+ assert.deepEqual(expander2.metadata.get('myNumberMetadata'), 2);
+ assert.deepEqual(expander2.metadata.get('myArrayMetadata'), [3]);
+ });
+
+ test('reads_a_single_event_with_single_group_and_single_row', function() {
+ const data = {
+ version: 1,
+ allocators: {
+ books: {
+ page_counts: [10, 20, 30],
+ },
+ },
+ };
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandData(data);
+ assert.deepEqual(expander2.raw.books, [
+ { page_counts: 10 },
+ { page_counts: 20 },
+ { page_counts: 30 },
+ ]);
+ // Note that the 'inflated' property singularizes key names.
+ assert.deepEqual(expander2.inflated.books, [
+ { page_count: 10 },
+ { page_count: 20 },
+ { page_count: 30 },
+ ]);
+ });
+
+ test('supports_persistant_maps', function() {
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandData({
+ version: 1,
+ maps: {
+ authors: [{ id: 0, name: 'DE Knuth' }],
+ },
+ });
+ const expander3 = expander2.expandData({
+ version: 1,
+ maps: {
+ authors: [{ id: 1, name: 'JH Morris' }],
+ papers: [{ id: 0, title: 'Literate programming', citations: 1378 }],
+ },
+ });
+ assert.isTrue(expander3.hasMap('authors'));
+ assert.isTrue(expander3.hasMap('papers'));
+ assert.isFalse(expander3.hasMap('books'));
+ assert.strictEqual(expander3.getMapValue('authors', 0).name, 'DE Knuth');
+ assert.strictEqual(expander3.getMapValue('authors', 1).name, 'JH Morris');
+ assert.strictEqual(expander3.getMapValue('papers', 0).citations, 1378);
+ assert.strictEqual(expander3.getMapValue('papers', 1), undefined);
+
+ assert.strictEqual(expander2.getMapValue('authors', 0).name, 'DE Knuth');
+ assert.strictEqual(expander2.getMapValue('authors', 1), undefined);
+ });
+
+ test('has_special_support_for_start_time', function() {
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandData({
+ version: 1,
+ startTime: 100,
+ allocators: {
+ sales: {
+ price: [1, 2, 3, 4],
+ },
+ },
+ });
+ assert.deepEqual(expander2.inflated.sales, [
+ { time: 100, price: 1 },
+ { time: 100, price: 2 },
+ { time: 100, price: 3 },
+ { time: 100, price: 4 },
+ ]);
+ });
+
+ test('has_special_support_for_time_deltas', function() {
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandData({
+ version: 1,
+ startTime: 100,
+ allocators: {
+ sales: {
+ timeDelta: [1, 1, 10, 10],
+ price: [1, 2, 3, 4],
+ },
+ },
+ });
+ assert.deepEqual(expander2.inflated.sales, [
+ { time: 101, timeDelta: 1, price: 1 },
+ { time: 102, timeDelta: 1, price: 2 },
+ { time: 112, timeDelta: 10, price: 3 },
+ { time: 122, timeDelta: 10, price: 4 },
+ ]);
+ });
+
+ test('has_special_support_for_strings', function() {
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandData({
+ version: 1,
+ allocators: {
+ books: {
+ authors: [1, 1, 2, 3],
+ isbn: [100, 101, 102, 103],
+ title_sid: [10, 11, 12, 13],
+ },
+ },
+ maps: {
+ authors: [
+ { id: 1, name_sid: 1 },
+ { id: 2, name_sid: 1 },
+ { id: 3, name_sid: 2 },
+ ],
+ strings: [
+ { id: 1, string: 'DFW' },
+ { id: 2, string: 'C. Stross' },
+ { id: 2, string: 'C. Stross' },
+ { id: 10, string: 'Infinite Jest' },
+ { id: 11, string: 'The Laundry Files' },
+ { id: 12, string: 'Accelerando' },
+ { id: 13, string: 'Halting State' },
+ ],
+ },
+ });
+
+ assert.strictEqual(expander2.inflated.books[0].author.name, 'DFW');
+ assert.strictEqual(expander2.inflated.books[0].isbn, 100);
+ assert.strictEqual(expander2.inflated.books[0].title, 'Infinite Jest');
+ });
+
+ test('can_parse_events_directly', function() {
+ const data = {
+ version: 1,
+ allocators: {
+ books: {
+ pages: [10, 20, 30],
+ },
+ },
+ };
+ const expander1 = new tr.e.importer.ProfilingDictionaryReader();
+ const expander2 = expander1.expandEvent(makeEvent(data));
+ assert.deepEqual(expander2.raw.books, [
+ { pages: 10 },
+ { pages: 20 },
+ { pages: 30 },
+ ]);
+ });
+
+ test('full_example', function() {
+ let reader = new tr.e.importer.ProfilingDictionaryReader();
+ reader = reader.expandData({
+ version: 1,
+ maps: {
+ authors: [
+ { id: 1, name_sid: 1 },
+ ],
+ strings: [
+ { id: 1, string: 'DFW' },
+ { id: 10, string: 'Book A' },
+ { id: 11, string: 'Book B' },
+ ],
+ }
+ });
+ reader = reader.expandData({
+ version: 1,
+ startTime: 1992,
+ allocators: {
+ books: {
+ timeDelta: [1, 0, 2, 3],
+ authors: [1, 1, 2, 2],
+ isbns: [100, 101, 102, 103],
+ title_sid: [10, 11, 12, 13],
+ },
+ papers: {
+ authors: [1, 1, 2, 2],
+ },
+ },
+ maps: {
+ authors: [
+ { id: 2, name_sid: 2 },
+ ],
+ strings: [
+ { id: 2, string: 'C. Stross' },
+ { id: 12, string: 'Book C' },
+ { id: 13, string: 'Book D' },
+ ],
+ }
+ });
+
+ assert.strictEqual(reader.getMapValue('strings', 1).string, 'DFW');
+ assert.strictEqual(reader.getString(2), 'C. Stross');
+ assert.strictEqual(reader.inflated.books[0].author.name, 'DFW');
+ assert.strictEqual(reader.inflated.books[0].isbn, 100);
+ assert.strictEqual(reader.inflated.books[0].title, 'Book A');
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer.html
new file mode 100644
index 00000000000..270db638f66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/base/trace_stream.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/importer/simple_line_reader.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.importer', function() {
+ function Trace2HTMLImporter(model, events) {
+ this.importPriority = 0;
+ }
+
+ Trace2HTMLImporter.subtraces_ = [];
+
+ function _extractEventsFromHTML(text) {
+ // Clear the array before pushing data to it.
+ Trace2HTMLImporter.subtraces_ = [];
+
+ const r = new tr.importer.SimpleLineReader(text);
+
+ // Try to find viewer-data...
+ while (true) {
+ if (!r.advanceToLineMatching(
+ new RegExp('^<\s*script id="viewer-data" ' +
+ 'type="(application\/json|text\/plain)">\r?$'))) {
+ break;
+ }
+
+ r.beginSavingLines();
+ if (!r.advanceToLineMatching(/^<\/\s*script>\r?$/)) return;
+
+ let rawEvents = r.endSavingLinesAndGetResult();
+
+ // Drop off first and last event as it contains the end script tag.
+ rawEvents = rawEvents.slice(1, rawEvents.length - 1);
+ const data64 = rawEvents.join('\n');
+ const buffer = new ArrayBuffer(
+ tr.b.Base64.getDecodedBufferLength(data64));
+ const len = tr.b.Base64.DecodeToTypedArray(data64, new DataView(buffer));
+ Trace2HTMLImporter.subtraces_.push(buffer.slice(0, len));
+ }
+ }
+
+ function _canImportFromHTML(text) {
+ if (!/^<!DOCTYPE html>/.test(text)) return false;
+
+ // Try to find viewer-data...
+ _extractEventsFromHTML(text);
+ if (Trace2HTMLImporter.subtraces_.length === 0) return false;
+ return true;
+ }
+
+ Trace2HTMLImporter.canImport = function(events) {
+ if (events instanceof tr.b.TraceStream) return false;
+ return _canImportFromHTML(events);
+ };
+
+ Trace2HTMLImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'Trace2HTMLImporter';
+ },
+
+ isTraceDataContainer() {
+ return true;
+ },
+
+ extractSubtraces() {
+ return Trace2HTMLImporter.subtraces_;
+ },
+
+ importEvents() {
+ }
+ };
+
+
+ tr.importer.Importer.register(Trace2HTMLImporter);
+
+
+ return {
+ Trace2HTMLImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer_test.html
new file mode 100644
index 00000000000..9e268b6e7b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace2html_importer_test.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="improt" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/importer/trace2html_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+
+ test('simple', function() {
+ const htmlLines = [
+ '<!DOCTYPE html>',
+ '<script id="viewer-data" type="application/json">',
+ Base64.btoa('hello'),
+ '<\/script>',
+ '<script id="viewer-data" type="text/plain">',
+ Base64.btoa('world'),
+ '<\/script>',
+ '</html>'
+ ];
+ const htmlText = htmlLines.join('\n');
+ assert.isTrue(tr.e.importer.Trace2HTMLImporter.canImport(htmlText));
+
+ const m = new tr.Model();
+ const imp = new tr.e.importer.Trace2HTMLImporter(m, htmlText);
+ const subTracesAsBuffers = imp.extractSubtraces();
+ const subTracesAsStrings = subTracesAsBuffers.map(function(buffer) {
+ let str = '';
+ const ary = new Uint8Array(buffer);
+ for (let i = 0; i < ary.length; i++) {
+ str += String.fromCharCode(ary[i]);
+ }
+ return str;
+ });
+ assert.deepEqual(subTracesAsStrings, ['hello', 'world']);
+ });
+
+ test('windowsLineEndings', function() {
+ const htmlLines = [
+ '<!DOCTYPE html>',
+ '<script id="viewer-data" type="application/json">',
+ Base64.btoa('hello'),
+ '<\/script>',
+ '<script id="viewer-data" type="text/plain">',
+ Base64.btoa('world'),
+ '<\/script>',
+ '</html>'
+ ];
+ const htmlText = htmlLines.join('\r\n');
+ assert.isTrue(tr.e.importer.Trace2HTMLImporter.canImport(htmlText));
+
+ const m = new tr.Model();
+ const imp = new tr.e.importer.Trace2HTMLImporter(m, htmlText);
+ const subTracesAsBuffers = imp.extractSubtraces();
+ const subTracesAsStrings = subTracesAsBuffers.map(function(buffer) {
+ let str = '';
+ const ary = new Uint8Array(buffer);
+ for (let i = 0; i < ary.length; i++) {
+ str += String.fromCharCode(ary[i]);
+ }
+ return str;
+ });
+ assert.deepEqual(subTracesAsStrings, ['hello', 'world']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry.html
new file mode 100644
index 00000000000..56201a0d2c0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/source_info/js_source_info.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview TraceCodeEntry is a wrapper around the V8 CodeEntry that
+ * extracts extra context information for each item. This includes things like
+ * the source file, line and if the function is a native method or not.
+ */
+tr.exportTo('tr.e.importer', function() {
+ function TraceCodeEntry(address, size, name, scriptId) {
+ this.id_ = tr.b.GUID.allocateSimple();
+ this.address_ = address;
+ this.size_ = size;
+
+ // Stolen from DevTools TimelineJSProfileProcessor._buildCallFrame
+ // Code states:
+ // (empty) -> compiled
+ // ~ -> optimizable
+ // * -> optimized
+ const rePrefix = /^(\w*:)?([*~]?)(.*)$/m;
+ const tokens = rePrefix.exec(name);
+ const prefix = tokens[1];
+ let state = tokens[2];
+ const body = tokens[3];
+
+ if (state === '*') {
+ state = tr.model.source_info.JSSourceState.OPTIMIZED;
+ } else if (state === '~') {
+ state = tr.model.source_info.JSSourceState.OPTIMIZABLE;
+ } else if (state === '') {
+ state = tr.model.source_info.JSSourceState.COMPILED;
+ } else {
+ state = tr.model.source_info.JSSourceState.UNKNOWN;
+ }
+
+ let rawName;
+ let rawUrl;
+ if (prefix === 'Script:') {
+ rawName = '';
+ rawUrl = body;
+ } else {
+ const spacePos = body.lastIndexOf(' ');
+ rawName = spacePos !== -1 ? body.substr(0, spacePos) : body;
+ rawUrl = spacePos !== -1 ? body.substr(spacePos + 1) : '';
+ }
+
+ function splitLineAndColumn(url) {
+ const lineColumnRegEx = /(?::(\d+))?(?::(\d+))?$/;
+ const lineColumnMatch = lineColumnRegEx.exec(url);
+ let lineNumber;
+ let columnNumber;
+
+ if (typeof(lineColumnMatch[1]) === 'string') {
+ lineNumber = parseInt(lineColumnMatch[1], 10);
+ // Immediately convert line and column to 0-based numbers.
+ lineNumber = isNaN(lineNumber) ? undefined : lineNumber - 1;
+ }
+ if (typeof(lineColumnMatch[2]) === 'string') {
+ columnNumber = parseInt(lineColumnMatch[2], 10);
+ columnNumber = isNaN(columnNumber) ? undefined : columnNumber - 1;
+ }
+
+ return {
+ url: url.substring(0, url.length - lineColumnMatch[0].length),
+ lineNumber,
+ columnNumber
+ };
+ }
+
+ const nativeSuffix = ' native';
+ const isNative = rawName.endsWith(nativeSuffix);
+ this.name_ =
+ isNative ? rawName.slice(0, -nativeSuffix.length) : rawName;
+
+ const urlData = splitLineAndColumn(rawUrl);
+ const url = urlData.url || '';
+ const line = urlData.lineNumber || 0;
+ const column = urlData.columnNumber || 0;
+
+ this.sourceInfo_ = new tr.model.source_info.JSSourceInfo(
+ url, line, column, isNative, scriptId, state);
+ }
+
+ TraceCodeEntry.prototype = {
+ get id() {
+ return this.id_;
+ },
+
+ get sourceInfo() {
+ return this.sourceInfo_;
+ },
+
+ get name() {
+ return this.name_;
+ },
+
+ set address(address) {
+ this.address_ = address;
+ },
+
+ get address() {
+ return this.address_;
+ },
+
+ set size(size) {
+ this.size_ = size;
+ },
+
+ get size() {
+ return this.size_;
+ }
+ };
+
+ return {
+ TraceCodeEntry,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry_test.html
new file mode 100644
index 00000000000..422350f04ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_entry_test.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/trace_code_entry.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('lazy_compile_method', function() {
+ const tce = new tr.e.importer.TraceCodeEntry(
+ '0x123', 10, 'Handler:timeStamp', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'timeStamp');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, '');
+ assert.strictEqual(tce.sourceInfo.line, -1);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('non_lazy_compile_method', function() {
+ const tce = new tr.e.importer.TraceCodeEntry(
+ '0x123', 10, 'Handler:timeStamp', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'timeStamp');
+ assert.strictEqual(tce.sourceInfo.state, 'compiled');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, '');
+ assert.strictEqual(tce.sourceInfo.line, -1);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('native_matching', function() {
+ const tce = new tr.e.importer.TraceCodeEntry('0x123', 10,
+ 'LazyCompile:~IsAccessorDescriptor native v8natives.js:183', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'IsAccessorDescriptor');
+ assert.strictEqual(tce.sourceInfo.isNative, true);
+ assert.strictEqual(tce.sourceInfo.state, 'optimizable');
+ assert.strictEqual(tce.sourceInfo.file, 'v8natives.js');
+ assert.strictEqual(tce.sourceInfo.line, 182);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('strips_*_from_name', function() {
+ const tce = new tr.e.importer.TraceCodeEntry('0x123', 10,
+ 'LazyCompile:*IsAccessorDescriptor native v8natives.js:183', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'IsAccessorDescriptor');
+ assert.strictEqual(tce.sourceInfo.isNative, true);
+ assert.strictEqual(tce.sourceInfo.state, 'optimized');
+ assert.strictEqual(tce.sourceInfo.file, 'v8natives.js');
+ assert.strictEqual(tce.sourceInfo.line, 182);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('non_native_matching', function() {
+ const tce = new tr.e.importer.TraceCodeEntry('0x123', 10,
+ 'LazyCompile:~IsAccessorDescriptor v8natives.js:183', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'IsAccessorDescriptor');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, 'v8natives.js');
+ assert.strictEqual(tce.sourceInfo.line, 182);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('lazy_compile_without_script', function() {
+ const tce = new tr.e.importer.TraceCodeEntry(
+ '0x123', 10, 'LazyCompile:~Object', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'Object');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, '');
+ assert.strictEqual(tce.sourceInfo.line, -1);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('line_matching_without_script', function() {
+ const tce = new tr.e.importer.TraceCodeEntry('0x123', 10,
+ 'LazyCompile:~Object :220', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, 'Object');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, '');
+ assert.strictEqual(tce.sourceInfo.line, 219);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+
+ test('unknown_method_name', function() {
+ let tce = new tr.e.importer.TraceCodeEntry('0x123', 10, 'LazyCompile:', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, '');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, '');
+ assert.strictEqual(tce.sourceInfo.line, -1);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+
+ tce = new tr.e.importer.TraceCodeEntry(
+ '0x123', 10, 'LazyCompile:~ :37', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, '');
+ assert.strictEqual(tce.sourceInfo.isNative, false);
+ assert.strictEqual(tce.sourceInfo.file, '');
+ assert.strictEqual(tce.sourceInfo.line, 36);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+
+ tce = new tr.e.importer.TraceCodeEntry('0x123', 10,
+ 'LazyCompile:~ native liveedit.js:37:10', 12);
+ assert.strictEqual(tce.size, 10);
+ assert.strictEqual(tce.name, '');
+ assert.strictEqual(tce.sourceInfo.isNative, true);
+ assert.strictEqual(tce.sourceInfo.file, 'liveedit.js');
+ assert.strictEqual(tce.sourceInfo.line, 36);
+ assert.strictEqual(tce.sourceInfo.column, 9);
+ assert.strictEqual(tce.sourceInfo.scriptId, 12);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_map.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_map.html
new file mode 100644
index 00000000000..b66df9155de
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_code_map.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel='import' href='/tracing/extras/importer/trace_code_entry.html'>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.importer', function() {
+ // This code is a tracification of:
+ // devtools/front_end/timeline/TimelineJSProfile.js
+ function TraceCodeMap() {
+ this.banks_ = new Map();
+ }
+
+ TraceCodeMap.prototype = {
+ addEntry(addressHex, size, name, scriptId) {
+ const entry = new tr.e.importer.TraceCodeEntry(
+ this.getAddress_(addressHex), size, name, scriptId);
+
+ this.addEntry_(addressHex, entry);
+ },
+
+ moveEntry(oldAddressHex, newAddressHex, size) {
+ const entry = this.getBank_(oldAddressHex)
+ .removeEntry(this.getAddress_(oldAddressHex));
+ if (!entry) return;
+
+ entry.address = this.getAddress_(newAddressHex);
+ entry.size = size;
+ this.addEntry_(newAddressHex, entry);
+ },
+
+ lookupEntry(addressHex) {
+ return this.getBank_(addressHex)
+ .lookupEntry(this.getAddress_(addressHex));
+ },
+
+ addEntry_(addressHex, entry) {
+ // FIXME: Handle bank spanning addresses ...
+ this.getBank_(addressHex).addEntry(entry);
+ },
+
+ getAddress_(addressHex) {
+ // 13 hex digits === 52 bits, double mantissa fits 53 bits.
+ const bankSizeHexDigits = 13;
+ addressHex = addressHex.slice(2); // cut 0x prefix.
+ return parseInt(addressHex.slice(-bankSizeHexDigits), 16);
+ },
+
+ getBank_(addressHex) {
+ addressHex = addressHex.slice(2); // cut 0x prefix.
+
+ // 13 hex digits === 52 bits, double mantissa fits 53 bits.
+ const bankSizeHexDigits = 13;
+ const maxHexDigits = 16;
+ const bankName = addressHex.slice(-maxHexDigits, -bankSizeHexDigits);
+ let bank = this.banks_.get(bankName);
+ if (!bank) {
+ bank = new TraceCodeBank();
+ this.banks_.set(bankName, bank);
+ }
+ return bank;
+ }
+ };
+
+ function TraceCodeBank() {
+ this.entries_ = [];
+ }
+
+ TraceCodeBank.prototype = {
+ removeEntry(address) {
+ // findLowIndexInSortedArray returns 1 for empty. Just handle the
+ // empty list and bail early.
+ 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) {
+ // findLowIndexInSortedArray returns 1 for empty list. Just push the
+ // new address as it's the only item.
+ 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,
+ };
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer.html
new file mode 100644
index 00000000000..2c9a84edfe4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer.html
@@ -0,0 +1,3394 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/trace_stream.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/extras/importer/heap_dump_trace_event_importer.html">
+<link rel="import" href="/tracing/extras/importer/legacy_heap_dump_trace_event_importer.html">
+<link rel="import" href="/tracing/extras/importer/oboe.html">
+<link rel="import" href="/tracing/extras/importer/profiling_dictionary_reader.html">
+<link rel="import" href="/tracing/extras/importer/trace_code_entry.html">
+<link rel="import" href="/tracing/extras/importer/trace_code_map.html">
+<link rel="import" href="/tracing/extras/importer/v8/codemap.html">
+<link rel="import" href="/tracing/extras/measure/measure_async_slice.html">
+<link rel="import" href="/tracing/importer/context_processor.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/comment_box_annotation.html">
+<link rel="import" href="/tracing/model/constants.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/flow_event.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/instant_event.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/profile_node.html">
+<link rel="import" href="/tracing/model/profile_tree.html">
+<link rel="import" href="/tracing/model/rect_annotation.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+<link rel="import" href="/tracing/model/x_marker_annotation.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview TraceEventImporter imports TraceEvent-formatted data
+ * into the provided model.
+ */
+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.';
+
+ // Map from raw memory dump byte stat names to model byte stat names. See
+ // //base/trace_event/process_memory_maps.cc in Chromium.
+ const BYTE_STAT_NAME_MAP = {
+ 'pc': 'privateCleanResident',
+ 'pd': 'privateDirtyResident',
+ 'sc': 'sharedCleanResident',
+ 'sd': 'sharedDirtyResident',
+ 'pss': 'proportionalResident',
+ 'sw': 'swapped'
+ };
+
+ // See tr.model.MemoryAllocatorDump 'weak' field and
+ // base::trace_event::MemoryAllocatorDump::Flags::WEAK in the Chromium
+ // codebase.
+ const WEAK_MEMORY_ALLOCATOR_DUMP_FLAG = 1 << 0;
+
+ // Object type name patterns for various compilers.
+ const OBJECT_TYPE_NAME_PATTERNS = [
+ {
+ // Clang.
+ prefix: 'const char *WTF::getStringWithTypeName() [T = ',
+ suffix: ']'
+ },
+ {
+ // GCC.
+ prefix: 'const char* WTF::getStringWithTypeName() [with T = ',
+ suffix: ']'
+ },
+ {
+ // Microsoft Visual C++
+ prefix: 'const char *__cdecl WTF::getStringWithTypeName<',
+ suffix: '>(void)'
+ }
+ ];
+
+ // The list of fields on the trace that are known to contain subtraces.
+ const SUBTRACE_FIELDS = new Set([
+ 'powerTraceAsString',
+ 'systemTraceEvents',
+ 'androidProcessDump',
+ ]);
+
+ // The complete list of fields on the trace that should not be treated as
+ // trace metadata.
+ const NON_METADATA_FIELDS = new Set([
+ 'displayTimeUnit',
+ 'samples',
+ 'stackFrames',
+ 'traceAnnotations',
+ 'traceEvents',
+ ...SUBTRACE_FIELDS
+ ]);
+
+ function TraceEventImporter(model, eventData) {
+ this.hasEvents_ = undefined; // Set properly when importEvents is called.
+ 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_ = [];
+
+ // Profile Tree Map.
+ // Type of Profile Tree -> Map(ID -> Profile Tree)
+ this.profileTrees_ = new Map();
+ // ID -> Info Object
+ this.profileInfo_ = new Map();
+
+ // For tracking async events that is used to create back-compat clock sync
+ // event.
+ this.legacyChromeClockSyncStartEvent_ = undefined;
+ this.legacyChromeClockSyncFinishEvent_ = undefined;
+
+ // Dump ID -> PID -> [process memory dump events].
+ this.allMemoryDumpEvents_ = {};
+
+ // Unpacks size, count, stackId and heapId from 'P' events. Also remembers
+ // stack frame and type information.
+ this.heapProfileExpander = new ProfilingDictionaryReader();
+
+ // PID -> Object type ID -> Object type name.
+ this.objectTypeNameMap_ = {};
+
+ // For old Chrome traces with no clock domain metadata, just use a
+ // placeholder clock domain.
+ this.clockDomainId_ = tr.model.ClockDomainId.UNKNOWN_CHROME_LEGACY;
+ // A function able to transform timestamps in |clockDomainId| to timestamps
+ // in the model clock domain.
+ this.toModelTime_ = undefined;
+
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ eventData = eventData.trim();
+ // If the event data begins with a [, then we know it should end with a ].
+ // The reason we check for this is because some tracing implementations
+ // cannot guarantee that a ']' gets written to the trace file. So, we are
+ // forgiving and if this is obviously the case, we fix it up before
+ // throwing the string at JSON.parse.
+ 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;
+ }
+
+ // Some trace_event implementations put the actual trace events
+ // inside a container. E.g { ... , traceEvents: [ ] }
+ // If we see that, just pull out the trace events.
+ if (this.events_.traceEvents) {
+ const container = this.events_;
+ this.events_ = this.events_.traceEvents;
+
+ for (const subtraceField of SUBTRACE_FIELDS) {
+ if (container[subtraceField]) {
+ this.storeSubtrace_(container[subtraceField]);
+ }
+ }
+ this.storeSamples_(container.samples);
+ this.storeStackFrames_(container.stackFrames);
+ this.storeDisplayTimeUnit_(container.displayTimeUnit);
+ this.storeTraceAnnotations_(container.traceAnnotations);
+ this.storeMetadata_(container);
+ } else if (this.events_ instanceof tr.b.TraceStream) {
+ const parser = oboe()
+ .node('{cat ph}', function(e) { return oboe.drop; })
+ .node('!.powerTraceAsString', this.storeSubtrace_.bind(this))
+ .node('!.systemTraceEvents', this.storeSubtrace_.bind(this))
+ .node('!.samples', this.storeSamples_.bind(this))
+ .node('!.stackFrames', this.storeStackFrames_.bind(this))
+ .node('!.displayTimeUnit', this.storeDisplayTimeUnit_.bind(this))
+ .node('!.traceAnnotations', this.storeTraceAnnotations_.bind(this))
+ .done(this.storeMetadata_.bind(this));
+ this.events_.rewind();
+ while (this.events_.hasData) {
+ parser.write(this.events_.readNumBytes());
+ }
+ parser.finish();
+ }
+ }
+
+ /**
+ * @return {boolean} Whether obj is a TraceEvent array.
+ */
+ TraceEventImporter.canImport = function(eventData) {
+ // May be encoded JSON. But we dont want to parse it fully yet.
+ // Use a simple heuristic:
+ // - eventData that starts with [ are probably trace_event
+ // - eventData that starts with { are probably trace_event
+ // May be encoded JSON. Treat files that start with { as importable by us.
+ if (eventData instanceof tr.b.TraceStream) {
+ if (eventData.isBinary) return false;
+ eventData = eventData.header;
+ }
+
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ eventData = eventData.trim();
+ return eventData[0] === '{' || eventData[0] === '[';
+ }
+
+ // Might just be an array of events
+ if (eventData instanceof Array && eventData.length && eventData[0].ph) {
+ return true;
+ }
+
+ // Might be an object with a traceEvents field in it.
+ if (eventData.traceEvents) {
+ if (eventData.traceEvents instanceof Array) {
+ if (eventData.traceEvents.length && eventData.traceEvents[0].ph) {
+ return true;
+ }
+ if (eventData.samples && eventData.samples.length &&
+ eventData.stackFrames !== undefined) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Extracts a scoped ID from an event.
+ *
+ * In legacy trace files, the ID of an event is always stored in the 'id'
+ * field and the event phase determines if the ID is process-local or
+ * global. For example, async event IDs are considered global and object
+ * event IDs are considered process-local.
+ *
+ * New trace files can explicitly specify whether an event ID is
+ * process-local or global. These new IDs are stored in the 'id2' field so
+ * that old trace importers that do not implement this logic break when they
+ * try to import a new trace file. The value of id2 can be either of the
+ * form '{global: 0x1000}' or of the form '{local: 0x1000}'.
+ *
+ * @param {!Object} event A trace event.
+ * @return {!tr.model.ScopedId}
+ */
+ TraceEventImporter.scopedIdForEvent_ = function(event) {
+ const scope = event.scope || tr.model.OBJECT_DEFAULT_SCOPE;
+ let pid = undefined;
+ if (event.id !== undefined) {
+ if (event.id2 !== undefined) {
+ throw new Error('Event has both id and id2');
+ }
+ pid = tr.model.LOCAL_ID_PHASES.has(event.ph) ? event.pid : undefined;
+ return new tr.model.ScopedId(scope, event.id, pid);
+ } else if (event.id2 !== undefined) {
+ if (event.id2.global !== undefined) {
+ return new tr.model.ScopedId(scope, event.id2.global);
+ } else if (event.id2.local !== undefined) {
+ return new tr.model.ScopedId(scope, event.id2.local, event.pid);
+ }
+ throw new Error(
+ 'Event that uses id2 must have either a global or local ID');
+ }
+ return undefined;
+ };
+
+ TraceEventImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'TraceEventImporter';
+ },
+
+ extractSubtraces() {
+ // Because subtraces can be quite large, we need to make sure that we
+ // don't hold a reference to the memory.
+ const subtraces = this.subtraces_;
+ this.subtraces_ = [];
+ return subtraces;
+ },
+
+ /**
+ * Deep copying is only needed if the trace was given to us as events.
+ */
+ deepCopyIfNeeded_(obj) {
+ if (obj === undefined) obj = {};
+ if (this.eventsWereFromString_) return obj;
+ return deepCopy(obj);
+ },
+
+ /**
+ * Always perform deep copying.
+ */
+ deepCopyAlways_(obj) {
+ if (obj === undefined) obj = {};
+ return deepCopy(obj);
+ },
+
+ /**
+ * Helper to process an async event.
+ */
+ processAsyncEvent(event) {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ this.allAsyncEvents_.push({
+ sequenceNumber: this.allAsyncEvents_.length,
+ event,
+ thread
+ });
+ },
+
+ /**
+ * Helper to process a flow event.
+ */
+ processFlowEvent(event, opt_slice) {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ this.allFlowEvents_.push({
+ refGuid: tr.b.GUID.getLastSimpleGuid(),
+ sequenceNumber: this.allFlowEvents_.length,
+ event,
+ slice: opt_slice, // slice for events that have flow info
+ thread
+ });
+ },
+
+ /**
+ * Helper that creates and adds samples to a Counter object based on
+ * 'C' phase events.
+ */
+ processCounterEvent(event) {
+ let ctrName;
+ if (event.id !== undefined) {
+ ctrName = event.name + '[' + event.id + ']';
+ } else {
+ ctrName = event.name;
+ }
+
+ const ctr = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateCounter(event.cat, ctrName);
+ const reservedColorId = event.cname ? getEventColor(event) : undefined;
+
+ // Initialize the counter's series fields if needed.
+ if (ctr.numSeries === 0) {
+ for (const seriesName in event.args) {
+ const colorId = reservedColorId ||
+ getEventColor(event, ctr.name + '.' + seriesName);
+ ctr.addSeries(new tr.model.CounterSeries(seriesName, colorId));
+ }
+
+ if (ctr.numSeries === 0) {
+ this.model_.importWarning({
+ type: 'counter_parse_error',
+ message: 'Expected counter ' + event.name +
+ ' to have at least one argument to use as a value.'
+ });
+
+ // Drop the counter.
+ delete ctr.parent.counters[ctr.name];
+ return;
+ }
+ }
+
+ const ts = this.toModelTimeFromUs_(event.ts);
+ ctr.series.forEach(function(series) {
+ const val = event.args[series.name] ? event.args[series.name] : 0;
+ series.addCounterSample(ts, val);
+ });
+ },
+
+ processObjectEvent(event) {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ this.allObjectEvents_.push({
+ sequenceNumber: this.allObjectEvents_.length,
+ event,
+ thread});
+ if (thread.guid in this.contextProcessorPerThread) {
+ const processor = this.contextProcessorPerThread[thread.guid];
+ const scopedId = TraceEventImporter.scopedIdForEvent_(event);
+ if (event.ph === 'D') {
+ processor.destroyContext(scopedId);
+ }
+ // The context processor maintains a cache of unique context objects and
+ // active context sets to reduce memory usage. If an object is modified,
+ // we should invalidate this cache, because otherwise context sets from
+ // before and after the modification may erroneously point to the same
+ // context snapshot (as both are the same set/object instances).
+ processor.invalidateContextCacheForSnapshot(scopedId);
+ }
+ },
+
+ processContextEvent(event) {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ if (!(thread.guid in this.contextProcessorPerThread)) {
+ this.contextProcessorPerThread[thread.guid] =
+ new tr.importer.ContextProcessor(this.model_);
+ }
+ const scopedId = TraceEventImporter.scopedIdForEvent_(event);
+ const contextType = event.name;
+ const processor = this.contextProcessorPerThread[thread.guid];
+ if (event.ph === '(') {
+ processor.enterContext(contextType, scopedId);
+ } else if (event.ph === ')') {
+ processor.leaveContext(contextType, scopedId);
+ } else {
+ this.model_.importWarning({
+ type: 'unknown_context_phase',
+ message: 'Unknown context event phase: ' + event.ph + '.'
+ });
+ }
+ },
+
+ setContextsFromThread_(thread, slice) {
+ if (thread.guid in this.contextProcessorPerThread) {
+ slice.contexts =
+ this.contextProcessorPerThread[thread.guid].activeContexts;
+ }
+ },
+
+ processDurationEvent(event) {
+ const thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+ const ts = this.toModelTimeFromUs_(event.ts);
+ if (event.dur === 0 &&
+ !thread.sliceGroup.isTimestampValidForBeginOrEnd(ts)) {
+ this.model_.importWarning({
+ type: 'duration_parse_error',
+ message: 'Timestamps are moving backward.'
+ });
+ return;
+ }
+
+ if (event.ph === 'B') {
+ const slice = thread.sliceGroup.beginSlice(
+ event.cat, event.name, this.toModelTimeFromUs_(event.ts),
+ this.deepCopyIfNeeded_(event.args),
+ this.toModelTimeFromUs_(event.tts), event.argsStripped,
+ getEventColor(event), event.bind_id);
+ slice.startStackFrame = this.getStackFrameForEvent_(event);
+ this.setContextsFromThread_(thread, slice);
+ } else if (event.ph === 'I' || event.ph === 'i' || event.ph === 'R') {
+ if (event.s !== undefined && event.s !== 't') {
+ throw new Error('This should never happen');
+ }
+
+ thread.sliceGroup.beginSlice(event.cat, event.name,
+ this.toModelTimeFromUs_(event.ts),
+ this.deepCopyIfNeeded_(event.args),
+ this.toModelTimeFromUs_(event.tts),
+ event.argsStripped,
+ getEventColor(event), event.bind_id);
+ const slice = thread.sliceGroup.endSlice(
+ this.toModelTimeFromUs_(event.ts),
+ this.toModelTimeFromUs_(event.tts));
+ slice.startStackFrame = this.getStackFrameForEvent_(event);
+ slice.endStackFrame = undefined;
+ } else {
+ if (!thread.sliceGroup.openSliceCount) {
+ this.model_.importWarning({
+ type: 'duration_parse_error',
+ message: 'E phase event without a matching B phase event.'
+ });
+ return;
+ }
+
+ const slice = thread.sliceGroup.endSlice(
+ this.toModelTimeFromUs_(event.ts),
+ this.toModelTimeFromUs_(event.tts),
+ getEventColor(event));
+ if (event.name && slice.title !== event.name) {
+ this.model_.importWarning({
+ type: 'title_match_error',
+ message: 'Titles do not match. Title is ' +
+ slice.title + ' in openSlice, and is ' +
+ event.name + ' in endSlice'
+ });
+ }
+ slice.endStackFrame = this.getStackFrameForEvent_(event);
+
+ this.mergeArgsInto_(slice.args, event.args, slice.title);
+ }
+ },
+
+ mergeArgsInto_(dstArgs, srcArgs, eventName) {
+ for (const arg in srcArgs) {
+ if (dstArgs[arg] !== undefined) {
+ this.model_.importWarning({
+ type: 'arg_merge_error',
+ message: 'Different phases of ' + eventName +
+ ' provided values for argument ' + arg + '.' +
+ ' The last provided value will be used.'
+ });
+ }
+ dstArgs[arg] = this.deepCopyIfNeeded_(srcArgs[arg]);
+ }
+ },
+
+ processCompleteEvent(event) {
+ // Preventing the overhead slices from making it into the model. This
+ // only applies to legacy traces, as the overhead traces have been
+ // removed from the chromium code.
+ if (event.cat !== undefined &&
+ event.cat.indexOf('trace_event_overhead') > -1) {
+ return undefined;
+ }
+
+ const thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+
+ if (event.flow_out) {
+ if (event.flow_in) {
+ event.flowPhase = STEP;
+ } else {
+ event.flowPhase = PRODUCER;
+ }
+ } else if (event.flow_in) {
+ event.flowPhase = CONSUMER;
+ }
+
+ const slice = thread.sliceGroup.pushCompleteSlice(event.cat, event.name,
+
+ this.toModelTimeFromUs_(event.ts),
+ this.maybeToModelTimeFromUs_(event.dur),
+ this.maybeToModelTimeFromUs_(event.tts),
+ this.maybeToModelTimeFromUs_(event.tdur),
+ this.deepCopyIfNeeded_(event.args),
+ event.argsStripped,
+ getEventColor(event),
+ event.bind_id);
+ slice.startStackFrame = this.getStackFrameForEvent_(event);
+ slice.endStackFrame = this.getStackFrameForEvent_(event, true);
+ this.setContextsFromThread_(thread, slice);
+
+ return slice;
+ },
+
+ processJitCodeEvent(event) {
+ if (this.v8ProcessCodeMaps_[event.pid] === undefined) {
+ this.v8ProcessCodeMaps_[event.pid] = new tr.e.importer.TraceCodeMap();
+ }
+ const map = this.v8ProcessCodeMaps_[event.pid];
+
+ const data = event.args.data;
+ // TODO(dsinclair): There are _a lot_ of JitCode events so I'm skipping
+ // the display for now. Can revisit later if we want to show them.
+ // Handle JitCodeMoved and JitCodeAdded event.
+ if (event.name === 'JitCodeMoved') {
+ map.moveEntry(data.code_start, data.new_code_start, data.code_len);
+ } else { // event.name === 'JitCodeAdded'
+ map.addEntry(data.code_start, data.code_len, data.name, data.script_id);
+ }
+ },
+
+ processMetadataEvent(event) {
+ // V8 JIT events are currently logged as phase 'M' so we need to
+ // separate them out and handle specially.
+ if (event.name === 'JitCodeAdded' || event.name === 'JitCodeMoved') {
+ this.v8SamplingData_.push(event);
+ return;
+ }
+
+ // The metadata events aren't useful without args.
+ if (event.argsStripped) return;
+
+ if (event.name === 'process_name') {
+ const process = this.model_.getOrCreateProcess(event.pid);
+ process.name = event.args.name;
+ } else if (event.name === 'process_labels') {
+ const process = this.model_.getOrCreateProcess(event.pid);
+ const labels = event.args.labels.split(',');
+ for (let i = 0; i < labels.length; i++) {
+ process.addLabelIfNeeded(labels[i]);
+ }
+ } else if (event.name === 'process_uptime_seconds') {
+ const process = this.model_.getOrCreateProcess(event.pid);
+ process.uptime_seconds = event.args.uptime;
+ } else if (event.name === 'process_sort_index') {
+ const process = this.model_.getOrCreateProcess(event.pid);
+ process.sortIndex = event.args.sort_index;
+ } else if (event.name === 'thread_name') {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ thread.name = event.args.name;
+ } else if (event.name === 'thread_sort_index') {
+ const thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ thread.sortIndex = event.args.sort_index;
+ } else if (event.name === 'num_cpus') {
+ let n = event.args.number;
+ // Not all render processes agree on the cpu count in trace_event. Some
+ // processes will report 1, while others will report the actual cpu
+ // count. To deal with this, take the max of what is reported.
+ if (this.softwareMeasuredCpuCount_ !== undefined) {
+ n = Math.max(n, this.softwareMeasuredCpuCount_);
+ }
+ 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) {
+ // V8 JIT events were logged as phase 'I' in the old format,
+ // so we need to separate them out and handle specially.
+ if (event.name === 'JitCodeAdded' || event.name === 'JitCodeMoved') {
+ this.v8SamplingData_.push(event);
+ return;
+ }
+
+ // Thread-level instant events are treated as zero-duration slices.
+ 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());
+ }
+ 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;
+ }
+ if (event.id === undefined) {
+ throw new Error('No event ID in sample');
+ }
+
+ const data = event.args.data;
+ // Sampling usually happens in a separate thread, but start time is issued
+ // in the main thread, in order to get the correct thread object,
+ // we should use pid and tid from main thread.
+ if (data.startTime !== undefined) {
+ this.profileInfo_.set(event.id, {
+ startTime: data.startTime,
+ pid: event.pid,
+ tid: event.tid
+ });
+ }
+ const timeDeltas = data.timeDeltas;
+ for (const sampleType in data) {
+ if (sampleType === 'timeDeltas' || sampleType === 'startTime') {
+ continue;
+ }
+ // The length of samples array and the length of timeDeltas array
+ // should be the same.
+ if (data[sampleType].samples && timeDeltas &&
+ data[sampleType].samples.length !== timeDeltas.length) {
+ // eslint-disable-next-line
+ throw new Error('samples and timeDeltas array should have same length');
+ }
+
+ const profileTree = this.getOrCreateProfileTree_(sampleType, event.id);
+ const nodes = data[sampleType].nodes;
+ const samples = data[sampleType].samples;
+ if (nodes !== undefined) {
+ for (const node of nodes) {
+ // Get the sepcific ProfileNode type based on sampleType.
+ const ProfileNodeType =
+ tr.model.ProfileNode.subTypes.getConstructor(undefined,
+ sampleType);
+ const profileNode = ProfileNodeType.constructFromObject(profileTree,
+ node);
+ if (profileNode === undefined) {
+ continue;
+ }
+ profileTree.add(profileNode);
+ }
+ }
+ // The samples array contains id pointing to the profile node.
+ if (samples !== undefined) {
+ const thread = this.model_.getOrCreateProcess(profileTree.pid)
+ .getOrCreateThread(profileTree.tid);
+ for (let i = 0, len = samples.length; i < len; ++i) {
+ const node = profileTree.getNode(samples[i]);
+ profileTree.endTime += timeDeltas[i];
+ if (node === undefined) continue;
+ const start = this.toModelTimeFromUs_(profileTree.endTime);
+ this.model_.samples.push(
+ new tr.model.Sample(start, node.sampleTitle, node, thread));
+ }
+ }
+ }
+ },
+
+ processLegacyV8Sample(event) {
+ const data = event.args.data;
+ const sampleType = 'legacySample';
+ const ProfileNodeType =
+ tr.model.ProfileNode.subTypes.getConstructor(undefined, sampleType);
+
+ // As-per DevTools, the backend sometimes creates bogus samples. Skip it.
+ if (data.vm_state === 'js' && !data.stack.length) return;
+
+ const profileTree = this.getOrCreateProfileTree_(sampleType, event.pid);
+ if (profileTree.getNode(-1) === undefined) {
+ profileTree.add(
+ new ProfileNodeType(-1, {
+ url: '',
+ scriptId: -1,
+ functionName: 'unknown'
+ }, undefined));
+ }
+ // There are several types of v8 sample events, gc, native, compiler, etc.
+ // Some of these types have stacks and some don't, we handle those two
+ // cases differently. For types that don't have any stack frames attached
+ // we synthesize one based on the type of thing that's happening so when
+ // we view all the samples we'll see something like 'external' or 'gc'
+ // as a fraction of the time spent.
+ let node = undefined;
+ if (data.stack.length > 0 && this.v8ProcessCodeMaps_[event.pid]) {
+ const map = this.v8ProcessCodeMaps_[event.pid];
+ // Stacks have the leaf node first, flip them around so the root
+ // comes first.
+ data.stack.reverse();
+ let parentNode = undefined;
+ for (let i = 0; i < data.stack.length; i++) {
+ const entry = map.lookupEntry(data.stack[i]);
+ if (entry === undefined) {
+ node = profileTree.getNode(-1);
+ } else {
+ node = profileTree.getNode(entry.id);
+ if (node === undefined) {
+ const sourceInfo = entry.sourceInfo;
+ node = new ProfileNodeType(entry.id, {
+ functionName: entry.name,
+ url: entry.sourceInfo.file,
+ lineNumber: sourceInfo.line !== -1 ? sourceInfo.line :
+ undefined,
+ columnNumber: sourceInfo.column !== -1 ? sourceInfo.column :
+ undefined,
+ scriptid: entry.sourceInfo.scriptId
+ }, parentNode);
+ profileTree.add(node);
+ }
+ }
+ parentNode = node;
+ }
+ } else {
+ node = profileTree.getNode(data.vm_state);
+ if (node === undefined) {
+ node = new ProfileNodeType(data.vm_state, {
+ url: '',
+ functionName: data.vm_state
+ }, undefined);
+ profileTree.add(node);
+ }
+ }
+
+ const thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+
+ this.model_.samples.push(
+ new tr.model.Sample(this.toModelTimeFromUs_(event.ts),
+ node.sampleTitle, node, thread));
+ },
+
+ processTraceSampleEvent(event) {
+ if (event.name === 'V8Sample' || event.name.startsWith('Profile')) {
+ this.v8SamplingData_.push(event);
+ return;
+ }
+
+ let node = this.stackFrameTree_.getNode(event.name);
+ if (node === undefined && event.sf !== undefined) {
+ node = this.stackFrameTree_.getNode('g' + event.sf);
+ }
+
+ if (node === undefined) {
+ let id = event.name;
+ if (event.sf) {
+ id = 'g' + event.sf;
+ }
+ const ProfileNodeType =
+ tr.model.ProfileNode.subTypes.getConstructor(undefined,
+ 'legacySample');
+ node = this.stackFrameTree_.add(new ProfileNodeType(
+ id, {
+ functionName: event.name
+ }, undefined
+ ));
+ }
+ const thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+
+ const sample = new tr.model.Sample(
+ this.toModelTimeFromUs_(event.ts), 'Trace Event Sample',
+ node, thread, undefined, 1,
+ this.deepCopyIfNeeded_(event.args));
+ this.setContextsFromThread_(thread, sample);
+ this.model_.samples.push(sample);
+ },
+
+ processMemoryDumpEvent(event) {
+ // TODO(chiniforooshan): Make memory dumps either use local or global IDs
+ // instead of the generic IDs with different phases.
+ // https://github.com/catapult-project/catapult/issues/2957
+ if (event.ph !== 'v') {
+ throw new Error('Invalid memory dump event phase "' + event.ph + '".');
+ }
+
+ const dumpId = event.id;
+ if (dumpId === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory dump event (phase \'' + event.ph +
+ '\') without a dump ID.'
+ });
+ return;
+ }
+
+ const pid = event.pid;
+ if (pid === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory dump event (phase\'' + event.ph + '\', dump ID \'' +
+ dumpId + '\') without a PID.'
+ });
+ return;
+ }
+
+ // Dump ID -> PID -> [process memory dump events].
+ const allEvents = this.allMemoryDumpEvents_;
+
+ // PID -> [process memory dump events].
+ let dumpIdEvents = allEvents[dumpId];
+ if (dumpIdEvents === undefined) {
+ allEvents[dumpId] = dumpIdEvents = {};
+ }
+
+ // [process memory dump events].
+ let processEvents = dumpIdEvents[pid];
+ if (processEvents === undefined) {
+ dumpIdEvents[pid] = processEvents = [];
+ }
+
+ processEvents.push(event);
+ },
+
+ processClockSyncEvent(event) {
+ if (event.ph !== 'c') {
+ throw new Error('Invalid clock sync event phase "' + event.ph + '".');
+ }
+
+ const syncId = event.args.sync_id;
+ if (syncId === undefined) {
+ this.model_.importWarning({
+ type: 'clock_sync_parse_error',
+ message: 'Clock sync at time ' + event.ts + ' without an ID.'
+ });
+ return;
+ }
+
+ if (event.args && event.args.issue_ts !== undefined) {
+ // When Chrome is the tracing controller and is the requester of the
+ // clock sync, the clock sync event looks like:
+ //
+ // {
+ // "args": {
+ // "sync_id": "abc123",
+ // "issue_ts": 12340
+ // }
+ // "ph": "c"
+ // "ts": 12345
+ // ...
+ // }
+ this.model_.clockSyncManager.addClockSyncMarker(
+ this.clockDomainId_, syncId,
+ tr.b.Unit.timestampFromUs(event.args.issue_ts),
+ tr.b.Unit.timestampFromUs(event.ts));
+ } else {
+ // When Chrome is a tracing agent and is the recipient of the clock
+ // sync request, the clock sync event looks like:
+ //
+ // {
+ // "args": { "sync_id": "abc123" }
+ // "ph": "c"
+ // "ts": 12345
+ // ...
+ // }
+ this.model_.clockSyncManager.addClockSyncMarker(
+ this.clockDomainId_, syncId, tr.b.Unit.timestampFromUs(event.ts));
+ }
+ },
+
+ processLegacyChromeClockSyncEvent(event) {
+ // Older versions of Chrome don't support the devtools clock sync API, but
+ // Telemetry can work around this by creating clock sync events via
+ // console.time & console.timeEnd, which issue trace events as long as the
+ // 'blink.console' category is enabled. When we encounter async events
+ // with names starting with the prefix 'ClockSyncEvent.' , treat these as
+ // clock sync markers.
+ if (event.ph === 'S') {
+ this.legacyChromeClockSyncStartEvent_ = event;
+ } else if (event.ph === 'F') {
+ this.legacyChromeClockSyncFinishEvent_ = event;
+ }
+
+ if (this.legacyChromeClockSyncStartEvent_ === undefined ||
+ this.legacyChromeClockSyncFinishEvent_ === undefined) {
+ return;
+ }
+
+ const startSyncId = this.legacyChromeClockSyncStartEvent_.name.substring(
+ LEGACY_CHROME_CLOCK_SYNC_EVENT_NAME_PREFIX.length);
+ const finishSyncId =
+ this.legacyChromeClockSyncFinishEvent_.name.substring(
+ LEGACY_CHROME_CLOCK_SYNC_EVENT_NAME_PREFIX.length);
+
+ if (startSyncId !== finishSyncId) {
+ throw new Error(
+ 'Inconsistent clock sync ID of legacy Chrome clock sync events');
+ }
+
+ this.model_.clockSyncManager.addClockSyncMarker(
+ this.clockDomainId_, startSyncId,
+ tr.b.Unit.timestampFromUs(this.legacyChromeClockSyncStartEvent_.ts),
+ tr.b.Unit.timestampFromUs(this.legacyChromeClockSyncFinishEvent_.ts));
+ },
+
+ // Because the order of Jit code events and V8 samples are not guaranteed,
+ // We store them in an array, sort by timestamp, and then process them.
+ processV8Events() {
+ this.v8SamplingData_.sort(function(a, b) {
+ if (a.ts !== b.ts) return a.ts - b.ts;
+ if (a.ph === 'M' || a.ph === 'I') {
+ return -1;
+ } else if (b.ph === 'M' || b.ph === 'I') {
+ return 1;
+ }
+ return 0;
+ });
+ const length = this.v8SamplingData_.length;
+ for (let i = 0; i < length; ++i) {
+ const event = this.v8SamplingData_[i];
+ if (event.ph === 'M' || event.ph === 'I') {
+ this.processJitCodeEvent(event);
+ } else if (event.ph === 'P') {
+ // The current sampling format and legacy sampling format
+ // have the same ph, but the current sampling format will
+ // start with 'Profile' in its name.
+ if (event.name.startsWith('Profile')) {
+ this.processSample(event);
+ } else {
+ this.processLegacyV8Sample(event);
+ }
+ }
+ }
+ },
+
+ importClockSyncMarkers() {
+ if (this.events_ instanceof tr.b.TraceStream) {
+ const parser = oboe().node(
+ '{cat ph}', this.importClockSyncMarker_.bind(this));
+ this.events_.rewind();
+ while (this.events_.hasData) {
+ parser.write(this.events_.readNumBytes());
+ }
+ parser.finish();
+ } else {
+ for (let i = 0; i < this.events_.length; i++) {
+ this.importClockSyncMarker_(this.events_[i]);
+ }
+ }
+ },
+
+ importClockSyncMarker_(event) {
+ const isLegacyChromeClockSync = isLegacyChromeClockSyncEvent(event);
+ if (event.ph !== 'c' && !isLegacyChromeClockSync) return;
+
+ const eventSizeInBytes =
+ this.model_.importOptions.trackDetailedModelStats ?
+ JSON.stringify(event).length : undefined;
+
+ this.model_.stats.willProcessBasicTraceEvent(
+ 'clock_sync', event.cat, event.name, event.ts, eventSizeInBytes);
+
+ if (isLegacyChromeClockSync) {
+ this.processLegacyChromeClockSyncEvent(event);
+ } else {
+ this.processClockSyncEvent(event);
+ }
+ },
+
+ /**
+ * Walks through the events_ list and outputs the structures discovered to
+ * model_.
+ */
+ importEvents() {
+ this.hasEvents_ = false;
+ if (this.stackFrameEvents_) {
+ this.importStackFrames_(this.stackFrameEvents_, 'g');
+ }
+
+ if (this.traceAnnotations_) this.importAnnotations_();
+
+ if (this.events_ instanceof tr.b.TraceStream) {
+ const parser = oboe().node('{cat ph}', this.processEvent_.bind(this));
+ this.events_.rewind();
+ while (this.events_.hasData) {
+ parser.write(this.events_.readNumBytes());
+ }
+ parser.finish();
+ } else {
+ for (let eI = 0; eI < this.events_.length; eI++) {
+ this.processEvent_(this.events_[eI]);
+ }
+ }
+
+ // Import calls importEvents then autoCloseOpenSlices then finalizeImport.
+ // AsyncSlices must be created in importEvents so they can be auto-closed.
+ this.createAsyncSlices_();
+
+ this.processV8Events();
+
+ // Remove all the root stack frame children as they should
+ // already be added.
+ for (const frame of Object.values(this.v8ProcessRootStackFrame_)) {
+ frame.removeAllChildren();
+ }
+ },
+
+ // Some trace authors store subtraces as specific properties of the trace.
+ storeSubtrace_(subtrace) {
+ this.subtraces_.push(subtrace);
+ return oboe.drop;
+ },
+
+ storeSamples_(samples) {
+ this.sampleEvents_ = samples;
+ return oboe.drop;
+ },
+
+ storeStackFrames_(stackFrames) {
+ this.stackFrameEvents_ = stackFrames;
+ return oboe.drop;
+ },
+
+ // Some implementations specify displayTimeUnit
+ storeDisplayTimeUnit_(unitName) {
+ if (!unitName) return;
+ const unit = tr.b.TimeDisplayModes[unitName];
+ if (unit === undefined) {
+ throw new Error('Unit ' + unitName + ' is not supported.');
+ }
+ this.model_.intrinsicTimeUnit = unit;
+ return oboe.drop;
+ },
+
+ storeTraceAnnotations_(traceAnnotations) {
+ this.traceAnnotations_ = traceAnnotations;
+ return oboe.drop;
+ },
+
+ // Any fields in the container that is not in NON_METADATA_FIELDS should be
+ // treated as metadata.
+ storeMetadata_(container) {
+ for (const fieldName of Object.keys(container)) {
+ if (NON_METADATA_FIELDS.has(fieldName)) continue;
+ this.model_.metadata.push(
+ { name: fieldName, value: container[fieldName] });
+ if (fieldName !== 'metadata') continue;
+ const metadata = container[fieldName];
+ if (metadata['highres-ticks']) {
+ this.model_.isTimeHighResolution = metadata['highres-ticks'];
+ }
+ if (metadata['clock-domain']) {
+ this.clockDomainId_ = metadata['clock-domain'];
+ }
+ }
+ return oboe.drop;
+ },
+
+ processEvent_(event) {
+ this.hasEvents_ = true;
+ const importOptions = this.model_.importOptions;
+ const trackDetailedModelStats = importOptions.trackDetailedModelStats;
+ const modelStats = this.model_.stats;
+
+ if (event.args === '__stripped__') {
+ event.argsStripped = true;
+ event.args = undefined;
+ }
+
+ let eventSizeInBytes = undefined;
+ if (trackDetailedModelStats) {
+ eventSizeInBytes = JSON.stringify(event).length;
+ }
+
+ switch (event.ph) {
+ case 'B':
+ case 'E':
+ modelStats.willProcessBasicTraceEvent(
+ 'begin_end (non-compact)', event.cat, event.name, event.ts,
+ eventSizeInBytes);
+ this.processDurationEvent(event);
+ break;
+
+ case 'X': {
+ modelStats.willProcessBasicTraceEvent(
+ 'begin_end (compact)', event.cat, event.name, event.ts,
+ eventSizeInBytes);
+ const slice = this.processCompleteEvent(event);
+ // TODO(yuhaoz): If Chrome supports creating other events with flow,
+ // we will need to call processFlowEvent for them also.
+ // https://github.com/catapult-project/catapult/issues/1259
+ if (slice !== undefined && event.bind_id !== undefined) {
+ this.processFlowEvent(event, slice);
+ }
+ break;
+ }
+
+ case 'b':
+ case 'e':
+ case 'n':
+ case 'S':
+ case 'F':
+ case 'T':
+ case 'p':
+ modelStats.willProcessBasicTraceEvent(
+ 'async', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processAsyncEvent(event);
+ break;
+
+ // Note, I is historic. The instant event marker got changed, but we
+ // want to support loading old trace files so we have both I and i.
+ case 'I':
+ case 'i':
+ case 'R':
+ modelStats.willProcessBasicTraceEvent(
+ 'instant', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processInstantEvent(event);
+ break;
+
+ case 'P':
+ modelStats.willProcessBasicTraceEvent(
+ 'samples', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processTraceSampleEvent(event);
+ break;
+
+ case 'C':
+ modelStats.willProcessBasicTraceEvent(
+ 'counters', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processCounterEvent(event);
+ break;
+
+ case 'M':
+ modelStats.willProcessBasicTraceEvent(
+ 'metadata', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processMetadataEvent(event);
+ break;
+
+ case 'N':
+ case 'D':
+ case 'O':
+ modelStats.willProcessBasicTraceEvent(
+ 'objects', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processObjectEvent(event);
+ break;
+
+ case 's':
+ case 't':
+ case 'f':
+ modelStats.willProcessBasicTraceEvent(
+ 'flows', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.processFlowEvent(event);
+ break;
+
+ case 'v':
+ modelStats.willProcessBasicTraceEvent(
+ 'memory_dumps', event.cat, event.name, event.ts,
+ eventSizeInBytes);
+ this.processMemoryDumpEvent(event);
+ break;
+
+ case '(':
+ case ')':
+ this.processContextEvent(event);
+ break;
+
+ // No-op. Clock sync events have already been processed in
+ // importClockSyncMarkers().
+ case 'c':
+ break;
+
+ default:
+ modelStats.willProcessBasicTraceEvent(
+ 'unknown', event.cat, event.name, event.ts, eventSizeInBytes);
+ this.model_.importWarning({
+ type: 'parse_error',
+ message: 'Unrecognized event phase: ' +
+ event.ph + ' (' + event.name + ')'
+ });
+ }
+ return oboe.drop;
+ },
+
+ importStackFrames_(rawStackFrames, idPrefix) {
+ const model = this.model_;
+
+ for (const id in rawStackFrames) {
+ const rawStackFrame = rawStackFrames[id];
+ const fullId = idPrefix + id;
+ const textForColor = rawStackFrame.category ?
+ rawStackFrame.category : rawStackFrame.name;
+ const stackFrame = new tr.model.StackFrame(
+ undefined /* parentFrame */, fullId, rawStackFrame.name,
+ ColorScheme.getColorIdForGeneralPurposeString(textForColor));
+ model.addStackFrame(stackFrame);
+ }
+
+ for (const id in rawStackFrames) {
+ const fullId = idPrefix + id;
+ const stackFrame = model.stackFrames[fullId];
+ if (stackFrame === undefined) {
+ throw new Error('Internal error');
+ }
+
+ const rawStackFrame = rawStackFrames[id];
+ const parentId = rawStackFrame.parent;
+ let parentStackFrame;
+ if (parentId === undefined) {
+ parentStackFrame = undefined;
+ } else {
+ const parentFullId = idPrefix + parentId;
+ parentStackFrame = model.stackFrames[parentFullId];
+ if (parentStackFrame === undefined) {
+ this.model_.importWarning({
+ type: 'metadata_parse_error',
+ message: 'Missing parent frame with ID ' + parentFullId +
+ ' for stack frame \'' + stackFrame.name + '\' (ID ' + fullId +
+ ').'
+ });
+ }
+ }
+ stackFrame.parentFrame = parentStackFrame;
+ }
+
+ // This section is a reimplementation of the above behaviour but using
+ // the new ProfileNode type instead of StackFrame.
+ // We keep the StackFrame version for compatibility for now.
+ const ProfileNodeType =
+ tr.model.ProfileNode.subTypes.getConstructor(undefined,
+ 'legacySample');
+ if (idPrefix === 'g') {
+ for (const id in rawStackFrames) {
+ const rawStackFrame = rawStackFrames[id];
+ const textForColor = rawStackFrame.category ?
+ rawStackFrame.category : rawStackFrame.name;
+ const node = this.stackFrameTree_.add(new ProfileNodeType(
+ 'g' + id, {
+ functionName: rawStackFrame.name
+ }, undefined));
+ node.colorId =
+ ColorScheme.getColorIdForGeneralPurposeString(textForColor);
+ node.parentId = rawStackFrame.parent;
+ }
+
+ for (const id in rawStackFrames) {
+ const node = this.stackFrameTree_.getNode('g' + id);
+ const parentId = node.parentId;
+ let parentNode = undefined;
+ if (parentId !== undefined) {
+ parentNode = this.stackFrameTree_.getNode('g' + parentId);
+ if (parentNode === undefined) {
+ this.model_.importWarning({
+ type: 'metadata_parse_error',
+ message: 'Missing parent frame with ID ' + parentId +
+ ' for stack frame \'' + node.name + '\' (ID ' + node.id +
+ ').'
+ });
+ }
+ node.parentNode = parentNode;
+ }
+ }
+ }
+ },
+
+ importObjectTypeNameMap_(rawObjectTypeNameMap, pid) {
+ if (pid in this.objectTypeNameMap_) {
+ this.model_.importWarning({
+ type: 'metadata_parse_error',
+ message: 'Mapping from object type IDs to names provided for pid=' +
+ pid + ' multiple times.'
+ });
+ return;
+ }
+
+ let objectTypeNamePrefix = undefined;
+ let objectTypeNameSuffix = undefined;
+ const objectTypeNameMap = {};
+ for (const objectTypeId in rawObjectTypeNameMap) {
+ const rawObjectTypeName = rawObjectTypeNameMap[objectTypeId];
+
+ // If we haven't figured out yet which compiler the object type names
+ // come from, we try to do it now.
+ if (objectTypeNamePrefix === undefined) {
+ for (let i = 0; i < OBJECT_TYPE_NAME_PATTERNS.length; i++) {
+ const pattern = OBJECT_TYPE_NAME_PATTERNS[i];
+ if (rawObjectTypeName.startsWith(pattern.prefix) &&
+ rawObjectTypeName.endsWith(pattern.suffix)) {
+ objectTypeNamePrefix = pattern.prefix;
+ objectTypeNameSuffix = pattern.suffix;
+ break;
+ }
+ }
+ }
+
+ if (objectTypeNamePrefix !== undefined &&
+ rawObjectTypeName.startsWith(objectTypeNamePrefix) &&
+ rawObjectTypeName.endsWith(objectTypeNameSuffix)) {
+ // With compiler-specific prefix and suffix (automatically annotated
+ // object types).
+ objectTypeNameMap[objectTypeId] = rawObjectTypeName.substring(
+ objectTypeNamePrefix.length,
+ rawObjectTypeName.length - objectTypeNameSuffix.length);
+ } else {
+ // Without compiler-specific prefix and suffix (manually annotated
+ // object types and '[unknown]').
+ objectTypeNameMap[objectTypeId] = rawObjectTypeName;
+ }
+ }
+
+ this.objectTypeNameMap_[pid] = objectTypeNameMap;
+ },
+
+ importAnnotations_() {
+ for (const id in this.traceAnnotations_) {
+ const annotation = tr.model.Annotation.fromDictIfPossible(
+ this.traceAnnotations_[id]);
+ if (!annotation) {
+ this.model_.importWarning({
+ type: 'annotation_warning',
+ message: 'Unrecognized traceAnnotation typeName \"' +
+ this.traceAnnotations_[id].typeName + '\"'
+ });
+ continue;
+ }
+ this.model_.addAnnotation(annotation);
+ }
+ },
+
+ /**
+ * Called by the Model after all other importers have imported their
+ * events.
+ */
+ finalizeImport() {
+ if (this.softwareMeasuredCpuCount_ !== undefined) {
+ this.model_.kernel.softwareMeasuredCpuCount =
+ this.softwareMeasuredCpuCount_;
+ }
+ this.createFlowSlices_();
+ this.createExplicitObjects_();
+ this.createImplicitObjects_();
+ this.createMemoryDumps_();
+ },
+
+ /* Events can have one or more stack frames associated with them, but
+ * that frame might be encoded either as a stack trace of program counters,
+ * or as a direct stack frame reference. This handles either case and
+ * if found, returns the stackframe.
+ */
+ getStackFrameForEvent_(event, opt_lookForEndEvent) {
+ let sf;
+ let stack;
+ if (opt_lookForEndEvent) {
+ sf = event.esf;
+ stack = event.estack;
+ } else {
+ sf = event.sf;
+ stack = event.stack;
+ }
+ if (stack !== undefined && sf !== undefined) {
+ this.model_.importWarning({
+ type: 'stack_frame_and_stack_error',
+ message: 'Event at ' + event.ts +
+ ' cannot have both a stack and a stackframe.'
+ });
+ return undefined;
+ }
+
+ if (stack !== undefined) {
+ return this.model_.resolveStackToStackFrame_(event.pid, stack);
+ }
+ if (sf === undefined) return undefined;
+
+ const stackFrame = this.model_.stackFrames['g' + sf];
+ if (stackFrame === undefined) {
+ this.model_.importWarning({
+ type: 'sample_import_error',
+ message: 'No frame for ' + sf
+ });
+ return;
+ }
+ return stackFrame;
+ },
+
+ resolveStackToStackFrame_(pid, stack) {
+ // TODO(alph,fmeawad): Add codemap resolution code here.
+ return undefined;
+ },
+
+ importSampleData() {
+ if (!this.sampleEvents_) return;
+ const m = this.model_;
+
+ // If this is the only importer, then fake-create the threads.
+ const events = this.sampleEvents_;
+ if (this.hasEvents_ === undefined) {
+ throw new Error('importEvents is not run before importSampleData');
+ } else if (!this.hasEvents_) {
+ for (let i = 0; i < events.length; i++) {
+ const event = events[i];
+ m.getOrCreateProcess(event.tid).getOrCreateThread(event.tid);
+ }
+ }
+
+ const threadsByTid = {};
+ m.getAllThreads().forEach(function(t) {
+ threadsByTid[t.tid] = t;
+ });
+
+ for (let i = 0; i < events.length; i++) {
+ const event = events[i];
+ const thread = threadsByTid[event.tid];
+ if (thread === undefined) {
+ m.importWarning({
+ type: 'sample_import_error',
+ message: 'Thread ' + events.tid + 'not found'
+ });
+ continue;
+ }
+
+ let cpu;
+ if (event.cpu !== undefined) {
+ cpu = m.kernel.getOrCreateCpu(event.cpu);
+ }
+
+ const leafNode = this.stackFrameTree_.getNode('g' + event.sf);
+
+ const sample = new tr.model.Sample(
+ this.toModelTimeFromUs_(event.ts),
+ event.name,
+ leafNode,
+ thread,
+ cpu,
+ event.weight
+ );
+ m.samples.push(sample);
+ }
+ },
+
+ createAsyncSlices_() {
+ if (this.allAsyncEvents_.length === 0) return;
+
+ this.allAsyncEvents_.sort(function(x, y) {
+ const d = x.event.ts - y.event.ts;
+ if (d !== 0) return d;
+ return x.sequenceNumber - y.sequenceNumber;
+ });
+
+ const legacyEvents = [];
+ // Group nestable async events by ID. Events with the same ID should
+ // belong to the same parent async event.
+ const nestableAsyncEventsByKey = {};
+ const nestableMeasureAsyncEventsByKey = {};
+ for (let i = 0; i < this.allAsyncEvents_.length; i++) {
+ const asyncEventState = this.allAsyncEvents_[i];
+ const event = asyncEventState.event;
+ if (event.ph === 'S' || event.ph === 'F' || event.ph === 'T' ||
+ event.ph === 'p') {
+ legacyEvents.push(asyncEventState);
+ continue;
+ }
+ 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 = 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) {
+ // ignored because we assume that we accidentally
+ // matched a performance.measure() where the user was
+ // not aware of this convention.
+ }
+ 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);
+ }
+ // Handle legacy async events.
+ this.createLegacyAsyncSlices_(legacyEvents);
+
+ // Parse nestable measure async events into AsyncSlices.
+ this.createNestableAsyncSlices_(nestableMeasureAsyncEventsByKey);
+
+ // Parse nestable async events into AsyncSlices.
+ this.createNestableAsyncSlices_(nestableAsyncEventsByKey);
+ },
+
+ createLegacyAsyncSlice_(events) {
+ const asyncEventState = events[events.length - 1];
+ const event = asyncEventState.event;
+ const name = event.name;
+ const id = TraceEventImporter.scopedIdForEvent_(event);
+ const key = id.toStringWithDelimiter(':');
+ const asyncSliceConstructor = tr.model.AsyncSlice.subTypes.getConstructor(
+ events[0].event.cat, name);
+ let duration;
+ if (event.ts !== undefined) {
+ duration = this.toModelTimeFromUs_(event.ts - events[0].event.ts);
+ }
+ const slice = new asyncSliceConstructor(
+ events[0].event.cat,
+ name,
+ getEventColor(events[0].event),
+ this.toModelTimeFromUs_(events[0].event.ts),
+ Object.assign({}, events[0].event.args, event.args),
+ duration || 0,
+ true,
+ undefined,
+ undefined,
+ events[0].event.argsStripped);
+ if (duration === undefined) {
+ slice.didNotFinish = true;
+ slice.error = 'Slice has no matching END. End time has been adjusted.';
+ // End time will be adjusted by AsyncSliceGroup.autoCloseOpenSlices().
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'Legacy async BEGIN event at ' +
+ events[0].event.ts + ' with name="' +
+ name + '" and id=' + key + ' was unmatched.'
+ });
+ }
+ slice.startThread = events[0].thread;
+ slice.endThread = asyncEventState.thread;
+ slice.id = key;
+
+ const stepType = events[1].event.ph;
+ let isValid = true;
+
+ // Create subSlices for each step. Skip the start and finish events,
+ // which are always first and last respectively.
+ for (let j = 1; j < events.length - 1; ++j) {
+ if (events[j].event.ph === 'T' || events[j].event.ph === 'p') {
+ isValid = this.assertStepTypeMatches_(stepType, events[j]);
+ if (!isValid) break;
+ }
+
+ if (events[j].event.ph === 'S') {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'At ' + events[j].event.ts + ', a slice named "' +
+ name + '" with id=' + id +
+ ' had a step before the start event.'
+ });
+ continue;
+ }
+
+ if (events[j].event.ph === 'F') {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'At ' + events[j].event.ts + ', a slice named ' +
+ name + ' with id=' + id +
+ ' had a step after the finish event.'
+ });
+ continue;
+ }
+
+ const startIndex = j + (stepType === 'T' ? 0 : -1);
+ const endIndex = startIndex + 1;
+
+ let subName = name;
+ if (!events[j].event.argsStripped &&
+ (events[j].event.ph === 'T' || events[j].event.ph === 'p')) {
+ subName = events[j].event.args.step;
+ }
+
+ const asyncSliceConstructor =
+ tr.model.AsyncSlice.subTypes.getConstructor(
+ events[0].event.cat,
+ subName);
+ let duration;
+ if (events[endIndex].event.ts !== undefined) {
+ duration = this.toModelTimeFromUs_(
+ events[endIndex].event.ts - events[startIndex].event.ts);
+ }
+ const subSlice = new asyncSliceConstructor(
+ events[0].event.cat,
+ subName,
+ getEventColor(events[0].event, subName + j),
+ this.toModelTimeFromUs_(events[startIndex].event.ts),
+ this.deepCopyIfNeeded_(events[j].event.args),
+ duration || 0,
+ undefined,
+ undefined,
+ events[startIndex].event.argsStripped);
+ if (duration === undefined) {
+ subSlice.didNotFinish = true;
+ subSlice.error =
+ 'Slice has no matching END. End time has been adjusted.';
+ // End time will be adjusted by AsyncSliceGroup.autoCloseOpenSlices().
+ }
+ subSlice.startThread = events[startIndex].thread;
+ subSlice.endThread = events[endIndex].thread;
+ subSlice.id = key;
+
+ slice.subSlices.push(subSlice);
+ }
+
+ if (isValid) {
+ // Add |slice| to the start-thread's asyncSlices.
+ slice.startThread.asyncSliceGroup.push(slice);
+ }
+ },
+
+ createLegacyAsyncSlices_(legacyEvents) {
+ if (legacyEvents.length === 0) return;
+
+ legacyEvents.sort(function(x, y) {
+ const d = x.event.ts - y.event.ts;
+ if (d !== 0) return d;
+ return x.sequenceNumber - y.sequenceNumber;
+ });
+
+ const asyncEventStatesByNameThenID = {};
+
+ for (let i = 0; i < legacyEvents.length; i++) {
+ const asyncEventState = legacyEvents[i];
+
+ const event = asyncEventState.event;
+ const name = event.name;
+ if (name === undefined) {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'Async events (ph: S, T, p, or F) require a name ' +
+ ' parameter.'
+ });
+ continue;
+ }
+
+ const id = TraceEventImporter.scopedIdForEvent_(event);
+ if (id === undefined) {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'Async events (ph: S, T, p, or F) require an id parameter.'
+ });
+ continue;
+ }
+ const key = id.toStringWithDelimiter(':');
+ // TODO(simonjam): Add a synchronous tick on the appropriate thread.
+
+ if (event.ph === 'S') {
+ if (asyncEventStatesByNameThenID[name] === undefined) {
+ asyncEventStatesByNameThenID[name] = {};
+ }
+ if (asyncEventStatesByNameThenID[name][key]) {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'At ' + event.ts + ', a slice of the same id ' + id +
+ ' was alrady open.'
+ });
+ continue;
+ }
+ asyncEventStatesByNameThenID[name][key] = [];
+ asyncEventStatesByNameThenID[name][key].push(asyncEventState);
+ } else {
+ if (asyncEventStatesByNameThenID[name] === undefined) {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: `At ${event.ts}, no slice named "${name}" was open.`,
+ });
+ continue;
+ }
+ if (asyncEventStatesByNameThenID[name][key] === undefined) {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message:
+ `At ${event.ts}, no slice named "${name}" with id=${id} was ` +
+ 'open.',
+ });
+ continue;
+ }
+ const events = asyncEventStatesByNameThenID[name][key];
+ events.push(asyncEventState);
+
+ if (event.ph === 'F') {
+ this.createLegacyAsyncSlice_(events);
+ delete asyncEventStatesByNameThenID[name][key];
+ }
+ }
+ }
+
+ // Auto-close unmatched S events by creating a fake F event.
+ for (const [name, statesByID] of
+ Object.entries(asyncEventStatesByNameThenID)) {
+ for (const [id, states] of Object.entries(statesByID)) {
+ const startEvent = states[0].event;
+ // Don't set 'ts' in the fake 'F' event. createLegacyAsyncSlice_ will
+ // set duration=undefined and didNotFinish=true.
+ // AsyncSliceGroup.autoCloseOpenSlices() will set duration so that the
+ // AsyncSlice ends when the trace ends.
+ states.push({
+ sequenceNumber: 1 + states[states.length - 1].sequenceNumber,
+ event: {
+ ph: 'F',
+ name,
+ id: startEvent.id,
+ id2: startEvent.id2,
+ scope: startEvent.scope,
+ pid: startEvent.pid,
+ tid: startEvent.tid,
+ cat: startEvent.cat,
+ args: {},
+ },
+ thread: this.model_.getOrCreateProcess(startEvent.pid).
+ getOrCreateThread(startEvent.tid),
+ });
+ this.createLegacyAsyncSlice_(states);
+ }
+ }
+ },
+
+ createNestableAsyncSlices_(nestableEventsByKey) {
+ for (const key in nestableEventsByKey) {
+ const eventStateEntries = nestableEventsByKey[key];
+ // Stack of enclosing BEGIN events.
+ const parentStack = [];
+ for (let i = 0; i < eventStateEntries.length; ++i) {
+ const eventStateEntry = eventStateEntries[i];
+ // If this is the end of an event, match it to the start.
+ if (eventStateEntry.event.ph === 'e') {
+ // Walk up the parent stack to find the corresponding BEGIN for
+ // this END.
+ let parentIndex = -1;
+ for (let k = parentStack.length - 1; k >= 0; --k) {
+ if (parentStack[k].event.name === eventStateEntry.event.name) {
+ parentIndex = k;
+ break;
+ }
+ }
+ if (parentIndex === -1) {
+ // Unmatched end.
+ eventStateEntry.finished = false;
+ } else {
+ parentStack[parentIndex].end = eventStateEntry;
+ // Pop off all enclosing unmatched BEGINs util parentIndex.
+ while (parentIndex < parentStack.length) {
+ parentStack.pop();
+ }
+ }
+ }
+ // Inherit the current parent.
+ if (parentStack.length > 0) {
+ eventStateEntry.parentEntry = parentStack[parentStack.length - 1];
+ }
+ if (eventStateEntry.event.ph === 'b') {
+ parentStack.push(eventStateEntry);
+ }
+ }
+ const topLevelSlices = [];
+ for (let i = 0; i < eventStateEntries.length; ++i) {
+ const eventStateEntry = eventStateEntries[i];
+ // Skip matched END, as its slice will be created when we
+ // encounter its corresponding BEGIN.
+ if (eventStateEntry.event.ph === 'e' &&
+ eventStateEntry.finished === undefined) {
+ continue;
+ }
+ let startState = undefined;
+ let endState = undefined;
+ let sliceArgs = eventStateEntry.event.args || {};
+ let sliceError = undefined;
+ const id = TraceEventImporter.scopedIdForEvent_(
+ eventStateEntry.event);
+ if (eventStateEntry.event.ph === 'n') {
+ startState = eventStateEntry;
+ endState = eventStateEntry;
+ } else if (eventStateEntry.event.ph === 'b') {
+ if (eventStateEntry.end === undefined) {
+ // Unmatched BEGIN. End it when last event with this ID ends.
+ eventStateEntry.end =
+ eventStateEntries[eventStateEntries.length - 1];
+ sliceError =
+ 'Slice has no matching END. End time has been adjusted.';
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'Nestable async BEGIN event at ' +
+ eventStateEntry.event.ts + ' with name="' +
+ eventStateEntry.event.name + '" and id=' + id +
+ ' was unmatched.'
+ });
+ } else {
+ // Include args for both END and BEGIN for a matched pair.
+ function concatenateArguments(args1, args2) {
+ if (args1.params === undefined || args2.params === undefined) {
+ return Object.assign({}, args1, args2);
+ }
+ // Make an argument object to hold the combined params.
+ const args3 = {};
+ args3.params = Object.assign({}, args1.params, args2.params);
+ return Object.assign({}, args1, args2, args3);
+ }
+ const endArgs = eventStateEntry.end.event.args || {};
+ sliceArgs = concatenateArguments(sliceArgs, endArgs);
+ }
+ startState = eventStateEntry;
+ endState = eventStateEntry.end;
+ } else {
+ // Unmatched END. Start it at the first event with this ID starts.
+ sliceError =
+ 'Slice has no matching BEGIN. Start time has been adjusted.';
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'Nestable async END event at ' +
+ eventStateEntry.event.ts + ' with name=' +
+ eventStateEntry.event.name +
+ ' and id=' + id + ' was unmatched.'
+ });
+ startState = eventStateEntries[0];
+ endState = eventStateEntry;
+ }
+
+ const isTopLevel = (eventStateEntry.parentEntry === undefined);
+ const asyncSliceConstructor =
+ tr.model.AsyncSlice.subTypes.getConstructor(
+ eventStateEntry.event.cat,
+ eventStateEntry.event.name);
+
+ let threadStart = undefined;
+ let threadDuration = undefined;
+ if (startState.event.tts && startState.event.use_async_tts) {
+ threadStart = this.toModelTimeFromUs_(startState.event.tts);
+ if (endState.event.tts) {
+ const threadEnd = this.toModelTimeFromUs_(endState.event.tts);
+ threadDuration = threadEnd - threadStart;
+ }
+ }
+
+ const slice = new asyncSliceConstructor(
+ eventStateEntry.event.cat,
+ eventStateEntry.event.name,
+ getEventColor(endState.event),
+ this.toModelTimeFromUs_(startState.event.ts),
+ sliceArgs,
+ this.toModelTimeFromUs_(endState.event.ts - startState.event.ts),
+ isTopLevel,
+ threadStart,
+ threadDuration,
+ startState.event.argsStripped);
+
+ slice.startThread = startState.thread;
+ slice.endThread = endState.thread;
+
+ slice.startStackFrame = this.getStackFrameForEvent_(startState.event);
+ slice.endStackFrame = this.getStackFrameForEvent_(endState.event);
+
+ slice.id = key;
+ if (sliceError !== undefined) {
+ slice.error = sliceError;
+ }
+ eventStateEntry.slice = slice;
+ // Add the slice to the topLevelSlices array if there is no parent.
+ // Otherwise, add the slice to the subSlices of its parent.
+ if (isTopLevel) {
+ topLevelSlices.push(slice);
+ } else if (eventStateEntry.parentEntry.slice !== undefined) {
+ eventStateEntry.parentEntry.slice.subSlices.push(slice);
+ }
+ }
+ for (let si = 0; si < topLevelSlices.length; si++) {
+ topLevelSlices[si].startThread.asyncSliceGroup.push(
+ topLevelSlices[si]);
+ }
+ }
+ },
+
+ assertStepTypeMatches_(stepType, event) {
+ if (stepType !== event.event.ph) {
+ this.model_.importWarning({
+ type: 'async_slice_parse_error',
+ message: 'At ' + event.event.ts + ', a slice named ' +
+ event.event.name + ' with id=' +
+ TraceEventImporter.scopedIdForEvent_(event.event) +
+ ' had both begin and end steps, which is not allowed.'
+ });
+ return false;
+ }
+ return true;
+ },
+
+ validateFlowEvent_(event) {
+ if (event.name === undefined) {
+ this.model_.importWarning({
+ type: 'flow_slice_parse_error',
+ message: 'Flow events (ph: s, t or f) require a name parameter.'
+ });
+ return false;
+ }
+
+ // Support Flow API v1.
+ if (event.ph === 's' || event.ph === 'f' || event.ph === 't') {
+ if (event.id === undefined) {
+ this.model_.importWarning({
+ type: 'flow_slice_parse_error',
+ message: 'Flow events (ph: s, t or f) require an id parameter.'
+ });
+ return false;
+ }
+ return true;
+ }
+
+ // Support Flow API v2.
+ if (event.bind_id) {
+ if (event.flow_in === undefined && event.flow_out === undefined) {
+ this.model_.importWarning({
+ type: 'flow_slice_parse_error',
+ message: 'Flow producer or consumer require flow_in or flow_out.'
+ });
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ },
+
+ createFlowSlices_() {
+ if (this.allFlowEvents_.length === 0) return;
+
+ const createFlowEvent = function(thread, event, opt_slice) {
+ let startSlice;
+ let flowId;
+ let flowStartTs;
+
+ if (event.bind_id) {
+ // Support Flow API v2.
+ startSlice = opt_slice;
+ flowId = event.bind_id;
+ flowStartTs = this.toModelTimeFromUs_(event.ts + event.dur);
+ } else {
+ // Support Flow API v1.
+ const ts = this.toModelTimeFromUs_(event.ts);
+ startSlice = thread.sliceGroup.findSliceAtTs(ts);
+ if (startSlice === undefined) return undefined;
+ flowId = event.id;
+ flowStartTs = ts;
+ }
+
+ const flowEvent = new tr.model.FlowEvent(
+ event.cat,
+ flowId,
+ event.name,
+ getEventColor(event),
+ flowStartTs,
+ this.deepCopyAlways_(event.args));
+ flowEvent.startSlice = startSlice;
+ flowEvent.startStackFrame = this.getStackFrameForEvent_(event);
+ flowEvent.endStackFrame = undefined;
+ startSlice.outFlowEvents.push(flowEvent);
+ return flowEvent;
+ }.bind(this);
+
+ const finishFlowEventWith = function(
+ flowEvent, thread, event, refGuid, bindToParent, opt_slice) {
+ let endSlice;
+
+ if (event.bind_id) {
+ // Support Flow API v2.
+ endSlice = opt_slice;
+ } else {
+ // Support Flow API v1.
+ const ts = this.toModelTimeFromUs_(event.ts);
+ if (bindToParent) {
+ endSlice = thread.sliceGroup.findSliceAtTs(ts);
+ } else {
+ endSlice = thread.sliceGroup.findNextSliceAfter(ts, refGuid);
+ }
+ if (endSlice === undefined) return false;
+ }
+
+ endSlice.inFlowEvents.push(flowEvent);
+ flowEvent.endSlice = endSlice;
+ flowEvent.duration =
+ this.toModelTimeFromUs_(event.ts) - flowEvent.start;
+ flowEvent.endStackFrame = this.getStackFrameForEvent_(event);
+ this.mergeArgsInto_(flowEvent.args, event.args, flowEvent.title);
+ return true;
+ }.bind(this);
+
+ const processFlowConsumer = function(
+ flowIdToEvent, sliceGuidToEvent, event, slice) {
+ let flowEvent = flowIdToEvent[event.bind_id];
+ if (flowEvent === undefined) {
+ this.model_.importWarning({
+ type: 'flow_slice_ordering_error',
+ message: 'Flow consumer ' + event.bind_id + ' does not have ' +
+ 'a flow producer'});
+ return false;
+ } else if (flowEvent.endSlice) {
+ // One flow producer can have more than one flow consumers.
+ // In this case, create a new flow event using the flow producer.
+ const flowProducer = flowEvent.startSlice;
+ flowEvent = createFlowEvent(undefined,
+ sliceGuidToEvent[flowProducer.guid], flowProducer);
+ }
+
+ const refGuid = undefined;
+ const ok = finishFlowEventWith(flowEvent, undefined, event,
+ refGuid, undefined, slice);
+ if (ok) {
+ this.model_.flowEvents.push(flowEvent);
+ } else {
+ this.model_.importWarning({
+ type: 'flow_slice_end_error',
+ message: 'Flow consumer ' + event.bind_id + ' does not end ' +
+ 'at an actual slice, so cannot be created.'});
+ return false;
+ }
+
+ return true;
+ }.bind(this);
+
+ const processFlowProducer = function(
+ flowIdToEvent, flowStatus, event, slice) {
+ if (flowIdToEvent[event.bind_id] &&
+ flowStatus[event.bind_id]) {
+ // Can't open the same flow again while it's still open.
+ // This is essentially the multi-producer case which we don't support
+ this.model_.importWarning({
+ type: 'flow_slice_start_error',
+ message: 'Flow producer ' + event.bind_id + ' already seen'});
+ return false;
+ }
+
+ const flowEvent = createFlowEvent(undefined, event, slice);
+ if (!flowEvent) {
+ this.model_.importWarning({
+ type: 'flow_slice_start_error',
+ message: 'Flow producer ' + event.bind_id + ' does not start' +
+ 'a flow'});
+ return false;
+ }
+ flowIdToEvent[event.bind_id] = flowEvent;
+ }.bind(this);
+
+ // Actual import.
+ this.allFlowEvents_.sort(function(x, y) {
+ const d = x.event.ts - y.event.ts;
+ if (d !== 0) return d;
+ return x.sequenceNumber - y.sequenceNumber;
+ });
+
+ const flowIdToEvent = {};
+ const sliceGuidToEvent = {};
+ const flowStatus = {}; // true: open; false: closed.
+ for (let i = 0; i < this.allFlowEvents_.length; ++i) {
+ const data = this.allFlowEvents_[i];
+ const refGuid = data.refGuid;
+ const event = data.event;
+ const thread = data.thread;
+
+ if (!this.validateFlowEvent_(event)) continue;
+
+ // Support for Flow API v2.
+ if (event.bind_id) {
+ const slice = data.slice;
+ sliceGuidToEvent[slice.guid] = event;
+
+ if (event.flowPhase === PRODUCER) {
+ if (!processFlowProducer(flowIdToEvent, flowStatus, event, slice)) {
+ continue;
+ }
+ flowStatus[event.bind_id] = true; // open the flow.
+ } else {
+ if (!processFlowConsumer(flowIdToEvent, sliceGuidToEvent,
+ event, slice)) {
+ continue;
+ }
+ flowStatus[event.bind_id] = false; // close the flow.
+
+ if (event.flowPhase === STEP) {
+ if (!processFlowProducer(flowIdToEvent, flowStatus,
+ event, slice)) {
+ continue;
+ }
+ flowStatus[event.bind_id] = true; // open the flow again.
+ }
+ }
+ continue;
+ }
+
+ // Support for Flow API v1.
+ 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;
+ }
+ 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;
+ }
+
+ let bindToParent = event.ph === 't';
+
+ if (event.ph === 'f') {
+ if (event.bp === undefined) {
+ // TODO(yuhaoz): In flow V2, there is no notion of binding point.
+ // Removal of binding point is tracked in
+ // https://github.com/google/trace-viewer/issues/991.
+ 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 this is a step, then create another flow event.
+ if (ok && event.ph === 't') {
+ flowEvent = createFlowEvent(thread, event);
+ flowIdToEvent[event.id] = flowEvent;
+ }
+ }
+ }
+ },
+
+ /**
+ * This function creates objects described via the N, D, and O phase
+ * events.
+ */
+ 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;
+ }
+ let snapshot;
+ try {
+ const args = this.deepCopyIfNeeded_(event.args.snapshot);
+ let cat;
+ if (args.cat) {
+ cat = args.cat;
+ delete args.cat;
+ } else {
+ cat = event.cat;
+ }
+
+ let baseTypename;
+ if (args.base_type) {
+ baseTypename = args.base_type;
+ delete args.base_type;
+ } else {
+ baseTypename = undefined;
+ }
+ snapshot = process.objects.addSnapshot(
+ scopedId, cat, event.name, ts, args, baseTypename);
+ snapshot.snapshottedOnThread = thread;
+ } catch (e) {
+ this.model_.importWarning({
+ type: 'object_parse_error',
+ message: 'While processing snapshot of ' +
+ scopedId + ' at ts=' + ts + ': ' + e
+ });
+ return;
+ }
+ instance = snapshot.objectInstance;
+ } else if (event.ph === 'D') {
+ try {
+ process.objects.idWasDeleted(scopedId, event.cat, event.name, ts);
+ const instanceMap = process.objects.getOrCreateInstanceMap_(
+ scopedId);
+ instance = instanceMap.lastInstance;
+ } catch (e) {
+ this.model_.importWarning({
+ type: 'object_parse_error',
+ message: 'While processing delete of ' +
+ scopedId + ' at ts=' + ts + ': ' + e
+ });
+ return;
+ }
+ }
+
+ if (instance) {
+ instance.colorId = getEventColor(event, instance.typeName);
+ }
+ }.bind(this);
+
+ this.allObjectEvents_.sort(function(x, y) {
+ const d = x.event.ts - y.event.ts;
+ if (d !== 0) return d;
+ return x.sequenceNumber - y.sequenceNumber;
+ });
+
+ const allObjectEvents = this.allObjectEvents_;
+ for (let i = 0; i < allObjectEvents.length; i++) {
+ const objectEventState = allObjectEvents[i];
+ try {
+ processEvent.call(this, objectEventState);
+ } catch (e) {
+ this.model_.importWarning({
+ type: 'object_parse_error',
+ message: e.message
+ });
+ }
+ }
+ },
+
+ createImplicitObjects_() {
+ for (const proc of Object.values(this.model_.processes)) {
+ this.createImplicitObjectsForProcess_(proc);
+ }
+ },
+
+ // Here, we collect all the snapshots that internally contain a
+ // Javascript-level object inside their args list that has an "id" field,
+ // and turn that into a snapshot of the instance referred to by id.
+ createImplicitObjectsForProcess_(process) {
+ function processField(referencingObject,
+ referencingObjectFieldName,
+ referencingObjectFieldValue,
+ containingSnapshot) {
+ if (!referencingObjectFieldValue) return;
+
+ if (referencingObjectFieldValue instanceof
+ tr.model.ObjectSnapshot) {
+ return null;
+ }
+ if (referencingObjectFieldValue.id === undefined) return;
+
+ const implicitSnapshot = referencingObjectFieldValue;
+
+ const rawId = implicitSnapshot.id;
+ const m = /(.+)\/(.+)/.exec(rawId);
+ if (!m) {
+ throw new Error('Implicit snapshots must have names.');
+ }
+ delete implicitSnapshot.id;
+ const name = m[1];
+ const id = m[2];
+ let res;
+
+ let cat;
+ if (implicitSnapshot.cat !== undefined) {
+ cat = implicitSnapshot.cat;
+ } else {
+ cat = containingSnapshot.objectInstance.category;
+ }
+
+ let baseTypename;
+ if (implicitSnapshot.base_type) {
+ baseTypename = implicitSnapshot.base_type;
+ } else {
+ baseTypename = undefined;
+ }
+
+ const scope = containingSnapshot.objectInstance.scopedId.scope;
+
+ try {
+ res = process.objects.addSnapshot(
+ new tr.model.ScopedId(scope, id), cat,
+ name, containingSnapshot.ts,
+ implicitSnapshot, baseTypename);
+ } catch (e) {
+ this.model_.importWarning({
+ type: 'object_snapshot_parse_error',
+ message: 'While processing implicit snapshot of ' +
+ rawId + ' at ts=' + containingSnapshot.ts + ': ' + e
+ });
+ return;
+ }
+ res.objectInstance.hasImplicitSnapshots = true;
+ res.containingSnapshot = containingSnapshot;
+ res.snapshottedOnThread = containingSnapshot.snapshottedOnThread;
+ referencingObject[referencingObjectFieldName] = res;
+ if (!(res instanceof tr.model.ObjectSnapshot)) {
+ throw new Error('Created object must be instanceof snapshot');
+ }
+ return res.args;
+ }
+
+ /**
+ * Iterates over the fields in the object, calling func for every
+ * field/value found.
+ *
+ * @return {object} If the function does not want the field's value to be
+ * iterated, return null. If iteration of the field value is desired, then
+ * return either undefined (if the field value did not change) or the new
+ * field value if it was changed.
+ */
+ function iterObject(object, func, containingSnapshot, thisArg) {
+ if (!(object instanceof Object)) return;
+
+ if (object instanceof Array) {
+ for (let i = 0; i < object.length; i++) {
+ const res = func.call(thisArg, object, i, object[i],
+ containingSnapshot);
+ if (res === null) continue;
+ if (res) {
+ iterObject(res, func, containingSnapshot, thisArg);
+ } else {
+ iterObject(object[i], func, containingSnapshot, thisArg);
+ }
+ }
+ return;
+ }
+
+ for (const key in object) {
+ const res = func.call(thisArg, object, key, object[key],
+ containingSnapshot);
+ if (res === null) continue;
+ if (res) {
+ iterObject(res, func, containingSnapshot, thisArg);
+ } else {
+ iterObject(object[key], func, containingSnapshot, thisArg);
+ }
+ }
+ }
+
+ // TODO(nduca): We may need to iterate the instances in sorted order by
+ // creationTs.
+ process.objects.iterObjectInstances(function(instance) {
+ instance.snapshots.forEach(function(snapshot) {
+ if (snapshot.args.id !== undefined) {
+ throw new Error('args cannot have an id field inside it');
+ }
+ iterObject(snapshot.args, processField, snapshot, this);
+ }, this);
+ }, this);
+ },
+
+ // Takes an object of type: PID -> [process memory dump events]
+ // Returns smallest timestamp among events or Infinity if there are none.
+ minimalTimestampInPidToEvents_(pidToEvents) {
+ let smallestTs = Infinity;
+ for (const events of Object.values(pidToEvents)) {
+ for (const event of events) {
+ if (event.ts < smallestTs) {
+ smallestTs = event.ts;
+ }
+ }
+ }
+ return smallestTs;
+ },
+
+ createMemoryDumps_() {
+ // We must create the global dumps in chronological order since later
+ // heap dumps can depend on earlier ones. Not all events within a single
+ // global dump have the same timestamp but events for two different
+ // global dumps can not be interspersed. However, for consistency,
+ // we sort the global dumps by the earliest timestamp present in an event
+ // in that dump.
+ const pairs = Object.entries(this.allMemoryDumpEvents_);
+ const key = x => this.minimalTimestampInPidToEvents_(x);
+ pairs.sort((x, y) => key(x[1]) - key(y[1]));
+ for (const [dumpId, pidToEvents] of pairs) {
+ this.createGlobalMemoryDump_(pidToEvents, dumpId);
+ }
+ },
+
+ createGlobalMemoryDump_(dumpIdEvents, dumpId) {
+ // 1. Create a GlobalMemoryDump for the provided process memory dump
+ // the events, all of which have the same dump ID.
+
+ // Calculate the range of the global memory dump.
+ const globalRange = new tr.b.math.Range();
+ for (const pid in dumpIdEvents) {
+ const processEvents = dumpIdEvents[pid];
+ for (let i = 0; i < processEvents.length; i++) {
+ globalRange.addValue(this.toModelTimeFromUs_(processEvents[i].ts));
+ }
+ }
+ if (globalRange.isEmpty) {
+ throw new Error('Internal error: Global memory dump without events');
+ }
+
+ // Create the global memory dump.
+ const globalMemoryDump = new tr.model.GlobalMemoryDump(
+ this.model_, globalRange.min);
+ globalMemoryDump.duration = globalRange.range;
+ this.model_.globalMemoryDumps.push(globalMemoryDump);
+
+ const globalMemoryAllocatorDumpsByFullName = {};
+ const levelsOfDetail = {};
+ const allMemoryAllocatorDumpsByGuid = {};
+
+ // 2. Create a ProcessMemoryDump for each PID in the provided process
+ // memory dump events. Everything except for edges between memory
+ // allocator dumps is parsed from the process memory dump trace events at
+ // this step.
+ for (const pid in dumpIdEvents) {
+ this.createProcessMemoryDump_(globalMemoryDump,
+ globalMemoryAllocatorDumpsByFullName, levelsOfDetail,
+ allMemoryAllocatorDumpsByGuid, dumpIdEvents[pid], pid, dumpId);
+ }
+
+ // 3. Set the level of detail and memory allocator dumps of the
+ // GlobalMemoryDump, which come from the process memory dump trace
+ // events parsed in the prebvious step.
+ globalMemoryDump.levelOfDetail = levelsOfDetail.global;
+
+ // Find the root allocator dumps and establish the parent links of
+ // the global memory dump.
+ globalMemoryDump.memoryAllocatorDumps =
+ this.inferMemoryAllocatorDumpTree_(
+ globalMemoryAllocatorDumpsByFullName);
+
+ // 4. Finally, parse the edges between all memory allocator dumps within
+ // the GlobalMemoryDump. This can only be done once all memory allocator
+ // dumps have been parsed (i.e. it is necessary to iterate over the
+ // process memory dump trace events once more).
+ this.parseMemoryDumpAllocatorEdges_(allMemoryAllocatorDumpsByGuid,
+ dumpIdEvents, dumpId);
+ },
+
+ createProcessMemoryDump_(globalMemoryDump,
+ globalMemoryAllocatorDumpsByFullName, levelsOfDetail,
+ allMemoryAllocatorDumpsByGuid, processEvents, pid, dumpId) {
+ // Calculate the range of the process memory dump.
+ const processRange = new tr.b.math.Range();
+ for (let i = 0; i < processEvents.length; i++) {
+ processRange.addValue(this.toModelTimeFromUs_(processEvents[i].ts));
+ }
+ if (processRange.isEmpty) {
+ throw new Error('Internal error: Process memory dump without events');
+ }
+
+ // Create the process memory dump.
+ const process = this.model_.getOrCreateProcess(pid);
+ const processMemoryDump = new tr.model.ProcessMemoryDump(
+ globalMemoryDump, process, processRange.min);
+ processMemoryDump.duration = processRange.range;
+ process.memoryDumps.push(processMemoryDump);
+ globalMemoryDump.processMemoryDumps[pid] = processMemoryDump;
+
+ const processMemoryAllocatorDumpsByFullName = {};
+
+ // Parse all process memory dump trace events for the newly created
+ // ProcessMemoryDump.
+ for (let i = 0; i < processEvents.length; i++) {
+ const processEvent = processEvents[i];
+
+ const dumps = processEvent.args.dumps;
+ if (dumps === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: '\'dumps\' field not found in a process memory dump' +
+ ' event for PID=' + pid + ' and dump ID=' + dumpId + '.'
+ });
+ continue;
+ }
+
+ // Totals, VM regions, and heap dumps for the newly created
+ // ProcessMemoryDump should be present in at most one event, so they
+ // can be added to the ProcessMemoryDump immediately.
+ this.parseMemoryDumpTotals_(processMemoryDump, dumps, pid, dumpId);
+ this.parseMemoryDumpVmRegions_(processMemoryDump, dumps, pid, dumpId);
+ this.parseMemoryDumpHeapDumps_(processMemoryDump, dumps, pid, dumpId);
+
+ // All process memory dump trace events for the newly created
+ // ProcessMemoryDump must be processed before level of detail and
+ // allocator dumps can be added to it.
+ this.parseMemoryDumpLevelOfDetail_(levelsOfDetail, dumps, pid,
+ dumpId);
+ this.parseMemoryDumpAllocatorDumps_(processMemoryDump, globalMemoryDump,
+ processMemoryAllocatorDumpsByFullName,
+ globalMemoryAllocatorDumpsByFullName,
+ allMemoryAllocatorDumpsByGuid, dumps, pid, dumpId);
+ }
+
+ if (levelsOfDetail.process === undefined) {
+ // Infer level of detail from the presence of VM regions in legacy
+ // traces (where raw process memory dump events don't contain the
+ // level_of_detail field). These traces will not have BACKGROUND mode.
+ levelsOfDetail.process = processMemoryDump.vmRegions ? DETAILED : LIGHT;
+ }
+ if (!this.updateMemoryDumpLevelOfDetail_(
+ levelsOfDetail, 'global', levelsOfDetail.process)) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'diffent levels of detail provided for global memory' +
+ ' dump (dump ID=' + dumpId + ').'
+ });
+ }
+ processMemoryDump.levelOfDetail = levelsOfDetail.process;
+ delete levelsOfDetail.process; // Reused for all process dumps.
+
+ // Find the root allocator dumps and establish the parent links of
+ // the process memory dump.
+ processMemoryDump.memoryAllocatorDumps =
+ this.inferMemoryAllocatorDumpTree_(
+ processMemoryAllocatorDumpsByFullName);
+ },
+
+ parseMemoryDumpTotals_(processMemoryDump, dumps, pid, dumpId) {
+ const rawTotals = dumps.process_totals;
+ if (rawTotals === undefined) return;
+
+ if (processMemoryDump.totals !== undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Process totals provided multiple times for' +
+ ' process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ return;
+ }
+
+ const totals = {};
+ let platformSpecificTotals = undefined;
+
+ for (const rawTotalName in rawTotals) {
+ const rawTotalValue = rawTotals[rawTotalName];
+ if (rawTotalValue === undefined) continue;
+
+ // Total resident bytes.
+ if (rawTotalName === 'resident_set_bytes') {
+ totals.residentBytes = parseInt(rawTotalValue, 16);
+ continue;
+ }
+
+ // Peak resident bytes.
+ if (rawTotalName === 'peak_resident_set_bytes') {
+ totals.peakResidentBytes = parseInt(rawTotalValue, 16);
+ continue;
+ }
+ if (rawTotalName === 'is_peak_rss_resetable') {
+ totals.arePeakResidentBytesResettable = !!rawTotalValue;
+ continue;
+ }
+
+ // Private footprint
+ if (rawTotalName === 'private_footprint_bytes') {
+ totals.privateFootprintBytes = parseInt(rawTotalValue, 16);
+ continue;
+ }
+
+ // OS-specific totals (e.g. private resident on Mac).
+ if (platformSpecificTotals === undefined) {
+ platformSpecificTotals = {};
+ totals.platformSpecific = platformSpecificTotals;
+ }
+ platformSpecificTotals[rawTotalName] = parseInt(rawTotalValue, 16);
+ }
+
+ // Either both peak_resident_set_bytes and is_peak_rss_resetable should
+ // be present in the trace, or neither.
+ if (totals.peakResidentBytes === undefined &&
+ totals.arePeakResidentBytesResettable !== undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Optional field peak_resident_set_bytes found' +
+ ' but is_peak_rss_resetable not found in' +
+ ' process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ }
+ if (totals.arePeakResidentBytesResettable !== undefined &&
+ totals.peakResidentBytes === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Optional field is_peak_rss_resetable found' +
+ ' but peak_resident_set_bytes not found in' +
+ ' process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ }
+
+ processMemoryDump.totals = totals;
+ },
+
+ parseMemoryDumpVmRegions_(processMemoryDump, dumps, pid, dumpId) {
+ const rawProcessMmaps = dumps.process_mmaps;
+ if (rawProcessMmaps === undefined) return;
+
+ const rawVmRegions = rawProcessMmaps.vm_regions;
+ if (rawVmRegions === undefined) return;
+
+ if (processMemoryDump.vmRegions !== undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'VM regions provided multiple times for' +
+ ' process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ return;
+ }
+
+ // In Chromium under //services/resource_coordinator/public see
+ // mojom/memory_instrumentation/memory_instrumentation.mojom and
+ // cpp/memory_instrumentation/tracing_observer.cc
+ const vmRegions = new Array(rawVmRegions.length);
+ for (let i = 0; i < rawVmRegions.length; i++) {
+ const rawVmRegion = rawVmRegions[i];
+
+ const byteStats = {};
+ const rawByteStats = rawVmRegion.bs;
+ for (const rawByteStatName in rawByteStats) {
+ const rawByteStatValue = rawByteStats[rawByteStatName];
+ if (rawByteStatValue === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Byte stat \'' + rawByteStatName + '\' of VM region ' +
+ i + ' (' + rawVmRegion.mf + ') in process memory dump for ' +
+ 'PID=' + pid + ' and dump ID=' + dumpId +
+ ' does not have a value.'
+ });
+ continue;
+ }
+ const byteStatName = BYTE_STAT_NAME_MAP[rawByteStatName];
+ if (byteStatName === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Unknown byte stat name \'' + rawByteStatName + '\' (' +
+ rawByteStatValue + ') of VM region ' + i + ' (' +
+ rawVmRegion.mf + ') in process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ continue;
+ }
+ byteStats[byteStatName] = parseInt(rawByteStatValue, 16);
+ // OSX does not report PSS and instead emits all zeros.
+ // Zero is a valid value for some byteStats but not PSS so this could
+ // could to confusion when we later compute with and display the PSS.
+ // To avoid this we ignore zero PSS values (since PSS should never
+ // be zero). See: github.com/catapult-project/catapult/issues/3501
+ if (byteStatName === 'proportionalResident' &&
+ byteStats[byteStatName] === 0) {
+ byteStats[byteStatName] = undefined;
+ }
+ }
+
+ vmRegions[i] = new tr.model.VMRegion(
+ parseInt(rawVmRegion.sa, 16), // startAddress
+ parseInt(rawVmRegion.sz, 16), // sizeInBytes
+ rawVmRegion.pf, // protectionFlags
+ rawVmRegion.mf, // mappedFile
+ byteStats);
+ }
+
+ processMemoryDump.vmRegions =
+ tr.model.VMRegionClassificationNode.fromRegions(vmRegions);
+ },
+
+ parseMemoryDumpHeapDumps_(processMemoryDump, dumps, pid, dumpId) {
+ const idPrefix = 'p' + pid + ':';
+ let importer;
+ if (dumps.heaps) {
+ const processTypeMap = this.objectTypeNameMap_[pid];
+ if (processTypeMap === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Missing mapping from object type IDs to names.'
+ });
+ }
+ importer = new LegacyHeapDumpTraceEventImporter(this.model_,
+ processMemoryDump, processTypeMap, idPrefix, dumpId, dumps.heaps);
+ } else if (dumps.heaps_v2) {
+ const data = dumps.heaps_v2;
+ this.heapProfileExpander = this.heapProfileExpander.expandData(data);
+ // TODO(hjd): Unify how we are reading & inflating stack frame data.
+ this.addNewStackFramesFromExpander_(this.heapProfileExpander, idPrefix);
+ importer = new HeapDumpTraceEventImporter(this.heapProfileExpander,
+ this.model_.stackFrames, processMemoryDump, idPrefix, this.model_);
+ }
+
+ if (!importer) return;
+
+ const heapDumps = importer.parse();
+ if (!heapDumps) return;
+
+ if (processMemoryDump.heapDumps !== undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Heap dumps provided multiple times for' +
+ ' process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ return;
+ }
+
+ if (Object.keys(heapDumps).length > 0) {
+ processMemoryDump.heapDumps = heapDumps;
+ }
+ },
+
+ addNewStackFramesFromExpander_(expander, idPrefix) {
+ const nodeMap = expander.getNewMap('nodes');
+ const newStackFrames = {};
+ for (const [id, stackFrame] of nodeMap.entries()) {
+ if (!this.model_.stackFrames[idPrefix + id]) {
+ newStackFrames[id] = {
+ id,
+ name: expander.getString(stackFrame.name_sid),
+ };
+ if (stackFrame.parent) newStackFrames[id].parent = stackFrame.parent;
+ }
+ }
+ this.importStackFrames_(newStackFrames, idPrefix);
+ },
+
+ parseMemoryDumpLevelOfDetail_(levelsOfDetail, dumps, pid,
+ dumpId) {
+ const rawLevelOfDetail = dumps.level_of_detail;
+ let level;
+ switch (rawLevelOfDetail) {
+ case 'background':
+ level = BACKGROUND;
+ break;
+ case 'light':
+ level = LIGHT;
+ break;
+ case 'detailed':
+ level = DETAILED;
+ break;
+ case undefined:
+ level = undefined;
+ break;
+ default:
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'unknown raw level of detail \'' + rawLevelOfDetail +
+ '\' of process memory dump for PID=' + pid +
+ ' and dump ID=' + dumpId + '.'
+ });
+ return;
+ }
+
+ if (!this.updateMemoryDumpLevelOfDetail_(
+ levelsOfDetail, 'process', level)) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'diffent levels of detail provided for process memory' +
+ ' dump for PID=' + pid + ' (dump ID=' + dumpId + ').'
+ });
+ }
+ },
+
+ updateMemoryDumpLevelOfDetail_(levelsOfDetail, scope, level) {
+ // If all process memory dump events have the same level of detail (for
+ // the particular 'process' or 'global' scope), return true.
+ if (!(scope in levelsOfDetail) || level === levelsOfDetail[scope]) {
+ levelsOfDetail[scope] = level;
+ return true;
+ }
+
+ // If the process memory dump events have different levels of detail (for
+ // the particular 'process' or 'global' scope), use the highest level and
+ // return false.
+ if (MEMORY_DUMP_LEVEL_OF_DETAIL_ORDER.indexOf(level) >
+ MEMORY_DUMP_LEVEL_OF_DETAIL_ORDER.indexOf(levelsOfDetail[scope])) {
+ levelsOfDetail[scope] = level;
+ }
+ return false;
+ },
+
+ parseMemoryDumpAllocatorDumps_(processMemoryDump,
+ globalMemoryDump, processMemoryAllocatorDumpsByFullName,
+ globalMemoryAllocatorDumpsByFullName, allMemoryAllocatorDumpsByGuid,
+ dumps, pid, dumpId) {
+ const rawAllocatorDumps = dumps.allocators;
+ if (rawAllocatorDumps === undefined) return;
+
+ // Construct the MemoryAllocatorDump objects without parent links
+ // and add them to the processMemoryAllocatorDumpsByName and
+ // globalMemoryAllocatorDumpsByName indices appropriately.
+ for (let fullName in rawAllocatorDumps) {
+ const rawAllocatorDump = rawAllocatorDumps[fullName];
+
+ // Every memory allocator dump should have a GUID. If not, then
+ // it cannot be associated with any edges.
+ const guid = rawAllocatorDump.guid;
+ if (guid === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory allocator dump ' + fullName + ' for PID=' + pid +
+ ' and dump ID=' + dumpId + ' does not have a GUID.'
+ });
+ }
+
+ // A memory allocator dump can have optional flags.
+ const flags = rawAllocatorDump.flags || 0;
+ const isWeakDump = !!(flags & WEAK_MEMORY_ALLOCATOR_DUMP_FLAG);
+
+ // Determine if this is a global memory allocator dump (check if
+ // it's prefixed with 'global/').
+ let containerMemoryDump;
+ let dstIndex;
+ if (fullName.startsWith(GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX)) {
+ // Global memory allocator dump.
+ fullName = fullName.substring(
+ GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX.length);
+ containerMemoryDump = globalMemoryDump;
+ dstIndex = globalMemoryAllocatorDumpsByFullName;
+ } else {
+ // Process memory allocator dump.
+ containerMemoryDump = processMemoryDump;
+ dstIndex = processMemoryAllocatorDumpsByFullName;
+ }
+
+ // Construct or retrieve a memory allocator dump with the provided
+ // GUID.
+ let allocatorDump = allMemoryAllocatorDumpsByGuid[guid];
+ if (allocatorDump === undefined) {
+ if (fullName in dstIndex) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Multiple GUIDs provided for' +
+ ' memory allocator dump ' + fullName + ': ' +
+ dstIndex[fullName].guid + ', ' + guid + ' (ignored) for' +
+ ' PID=' + pid + ' and dump ID=' + dumpId + '.'
+ });
+ continue;
+ }
+ allocatorDump = new tr.model.MemoryAllocatorDump(
+ containerMemoryDump, fullName, guid);
+ allocatorDump.weak = isWeakDump;
+ dstIndex[fullName] = allocatorDump;
+ if (guid !== undefined) {
+ allMemoryAllocatorDumpsByGuid[guid] = allocatorDump;
+ }
+ } else {
+ // A memory allocator dump with this GUID has already been
+ // dumped (so we will only add new attributes). Check that it
+ // belonged to the same process or was also global.
+ if (allocatorDump.containerMemoryDump !== containerMemoryDump) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory allocator dump ' + fullName +
+ ' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' +
+ dumpId + ' dumped in different contexts.'
+ });
+ continue;
+ }
+ // Check that the names of the memory allocator dumps match.
+ if (allocatorDump.fullName !== fullName) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory allocator dump with GUID=' + guid + ' for PID=' +
+ pid + ' and dump ID=' + dumpId + ' has multiple names: ' +
+ allocatorDump.fullName + ', ' + fullName + ' (ignored).'
+ });
+ continue;
+ }
+ if (!isWeakDump) {
+ // A MemoryAllocatorDump is non-weak if at least one process dumped
+ // it without WEAK_MEMORY_ALLOCATOR_DUMP_FLAG.
+ allocatorDump.weak = false;
+ }
+ }
+
+ // Add all new attributes to the memory allocator dump.
+ let attributes = rawAllocatorDump.attrs;
+ if (attributes === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory allocator dump ' + fullName + ' (GUID=' + guid +
+ ') for PID=' + pid + ' and dump ID=' + dumpId +
+ ' does not have attributes.'
+ });
+ attributes = {};
+ }
+
+ for (const attrName in attributes) {
+ const attrArgs = attributes[attrName];
+ const attrType = attrArgs.type;
+ const attrValue = attrArgs.value;
+
+ switch (attrType) {
+ case 'scalar': {
+ if (attrName in allocatorDump.numerics) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Multiple values provided for scalar attribute ' +
+ attrName + ' of memory allocator dump ' + fullName +
+ ' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' +
+ dumpId + '.'
+ });
+ break;
+ }
+ const unit = attrArgs.units === 'bytes' ?
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter :
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const value = parseInt(attrValue, 16);
+ allocatorDump.addNumeric(attrName,
+ new tr.b.Scalar(unit, value));
+ break;
+ }
+
+ case 'string':
+ if (attrName in allocatorDump.diagnostics) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Multiple values provided for string attribute ' +
+ attrName + ' of memory allocator dump ' + fullName +
+ ' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' +
+ dumpId + '.'
+ });
+ break;
+ }
+ allocatorDump.addDiagnostic(attrName, attrValue);
+ break;
+
+ default:
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Unknown type provided for attribute ' + attrName +
+ ' of memory allocator dump ' + fullName + ' (GUID=' + guid +
+ ') for PID=' + pid + ' and dump ID=' + dumpId + ': ' +
+ attrType
+ });
+ break;
+ }
+ }
+ }
+ },
+
+ inferMemoryAllocatorDumpTree_(memoryAllocatorDumpsByFullName) {
+ const rootAllocatorDumps = [];
+
+ const fullNames = Object.keys(memoryAllocatorDumpsByFullName);
+ fullNames.sort();
+ for (let i = 0; i < fullNames.length; i++) {
+ let fullName = fullNames[i];
+ let allocatorDump = memoryAllocatorDumpsByFullName[fullName];
+
+ // This is a loop because we might need to build implicit
+ // ancestors in case they were not present in the trace.
+ while (true) {
+ const lastSlashIndex = fullName.lastIndexOf('/');
+ if (lastSlashIndex === -1) {
+ // If the dump is a root, add it to the top-level
+ // rootAllocatorDumps list.
+ rootAllocatorDumps.push(allocatorDump);
+ break;
+ }
+
+ // If the dump is not a root, find its parent.
+ const parentFullName = fullName.substring(0, lastSlashIndex);
+ let parentAllocatorDump =
+ memoryAllocatorDumpsByFullName[parentFullName];
+
+ // If the parent dump does not exist yet, we build an implicit
+ // one and continue up the ancestor chain.
+ let parentAlreadyExisted = true;
+ if (parentAllocatorDump === undefined) {
+ parentAlreadyExisted = false;
+ parentAllocatorDump = new tr.model.MemoryAllocatorDump(
+ allocatorDump.containerMemoryDump, parentFullName);
+ if (allocatorDump.weak !== false) {
+ // If we are inferring a parent dump (e.g. 'root/parent') of a
+ // current dump (e.g. 'root/parent/current') which is weak (or
+ // was also inferred and we don't know yet whether it's weak or
+ // not), then we clear the weak flag on the parent dump because
+ // we don't know yet whether it should be weak or non-weak:
+ //
+ // * We can't mark the parent as non-weak straightaway because
+ // the parent might have no non-weak descendants (in which
+ // case we want the inferred parent to be weak, so that it
+ // would be later removed like the current dump).
+ // * We can't mark the parent as weak immediately either. If we
+ // did and later encounter a non-weak child of the parent
+ // (e.g. 'root/parent/another_child'), then we couldn't
+ // retroactively mark the inferred parent dump as non-weak
+ // because we couldn't tell whether the parent dump was
+ // dumped in the trace as weak (in which case it should stay
+ // weak and be subsequently removed) or whether it was
+ // inferred as weak (in which case it should be changed to
+ // non-weak).
+ //
+ // Therefore, we defer marking the inferred parent as
+ // weak/non-weak. If an inferred parent dump does not have any
+ // non-weak child, it will be marked as weak at the end of this
+ // method.
+ //
+ // Note that this should not be confused with the recursive
+ // propagation of the weak flag from parent dumps to their
+ // children and from owned dumps to their owners, which is
+ // performed in GlobalMemoryDump.prototype.removeWeakDumps().
+ parentAllocatorDump.weak = undefined;
+ }
+ memoryAllocatorDumpsByFullName[parentFullName] =
+ parentAllocatorDump;
+ }
+
+ // Setup the parent <-> children relationships
+ allocatorDump.parent = parentAllocatorDump;
+ parentAllocatorDump.children.push(allocatorDump);
+
+ // If the parent already existed, then its ancestors were/will be
+ // constructed in another iteration of the forEach loop.
+ if (parentAlreadyExisted) {
+ if (!allocatorDump.weak) {
+ // If the current dump is non-weak, then we must ensure that all
+ // its inferred ancestors are also non-weak.
+ while (parentAllocatorDump !== undefined &&
+ parentAllocatorDump.weak === undefined) {
+ parentAllocatorDump.weak = false;
+ parentAllocatorDump = parentAllocatorDump.parent;
+ }
+ }
+ break;
+ }
+
+ fullName = parentFullName;
+ allocatorDump = parentAllocatorDump;
+ }
+ }
+
+ // All inferred ancestor dumps that have a non-weak child have already
+ // been marked as non-weak. We now mark the rest as weak.
+ for (const fullName in memoryAllocatorDumpsByFullName) {
+ const allocatorDump = memoryAllocatorDumpsByFullName[fullName];
+ if (allocatorDump.weak === undefined) {
+ allocatorDump.weak = true;
+ }
+ }
+
+ return rootAllocatorDumps;
+ },
+
+ parseMemoryDumpAllocatorEdges_(allMemoryAllocatorDumpsByGuid,
+ dumpIdEvents, dumpId) {
+ for (const pid in dumpIdEvents) {
+ const processEvents = dumpIdEvents[pid];
+
+ for (let i = 0; i < processEvents.length; i++) {
+ const processEvent = processEvents[i];
+
+ const dumps = processEvent.args.dumps;
+ if (dumps === undefined) continue;
+
+ const rawEdges = dumps.allocators_graph;
+ if (rawEdges === undefined) continue;
+
+ for (let j = 0; j < rawEdges.length; j++) {
+ const rawEdge = rawEdges[j];
+
+ const sourceGuid = rawEdge.source;
+ const sourceDump = allMemoryAllocatorDumpsByGuid[sourceGuid];
+ if (sourceDump === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Edge for PID=' + pid + ' and dump ID=' + dumpId +
+ ' is missing source memory allocator dump (GUID=' +
+ sourceGuid + ').'
+ });
+ continue;
+ }
+
+ const targetGuid = rawEdge.target;
+ const targetDump = allMemoryAllocatorDumpsByGuid[targetGuid];
+ if (targetDump === undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Edge for PID=' + pid + ' and dump ID=' + dumpId +
+ ' is missing target memory allocator dump (GUID=' +
+ targetGuid + ').'
+ });
+ continue;
+ }
+
+ const importance = rawEdge.importance;
+ const edge = new tr.model.MemoryAllocatorDumpLink(
+ sourceDump, targetDump, importance);
+
+ switch (rawEdge.type) {
+ case 'ownership':
+ if (sourceDump.owns !== undefined) {
+ this.model_.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Memory allocator dump ' + sourceDump.fullName +
+ ' (GUID=' + sourceGuid + ') already owns a memory' +
+ ' allocator dump (' +
+ 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 + ').'
+ });
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Converts |ts| (in microseconds) to a timestamp in the model clock domain
+ * (in milliseconds).
+ */
+ toModelTimeFromUs_(ts) {
+ if (!this.toModelTime_) {
+ this.toModelTime_ =
+ this.model_.clockSyncManager.getModelTimeTransformer(
+ this.clockDomainId_);
+ }
+
+ return this.toModelTime_(tr.b.Unit.timestampFromUs(ts));
+ },
+
+ /**
+ * Converts |ts| (in microseconds) to a timestamp in the model clock domain
+ * (in milliseconds). If |ts| is undefined, undefined is returned.
+ */
+ maybeToModelTimeFromUs_(ts) {
+ if (ts === undefined) {
+ return undefined;
+ }
+
+ return this.toModelTimeFromUs_(ts);
+ }
+ };
+
+ tr.importer.Importer.register(TraceEventImporter);
+
+ return {
+ TraceEventImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_perf_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_perf_test.html
new file mode 100644
index 00000000000..1c25c83fb57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_perf_test.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const eventStrings = {};
+
+ // @const
+ const N_ITERATIONS = 200;
+
+ // @const
+ const TEST_NAMES = [
+ 'simple_trace',
+ 'flow',
+ ];
+ // @const
+ const TEST_FILES_PATHS = [
+ '/test_data/simple_trace.json',
+ '/test_data/flow.json',
+ ];
+
+ function stringToStream(str) {
+ const buffer = new ArrayBuffer(str.length);
+ const bufferView = new Uint8Array(buffer);
+ for (let i = 0; i < bufferView.length; i++) {
+ bufferView[i] = str.charCodeAt(i);
+ }
+ const stream = new tr.b.InMemoryTraceStream(bufferView, false);
+ return stream;
+ }
+
+ function getEvents(url, asStream) {
+ if (url in eventStrings && asStream in eventStrings[url]) {
+ return eventStrings[url][asStream];
+ }
+ if (!(url in eventStrings)) {
+ eventStrings[url] = {};
+ }
+ // TODO(ehmaldonado): See catapult:#3845.
+ // tr.b.getSync does not return a trace stream in browser mode, so we
+ // convert to stream manually.
+ eventStrings[url][asStream] = tr.b.getSync(url);
+ if (asStream) {
+ eventStrings[url][asStream] = stringToStream(eventStrings[url][asStream]);
+ }
+ return eventStrings[url][asStream];
+ }
+
+ function timedPerfTestWithEvents(name, testFn, initialOptions, asStream) {
+ if (initialOptions.setUp) {
+ throw new Error(
+ 'Per-test setUp not supported. Trivial to fix if needed.');
+ }
+
+ const options = {};
+ for (const k in initialOptions) {
+ options[k] = initialOptions[k];
+ }
+ options.setUp = function() {
+ TEST_FILES_PATHS.forEach(
+ function warmup(url) {
+ getEvents(url, asStream);
+ });
+ };
+ timedPerfTest(name, testFn, options);
+ }
+
+ for (let i = 0; i < TEST_NAMES.length; ++i) {
+ const testName = TEST_NAMES[i];
+ const testFilePath = TEST_FILES_PATHS[i];
+
+ // Import trace as a string.
+ timedPerfTestWithEvents(testName + '_string', function() {
+ const events = getEvents(testFilePath, false);
+ const m = tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false,
+ pruneContainers: false
+ });
+ }, {iterations: N_ITERATIONS}, false);
+
+ // Import trace as a stream.
+ timedPerfTestWithEvents(testName + '_stream', function() {
+ const events = getEvents(testFilePath, true);
+ const m = tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false,
+ pruneContainers: false
+ });
+ }, {iterations: N_ITERATIONS}, true);
+ }
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_test.html
new file mode 100644
index 00000000000..c0a0b6edec3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/trace_event_importer_test.html
@@ -0,0 +1,6593 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/battor_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/extras/measure/measure.html">
+<link rel="import" href="/tracing/extras/v8_config.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const findSliceNamed = tr.c.TestUtils.findSliceNamed;
+ const ClockDomainId = tr.model.ClockDomainId;
+ const ColorScheme = tr.b.ColorScheme;
+ const MeasureAsyncSlice = tr.e.measure.MeasureAsyncSlice;
+ const ScopedId = tr.model.ScopedId;
+ const VMRegion = tr.model.VMRegion;
+ const Scalar = tr.b.Scalar;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const checkDumpNumericsAndDiagnostics =
+ tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics;
+ const checkVMRegions = tr.model.MemoryDumpTestUtils.checkVMRegions;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ function makeModel(events, opt_shift, opt_prune) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: opt_shift,
+ pruneEmptyContainers: opt_prune
+ });
+ }
+
+ function checkHeapEntry(entry, expectedDump, expectedSize, expectedTitles,
+ expectedObjectTypeName, expectedCount) {
+ assert.strictEqual(entry.heapDump, expectedDump);
+ assert.strictEqual(entry.size, expectedSize);
+ assert.strictEqual(entry.objectTypeName, expectedObjectTypeName);
+ assert.strictEqual(entry.count, expectedCount);
+ if (expectedTitles === undefined) {
+ assert.isUndefined(entry.leafStackFrame);
+ } else {
+ assert.deepEqual(
+ entry.leafStackFrame.getUserFriendlyStackTrace(), expectedTitles);
+ }
+ }
+
+ function getFrame(heapEntry, distance) {
+ let frame = heapEntry.leafStackFrame;
+ for (; distance > 0; distance--) {
+ frame = frame.parentFrame;
+ }
+ return frame;
+ }
+
+ function stringToUint8Array(str) {
+ const buffer = new ArrayBuffer(str.length);
+ const bufferView = new Uint8Array(buffer);
+ for (let i = 0; i < bufferView.length; i++) {
+ bufferView[i] = str.charCodeAt(i);
+ }
+ return bufferView;
+ }
+
+ function checkParsedAndStreamInput(events, checkFn, opt_shift, opt_prune) {
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array(JSON.stringify(events)), false);
+
+ checkFn(makeModel(events, opt_shift, opt_prune));
+ checkFn(makeModel(stream, opt_shift, opt_prune));
+ }
+
+ test('canImportEmpty', function() {
+ assert.isFalse(tr.e.importer.TraceEventImporter.canImport([]));
+ assert.isFalse(tr.e.importer.TraceEventImporter.canImport(''));
+ });
+
+ test('basicSingleThreadNonnestedParsing', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 2);
+ assert.strictEqual(t.tid, 53);
+ let slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.closeTo((560 - 520) / 1000, slice.duration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'bar');
+ assert.closeTo((629 - 520) / 1000, slice.start, 1e-5);
+ assert.closeTo((631 - 629) / 1000, slice.duration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 0);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'bar', tid: 53, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nonMonotonicInstantEvents', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 3);
+ const t1 = p.threads[1];
+ assert.isDefined(t1);
+ assert.strictEqual(t1.sliceGroup.length, 3);
+
+ let slice = t1.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 2);
+ assert.strictEqual(slice.duration, 0);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t1.sliceGroup.slices[2];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 3);
+ assert.strictEqual(slice.duration, 5);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ const t2 = p.threads[2];
+ assert.isDefined(t2);
+ assert.strictEqual(t2.sliceGroup.length, 3);
+
+ slice = t2.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.start, 2);
+ assert.strictEqual(slice.duration, 0);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t2.sliceGroup.slices[2];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 3);
+ assert.strictEqual(slice.duration, 5);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ const t3 = p.threads[3];
+ assert.isDefined(t3);
+ assert.strictEqual(t3.sliceGroup.length, 3);
+
+ slice = t3.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.start, 2);
+ assert.strictEqual(slice.duration, 0);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t3.sliceGroup.slices[2];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 3);
+ assert.strictEqual(slice.duration, 5);
+ assert.strictEqual(slice.subSlices.length, 0);
+ }
+
+ const events = [
+ {name: 'x', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 1, ph: 'R'},
+ {name: 'a', args: {}, pid: 52, ts: 3000, cat: 'foo', tid: 1, ph: 'X',
+ dur: 5000},
+ {name: 'b', args: {}, pid: 52, ts: 2000, cat: 'foo', tid: 1, ph: 'R'},
+
+ {name: 'x', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 2, ph: 'R'},
+ {name: 'b', args: {}, pid: 52, ts: 2000, cat: 'foo', tid: 2, ph: 'R'},
+ {name: 'a', args: {}, pid: 52, ts: 3000, cat: 'foo', tid: 2, ph: 'X',
+ dur: 5000},
+
+ {name: 'x', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 3, ph: 'R'},
+ {name: 'a', args: {}, pid: 52, ts: 3000, cat: 'foo', tid: 3, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 2000, cat: 'foo', tid: 3, ph: 'R'},
+ {name: 'a', args: {}, pid: 52, ts: 8000, cat: 'foo', tid: 3, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('basicSingleThreadNonnestedParsingWithCpuDuration', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 2);
+ assert.strictEqual(t.tid, 53);
+ let slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.closeTo((560 - 520) / 1000, slice.duration, 1e-5);
+ assert.closeTo((259 - 221) / 1000, slice.cpuDuration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'bar');
+ assert.closeTo((629 - 520) / 1000, slice.start, 1e-5);
+ assert.closeTo((631 - 629) / 1000, slice.duration, 1e-5);
+ assert.closeTo((331 - 329) / 1000, slice.cpuDuration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 0);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B', tts: 221}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E', tts: 259}, // @suppress longLineCheck
+ {name: 'b', args: {}, pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'B', tts: 329}, // @suppress longLineCheck
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'bar', tid: 53, ph: 'E', tts: 331} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('argumentDupeCreatesNonFailingImportError', function() {
+ function checkModel(m) {
+ const t = m.processes[1].threads[1];
+ const sA = findSliceNamed(t.sliceGroup, 'a');
+
+ assert.strictEqual(sA.args.x, 2);
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(1, m.importWarnings.length);
+ }
+
+ const events = [
+ {name: 'a',
+ args: {'x': 1},
+ pid: 1,
+ ts: 520,
+ cat: 'foo',
+ tid: 1,
+ ph: 'B'},
+ {name: 'a',
+ args: {'x': 2},
+ pid: 1,
+ ts: 560,
+ cat: 'foo',
+ tid: 1,
+ ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMissingArgs', function() {
+ const events = [
+ {name: 'a', pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'I'}
+ ];
+
+ // This should not throw an exception.
+ checkParsedAndStreamInput(events, m => {});
+ });
+
+ test('importDoesNotChokeOnNulls', function() {
+ const events = [
+ {name: 'a', args: { foo: null }, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, // @suppress longLineCheck
+ {name: 'a', pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ // This should not throw an exception.
+ checkParsedAndStreamInput(events, m => {});
+ });
+
+ test('categoryBeginEndMismatchPrefersBegin', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.tid, 53);
+ const slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'bar', tid: 53, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('beginEndNameMismatch', function() {
+ function checkModel(m) {
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(m.importWarnings.length, 1);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestedParsing', function() {
+ function checkModel(m) {
+ const t = m.processes[1].threads[1];
+
+ const sA = findSliceNamed(t.sliceGroup, 'a');
+ const sB = findSliceNamed(t.sliceGroup, 'b');
+
+ assert.strictEqual(sA.title, 'a');
+ assert.strictEqual(sA.category, 'foo');
+ assert.strictEqual(sA.start, 0.001);
+ assert.strictEqual(sA.duration, 0.003);
+ assert.strictEqual(sA.selfTime, 0.002);
+ assert.strictEqual(sA.cpuSelfTime, 0.001);
+
+ assert.strictEqual(sB.title, 'b');
+ assert.strictEqual(sB.category, 'bar');
+ assert.strictEqual(sB.start, 0.002);
+ assert.strictEqual(sB.duration, 0.001);
+
+ assert.strictEqual(1, sA.subSlices.length);
+ assert.strictEqual(sB, sA.subSlices[0]);
+ assert.strictEqual(sA, sB.parentSlice);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, tts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, tts: 2, cat: 'bar', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 3, tts: 3, cat: 'bar', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 4, tts: 3, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('nestedParsingWithTwoSubSlices', function() {
+ function checkModel(m) {
+ const t = m.processes[1].threads[1];
+
+ const sA = findSliceNamed(t.sliceGroup, 'a');
+ const sB = findSliceNamed(t.sliceGroup, 'b');
+ const sC = findSliceNamed(t.sliceGroup, 'c');
+
+ assert.strictEqual(sA.title, 'a');
+ assert.strictEqual(sA.category, 'foo');
+ assert.strictEqual(sA.start, 0.001);
+ assert.strictEqual(sA.duration, 0.007);
+ assert.strictEqual(sA.selfTime, 0.004);
+ assert.strictEqual(sA.cpuSelfTime, 0.005);
+
+ assert.strictEqual(sB.title, 'b');
+ assert.strictEqual(sB.category, 'bar');
+ assert.strictEqual(sB.start, 0.002);
+ assert.strictEqual(sB.duration, 0.001);
+
+ assert.strictEqual(sC.title, 'c');
+ assert.strictEqual(sC.category, 'baz');
+ assert.strictEqual(sC.start, 0.005);
+ assert.strictEqual(sC.duration, 0.002);
+
+ assert.strictEqual(sA.subSlices.length, 2);
+ assert.strictEqual(sA.subSlices[0], sB);
+ assert.strictEqual(sA.subSlices[1], sC);
+ assert.strictEqual(sB.parentSlice, sA);
+ assert.strictEqual(sC.parentSlice, sA);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, tts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, tts: 2, cat: 'bar', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 3, tts: 3, cat: 'bar', tid: 1, ph: 'E'},
+ {name: 'c', args: {}, pid: 1, ts: 5, tts: 5, cat: 'baz', tid: 1, ph: 'B'},
+ {name: 'c', args: {}, pid: 1, ts: 7, tts: 6, cat: 'baz', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 8, tts: 8, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('nestedParsingWithDoubleNesting', function() {
+ function checkModel(m) {
+ const t = m.processes[1].threads[1];
+
+ const sA = findSliceNamed(t.sliceGroup, 'a');
+ const sB = findSliceNamed(t.sliceGroup, 'b');
+ const sC = findSliceNamed(t.sliceGroup, 'c');
+
+ assert.strictEqual(sA.title, 'a');
+ assert.strictEqual(sA.category, 'foo');
+ assert.strictEqual(sA.start, 0.001);
+ assert.strictEqual(sA.duration, 0.007);
+ assert.strictEqual(sA.selfTime, 0.002);
+
+ assert.strictEqual(sB.title, 'b');
+ assert.strictEqual(sB.category, 'bar');
+ assert.strictEqual(sB.start, 0.002);
+ assert.strictEqual(sB.duration, 0.005);
+ assert.strictEqual(sA.selfTime, 0.002);
+
+ assert.strictEqual(sC.title, 'c');
+ assert.strictEqual(sC.category, 'baz');
+ assert.strictEqual(sC.start, 0.003);
+ assert.strictEqual(sC.duration, 0.002);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.strictEqual(sA.subSlices[0], sB);
+ assert.strictEqual(sB.parentSlice, sA);
+
+ assert.strictEqual(sB.subSlices.length, 1);
+ assert.strictEqual(sB.subSlices[0], sC);
+ assert.strictEqual(sC.parentSlice, sB);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 1, ph: 'B'},
+ {name: 'c', args: {}, pid: 1, ts: 3, cat: 'baz', tid: 1, ph: 'B'},
+ {name: 'c', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 1, ts: 7, cat: 'bar', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 8, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+
+ test('autoclosing', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t1 = p.threads[1];
+ const t2 = p.threads[2];
+ assert.isTrue(t1.sliceGroup.slices[0].didNotFinish);
+ assert.strictEqual(t1.sliceGroup.slices[0].end, 0.001);
+ }
+
+ const events = [
+ // Slice that doesn't finish.
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+
+ // Slice that does finish to give an 'end time' to make autoclosing work.
+ {name: 'b', args: {}, pid: 1, ts: 1, cat: 'bar', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 2, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('autoclosingLoneBegin', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ const slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.isTrue(slice.didNotFinish);
+ assert.strictEqual(slice.start, 0);
+ assert.strictEqual(slice.duration, 0);
+ }
+
+ const events = [
+ // Slice that doesn't finish.
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('autoclosingWithSubTasks', function() {
+ function checkModel(m) {
+ const t = m.processes[1].threads[1];
+
+ const sA = findSliceNamed(t.sliceGroup, 'a');
+ const sB1 = findSliceNamed(t.sliceGroup, 'b1');
+ const sB2 = findSliceNamed(t.sliceGroup, 'b2');
+
+ assert.strictEqual(sA.end, 0.003);
+ assert.strictEqual(sB1.end, 0.003);
+ assert.strictEqual(sB2.end, 0.003);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b1', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b1', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b2', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('autoclosingWithEventsOutsideBounds', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.sliceGroup.length, 2);
+
+ const slice = findSliceNamed(t.sliceGroup, 'a');
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.strictEqual(slice.duration, 0.003);
+
+ const t2 = p.threads[2];
+ const slice2 = findSliceNamed(t2.sliceGroup, 'c');
+ assert.strictEqual(slice2.title, 'c');
+ assert.strictEqual(slice2.category, 'bar');
+ assert.strictEqual(slice2.start, 0.001);
+ assert.strictEqual(slice2.duration, 0.001);
+
+ assert.strictEqual(m.bounds.min, 0.000);
+ assert.strictEqual(m.bounds.max, 0.003);
+ }
+
+ const events = [
+ // Slice that begins before min and ends after max of the other threads.
+ {name: 'a', args: {}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},
+
+ // Slice that does finish to give an 'end time' to establish a basis
+ {name: 'c', args: {}, pid: 1, ts: 1, cat: 'bar', tid: 2, ph: 'B'},
+ {name: 'c', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 2, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestedAutoclosing', function() {
+ function checkModel(m) {
+ const t1 = m.processes[1].threads[1];
+ const t2 = m.processes[1].threads[2];
+
+ const sA1 = findSliceNamed(t1.sliceGroup, 'a1');
+ const sA2 = findSliceNamed(t1.sliceGroup, 'a2');
+ const sB = findSliceNamed(t2.sliceGroup, 'b');
+
+ assert.strictEqual(sA1.end, 0.0015);
+ assert.strictEqual(sA2.end, 0.0015);
+ }
+
+ const events = [
+ // Tasks that don't finish.
+ {name: 'a1', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a2', args: {}, pid: 1, ts: 1.5, cat: 'foo', tid: 1, ph: 'B'},
+
+ // Slice that does finish to give an 'end time' to make autoclosing work.
+ {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('taskColoring', function() {
+ // The test below depends on hashing of 'a' !== 'b'. Fail early if that
+ // assumption is incorrect.
+ assert.notEqual(ColorScheme.getStringHash('a'),
+ ColorScheme.getStringHash('b'));
+
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ const a1 = t.sliceGroup.slices[0];
+ assert.strictEqual(a1.title, 'a');
+ assert.strictEqual(a1.category, 'foo');
+ const b = t.sliceGroup.slices[1];
+ assert.strictEqual(b.title, 'b');
+ assert.strictEqual(b.category, 'bar');
+ assert.notEqual(b.colorId, a1.colorId);
+ const a2 = t.sliceGroup.slices[2];
+ assert.strictEqual(a2.title, 'a');
+ assert.strictEqual(a2.category, 'baz');
+ assert.strictEqual(a1.colorId, a2.colorId);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 1, ts: 3, cat: 'bar', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 4, cat: 'bar', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 6, cat: 'baz', tid: 1, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('durationColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.sliceGroup.slices[0].colorId,
+ ColorScheme.getColorIdForReservedName('thread_state_unknown'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B', cname: 'thread_state_unknown'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E', cname: 'thread_state_unknown'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('durationColorEnd', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.sliceGroup.slices[0].colorId,
+ ColorScheme.getColorIdForReservedName('thread_state_unknown'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B', cname: 'thread_state_sleeping'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E', cname: 'thread_state_unknown'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('completeColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.sliceGroup.slices[0].colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, dur: 1, cat: 'foo', tid: 1, ph: 'X', cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.asyncSliceGroup.slices[0].colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'b', id: 1, cname: 'generic_work'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'e', id: 1, cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncColorEnd', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.asyncSliceGroup.slices[0].colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'b', id: 1, cname: 'thread_state_unknown'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'e', id: 1, cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('instantThreadColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.sliceGroup.slices[0].colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'I', id: 1, cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('instantProcessColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ assert.strictEqual(p.instantEvents[0].colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'I', id: 1, s: 'p', cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('counterColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ assert.strictEqual(p.counters['foo.a[1]'].series[0].color,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ assert.strictEqual(p.counters['foo.a[1]'].series.length, 1);
+ }
+
+ const events = [
+ {name: 'a', args: {'cats': 10}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'C', id: 1, cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('objectColorArgument', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const i = p.objects.instanceMapsByScopedId_.ptr[1].instances[0];
+ assert.strictEqual(i.colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'N', id: 1, cname: 'generic_work'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'O', id: 1, cname: 'generic_work'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'D', id: 1, cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('objectColorEnd', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const i = p.objects.instanceMapsByScopedId_.ptr[1].instances[0];
+ assert.strictEqual(i.colorId,
+ ColorScheme.getColorIdForReservedName('generic_work'));
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'N', id: 1, cname: 'thread_state_sleeping'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'O', id: 1, cname: 'thread_state_unknown'}, // @suppress longLineCheck
+ {name: 'a', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'D', id: 1, cname: 'generic_work'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('multipleThreadParsing', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[1];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 2);
+
+ // Check thread 1.
+ let t = p.threads[1];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.tid, 1);
+
+ let slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.strictEqual(slice.duration, (2 - 1) / 1000);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ // Check thread 2.
+ t = p.threads[2];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.tid, 2);
+
+ slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'bar');
+ assert.strictEqual(slice.start, (3 - 1) / 1000);
+ assert.strictEqual(slice.duration, (4 - 3) / 1000);
+ assert.strictEqual(slice.subSlices.length, 0);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 1, ts: 3, cat: 'bar', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 4, cat: 'bar', tid: 2, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('multiplePidParsing', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 2);
+ let p = m.processes[1];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+
+ // Check process 1 thread 1.
+ let t = p.threads[1];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.tid, 1);
+
+ let slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.strictEqual(slice.duration, (2 - 1) / 1000);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ // Check process 2 thread 2.
+ p = m.processes[2];
+ assert.isDefined(p);
+ assert.strictEqual(p.numThreads, 1);
+ t = p.threads[2];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.tid, 2);
+
+ slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'bar');
+ assert.strictEqual(slice.start, (3 - 1) / 1000);
+ assert.strictEqual(slice.duration, (4 - 3) / 1000);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ // Check getAllThreads.
+ assert.deepEqual(m.getAllThreads(),
+ [m.processes[1].threads[1], m.processes[2].threads[2]]);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 2, ts: 3, cat: 'bar', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 2, ts: 4, cat: 'bar', tid: 2, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ // Process names.
+ test('processNames', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.processes[1].name, 'SomeProcessName');
+ }
+
+ const events = [
+ {name: 'process_name', args: {name: 'SomeProcessName'},
+ pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
+ {name: 'process_name', args: {name: 'SomeProcessName'},
+ pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ // Process labels.
+ test('processLabels', function() {
+ function checkModel(m) {
+ assert.deepEqual(m.processes[1].labels, ['foo', 'bar', 'baz']);
+ assert.deepEqual(m.processes[2].labels, ['baz']);
+ }
+
+ const events = [
+ {name: 'process_labels', args: {labels: 'foo,bar,bar,foo,baz'},
+ pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
+ {name: 'process_labels', args: {labels: 'baz'},
+ pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ // Process uptime.
+ test('processUptime', function() {
+ const events = [
+ {name: 'process_uptime_seconds', args: {uptime: 10},
+ pid: 1, ts: 0, tid: 1, ph: 'M'},
+ {name: 'process_uptime_seconds', args: {uptime: 20},
+ pid: 2, ts: 0, tid: 1, ph: 'M'}
+ ];
+ const m = makeModel(events);
+ assert.strictEqual(m.processes[1].uptime_seconds, 10);
+ assert.strictEqual(m.processes[2].uptime_seconds, 20);
+ });
+
+ // Process sort index.
+ test('processSortIndex', function() {
+ function checkModel(m) {
+ // By name, p1 is before p2. But, its sort index overrides that.
+ assert.isAbove(m.processes[1].compareTo(m.processes[2]), 0);
+ }
+
+ const events = [
+ {name: 'process_name', args: {name: 'First'},
+ pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
+ {name: 'process_name', args: {name: 'Second'},
+ pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
+ {name: 'process_sort_index', args: {sort_index: 1},
+ pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ // Thread names.
+ test('threadNames', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.processes[1].threads[1].name, 'Thread 1');
+ assert.strictEqual(m.processes[2].threads[2].name, 'Thread 2');
+ }
+
+ const events = [
+ {name: 'thread_name', args: {name: 'Thread 1'},
+ pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
+ {name: 'thread_name', args: {name: 'Thread 2'},
+ pid: 2, ts: 0, cat: 'foo', tid: 2, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false, false);
+ });
+
+ // Thread sort index.
+ test('threadSortIndex', function() {
+ function checkModel(m) {
+ // By name, t1 is before t2. But, its sort index overrides that.
+ const t1 = m.processes[1].threads[1];
+ const t2 = m.processes[1].threads[2];
+ assert.isAbove(t1.compareTo(t2), 0);
+ }
+
+ const events = [
+ {name: 'thread_name', args: {name: 'Thread 1'},
+ pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
+ {name: 'thread_name', args: {name: 'Thread 2'},
+ pid: 1, ts: 0, cat: 'foo', tid: 2, ph: 'M'},
+ {name: 'thread_sort_index', args: {sort_index: 1},
+ pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false, false);
+ });
+
+ // CPU counts.
+ test('cpuCounts', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.kernel.softwareMeasuredCpuCount, 4);
+ assert.strictEqual(m.kernel.bestGuessAtCpuCount, 4);
+ }
+
+ const events = [
+ {name: 'num_cpus', args: {number: 4},
+ pid: 7, ts: 0, cat: 'foo', tid: 0, ph: 'M'},
+ {name: 'num_cpus', args: {number: 4},
+ pid: 14, ts: 0, cat: 'foo', tid: 0, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('cpuCountsWithSandboxBeingConfused', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.kernel.softwareMeasuredCpuCount, 4);
+ assert.strictEqual(m.kernel.bestGuessAtCpuCount, 4);
+ }
+
+ const events = [
+ {name: 'num_cpus', args: {number: 4},
+ pid: 7, ts: 0, cat: 'foo', tid: 0, ph: 'M'},
+ {name: 'num_cpus', args: {number: 1},
+ pid: 14, ts: 0, cat: 'foo', tid: 0, ph: 'M'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('parsingWhenEndComesFirst', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.sliceGroup.slices[0].title, 'a');
+ assert.strictEqual(t.sliceGroup.slices[0].category, 'foo');
+ assert.strictEqual(t.sliceGroup.slices[0].start, 0.004);
+ assert.strictEqual(t.sliceGroup.slices[0].duration, 0.001);
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(m.importWarnings.length, 1);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 5, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('immediateParsing', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[1];
+
+ assert.strictEqual(t.sliceGroup.length, 3);
+ assert.strictEqual(t.sliceGroup.slices[0].start, 0.001);
+ assert.strictEqual(t.sliceGroup.slices[0].duration, 0.003);
+ assert.strictEqual(t.sliceGroup.slices[1].start, 0.002);
+ assert.strictEqual(t.sliceGroup.slices[1].duration, 0);
+ assert.strictEqual(t.sliceGroup.slices[2].start, 0.004);
+
+ const slice = findSliceNamed(t.sliceGroup, 'a');
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.duration, 0.003);
+
+ const immed = findSliceNamed(t.sliceGroup, 'immediate');
+ assert.strictEqual(immed.title, 'immediate');
+ assert.strictEqual(immed.category, 'bar');
+ assert.strictEqual(immed.start, 0.002);
+ assert.strictEqual(immed.duration, 0);
+
+ const slower = findSliceNamed(t.sliceGroup, 'slower');
+ assert.strictEqual(slower.title, 'slower');
+ assert.strictEqual(slower.category, 'baz');
+ assert.strictEqual(slower.start, 0.004);
+ assert.strictEqual(slower.duration, 0);
+ }
+
+ const events = [
+ // Need to include immediates inside a task so the timeline
+ // recentering/zeroing doesn't clobber their timestamp.
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'immediate', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 1, ph: 'I'},
+ {name: 'slower', args: {}, pid: 1, ts: 4, cat: 'baz', tid: 1, ph: 'i'},
+ {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('simpleCounter', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const ctr = m.processes[1].counters['foo.ctr'];
+
+ assert.strictEqual(ctr.name, 'ctr');
+ assert.strictEqual(ctr.category, 'foo');
+ assert.strictEqual(ctr.numSamples, 3);
+ assert.strictEqual(ctr.numSeries, 1);
+
+ assert.strictEqual(ctr.series[0].name, 'value');
+ assert.strictEqual(ctr.series[0].color,
+ ColorScheme.getColorIdForGeneralPurposeString('ctr.value'));
+
+ assert.deepEqual(ctr.timestamps, [0, 0.01, 0.02]);
+
+ const samples = [];
+ ctr.series[0].samples.forEach(function(sample) {
+ samples.push(sample.value);
+ });
+ assert.deepEqual(samples, [0, 10, 0]);
+
+ assert.deepEqual(ctr.totals, [0, 10, 0]);
+ assert.strictEqual(ctr.maxTotal, 10);
+ }
+
+ const events = [
+ {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
+ ph: 'C'},
+ {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+ ph: 'C'},
+ {name: 'ctr', args: {'value': 0}, pid: 1, ts: 20, cat: 'foo', tid: 1,
+ ph: 'C'}
+
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('instanceCounter', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ let ctr = m.processes[1].counters['foo.ctr[0]'];
+ assert.strictEqual(ctr.name, 'ctr[0]');
+ assert.strictEqual(ctr.category, 'foo');
+ assert.strictEqual(ctr.numSamples, 2);
+ assert.strictEqual(ctr.numSeries, 1);
+
+ assert.deepEqual(ctr.timestamps, [0, 0.01]);
+ let samples = [];
+ ctr.series[0].samples.forEach(function(sample) {
+ samples.push(sample.value);
+ });
+ assert.deepEqual(samples, [0, 10]);
+
+ ctr = m.processes[1].counters['foo.ctr[1]'];
+ assert.strictEqual(ctr.name, 'ctr[1]');
+ assert.strictEqual(ctr.category, 'foo');
+ assert.strictEqual(ctr.numSamples, 3);
+ assert.strictEqual(ctr.numSeries, 1);
+ assert.deepEqual(ctr.timestamps, [0.01, 0.015, 0.02]);
+
+ samples = [];
+ ctr.series[0].samples.forEach(function(sample) {
+ samples.push(sample.value);
+ });
+ assert.deepEqual(samples, [10, 20, 30]);
+
+ ctr = m.processes[1].counters['bar.ctr[2]'];
+ assert.strictEqual(ctr.name, 'ctr[2]');
+ assert.strictEqual(ctr.category, 'bar');
+ assert.strictEqual(ctr.numSamples, 1);
+ assert.strictEqual(ctr.numSeries, 1);
+ assert.deepEqual(ctr.timestamps, [0.025]);
+ samples = [];
+ ctr.series[0].samples.forEach(function(sample) {
+ samples.push(sample.value);
+ });
+ assert.deepEqual(samples, [40]);
+ }
+
+ const events = [
+ {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
+ ph: 'C', id: 0},
+ {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+ ph: 'C', id: 0},
+ {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+ ph: 'C', id: 1},
+ {name: 'ctr', args: {'value': 20}, pid: 1, ts: 15, cat: 'foo', tid: 1,
+ ph: 'C', id: 1},
+ {name: 'ctr', args: {'value': 30}, pid: 1, ts: 20, cat: 'foo', tid: 1,
+ ph: 'C', id: 1},
+ {name: 'ctr', args: {'value': 40}, pid: 1, ts: 25, cat: 'bar', tid: 1,
+ ph: 'C', id: 2}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('multiCounterUpdateBounds', function() {
+ const ctr = new tr.model.Counter(undefined, 'testBasicCounter',
+ '', 'testBasicCounter');
+ const value1Series = new tr.model.CounterSeries(
+ 'value1', 'testBasicCounter.value1');
+ const value2Series = new tr.model.CounterSeries(
+ 'value2', 'testBasicCounter.value2');
+ ctr.addSeries(value1Series);
+ ctr.addSeries(value2Series);
+
+ value1Series.addCounterSample(0, 0);
+ value1Series.addCounterSample(1, 1);
+ value1Series.addCounterSample(2, 1);
+ value1Series.addCounterSample(3, 2);
+ value1Series.addCounterSample(4, 3);
+ value1Series.addCounterSample(5, 1);
+ value1Series.addCounterSample(6, 3);
+ value1Series.addCounterSample(7, 3.1);
+
+ value2Series.addCounterSample(0, 0);
+ value2Series.addCounterSample(1, 0);
+ value2Series.addCounterSample(2, 1);
+ value2Series.addCounterSample(3, 1.1);
+ value2Series.addCounterSample(4, 0);
+ value2Series.addCounterSample(5, 7);
+ value2Series.addCounterSample(6, 0);
+ value2Series.addCounterSample(7, 0.5);
+
+ ctr.updateBounds();
+
+ assert.strictEqual(ctr.bounds.min, 0);
+ assert.strictEqual(ctr.bounds.max, 7);
+ assert.strictEqual(ctr.maxTotal, 8);
+ assert.deepEqual([0, 0,
+ 1, 1,
+ 1, 2,
+ 2, 3.1,
+ 3, 3,
+ 1, 8,
+ 3, 3,
+ 3.1, 3.6], ctr.totals);
+ });
+
+ test('multiCounter', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const ctr = m.processes[1].counters['foo.ctr'];
+ assert.strictEqual(ctr.name, 'ctr');
+
+ assert.strictEqual(ctr.name, 'ctr');
+ assert.strictEqual(ctr.category, 'foo');
+ assert.strictEqual(ctr.numSamples, 3);
+ assert.strictEqual(ctr.numSeries, 2);
+
+ assert.strictEqual(ctr.series[0].name, 'value1');
+ assert.strictEqual(ctr.series[1].name, 'value2');
+ assert.strictEqual(ctr.series[0].color,
+ ColorScheme.getColorIdForGeneralPurposeString('ctr.value1'));
+ assert.strictEqual(ctr.series[1].color,
+ ColorScheme.getColorIdForGeneralPurposeString('ctr.value2'));
+
+ assert.deepEqual(ctr.timestamps, [0, 0.01, 0.02]);
+ const samples = [];
+ ctr.series[0].samples.forEach(function(sample) {
+ samples.push(sample.value);
+ });
+ assert.deepEqual(samples, [0, 10, 0]);
+
+ const samples1 = [];
+ ctr.series[1].samples.forEach(function(sample) {
+ samples1.push(sample.value);
+ });
+ assert.deepEqual(samples1, [7, 4, 1]);
+ assert.deepEqual([0, 7,
+ 10, 14,
+ 0, 1], ctr.totals);
+ assert.strictEqual(ctr.maxTotal, 14);
+ }
+
+ const events = [
+ {name: 'ctr', args: {'value1': 0, 'value2': 7}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'C'}, // @suppress longLineCheck
+ {name: 'ctr', args: {'value1': 10, 'value2': 4}, pid: 1, ts: 10, cat: 'foo', tid: 1, ph: 'C'}, // @suppress longLineCheck
+ {name: 'ctr', args: {'value1': 0, 'value2': 1 }, pid: 1, ts: 20, cat: 'foo', tid: 1, ph: 'C'} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importObjectInsteadOfArray', function() {
+ const events = { traceEvents: [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ] };
+
+ const m = makeModel(events);
+ assert.strictEqual(m.numProcesses, 1);
+ });
+
+ test('importString', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const m = makeModel(JSON.stringify(events));
+ assert.strictEqual(m.numProcesses, 1);
+ });
+
+ test('importStringWithLeadingSpaces', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const m = makeModel(' ' + JSON.stringify(events));
+ assert.strictEqual(m.numProcesses, 1);
+ });
+
+ test('importStringWithTrailingNewLine', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const m = makeModel(JSON.stringify(events) + '\n');
+ assert.strictEqual(m.numProcesses, 1);
+ });
+
+ test('importStringWithMissingCloseSquareBracket', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const tmp = JSON.stringify(events);
+ assert.strictEqual(tmp[tmp.length - 1], ']');
+
+ // Drop off the trailing ]
+ const dropped = tmp.substring(0, tmp.length - 1);
+ const m = makeModel(dropped);
+ assert.strictEqual(m.numProcesses, 1);
+ });
+
+ test('importStringWithEndingCommaButMissingCloseSquareBracket', function() {
+ const lines = [
+ '[',
+ '{"name": "a", "args": {}, "pid": 52, "ts": 524, "cat": "foo", "tid": 53, "ph": "B"},', // @suppress longLineCheck
+ '{"name": "a", "args": {}, "pid": 52, "ts": 560, "cat": "foo", "tid": 53, "ph": "E"},' // @suppress longLineCheck
+ ];
+ const text = lines.join('\n');
+
+ const m = makeModel(text);
+ assert.strictEqual(m.numProcesses, 1);
+ assert.strictEqual(m.processes[52].threads[53].sliceGroup.length, 1);
+ });
+
+ test('importStringWithMissingCloseSquareBracketAndNewline', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const tmp = JSON.stringify(events);
+ assert.strictEqual(tmp[tmp.length - 1], ']');
+
+ // Drop off the trailing ] and add a newline
+ const dropped = tmp.substring(0, tmp.length - 1);
+ const m = makeModel(dropped + '\n');
+ assert.strictEqual(m.numProcesses, 1);
+ });
+
+ test('ImportStringEndingCommaButMissingCloseSquareBracketCRLF', function() {
+ const lines = [
+ '[',
+ '{"name": "a", "args": {}, "pid": 52, "ts": 524, "cat": "foo", "tid": 53, "ph": "B"},', // @suppress longLineCheck
+ '{"name": "a", "args": {}, "pid": 52, "ts": 560, "cat": "foo", "tid": 53, "ph": "E"},' // @suppress longLineCheck
+ ];
+ const text = lines.join('\r\n');
+
+ const m = makeModel(text);
+ assert.strictEqual(m.numProcesses, 1);
+ assert.strictEqual(m.processes[52].threads[53].sliceGroup.length, 1);
+ });
+
+ test('importOldFormat', function() {
+ const lines = [
+ '[',
+ '{"cat":"a","pid":9,"tid":8,"ts":194,"ph":"E","name":"I","args":{}},',
+ '{"cat":"b","pid":9,"tid":8,"ts":194,"ph":"B","name":"I","args":{}}',
+ ']'
+ ];
+ const text = lines.join('\n');
+ const m = makeModel(text);
+ assert.strictEqual(m.numProcesses, 1);
+ assert.strictEqual(m.processes[9].threads[8].sliceGroup.length, 1);
+ });
+
+ test('startFinishOneSliceOneThread', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ assert.strictEqual(t.asyncSliceGroup.slices[0].title, 'a');
+ assert.strictEqual(t.asyncSliceGroup.slices[0].category, 'cat');
+ assert.isTrue(t.asyncSliceGroup.slices[0].isTopLevel);
+ assert.strictEqual(t.asyncSliceGroup.slices[0].id, ':ptr:72');
+ assert.strictEqual(t.asyncSliceGroup.slices[0].args.foo, 'bar');
+ assert.strictEqual(t.asyncSliceGroup.slices[0].start, 0);
+ assert.closeTo(
+ (60 - 24) / 1000, t.asyncSliceGroup.slices[0].duration, 1e-5);
+ assert.strictEqual(t.asyncSliceGroup.slices[0].startThread, t);
+ assert.strictEqual(t.asyncSliceGroup.slices[0].endThread, t);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'cat', tid: 53,
+ ph: 'F', id: 72},
+ {name: 'a', pid: 52, ts: 524, cat: 'cat', tid: 53,
+ ph: 'S', id: 72, args: {'foo': 'bar'}}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('endArgsAddedToSlice', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.length, 1);
+ assert.strictEqual(t.tid, 53);
+ const slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.strictEqual(slice.subSlices.length, 0);
+ assert.strictEqual(slice.args.x, 1);
+ assert.strictEqual(slice.args.y, 2);
+ }
+
+ const events = [
+ {name: 'a', args: {x: 1}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('endArgOverrwritesOriginalArgValueIfDuplicated', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ const slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'foo');
+ assert.strictEqual(slice.start, 0);
+ assert.strictEqual(slice.subSlices.length, 0);
+ assert.strictEqual(slice.args.z, 4);
+ }
+
+ const events = [
+ {name: 'b', args: {z: 3}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {z: 4}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncEndArgsAddedToSlice', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'c');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.strictEqual(parentSlice.args.x, 1);
+ assert.strictEqual(parentSlice.args.y, 2);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 0);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'c', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'F', id: 72},
+ {name: 'c', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'S', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncEndArgOverwritesOriginalArgValueIfDuplicated', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'd');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.strictEqual(parentSlice.args.z, 4);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 0);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'd', args: {z: 4}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'F', id: 72},
+ {name: 'd', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'S', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncStepsInOneThread', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.strictEqual(parentSlice.start, 0);
+ assert.strictEqual(parentSlice.args.x, 1);
+ assert.isUndefined(parentSlice.args.y);
+ assert.strictEqual(parentSlice.args.z, 3);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 1);
+
+ const subSlice = parentSlice.subSlices[0];
+ assert.strictEqual(subSlice.title, 's1');
+ assert.strictEqual(subSlice.category, 'foo');
+ assert.isFalse(subSlice.isTopLevel);
+ assert.closeTo((548 - 524) / 1000, subSlice.start, 1e-5);
+ assert.closeTo((560 - 548) / 1000, subSlice.duration, 1e-5);
+ assert.isUndefined(subSlice.args.x);
+ assert.strictEqual(subSlice.args.y, 2);
+ assert.isUndefined(subSlice.args.z);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'F', id: 72}, // @suppress longLineCheck
+ {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo', tid: 53, ph: 'T', id: 72}, // @suppress longLineCheck
+ {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'S', id: 72} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncStepsMissingStart', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isUndefined(t);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'F', id: 72},
+ {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
+ tid: 53, ph: 'T', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('autoClosingAsyncStepsMissingFinish', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.strictEqual(parentSlice.start, 0);
+ assert.strictEqual(parentSlice.duration, 2);
+ assert.isTrue(parentSlice.didNotFinish);
+ assert.isDefined(parentSlice.error);
+ assert.isUndefined(parentSlice.args.y);
+ assert.strictEqual(parentSlice.args.z, 3);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 1);
+ const subSlice = parentSlice.subSlices[0];
+ assert.strictEqual(subSlice.title, 's1');
+ assert.strictEqual(subSlice.category, 'foo');
+ assert.isFalse(subSlice.isTopLevel);
+ assert.strictEqual(subSlice.start, 0);
+ assert.strictEqual(subSlice.duration, 2);
+ assert.isUndefined(subSlice.args.x);
+ assert.strictEqual(subSlice.args.y, 2);
+ assert.isUndefined(subSlice.args.z);
+ assert.isTrue(subSlice.didNotFinish);
+ assert.isDefined(subSlice.error);
+ }
+
+ const events = [
+ {name: 'a', args: {z: 3}, pid: 52, ts: 0, cat: 'foo', tid: 53,
+ ph: 'S', id: 72},
+ {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 0, cat: 'foo',
+ tid: 53, ph: 'T', id: 72},
+
+ // Slice that does finish to give an 'end time' to make autoclosing work.
+ {name: 'b', args: {}, pid: 1, ts: 1000, cat: 'bar', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2000, cat: 'bar', tid: 2, ph: 'E'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncStepEndEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.strictEqual(parentSlice.start, 0);
+ assert.strictEqual(parentSlice.args.x, 1);
+ assert.isUndefined(parentSlice.args.y);
+ assert.strictEqual(parentSlice.args.z, 3);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 1);
+ const subSlice = parentSlice.subSlices[0];
+ assert.strictEqual(subSlice.title, 's1');
+ assert.strictEqual(subSlice.category, 'foo');
+ assert.isFalse(subSlice.isTopLevel);
+ assert.strictEqual(subSlice.start, 0);
+ assert.closeTo((548 - 524) / 1000, subSlice.duration, 1e-5);
+ assert.isUndefined(subSlice.args.x);
+ assert.strictEqual(subSlice.args.y, 2);
+ assert.isUndefined(subSlice.args.z);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'F', id: 72},
+ {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
+ tid: 53, ph: 'p', id: 72},
+ {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'S', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncStepMismatch', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isUndefined(t);
+ assert.isTrue(m.hasImportWarnings);
+ }
+
+ const events = [
+ // Time is intentionally out of order.
+ {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'F', id: 72},
+ {name: 'a', args: {step: 's2'}, pid: 52, ts: 548, cat: 'foo', tid: 53,
+ ph: 'T', id: 72},
+ {name: 'a', args: {step: 's1'}, pid: 52, ts: 548, cat: 'foo', tid: 53,
+ ph: 'p', id: 72},
+ {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'S', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncSliceWithoutCPUDuration', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 3);
+
+ const noTTSNoField = t.asyncSliceGroup.slices[0];
+ assert.isUndefined(noTTSNoField.cpuStart);
+ assert.isUndefined(noTTSNoField.cpuDuration);
+
+ const TTSNoField = t.asyncSliceGroup.slices[1];
+ assert.isUndefined(TTSNoField.cpuStart);
+ assert.isUndefined(TTSNoField.cpuDuration);
+
+ const TTSZeroField = t.asyncSliceGroup.slices[2];
+ assert.isUndefined(TTSZeroField.cpuStart);
+ assert.isUndefined(TTSZeroField.cpuDuration);
+ }
+
+ const events = [
+ // Async slice without tts field.
+ {name: 'a', args: {params: ''}, pid: 52, ts: 10, cat: 'foo', tid: 53,
+ id: 72, ph: 'b'},
+ {name: 'a', args: {params: ''}, pid: 52, ts: 20, cat: 'foo', tid: 53,
+ id: 72, ph: 'e'},
+ // Async slice with tts field but without use_async_tts marker field.
+ {name: 'b', args: {params: ''}, pid: 52, ts: 30, cat: 'foo', tid: 53,
+ id: 72, ph: 'b', tts: 30000},
+ {name: 'b', args: {params: ''}, pid: 52, ts: 40, cat: 'foo', tid: 53,
+ id: 72, ph: 'e', tts: 40000},
+ // Async slice with tts field but with use_async_tts marker set to 0.
+ {name: 'c', args: {params: ''}, pid: 52, ts: 50000, cat: 'foo', tid: 53,
+ id: 72, ph: 'b', tts: 50000, use_async_tts: 0},
+ {name: 'c', args: {params: ''}, pid: 52, ts: 60000, cat: 'foo', tid: 53,
+ id: 72, ph: 'e', tts: 60000, use_async_tts: 0}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('asyncSliceWithCPUDuration', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+
+ const asyncSlice = t.asyncSliceGroup.slices[0];
+ assert.isDefined(asyncSlice);
+ assert.strictEqual(asyncSlice.duration, 10);
+ assert.strictEqual(asyncSlice.cpuStart, 100);
+ assert.strictEqual(asyncSlice.cpuDuration, 5);
+ }
+
+ const events = [
+ {name: 'a', args: {params: ''}, pid: 52, ts: 50000, cat: 'foo', tid: 53,
+ id: 72, ph: 'b', tts: 100000, use_async_tts: 1},
+ {name: 'a', args: {params: ''}, pid: 52, ts: 60000, cat: 'foo', tid: 53,
+ id: 72, ph: 'e', tts: 105000, use_async_tts: 1}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncBasic', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 1);
+ const subSlice = parentSlice.subSlices[0];
+ assert.isFalse(subSlice.isTopLevel);
+ // Arguments should include both BEGIN and END event.
+ assert.strictEqual(subSlice.args.x, 1);
+ assert.strictEqual(subSlice.args.y, 2);
+ assert.sameMembers(subSlice.subSlices, []);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+
+ test('nestableAsyncNoArgs', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const slice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'name');
+ assert.strictEqual(slice.category, 'foo');
+ assert.isTrue(slice.isTopLevel);
+
+ assert.isDefined(slice.subSlices);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ assert.deepEqual(slice.args, {});
+ }
+
+ const events = [
+ {name: 'name', pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'name', pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncCombinedParams', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 3);
+
+ const sliceA = t.asyncSliceGroup.slices[0];
+ // Arguments should include both BEGIN and END event.
+ assert.strictEqual(sliceA.args.x, 1);
+ assert.strictEqual(sliceA.args.y, 2);
+ const paramsA = sliceA.args.params;
+ assert.isDefined(paramsA);
+ assert.strictEqual(paramsA.p1, 'hello');
+ assert.strictEqual(paramsA.p2, 123);
+ assert.strictEqual(paramsA.p3, 'hi');
+ assert.isTrue(sliceA.isTopLevel);
+
+ const sliceB = t.asyncSliceGroup.slices[1];
+ // Arguments should include both BEGIN and END event.
+ const paramsB = sliceB.args.params;
+ assert.isDefined(paramsB);
+ assert.strictEqual(paramsB.p4, 'foo');
+ assert.isTrue(sliceB.isTopLevel);
+
+ const sliceC = t.asyncSliceGroup.slices[2];
+ // Arguments should include both BEGIN and END event.
+ const paramsC = sliceC.args.params;
+ assert.isDefined(paramsC);
+ assert.strictEqual(paramsC.p5, 'bar');
+ assert.isTrue(sliceC.isTopLevel);
+ }
+
+ const events = [
+ {name: 'a', args: {x: 1, params: {p1: 'hello', p2: 123}},
+ pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 72},
+ {name: 'a', args: {y: 2, params: {p3: 'hi'}}, pid: 52, ts: 560,
+ cat: 'foo', tid: 53, ph: 'e', id: 72},
+ {name: 'b', args: {params: {p4: 'foo'}},
+ pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 73},
+ {name: 'b', args: {params: ''}, pid: 52, ts: 560,
+ cat: 'foo', tid: 53, ph: 'e', id: 73},
+ {name: 'c', args: {params: {p5: 'bar'}},
+ pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 74},
+ {name: 'c', args: {}, pid: 52, ts: 560,
+ cat: 'foo', tid: 53, ph: 'e', id: 74}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncManyLevels', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ // Perfectly matched events should not produce a warning.
+ assert.isFalse(m.hasImportWarnings);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+
+ const l1Slice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(l1Slice.title, 'l1');
+ assert.closeTo(0, l1Slice.start, 1e-5);
+ assert.closeTo(9 / 1000, l1Slice.duration, 1e-5);
+ assert.isTrue(l1Slice.isTopLevel);
+
+ assert.isDefined(l1Slice.subSlices);
+ assert.strictEqual(l1Slice.subSlices.length, 1);
+ const l2Slice = l1Slice.subSlices[0];
+ assert.strictEqual(l2Slice.title, 'l2');
+ assert.closeTo(1 / 1000, l2Slice.start, 1e-5);
+ assert.closeTo(7 / 1000, l2Slice.duration, 1e-5);
+ assert.isFalse(l2Slice.isTopLevel);
+
+ assert.isDefined(l2Slice.subSlices);
+ assert.strictEqual(l2Slice.subSlices.length, 1);
+ const l3Slice = l2Slice.subSlices[0];
+ assert.strictEqual(l3Slice.title, 'l3');
+ assert.closeTo(2 / 1000, l3Slice.start, 1e-5);
+ assert.closeTo(5 / 1000, l3Slice.duration, 1e-5);
+ assert.isFalse(l3Slice.isTopLevel);
+
+ assert.isDefined(l3Slice.subSlices);
+ assert.strictEqual(l3Slice.subSlices.length, 1);
+ const l4Slice = l3Slice.subSlices[0];
+ assert.strictEqual(l4Slice.title, 'l4');
+ assert.closeTo(3 / 1000, l4Slice.start, 1e-5);
+ assert.closeTo(3 / 1000, l4Slice.duration, 1e-5);
+ assert.isFalse(l4Slice.isTopLevel);
+
+ assert.isDefined(l4Slice.subSlices);
+ assert.strictEqual(l4Slice.subSlices.length, 1);
+ const l5Slice = l4Slice.subSlices[0];
+ assert.strictEqual(l5Slice.title, 'l5');
+ assert.closeTo(4 / 1000, l5Slice.start, 1e-5);
+ assert.closeTo(1 / 1000, l5Slice.duration, 1e-5);
+ assert.isFalse(l5Slice.isTopLevel);
+ }
+
+ // There are 5 nested levels.
+ const events = [
+ {name: 'l1', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'l2', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'l3', args: {}, pid: 52, ts: 526, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'l4', args: {}, pid: 52, ts: 527, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'l5', args: {}, pid: 52, ts: 528, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'l5', args: {}, pid: 52, ts: 529, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'l4', args: {}, pid: 52, ts: 530, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'l3', args: {}, pid: 52, ts: 531, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'l2', args: {}, pid: 52, ts: 532, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'l1', args: {}, pid: 52, ts: 533, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncInstantEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 2);
+ const instantSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(instantSlice.title, 'c');
+ assert.closeTo(0, instantSlice.start, 1e-5);
+ assert.closeTo(0, instantSlice.duration, 1e-5);
+ assert.sameMembers(instantSlice.subSlices, []);
+ assert.isTrue(instantSlice.isTopLevel);
+
+ const nestedSlice = t.asyncSliceGroup.slices[1];
+ assert.strictEqual(nestedSlice.title, 'a');
+ assert.closeTo(0, nestedSlice.start, 1e-5);
+ assert.closeTo((565 - 524) / 1000, nestedSlice.duration, 1e-5);
+ assert.isTrue(nestedSlice.isTopLevel);
+ assert.isDefined(nestedSlice.subSlices);
+ assert.strictEqual(nestedSlice.subSlices.length, 1);
+ const nestedInstantSlice = nestedSlice.subSlices[0];
+ assert.sameMembers(nestedInstantSlice.subSlices, []);
+ assert.strictEqual(nestedInstantSlice.title, 'd');
+ assert.isFalse(nestedInstantSlice.isTopLevel);
+ }
+
+ const events = [
+ {name: 'c', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'n', id: 71},
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'd', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'n', id: 72},
+ {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncUnmatchedOuterBeginEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ // Unmatched BEGIN should produce a warning.
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.closeTo(0, parentSlice.start, 0.0001);
+ // Unmatched BEGIN event ends at the last event of that ID.
+ assert.closeTo(36 / 1000, parentSlice.duration, 0.0001);
+ // Arguments should include only include its arguments.
+ assert.isUndefined(parentSlice.args.y);
+ assert.strictEqual(parentSlice.args.x, 1);
+ assert.isDefined(parentSlice.error);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 1);
+ const subSlice = parentSlice.subSlices[0];
+ assert.isFalse(subSlice.isTopLevel);
+ assert.closeTo(1 / 1000, subSlice.start, 1e-5);
+ assert.closeTo(35 / 1000, subSlice.duration, 1e-5);
+ assert.sameMembers(subSlice.subSlices, []);
+ // Arguments should include those of the END event.
+ assert.strictEqual(subSlice.args.y, 2);
+ assert.sameMembers(subSlice.subSlices, []);
+ }
+
+ const events = [
+ {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'b', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncUnmatchedInnerBeginEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ // Unmatched BEGIN should produce a warning.
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.closeTo(0, parentSlice.start, 1e-5);
+ assert.closeTo(41 / 1000, parentSlice.duration, 1e-5);
+ // Arguments should include both BEGIN and END event.
+ assert.strictEqual(parentSlice.args.y, 2);
+ assert.strictEqual(parentSlice.args.z, 3);
+ assert.isUndefined(parentSlice.args.x);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 2);
+ const subSliceInstant = parentSlice.subSlices[0];
+ const subSliceUnmatched = parentSlice.subSlices[1];
+ assert.strictEqual(subSliceInstant.title, 'c');
+ assert.isFalse(subSliceInstant.isTopLevel);
+ assert.strictEqual(subSliceUnmatched.title, 'b');
+ assert.isFalse(subSliceUnmatched.isTopLevel);
+ // Unmatched BEGIN ends at the last event of that ID.
+ assert.closeTo(1 / 1000, subSliceUnmatched.start, 1e-5);
+ assert.closeTo(40 / 1000, subSliceUnmatched.duration, 1e-5);
+ assert.sameMembers(subSliceUnmatched.subSlices, []);
+ assert.strictEqual(subSliceUnmatched.args.x, 1);
+ assert.isUndefined(subSliceUnmatched.y);
+ assert.isDefined(subSliceUnmatched.error);
+ assert.closeTo(1 / 1000, subSliceInstant.start, 1e-5);
+ assert.closeTo(0, subSliceInstant.duration, 1e-5);
+ assert.sameMembers(subSliceInstant.subSlices, []);
+ }
+
+ const events = [
+ {name: 'a', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'c', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'n', id: 72},
+ {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'a', args: {y: 2}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncUnmatchedOuterEndEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ // Unmatched END should produce a warning.
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 2);
+ const unmatchedSlice = t.asyncSliceGroup.slices[0];
+ const slice = t.asyncSliceGroup.slices[1];
+ assert.strictEqual(unmatchedSlice.title, 'a');
+ assert.closeTo(0, unmatchedSlice.start, 1e-5);
+ assert.isTrue(unmatchedSlice.isTopLevel);
+ // Unmatched END event begins at the first event of that ID. In this
+ // case, the first event happens to be the same unmatched event.
+ assert.closeTo(0 / 1000, unmatchedSlice.duration, 1e-5);
+ assert.isUndefined(unmatchedSlice.args.x);
+ assert.isUndefined(unmatchedSlice.args.y);
+ assert.strictEqual(unmatchedSlice.args.z, 3);
+ assert.isDefined(unmatchedSlice.error);
+ assert.sameMembers(unmatchedSlice.subSlices, []);
+
+ assert.strictEqual(slice.title, 'b');
+ assert.isTrue(slice.isTopLevel);
+ assert.closeTo(1 / 1000, slice.start, 1e-5);
+ assert.closeTo(35 / 1000, slice.duration, 1e-5);
+ // Arguments should include both BEGIN and END event.
+ assert.strictEqual(slice.args.x, 1);
+ assert.strictEqual(slice.args.y, 2);
+ assert.sameMembers(slice.subSlices, []);
+ }
+
+ // Events are intentionally out-of-order.
+ const events = [
+ {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'a', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncUnmatchedInnerEndEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ // Unmatched END should produce a warning.
+ assert.isTrue(m.hasImportWarnings);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.closeTo(0, parentSlice.start, 1e-5);
+ assert.closeTo(41 / 1000, parentSlice.duration, 1e-5);
+ // Arguments should include both BEGIN and END event.
+ assert.strictEqual(parentSlice.args.x, 1);
+ assert.strictEqual(parentSlice.args.y, 2);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 2);
+ const subSliceInstant = parentSlice.subSlices[0];
+ const subSliceUnmatched = parentSlice.subSlices[1];
+ assert.strictEqual(subSliceInstant.title, 'c');
+ assert.isFalse(subSliceInstant.isTopLevel);
+ assert.strictEqual(subSliceUnmatched.title, 'b');
+ assert.isFalse(subSliceUnmatched.isTopLevel);
+ // Unmatched END begins at the first event of that ID.
+ assert.closeTo(0 / 1000, subSliceUnmatched.start, 1e-5);
+ assert.closeTo(1 / 1000, subSliceUnmatched.duration, 1e-5);
+ // Arguments should include both BEGIN and END event.
+ assert.isUndefined(subSliceUnmatched.args.x);
+ assert.isUndefined(subSliceUnmatched.args.y);
+ assert.strictEqual(subSliceUnmatched.args.z, 3);
+ assert.isDefined(subSliceUnmatched.error);
+
+ assert.sameMembers(subSliceUnmatched.subSlices, []);
+ assert.closeTo(1 / 1000, subSliceInstant.start, 1e-5);
+ assert.closeTo(0, subSliceInstant.duration, 1e-5);
+ assert.sameMembers(subSliceInstant.subSlices, []);
+ }
+
+ const events = [
+ {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'c', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'n', id: 72},
+ {name: 'b', args: {z: 3}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'a', args: {y: 2}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncSameIDDifferentCategory', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 2);
+ const eventASlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(eventASlice.title, 'EVENT_A');
+ assert.strictEqual(eventASlice.category, 'foo');
+ assert.strictEqual(eventASlice.id, 'foo::ptr:72');
+ assert.isTrue(eventASlice.isTopLevel);
+ assert.strictEqual(eventASlice.args.x, 1);
+ assert.sameMembers(eventASlice.subSlices, []);
+
+ const eventBSlice = t.asyncSliceGroup.slices[1];
+ assert.strictEqual(eventBSlice.title, 'EVENT_B');
+ assert.strictEqual(eventBSlice.category, 'bar');
+ assert.strictEqual(eventBSlice.id, 'bar::ptr:72');
+ assert.isTrue(eventBSlice.isTopLevel);
+ assert.strictEqual(eventBSlice.args.y, 2);
+ assert.sameMembers(eventBSlice.subSlices, []);
+ }
+
+ // Events with the same ID, but different categories should not be
+ // considered as nested.
+ const events = [
+ {name: 'EVENT_A', args: {}, pid: 52, ts: 500, cat: 'foo', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'EVENT_B', args: {y: 2}, pid: 52, ts: 550, cat: 'bar', tid: 53,
+ ph: 'b', id: 72},
+ {name: 'EVENT_B', args: {}, pid: 52, ts: 600, cat: 'bar', tid: 53,
+ ph: 'e', id: 72},
+ {name: 'EVENT_A', args: {x: 1}, pid: 52, ts: 650, cat: 'foo', tid: 53,
+ ph: 'e', id: 72}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('nestableAsyncStackFrame', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const slice = t.asyncSliceGroup.slices[0];
+
+ assert.strictEqual(slice.startStackFrame.title, 'main');
+ assert.strictEqual(slice.endStackFrame.title, 'frame7');
+ }
+
+ const events = {
+ traceEvents: [
+ {name: 'name', pid: 52, ts: 525, cat: 'foo', tid: 53,
+ ph: 'b', id: 72, sf: 1},
+ {name: 'name', pid: 52, ts: 560, cat: 'foo', tid: 53,
+ ph: 'e', id: 72, sf: 7}
+ ],
+ stackFrames: {
+ '1': {
+ category: 'm1',
+ name: 'main'
+ },
+ '7': {
+ category: 'm2',
+ name: 'frame7',
+ parent: '1'
+ }
+ }
+ };
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('processLocalAsync', function() {
+ function checkModel(m) {
+ let t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ let parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 0);
+
+ t = m.processes[54].threads[55];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'b');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.strictEqual(parentSlice.args.x, 1);
+ assert.strictEqual(parentSlice.args.y, 2);
+ assert.isTrue(parentSlice.isTopLevel);
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 0);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id2: {local: 72}},
+ {name: 'b', args: {x: 1}, pid: 54, ts: 525, cat: 'foo', tid: 55,
+ ph: 'b', id2: {local: 72}},
+ {name: 'b', args: {y: 2}, pid: 54, ts: 560, cat: 'foo', tid: 55,
+ ph: 'e', id2: {local: 72}},
+ {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+ ph: 'e', id2: {local: 72}}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('globalAsync', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+
+ assert.isDefined(parentSlice.subSlices);
+ assert.strictEqual(parentSlice.subSlices.length, 1);
+ const subSlice = parentSlice.subSlices[0];
+ assert.isFalse(subSlice.isTopLevel);
+ // Arguments should include both BEGIN and END event.
+ assert.strictEqual(subSlice.args.x, 1);
+ assert.strictEqual(subSlice.args.y, 2);
+ assert.sameMembers(subSlice.subSlices, []);
+
+ assert.isUndefined(m.processes[54].threads[55]);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+ ph: 'b', id2: {global: 72}},
+ {name: 'b', args: {x: 1}, pid: 54, ts: 525, cat: 'foo', tid: 55,
+ ph: 'b', id2: {global: 72}},
+ {name: 'b', args: {y: 2}, pid: 54, ts: 560, cat: 'foo', tid: 55,
+ ph: 'e', id2: {global: 72}},
+ {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+ ph: 'e', id2: {global: 72}}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importSamples', function() {
+ function checkModel(m) {
+ const p = m.processes[52];
+ assert.isDefined(p);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.samples_.length, 4);
+ assert.strictEqual(t.samples_[0].start, 0.0);
+ assert.strictEqual(t.samples_[1].start, 0.0);
+ assert.closeTo(0.01, t.samples_[2].start, 1e-5);
+ assert.strictEqual(t.samples_[0].leafNode.title, 'a');
+ assert.strictEqual(t.samples_[1].leafNode.title, 'b');
+ assert.strictEqual(t.samples_[2].leafNode.title, 'c');
+ assert.strictEqual(t.samples_[3].leafNode, t.samples[0].leafNode);
+ assert.isFalse(m.hasImportWarnings);
+ }
+
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+ {name: 'b', args: {}, pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+ {name: 'c', args: {}, pid: 52, ts: 558, cat: 'test', tid: 53, ph: 'P'},
+ {name: 'a', args: {}, pid: 52, ts: 568, cat: 'test', tid: 53, ph: 'P'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importSamplesWithStackFrames', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+
+ assert.strictEqual(t.samples.length, 1);
+ assert.strictEqual(t.samples_[0].start, 0.0);
+ assert.strictEqual(t.samples_[0].leafNode.title, 'frame7');
+ assert.isFalse(m.hasImportWarnings);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'a', args: {}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'P', sf: 7 } // @suppress longLineCheck
+ ],
+ stackFrames: {
+ '1': {
+ category: 'm1',
+ name: 'main'
+ },
+ '7': {
+ category: 'm2',
+ name: 'frame7',
+ parent: '1'
+ }
+ }
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importSamplesMissingArgs', function() {
+ function checkModel(m) {
+ const p = m.processes[52];
+ assert.isDefined(p);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.isDefined(t);
+ assert.strictEqual(t.samples_.length, 3);
+ assert.isFalse(m.hasImportWarnings);
+ }
+
+ const events = [
+ {name: 'a', pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+ {name: 'b', pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+ {name: 'c', pid: 52, ts: 549, cat: 'test', tid: 53, ph: 'P'}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importV8Samples', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+
+ assert.isFalse(m.hasImportWarnings);
+ assert.strictEqual(t.samples.length, 2);
+
+ let sample = t.samples_[0];
+ assert.deepEqual(
+ sample.userFriendlyStack,
+ ['foo url: http://example.com/bar.js:22', 'bar url: unknown']);
+
+ sample = t.samples_[1];
+ assert.deepEqual(sample.userFriendlyStack, ['gc url: unknown']);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'V8Sample', args: {data: {stack: ['0x2a574306061', '0x2a574306224'], vm_state: 'js'}}, pid: 1, ts: 4, cat: 'test', tid: 2, ph: 'P' }, // @suppress longLineCheck
+ { name: 'V8Sample', args: {data: {stack: [], vm_state: 'gc'}}, pid: 1, ts: 6, cat: 'test', tid: 2, ph: 'P' }, // @suppress longLineCheck
+ { name: 'JitCodeAdded', args: {data: {code_len: 2, name: 'LazyCompile:~foo http://example.com/bar.js:23', code_start: '0x2a574306060'}}, pid: 1, ts: 1, cat: 'test', tid: 2, ph: 'M' }, // @suppress longLineCheck
+ { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'bar', code_start: '0x2a574306220'}}, pid: 1, ts: 2, cat: 'test', tid: 2, ph: 'M' }, // @suppress longLineCheck
+ { name: 'JitCodeMoved', args: {data: {code_len: 2, old_code_start: '0x2a574306220', code_start: '0x2a574306222'}}, pid: 1, ts: 3, cat: 'test', tid: 2, ph: 'M' }, // @suppress longLineCheck
+ { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'baz', code_start: '0xffffffff9f90a1a0'}}, pid: 1, ts: 4, cat: 'test', tid: 2, ph: 'M' } // @suppress longLineCheck
+ ]
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importOldFormatV8Samples', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+
+ assert.isFalse(m.hasImportWarnings);
+ assert.strictEqual(t.samples.length, 2);
+
+ let sample = t.samples_[0];
+ assert.deepEqual(
+ sample.userFriendlyStack,
+ ['foo url: http://example.com/bar.js:22', 'bar url: unknown']);
+
+ sample = t.samples_[1];
+ assert.deepEqual(sample.userFriendlyStack, ['gc url: unknown']);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'JitCodeAdded', args: {data: {code_len: 2, name: 'LazyCompile:~foo http://example.com/bar.js:23', code_start: '0x2a574306060'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
+ { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'bar', code_start: '0x2a574306220'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
+ { name: 'JitCodeMoved', args: {data: {code_len: 2, old_code_start: '0x2a574306220', code_start: '0x2a574306222'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
+ { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'baz', code_start: '0xffffffff9f90a1a0'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
+ { name: 'V8Sample', args: {data: {stack: ['0x2a574306061', '0x2a574306224']}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'P' }, // @suppress longLineCheck
+ { name: 'V8Sample', args: {data: {stack: [], vm_state: 'gc'}}, pid: 1, ts: 10, cat: 'test', tid: 2, ph: 'P' } // @suppress longLineCheck
+ ]
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importSimpleObject', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.bounds.min, 10);
+ assert.strictEqual(m.bounds.max, 50);
+ assert.isFalse(m.hasImportWarnings);
+
+ const p = m.processes[1];
+ assert.isDefined(p);
+
+ const i10 = p.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1000'), 10);
+ assert.strictEqual(i10.category, 'c');
+ assert.strictEqual(i10.creationTs, 10);
+ assert.strictEqual(i10.deletionTs, 50);
+ assert.strictEqual(i10.snapshots.length, 2);
+
+ const s15 = i10.snapshots[0];
+ assert.strictEqual(s15.ts, 15);
+ assert.strictEqual(s15.args, 15);
+
+ const s20 = i10.snapshots[1];
+ assert.strictEqual(s20.ts, 20);
+ assert.strictEqual(s20.args, 20);
+ }
+
+ const events = [
+ {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 15}}, // @suppress longLineCheck
+ {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 20}}, // @suppress longLineCheck
+ {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importImplicitObjects', function() {
+ function checkModel(m) {
+ const p1 = m.processes[1];
+
+ const iA = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1000'), 10);
+ const subObjectInstances =
+ p1.objects.getAllInstancesByTypeName().subObject;
+
+ assert.strictEqual(subObjectInstances.length, 2);
+ const subObject1 = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1'), 15);
+ assert.strictEqual(subObject1.name, 'subObject');
+ assert.strictEqual(subObject1.creationTs, 15);
+
+ assert.strictEqual(subObject1.snapshots.length, 2);
+ assert.strictEqual(subObject1.snapshots[0].ts, 15);
+ assert.strictEqual(subObject1.snapshots[0].args.foo, 1);
+ assert.strictEqual(subObject1.snapshots[1].ts, 20);
+ assert.strictEqual(subObject1.snapshots[1].args.foo, 2);
+
+ const subObject2 = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x2'), 20);
+ assert.strictEqual(subObject2.name, 'subObject');
+ assert.strictEqual(subObject2.creationTs, 20);
+ assert.strictEqual(subObject2.snapshots.length, 1);
+ assert.strictEqual(subObject2.snapshots[0].ts, 20);
+ }
+
+ const events = [
+ {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
+ args: { snapshot: [
+ { id: 'subObject/0x1',
+ foo: 1
+ }
+ ]}},
+ {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
+ args: { snapshot: [
+ { id: 'subObject/0x1',
+ foo: 2
+ },
+ { id: 'subObject/0x2',
+ foo: 1
+ }
+ ]}}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importImplicitObjectWithCategoryOverride', function() {
+ function checkModel(m) {
+ const p1 = m.processes[1];
+
+ const iA = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1000'), 10);
+ const subObjectInstances =
+ p1.objects.getAllInstancesByTypeName().subObject;
+
+ assert.strictEqual(subObjectInstances.length, 1);
+ }
+
+ const events = [
+ {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'cat', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'otherCat', id: '0x1000', name: 'a', // @suppress longLineCheck
+ args: { snapshot: [
+ { id: 'subObject/0x1',
+ cat: 'cat',
+ foo: 1
+ }
+ ]}}
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importImplicitObjectWithBaseTypeOverride', function() {
+ function checkModel(m) {
+ const p1 = m.processes[1];
+ assert.strictEqual(m.importWarnings.length, 0);
+
+ const iA = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1000'), 10);
+ assert.strictEqual(iA.snapshots.length, 1);
+ }
+
+ const events = [
+ {ts: 10000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'PictureLayerImpl', args: { // @suppress longLineCheck
+ snapshot: {
+ base_type: 'LayerImpl'
+ }
+ }},
+ {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'LayerImpl', args: {}} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importIDRefs', function() {
+ function checkModel(m) {
+ const p1 = m.processes[1];
+
+ const iA = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1000'), 10);
+ const s15 = iA.getSnapshotAt(15);
+
+ const taskSlice = p1.threads[1].sliceGroup.slices[0];
+ assert.strictEqual(taskSlice.args.my_object, s15);
+ }
+
+ const events = [
+ // An object with two snapshots.
+ {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 15}}, // @suppress longLineCheck
+ {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 20}}, // @suppress longLineCheck
+ {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+
+ // A slice that references the object.
+ {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {my_object: {id_ref: '0x1000'}}}, // @suppress longLineCheck
+ {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importIDRefsThatPointAtEachOther', function() {
+ function checkModel(m) {
+ const p1 = m.processes[1];
+
+ const iA = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1000'), 15);
+ const iFoo = p1.objects.getObjectInstanceAt(
+ new ScopedId('ptr', '0x1001'), 15);
+ assert.isDefined(iA);
+ assert.isDefined(iFoo);
+
+ const a15 = iA.getSnapshotAt(15);
+ const foo15 = iFoo.getSnapshotAt(15);
+
+ const taskSlice = p1.threads[1].sliceGroup.slices[0];
+ assert.strictEqual(taskSlice.args.my_object, foo15);
+ }
+
+ const events = [
+ // An object.
+ {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
+ snapshot: { x: {
+ id: 'foo/0x1001',
+ value: 'bar'
+ }}}},
+ {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+
+ // A slice that references the object.
+ {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {my_object: {id_ref: '0x1001'}}}, // @suppress longLineCheck
+ {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importArrayWithIDs', function() {
+ function checkModel(m) {
+ const p1 = m.processes[1];
+
+ const sA = p1.objects.getSnapshotAt(new ScopedId('ptr', '0x1000'), 15);
+ assert.isTrue(sA.args.x instanceof Array);
+ assert.strictEqual(sA.args.x.length, 3);
+ assert.isTrue(sA.args.x[0] instanceof tr.model.ObjectSnapshot);
+ assert.isTrue(sA.args.x[1] instanceof tr.model.ObjectSnapshot);
+ assert.isTrue(sA.args.x[2] instanceof tr.model.ObjectSnapshot);
+ }
+
+ const events = [
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
+ snapshot: { x: [
+ {id: 'foo/0x1001', value: 'bar1'},
+ {id: 'foo/0x1002', value: 'bar2'},
+ {id: 'foo/0x1003', value: 'bar3'}
+ ]}}}
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importDoesNotMutateEventList', function() {
+ const events = [
+ // An object.
+ {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+ {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
+ snapshot: {foo: 15}}},
+ {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+
+ // A slice that references the object.
+ {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {
+ my_object: {id_ref: '0x1000'}}
+ },
+ {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
+ ];
+
+ // The A type family exists to mutate the args list provided to
+ // snapshots.
+ function ASnapshot() {
+ tr.model.ObjectSnapshot.apply(this, arguments);
+ this.args.foo = 7;
+ }
+ ASnapshot.prototype = {
+ __proto__: tr.model.ObjectSnapshot.prototype
+ };
+
+ // Import event while the A types are registered, causing the
+ // arguments of the snapshots to be mutated.
+ tr.model.ObjectSnapshot.subTypes.register(ASnapshot, {typeName: 'a'});
+ const m = makeModel(events);
+ tr.model.ObjectSnapshot.subTypes.unregister(ASnapshot);
+ assert.isFalse(m.hasImportWarnings);
+
+ // Verify that the events array wasn't modified.
+ assert.deepEqual(
+ events[1].args,
+ {snapshot: {foo: 15}});
+ assert.deepEqual(
+ events[3].args,
+ {my_object: {id_ref: '0x1000'}});
+ });
+
+ test('importFlowEvent', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+
+ assert.isDefined(t);
+ assert.strictEqual(m.flowEvents.length, 2);
+ assert.strictEqual(m.flowIntervalTree.size, 2);
+
+ const f0 = m.flowEvents[0];
+ assert.strictEqual(f0.title, 'a');
+ assert.strictEqual(f0.category, 'foo');
+ assert.strictEqual(f0.id, 72);
+ assert.closeTo(f0.start, 0.001, 1e-5);
+ assert.closeTo(12 / 1000, f0.duration, 1e-5);
+ assert.strictEqual(f0.startSlice.title, 'aSlice');
+ assert.strictEqual(f0.endSlice.title, 'bSlice');
+
+ // TODO(nduca): Replace this assertion with something better when
+ // flow events don't create synthetic slices on their own.
+ assert.isDefined(f0.startSlice);
+ assert.isDefined(f0.endSlice);
+
+ const f1 = m.flowEvents[1];
+ assert.strictEqual(f1.title, f0.title);
+ assert.strictEqual(f1.category, f0.category);
+ assert.strictEqual(f1.id, f0.id);
+ assert.closeTo(20 / 1000, f1.duration, 1e-5);
+
+ assert.strictEqual(f1.startSlice.title, 'bSlice');
+ assert.strictEqual(f1.endSlice.title, 'cSlice');
+
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ assert.deepEqual(f1.startSlice.outFlowEvents, [f1]);
+ assert.deepEqual(f1.endSlice.inFlowEvents, [f1]);
+ }
+
+ const events = [
+ { name: 'aSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 547, ph: 'B', args: {} }, // @suppress longLineCheck
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} }, // @suppress longLineCheck
+ { id: 72, pid: 52, tid: 53, ts: 549, ph: 'E', args: {} }, // @suppress longLineCheck
+
+ { name: 'bSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 559, ph: 'B', args: {} }, // @suppress longLineCheck
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} }, // @suppress longLineCheck
+ { id: 72, pid: 52, tid: 53, ts: 561, ph: 'E', args: {} }, // @suppress longLineCheck
+
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} }, // @suppress longLineCheck
+ { name: 'cSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 581, ph: 'B', args: {} }, // @suppress longLineCheck
+ { id: 72, pid: 52, tid: 53, ts: 582, ph: 'E', args: {} } // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importOldFlowEventBindtoNext', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 1);
+
+ const f0 = m.flowEvents[0];
+
+ assert.strictEqual(f0.title, 'flow');
+ assert.strictEqual(f0.category, 'foo');
+ assert.strictEqual(f0.id, 72);
+ assert.strictEqual(f0.start, .548);
+ assert.closeTo(32 / 1000, f0.duration, 1e-5);
+ assert.strictEqual(f0.startSlice.title, 'slice1');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'slice3');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ }
+
+ // Old trace format without event.bp, and event.cat doesn't contain input
+ const events = [
+ { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100}, // @suppress longLineCheck
+ { name: 'flow', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}}, // @suppress longLineCheck
+
+ { name: 'flow', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: {}}, // @suppress longLineCheck
+ { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100}, // @suppress longLineCheck
+ { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000} // @suppress longLineCheck
+
+ ];
+ checkParsedAndStreamInput(events, checkModel, false, false);
+ });
+
+ test('importOldInputFlowEventBindtoParent', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 1);
+
+ const f0 = m.flowEvents[0];
+
+ assert.strictEqual(f0.title, 'flow');
+ assert.strictEqual(f0.category, 'input');
+ assert.strictEqual(f0.id, 72);
+ assert.strictEqual(f0.start, .548);
+ assert.closeTo(32 / 1000, f0.duration, 1e-5);
+ assert.strictEqual(f0.startSlice.title, 'slice1');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'slice2');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ }
+
+ // Old trace format without event.bp, but event.cat contains input
+ const events = [
+ { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100}, // @suppress longLineCheck
+ { name: 'flow', cat: 'input', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}}, // @suppress longLineCheck
+
+ { name: 'flow', cat: 'input', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: {}}, // @suppress longLineCheck
+ { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100}, // @suppress longLineCheck
+ { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000} // @suppress longLineCheck
+
+ ];
+ checkParsedAndStreamInput(events, checkModel, false, false);
+ });
+
+ test('importOldIPCFlowEventBindtoParent', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 1);
+
+ const f0 = m.flowEvents[0];
+
+ assert.strictEqual(f0.title, 'flow');
+ assert.strictEqual(f0.category, 'disabled-by-default-ipc.flow');
+ assert.strictEqual(f0.id, 72);
+ assert.strictEqual(f0.start, .548);
+ assert.closeTo(32 / 1000, f0.duration, 1e-5);
+ assert.strictEqual(f0.startSlice.title, 'slice1');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'slice2');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ }
+
+ // Old trace format without event.bp, but event.cat contains ipc.flow
+ const events = [
+ { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100}, // @suppress longLineCheck
+ { name: 'flow', cat: 'disabled-by-default-ipc.flow', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}}, // @suppress longLineCheck
+
+ { name: 'flow', cat: 'disabled-by-default-ipc.flow', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: {}}, // @suppress longLineCheck
+ { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100}, // @suppress longLineCheck
+ { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000} // @suppress longLineCheck
+
+ ];
+ checkParsedAndStreamInput(events, checkModel, false, false);
+ });
+
+ test('importNewFlowEventBindtoParent', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 1);
+
+ const f0 = m.flowEvents[0];
+
+ assert.strictEqual(f0.title, 'flow');
+ assert.strictEqual(f0.category, 'foo');
+ assert.strictEqual(f0.id, 72);
+ assert.strictEqual(f0.start, .548);
+ assert.closeTo(32 / 1000, f0.duration, 1e-5);
+ assert.strictEqual(f0.startSlice.title, 'slice1');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'slice2');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ }
+
+ // New trace format with event.bp
+ const events = [
+ { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100}, // @suppress longLineCheck
+ { name: 'flow', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', bp: 'e', args: {}}, // @suppress longLineCheck
+
+ { name: 'flow', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', bp: 'e', args: {}}, // @suppress longLineCheck
+ { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100}, // @suppress longLineCheck
+ { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000} // @suppress longLineCheck
+
+ ];
+ checkParsedAndStreamInput(events, checkModel, false, false);
+ });
+
+ test('importNewFlowEventWithInvalidBindingPoint', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 0);
+ }
+
+ // New trace format with event.bp, which however !== 'e'
+ const events = [
+ { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100}, // @suppress longLineCheck
+ { name: 'flow', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', bp: 'z', args: {}}, // @suppress longLineCheck
+
+ { name: 'flow', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', bp: 'z', args: {}}, // @suppress longLineCheck
+ { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100}, // @suppress longLineCheck
+ { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000} // @suppress longLineCheck
+
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importFlowV2OnePair', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 1);
+
+ const f0 = m.flowEvents[0];
+
+ assert.strictEqual(f0.startSlice.title, 'producer');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'consumer');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ }
+
+ // Flow V2: one flow producer one flow consumer
+ const events = [
+ { name: 'producer', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true}, // @suppress longLineCheck
+ { name: 'consumer', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importFlowV2OneFlowStep', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 2);
+
+ const f0 = m.flowEvents[0];
+ const f1 = m.flowEvents[1];
+
+ assert.strictEqual(f0.startSlice.title, 'producer');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'step');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+
+ assert.strictEqual(f1.startSlice.title, 'step');
+ assert.deepEqual(f1.startSlice.outFlowEvents, [f1]);
+ assert.strictEqual(f1.endSlice.title, 'consumer');
+ assert.deepEqual(f1.endSlice.inFlowEvents, [f1]);
+ }
+
+ // Flow V2: one flow producer one flow consumer
+ const events = [
+ { name: 'producer', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true}, // @suppress longLineCheck
+ { name: 'step', cat: 'foo', pid: 62, tid: 63, ts: 647, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true, flow_in: true}, // @suppress longLineCheck
+ { name: 'consumer', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', args: { 'queue_duration': 0}, flow_in: true} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importFlowV2MultipleConsumers', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 2);
+
+ const f0 = m.flowEvents[0];
+ const f1 = m.flowEvents[1];
+
+ assert.strictEqual(f0.startSlice.title, 'producer');
+ assert.strictEqual(f1.startSlice.title, 'producer');
+
+ assert.strictEqual(f0.startSlice.outFlowEvents.length, 2);
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0, f1]);
+ assert.deepEqual(f1.startSlice.outFlowEvents, [f0, f1]);
+
+ assert.strictEqual(f0.endSlice.title, 'consumer1');
+ assert.strictEqual(f1.endSlice.title, 'consumer2');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+ assert.deepEqual(f1.endSlice.inFlowEvents, [f1]);
+ }
+
+ // Flow V2: one flow producer multiple flow consumers
+ const events = [
+ { name: 'producer', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true}, // @suppress longLineCheck
+ { name: 'consumer1', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true}, // @suppress longLineCheck
+ { name: 'consumer2', cat: 'foo', pid: 70, tid: 72, ts: 870, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importFlowV2MultipleProducers', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 1);
+ }
+
+ // Flow V2: multiple flow producers, which is not allowed
+ const events = [
+ { name: 'producer1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true}, // @suppress longLineCheck
+ { name: 'producer2', cat: 'foo', pid: 52, tid: 54, ts: 567, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true}, // @suppress longLineCheck
+ { name: 'consumer', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ // This test creates a flow event that stops on the same timestamp that
+ // the 'X' event which it triggers begins.
+ test('importFlowEventOverlaps', function() {
+ function checkModel(m) {
+ const startT = m.processes[52].threads[53];
+ const endT = m.processes[70].threads[71];
+
+ assert.isDefined(startT);
+ assert.strictEqual(startT.sliceGroup.slices.length, 1);
+
+ assert.isDefined(endT);
+ assert.strictEqual(endT.sliceGroup.slices.length, 1);
+
+ assert.strictEqual(m.flowEvents.length, 1);
+
+ // f0 represents 's' to 'f'
+ const f0 = m.flowEvents[0];
+
+ assert.strictEqual(f0.title, 'PostTask');
+ assert.strictEqual(f0.category, 'foo');
+ assert.strictEqual(f0.id, 72);
+ assert.strictEqual(f0.start, .548);
+ assert.closeTo(32 / 1000, f0.duration, 1e-5);
+ assert.strictEqual(f0.startSlice.title, 'SomeTask');
+ assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
+ assert.strictEqual(f0.endSlice.title, 'RunTask');
+ assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
+
+ // TODO(nduca): Add assertions about the flow slices, esp that they were
+ // found correctly.
+ }
+
+ const events = [
+ { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100}, // @suppress longLineCheck
+ { name: 'PostTask', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}}, // @suppress longLineCheck
+
+ { name: 'PostTask', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: { 'queue_duration': 0}}, // @suppress longLineCheck
+ // Note that RunTask has the same time-stamp as PostTask 'f'
+ { name: 'RunTask', cat: 'foo', pid: 70, tid: 71, ts: 580, ph: 'X', args: {'src_func': 'PostRunTask'}, dur: 1000} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importOutOfOrderFlowEvent', function() {
+ function checkModel(m) {
+ const expected = [0.4, 0.0, 0.412];
+ assert.strictEqual(m.flowIntervalTree.size, 3);
+
+ const order = m.flowEvents.map(function(x) { return x.start; });
+ for (let i = 0; i < expected.length; ++i) {
+ assert.closeTo(expected[i], order[i], 1e-5);
+ }
+ }
+
+ const events = [
+ { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 548, ph: 'X', dur: 10}, // @suppress longLineCheck
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} }, // @suppress longLineCheck
+
+ { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 148, ph: 'X', dur: 10}, // @suppress longLineCheck
+ { name: 'b', cat: 'foo', id: 73, pid: 52, tid: 53, ts: 148, ph: 's', args: {} }, // @suppress longLineCheck
+
+ { name: 'b', cat: 'foo', id: 73, pid: 52, tid: 53, ts: 570, ph: 'f', args: {} }, // @suppress longLineCheck
+ { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 571, ph: 'X', dur: 10}, // @suppress longLineCheck
+
+ { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 560, ph: 'X', dur: 10}, // @suppress longLineCheck
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} }, // @suppress longLineCheck
+
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} }, // @suppress longLineCheck
+ { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 581, ph: 'X', dur: 10} // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importCompleteEvent', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.slices.length, 3);
+ assert.strictEqual(t.tid, 53);
+
+ let slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'baz');
+ assert.closeTo(0, slice.start, 1e-5);
+ assert.closeTo(1 / 1000, slice.duration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'foo');
+ assert.closeTo((730 - 629) / 1000, slice.start, 1e-5);
+ assert.closeTo(20 / 1000, slice.duration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 1);
+
+ slice = t.sliceGroup.slices[2];
+ assert.strictEqual(slice.title, 'c');
+ assert.isTrue(slice.didNotFinish);
+ assert.closeTo(10 / 1000, slice.duration, 1e-5);
+ }
+
+ const events = [
+ { name: 'a', args: {}, pid: 52, ts: 629, dur: 1, cat: 'baz', tid: 53, ph: 'X' }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 52, ts: 730, dur: 20, cat: 'foo', tid: 53, ph: 'X' }, // @suppress longLineCheck
+ { name: 'c', args: {}, pid: 52, ts: 740, cat: 'baz', tid: 53, ph: 'X' }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importFlowEventsWithStackFrame', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.flowEvents.length, 2);
+
+ const f0 = m.flowEvents[0];
+ assert.strictEqual(f0.startStackFrame.title, 'fn1');
+ assert.strictEqual(f0.endStackFrame.title, 'fn2');
+
+ const f1 = m.flowEvents[1];
+ assert.strictEqual(f1.startStackFrame.title, 'fn2');
+ assert.strictEqual(f1.endStackFrame.title, 'fn3');
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'aSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 547, ph: 'B', args: {} }, // @suppress longLineCheck
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}, sf: 1 }, // @suppress longLineCheck
+ { id: 72, pid: 52, tid: 53, ts: 549, ph: 'E', args: {} }, // @suppress longLineCheck
+
+ { name: 'bSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 559, ph: 'B', args: {} }, // @suppress longLineCheck
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {}, sf: 2 }, // @suppress longLineCheck
+ { id: 72, pid: 52, tid: 53, ts: 561, ph: 'E', args: {} }, // @suppress longLineCheck
+
+ { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {}, sf: 3 }, // @suppress longLineCheck
+ { name: 'cSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 581, ph: 'B', args: {} }, // @suppress longLineCheck
+ { id: 72, pid: 52, tid: 53, ts: 582, ph: 'E', args: {} } // @suppress longLineCheck
+ ],
+ stackFrames: {
+ '1': {
+ category: 'm1',
+ name: 'fn1'
+ },
+ '2': {
+ category: 'm2',
+ name: 'fn2'
+ },
+ '3': {
+ category: 'm3',
+ name: 'fn3'
+ }
+ }
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importCompleteEventWithCpuDuration', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.numProcesses, 1);
+ const p = m.processes[52];
+ assert.isDefined(p);
+
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[53];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.slices.length, 3);
+ assert.strictEqual(t.tid, 53);
+
+ let slice = t.sliceGroup.slices[0];
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.category, 'baz');
+ assert.closeTo(0, slice.start, 1e-5);
+ assert.closeTo(1 / 1000, slice.duration, 1e-5);
+ assert.closeTo(12 / 1000, slice.cpuStart, 1e-5);
+ assert.closeTo(1 / 1000, slice.cpuDuration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 0);
+
+ slice = t.sliceGroup.slices[1];
+ assert.strictEqual(slice.title, 'b');
+ assert.strictEqual(slice.category, 'foo');
+ assert.closeTo((730 - 629) / 1000, slice.start, 1e-5);
+ assert.closeTo(20 / 1000, slice.duration, 1e-5);
+ assert.closeTo(110 / 1000, slice.cpuStart, 1e-5);
+ assert.closeTo(16 / 1000, slice.cpuDuration, 1e-5);
+ assert.strictEqual(slice.subSlices.length, 1);
+
+ slice = t.sliceGroup.slices[2];
+ assert.strictEqual(slice.title, 'c');
+ assert.isTrue(slice.didNotFinish);
+ assert.closeTo(10 / 1000, slice.duration, 1e-5);
+ }
+
+ const events = [
+ { name: 'a', args: {}, pid: 52, ts: 629, dur: 1, cat: 'baz', tid: 53, ph: 'X', tts: 12, tdur: 1 }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 52, ts: 730, dur: 20, cat: 'foo', tid: 53, ph: 'X', tts: 110, tdur: 16 }, // @suppress longLineCheck
+ { name: 'c', args: {}, pid: 52, ts: 740, cat: 'baz', tid: 53, ph: 'X', tts: 115 } // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importNestedCompleteEventWithTightBounds', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+
+ const sA = findSliceNamed(t.sliceGroup, 'a');
+ const sB = findSliceNamed(t.sliceGroup, 'b');
+
+ assert.strictEqual(sA.title, 'a');
+ assert.strictEqual(sA.category, 'baz');
+ assert.strictEqual(sA.start, 244654227.065);
+ assert.strictEqual(sA.duration, 36.075);
+ assert.closeTo(0.03, sA.selfTime, 1e-5);
+
+ assert.strictEqual(sB.title, 'b');
+ assert.strictEqual(sB.category, 'foo');
+ assert.strictEqual(sB.start, 244654227.095);
+ assert.strictEqual(sB.duration, 36.045);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.strictEqual(sA.subSlices[0], sB);
+ assert.strictEqual(sB.parentSlice, sA);
+ }
+
+ const events = [
+ { name: 'a', args: {}, pid: 52, ts: 244654227065, dur: 36075, cat: 'baz', tid: 53, ph: 'X' }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 52, ts: 244654227095, dur: 36045, cat: 'foo', tid: 53, ph: 'X' } // @suppress longLineCheck
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+
+ test('importCompleteEventWithStackFrame', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.slices.length, 2);
+
+ const s0 = t.sliceGroup.slices[0];
+ assert.strictEqual(s0.startStackFrame.title, 'frame7');
+ assert.isUndefined(s0.endStackFrame);
+
+ const s1 = t.sliceGroup.slices[1];
+ assert.strictEqual(s1.startStackFrame.title, 'frame8');
+ assert.strictEqual(s1.endStackFrame.title, 'frame9');
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'a', args: {}, pid: 1, ts: 0, dur: 1, cat: 'baz', tid: 2, ph: 'X', sf: 7 }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 1, ts: 5, dur: 1, cat: 'baz', tid: 2, ph: 'X', sf: 8, esf: 9 } // @suppress longLineCheck
+ ],
+ stackFrames: {
+ '1': {
+ category: 'm1',
+ name: 'main'
+ },
+ '7': {
+ category: 'm2',
+ name: 'frame7',
+ parent: '1'
+ },
+ '8': {
+ category: 'm2',
+ name: 'frame8',
+ parent: '1'
+ },
+ '9': {
+ category: 'm2',
+ name: 'frame9',
+ parent: '1'
+ }
+ }
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importAsyncEventWithSameTimestamp', function() {
+ function checkModel(m) {
+ const t = m.processes[52].threads[53];
+
+ assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
+ const parentSlice = t.asyncSliceGroup.slices[0];
+ assert.strictEqual(parentSlice.title, 'a');
+ assert.strictEqual(parentSlice.category, 'foo');
+ assert.isTrue(parentSlice.isTopLevel);
+
+ assert.isDefined(parentSlice.subSlices);
+ const subSlices = parentSlice.subSlices;
+ assert.strictEqual(subSlices.length, 1000);
+ // Slices should be sorted according to 'ts'. And if 'ts' is the same,
+ // slices should keep the order that they were recorded.
+ for (let i = 0; i < 1000; i++) {
+ assert.strictEqual(i + 1, subSlices[i].args.seq);
+ assert.isFalse(subSlices[i].isTopLevel);
+ }
+ }
+
+ const events = [];
+ // Events are added with ts 0, 1, 1, 2, 2, 3, 3 ...500, 500, 1000
+ // and use 'seq' to track the order of when the event is recorded.
+ events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 0, ph: 'S', args: {'seq': 0}}); // @suppress longLineCheck
+
+ for (let i = 1; i <= 1000; i++) {
+ events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: Math.round(i / 2), ph: 'T', args: {'seq': i}}); // @suppress longLineCheck
+ }
+
+ events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 1000, ph: 'F', args: {'seq': 1001}}); // @suppress longLineCheck
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('sampleDataSimple', function() {
+ function checkModel(m) {
+ assert.isDefined(m.kernel.cpus[0]);
+ assert.strictEqual(m.getAllThreads().length, 1);
+
+ assert.strictEqual(m.samples.length, 3);
+
+ const t1 = m.processes[1].threads[1];
+ assert.strictEqual(t1.samples.length, 3);
+
+ const c0 = m.kernel.cpus[0];
+ const c1 = m.kernel.cpus[1];
+ assert.strictEqual(c0.samples.length, 2);
+ assert.strictEqual(c1.samples.length, 1);
+
+ assert.strictEqual(m.samples[0].cpu, c0);
+ assert.strictEqual(m.samples[0].thread, t1);
+ assert.strictEqual(m.samples[0].title, 'cycles:HG');
+ assert.strictEqual(m.samples[0].start, 1);
+ assert.deepEqual(
+ ['a_sub', 'a', 'main'],
+ m.samples[0].userFriendlyStack
+ .map(x => x.substr(0, x.indexOf('url') - 1)));
+ assert.strictEqual(m.samples[0].weight, 1);
+ }
+
+ const events = {
+ 'traceEvents': [],
+ 'stackFrames': {
+ '1': {
+ 'category': 'mod',
+ 'name': 'main'
+ },
+ '2': {
+ 'category': 'mod',
+ 'name': 'a',
+ 'parent': 1
+ },
+ '3': {
+ 'category': 'mod',
+ 'name': 'a_sub',
+ 'parent': 2
+ },
+ '4': {
+ 'category': 'mod',
+ 'name': 'b',
+ 'parent': 1
+ }
+ },
+ 'samples': [
+ {
+ 'cpu': 0, 'tid': 1, 'ts': 1000.0,
+ 'name': 'cycles:HG', 'sf': 3, 'weight': 1
+ },
+ {
+ 'cpu': 0, 'tid': 1, 'ts': 2000.0,
+ 'name': 'cycles:HG', 'sf': 2, 'weight': 1
+ },
+ {
+ 'cpu': 1, 'tid': 1, 'ts': 3000.0,
+ 'name': 'cycles:HG', 'sf': 3, 'weight': 1
+ }
+ ]
+ };
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importMemoryDumps_verifyProcessAndGlobalMemoryDumpLinks', function() {
+ function checkModel(m) {
+ const p1 = m.getProcess(42);
+ const p2 = m.getProcess(43);
+ assert.isDefined(p1);
+ assert.isDefined(p2);
+
+ // Check that Model and Process objects contain the right dumps.
+ assert.strictEqual(m.globalMemoryDumps.length, 2);
+ assert.strictEqual(p1.memoryDumps.length, 2);
+ assert.strictEqual(p2.memoryDumps.length, 1);
+
+ assert.strictEqual(m.globalMemoryDumps[0].start, 10);
+ assert.strictEqual(p1.memoryDumps[0].start, 10);
+ assert.strictEqual(p2.memoryDumps[0].start, 11);
+ assert.strictEqual(m.globalMemoryDumps[0].duration, 1);
+ assert.strictEqual(p1.memoryDumps[0].duration, 0);
+ assert.strictEqual(p2.memoryDumps[0].duration, 0);
+
+ assert.strictEqual(m.globalMemoryDumps[1].start, 13);
+ assert.strictEqual(p1.memoryDumps[1].start, 13);
+ assert.strictEqual(m.globalMemoryDumps[1].duration, 0);
+ assert.strictEqual(p1.memoryDumps[1].duration, 0);
+
+ // Check that GlobalMemoryDump and ProcessMemoryDump objects are
+ // interconnected correctly.
+ assert.strictEqual(p1.memoryDumps[0],
+ m.globalMemoryDumps[0].processMemoryDumps[42]);
+ assert.strictEqual(p2.memoryDumps[0],
+ m.globalMemoryDumps[0].processMemoryDumps[43]);
+ assert.strictEqual(
+ p1.memoryDumps[0].globalMemoryDump, m.globalMemoryDumps[0]);
+ assert.strictEqual(
+ p2.memoryDumps[0].globalMemoryDump, m.globalMemoryDumps[0]);
+
+ assert.strictEqual(p1.memoryDumps[1],
+ m.globalMemoryDumps[1].processMemoryDumps[42]);
+ assert.strictEqual(
+ p1.memoryDumps[1].globalMemoryDump, m.globalMemoryDumps[1]);
+ }
+
+ const events = [
+ // 2 process memory dump events.
+ {
+ name: 'a',
+ pid: 42,
+ ts: 10000,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '100'
+ }
+ }
+ }
+ },
+ {
+ name: 'b',
+ pid: 43,
+ ts: 11000,
+ cat: 'test',
+ tid: 54,
+ ph: 'v',
+ id: '0x0001',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '200'
+ }
+ }
+ }
+ },
+ // 1 process memory dump event.
+ {
+ name: 'd',
+ pid: 42,
+ ts: 13000,
+ cat: 'test',
+ tid: 56,
+ ph: 'v',
+ id: '0xfffffff12345678',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '300'
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importMemoryDumps_totalResidentBytesOnly', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ assert.strictEqual(d.totals.residentBytes, 9007199254740991);
+ assert.isUndefined(d.totals.peakResidentBytes);
+ assert.isUndefined(d.totals.arePeakResidentBytesResettable);
+ assert.isUndefined(d.totals.platformSpecific);
+ assert.isUndefined(d.mostRecentVmRegions);
+ assert.lengthOf(d.memoryAllocatorDumps, 0);
+ }
+
+ const events = [
+ {
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0x01',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '1fffffffffffff'
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_withPeakResidentBytes', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ assert.strictEqual(d.totals.residentBytes, 9007199254740991);
+ assert.strictEqual(d.totals.peakResidentBytes, 13510798882111488);
+ assert.isTrue(d.totals.arePeakResidentBytesResettable);
+ assert.isUndefined(d.totals.platformSpecific);
+ assert.isUndefined(d.mostRecentVmRegions);
+ assert.lengthOf(d.memoryAllocatorDumps, 0);
+ }
+
+ const events = [
+ {
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0x01',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '1fffffffffffff',
+ peak_resident_set_bytes: '2fffffffffffff',
+ is_peak_rss_resetable: true
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_platformSpecificTotals', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ assert.strictEqual(d.totals.residentBytes, 9007199254740991);
+ assert.isUndefined(d.totals.peakResidentBytes);
+ assert.isUndefined(d.totals.arePeakResidentBytesResettable);
+ assert.deepEqual(d.totals.platformSpecific,
+ {private_bytes: 4503599627370495, shared_bytes: 4503599627370496});
+ assert.isUndefined(d.mostRecentVmRegions);
+ assert.lengthOf(d.memoryAllocatorDumps, 0);
+ }
+
+ const events = [
+ {
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0x01',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '1fffffffffffff',
+ private_bytes: 'fffffffffffff',
+ shared_bytes: '10000000000000'
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_vmRegions', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ checkVMRegions(d.vmRegions, [
+ {
+ startAddress: 240,
+ sizeInBytes: 336,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ mappedFile: '[stack:20310]',
+ byteStats: {
+ privateCleanResident: 64,
+ privateDirtyResident: 32,
+ sharedCleanResident: 256,
+ sharedDirtyResident: 0,
+ proportionalResident: 158,
+ swapped: 80
+ }
+ },
+ {
+ startAddress: 848,
+ sizeInBytes: 592,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ mappedFile: '/dev/ashmem/dalvik',
+ byteStats: {
+ proportionalResident: 205,
+ privateDirtyResident: 205,
+ swapped: 0
+ }
+ },
+ {
+ startAddress: 140673331539968,
+ sizeInBytes: 262144,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE |
+ VMRegion.PROTECTION_FLAG_MAYSHARE,
+ mappedFile: '/run/shm/.org.chromium.Chromium.sqqN11 (deleted)',
+ byteStats: {
+ privateCleanResident: 0,
+ privateDirtyResident: 262144,
+ sharedCleanResident: 0,
+ sharedDirtyResident: 0,
+ proportionalResident: 262144,
+ swapped: 0
+ }
+ }
+ ]);
+
+ assert.strictEqual(d.totals.residentBytes, 0);
+ assert.isUndefined(d.totals.peakResidentBytes);
+ assert.isUndefined(d.totals.arePeakResidentBytesResettable);
+ assert.isUndefined(d.totals.platformSpecific);
+ assert.lengthOf(d.memoryAllocatorDumps, 0);
+ }
+
+ const events = [
+ {
+ name: 'some_dump_name',
+ pid: 42,
+ ts: 10,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '000',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '0'
+ },
+ process_mmaps: {
+ vm_regions: [
+ {
+ sa: 'f0',
+ sz: '150',
+ pf: 6,
+ mf: '[stack:20310]',
+ bs: {
+ pss: '9e',
+ pc: '40',
+ pd: '20',
+ sc: '100',
+ sd: '0',
+ sw: '50'
+ }
+ },
+ {
+ sa: '350',
+ sz: '250',
+ pf: 5,
+ mf: '/dev/ashmem/dalvik',
+ bs: {
+ pss: 'cd',
+ pd: 'cd',
+ sc: undefined,
+ sw: '0'
+ }
+ },
+ {
+ sa: '7ff10ff4b000',
+ sz: '40000',
+ pf: 134,
+ mf: '/run/shm/.org.chromium.Chromium.sqqN11 (deleted)',
+ bs: {
+ pss: '40000',
+ pc: '0',
+ pd: '40000',
+ sc: '0',
+ sd: '0',
+ sw: '0'
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_explicitMemoryAllocatorDumps', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ assert.strictEqual(d.memoryAllocatorDumps.length, 2);
+
+ const oilpanRoot = d.getMemoryAllocatorDumpByFullName('oilpan');
+ const v8Root = d.getMemoryAllocatorDumpByFullName('v8');
+ assert.isDefined(oilpanRoot);
+ assert.isDefined(v8Root);
+ assert.include(d.memoryAllocatorDumps, oilpanRoot);
+ assert.include(d.memoryAllocatorDumps, v8Root);
+
+ checkDumpNumericsAndDiagnostics(oilpanRoot, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 47),
+ 'size': 32768,
+ 'effective_size': 32768,
+ 'inner_size': 4096
+ }, {});
+ assert.strictEqual(oilpanRoot.children.length, 2);
+
+ const oilpanBucket1 = d.getMemoryAllocatorDumpByFullName(
+ 'oilpan/heap2/bucket1');
+ assert.isDefined(oilpanBucket1);
+ assert.strictEqual(oilpanBucket1.fullName, 'oilpan/heap2/bucket1');
+ assert.strictEqual(oilpanBucket1.name, 'bucket1');
+ checkDumpNumericsAndDiagnostics(oilpanBucket1, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
+ 'size': 8192,
+ 'effective_size': 8192,
+ 'inner_size': 8192
+ }, {});
+ assert.strictEqual(oilpanBucket1.children.length, 0);
+
+ assert.isDefined(oilpanBucket1.parent);
+ assert.strictEqual(oilpanBucket1.parent.fullName, 'oilpan/heap2');
+ assert.strictEqual(oilpanBucket1.parent.name, 'heap2');
+ assert.include(oilpanBucket1.parent.children, oilpanBucket1);
+
+ assert.isDefined(oilpanBucket1.parent.parent);
+ assert.strictEqual(oilpanBucket1.parent.parent, oilpanRoot);
+
+ assert.strictEqual(d.totals.residentBytes, 256);
+ assert.isUndefined(d.totals.peakResidentBytes);
+ assert.isUndefined(d.totals.arePeakResidentBytesResettable);
+ assert.isUndefined(d.totals.platformSpecific);
+ assert.isUndefined(d.mostRecentVmRegions);
+ }
+
+ const events = [
+ {
+ name: 'a',
+ pid: 42,
+ ts: 10,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '100'
+ },
+ allocators: {
+ 'oilpan': {
+ guid: '1a',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '2f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '1000'},
+ size: {type: 'scalar', units: 'bytes', value: '8000'}
+ }
+ },
+ 'oilpan/heap1': {
+ guid: '2b',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '3f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '3000'},
+ size: {type: 'scalar', units: 'bytes', value: '4000'}
+ }
+ },
+ 'oilpan/heap2': {
+ guid: '3c',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '4f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '4000'},
+ size: {type: 'scalar', units: 'bytes', value: '4000'}
+ }
+ },
+ 'oilpan/heap2/bucket1': {
+ // Deliberately missing GUID (to check that the importer does
+ // not skip memory allocator dump without GUID).
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '1f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '2000'},
+ size: {type: 'scalar', units: 'bytes', value: '2000'}
+ }
+ },
+ 'v8': {
+ guid: '5e',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '5f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '5000'},
+ size: {type: 'scalar', units: 'bytes', value: '6000'}
+ }
+ }
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_implicitMemoryAllocatorDumps', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ assert.strictEqual(d.memoryAllocatorDumps.length, 2);
+
+ const oilpanRoot = d.getMemoryAllocatorDumpByFullName('oilpan');
+ const v8Root = d.getMemoryAllocatorDumpByFullName('v8');
+ assert.isDefined(oilpanRoot);
+ assert.isDefined(v8Root);
+ assert.include(d.memoryAllocatorDumps, oilpanRoot);
+ assert.include(d.memoryAllocatorDumps, v8Root);
+
+ checkDumpNumericsAndDiagnostics(oilpanRoot, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 94),
+ 'size': 24576,
+ 'effective_size': 24576,
+ 'inner_size': 20480
+ }, {});
+ assert.strictEqual(oilpanRoot.children.length, 2);
+
+ const oilpanBucket1 = d.getMemoryAllocatorDumpByFullName(
+ 'oilpan/heap2/bucket1');
+ assert.isDefined(oilpanBucket1);
+ assert.strictEqual(oilpanBucket1.fullName, 'oilpan/heap2/bucket1');
+ assert.strictEqual(oilpanBucket1.name, 'bucket1');
+ checkDumpNumericsAndDiagnostics(oilpanBucket1, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
+ 'size': 8192,
+ 'effective_size': 8192,
+ 'inner_size': 8192
+ }, {});
+ assert.strictEqual(oilpanBucket1.children.length, 0);
+
+ assert.isDefined(oilpanBucket1.parent);
+ assert.strictEqual(oilpanBucket1.parent.fullName, 'oilpan/heap2');
+ assert.strictEqual(oilpanBucket1.parent.name, 'heap2');
+ assert.include(oilpanBucket1.parent.children, oilpanBucket1);
+
+ assert.isDefined(oilpanBucket1.parent.parent);
+ assert.strictEqual(oilpanBucket1.parent.parent, oilpanRoot);
+
+ assert.strictEqual(d.totals.residentBytes, 256);
+ assert.isUndefined(d.totals.peakResidentBytes);
+ assert.isUndefined(d.totals.arePeakResidentBytesResettable);
+ assert.isUndefined(d.totals.platformSpecific);
+ assert.isUndefined(d.mostRecentVmRegions);
+ }
+
+ const events = [
+ {
+ name: 'a',
+ pid: 42,
+ ts: 10,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '100'
+ },
+ allocators: {
+ 'oilpan/heap1': {
+ guid: '999',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '3f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '3000'},
+ size: {type: 'scalar', units: 'bytes', value: '4000'}
+ }
+ },
+ 'oilpan/heap2/bucket1': {
+ guid: '888',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '1f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '2000'},
+ size: {type: 'scalar', units: 'bytes', value: '2000'}
+ }
+ },
+ 'v8': {
+ guid: '777',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '5f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '5000'},
+ size: {type: 'scalar', units: 'bytes', value: '6000'}
+ }
+ }
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_globalMemoryAllocatorDumps', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const gmd = m.globalMemoryDumps[0];
+ const pmd = p.memoryDumps[0];
+
+ assert.isUndefined(gmd.totals);
+ assert.strictEqual(pmd.totals.residentBytes, 256);
+ assert.isUndefined(pmd.totals.peakResidentBytes);
+ assert.isUndefined(pmd.totals.arePeakResidentBytesResettable);
+ assert.isUndefined(pmd.totals.platformSpecific);
+
+ assert.isUndefined(gmd.mostRecentVmRegions);
+ assert.isUndefined(pmd.mostRecentVmRegions);
+
+ assert.strictEqual(gmd.memoryAllocatorDumps.length, 1);
+ assert.strictEqual(pmd.memoryAllocatorDumps.length, 1);
+
+ // Global memory allocator dumps.
+ const sharedBitmapManager = gmd.getMemoryAllocatorDumpByFullName(
+ 'shared_bitmap_manager');
+ assert.isDefined(sharedBitmapManager);
+ assert.include(gmd.memoryAllocatorDumps, sharedBitmapManager);
+
+ checkDumpNumericsAndDiagnostics(sharedBitmapManager, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
+ 'size': 8192,
+ 'effective_size': 8192,
+ 'inner_size': 8192
+ }, {});
+ assert.lengthOf(sharedBitmapManager.children, 1);
+
+ const bitmap2 = gmd.getMemoryAllocatorDumpByFullName(
+ 'shared_bitmap_manager/bitmap2');
+ assert.isDefined(bitmap2);
+ assert.include(sharedBitmapManager.children, bitmap2);
+ assert.strictEqual(bitmap2.parent, sharedBitmapManager);
+
+ checkDumpNumericsAndDiagnostics(bitmap2, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
+ 'size': 8192,
+ 'effective_size': 8192,
+ 'inner_size': 8192
+ }, { 'weather': 'sunny' });
+ assert.lengthOf(bitmap2.children, 0);
+
+ assert.isUndefined(gmd.getMemoryAllocatorDumpByFullName('tile_manager'));
+ assert.isUndefined(
+ gmd.getMemoryAllocatorDumpByFullName('tile_manager/tile1'));
+
+ // Process memory allocator dumps.
+ const tileManagerRoot = pmd.getMemoryAllocatorDumpByFullName(
+ 'tile_manager');
+ assert.isDefined(tileManagerRoot);
+ assert.include(pmd.memoryAllocatorDumps, tileManagerRoot);
+ assert.isUndefined(tileManagerRoot.parent);
+
+ checkDumpNumericsAndDiagnostics(tileManagerRoot, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 63),
+ 'size': 16384,
+ 'effective_size': 16384,
+ 'inner_size': 12288
+ }, {});
+ assert.lengthOf(tileManagerRoot.children, 1);
+
+ const tile1 = pmd.getMemoryAllocatorDumpByFullName(
+ 'tile_manager/tile1');
+ assert.isDefined(tile1);
+ assert.include(tileManagerRoot.children, tile1);
+ assert.strictEqual(tile1.parent, tileManagerRoot);
+
+ checkDumpNumericsAndDiagnostics(tile1, {
+ 'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 63),
+ 'size': 16384,
+ 'effective_size': 16384,
+ 'inner_size': 12288
+ }, { 'weather': 'rainy' });
+ assert.lengthOf(tile1.children, 0);
+
+ assert.isUndefined(
+ pmd.getMemoryAllocatorDumpByFullName('shared_bitmap_manager'));
+ assert.isUndefined(
+ pmd.getMemoryAllocatorDumpByFullName(
+ 'shared_bitmap_manager/bitmap2'));
+ }
+
+ const events = [
+ {
+ name: 'a',
+ pid: 42,
+ ts: 10,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '100'
+ },
+ allocators: {
+ 'tile_manager/tile1': {
+ guid: '21',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '3f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '3000'},
+ size: {type: 'scalar', units: 'bytes', value: '4000'},
+ weather: {type: 'string', units: '', value: 'rainy'}
+ }
+ },
+ 'global/shared_bitmap_manager/bitmap2': {
+ guid: '42',
+ attrs: {
+ objects_count: {
+ type: 'scalar', units: 'objects', value: '1f'
+ },
+ inner_size: {type: 'scalar', units: 'bytes', value: '2000'},
+ size: {type: 'scalar', units: 'bytes', value: '2000'},
+ weather: {type: 'string', units: '', value: 'sunny'}
+ }
+ }
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_memoryAllocatorDumpEdges', function() {
+ function checkModel(model) {
+ const browserProcess = model.getProcess(42);
+ const rendererProcess = model.getProcess(43);
+ const gpuProcess = model.getProcess(44);
+
+ assert.lengthOf(model.globalMemoryDumps, 1);
+ assert.lengthOf(browserProcess.memoryDumps, 1);
+ assert.lengthOf(rendererProcess.memoryDumps, 1);
+ assert.lengthOf(gpuProcess.memoryDumps, 1);
+
+ const globalDump = model.globalMemoryDumps[0];
+ const browserDump = browserProcess.memoryDumps[0];
+ const rendererDump = rendererProcess.memoryDumps[0];
+ const gpuDump = gpuProcess.memoryDumps[0];
+
+ // Global memory allocator dump.
+ assert.lengthOf(globalDump.memoryAllocatorDumps, 1);
+
+ const globalDumpShared = globalDump.getMemoryAllocatorDumpByFullName(
+ 'shared');
+ assert.isDefined(globalDumpShared);
+ assert.include(globalDump.memoryAllocatorDumps, globalDumpShared);
+ checkDumpNumericsAndDiagnostics(globalDumpShared, {
+ 'area': new Scalar(unitlessNumber_smallerIsBetter, 9)
+ }, { 'color': 'blue' });
+ assert.lengthOf(globalDumpShared.children, 0);
+ assert.isUndefined(globalDumpShared.parent);
+
+ assert.isUndefined(globalDumpShared.owns);
+ assert.lengthOf(globalDumpShared.ownedBy, 3);
+ assert.lengthOf(globalDumpShared.retains, 0);
+ assert.lengthOf(globalDumpShared.retainedBy, 0);
+
+ // Browser memory allocator dump.
+ assert.lengthOf(browserDump.memoryAllocatorDumps, 1);
+
+ const browserDumpLocal = browserDump.getMemoryAllocatorDumpByFullName(
+ 'local');
+ assert.isDefined(browserDumpLocal);
+ assert.include(browserDump.memoryAllocatorDumps, browserDumpLocal);
+ checkDumpNumericsAndDiagnostics(browserDumpLocal, {
+ 'area': new Scalar(unitlessNumber_smallerIsBetter, 9)
+ }, { 'color': 'blue', 'mood': 'very good' });
+ assert.lengthOf(browserDumpLocal.children, 0);
+ assert.isUndefined(browserDumpLocal.parent);
+
+ assert.isDefined(browserDumpLocal.owns);
+ assert.lengthOf(browserDumpLocal.ownedBy, 0);
+ assert.lengthOf(browserDumpLocal.retains, 0);
+ assert.lengthOf(browserDumpLocal.retainedBy, 0);
+
+ const browserDumpLocalOwnsLink = browserDumpLocal.owns;
+ assert.include(globalDumpShared.ownedBy, browserDumpLocalOwnsLink);
+ assert.strictEqual(browserDumpLocalOwnsLink.source, browserDumpLocal);
+ assert.strictEqual(browserDumpLocalOwnsLink.target, globalDumpShared);
+ assert.strictEqual(browserDumpLocalOwnsLink.importance, 0);
+
+ // Renderer memory allocator dump.
+ assert.lengthOf(rendererDump.memoryAllocatorDumps, 1);
+
+ const rendererDumpLocal = rendererDump.getMemoryAllocatorDumpByFullName(
+ 'local');
+ assert.isDefined(rendererDumpLocal);
+ assert.include(rendererDump.memoryAllocatorDumps, rendererDumpLocal);
+ checkDumpNumericsAndDiagnostics(rendererDumpLocal, {
+ 'area': new Scalar(unitlessNumber_smallerIsBetter, 9),
+ 'length': 3
+ }, { 'color': 'blue' });
+ assert.lengthOf(rendererDumpLocal.children, 0);
+ assert.isUndefined(rendererDumpLocal.parent);
+
+ assert.isDefined(rendererDumpLocal.owns);
+ assert.lengthOf(rendererDumpLocal.ownedBy, 0);
+ assert.lengthOf(rendererDumpLocal.retains, 0);
+ assert.lengthOf(rendererDumpLocal.retainedBy, 1);
+
+ const rendererDumpLocalOwnsLink = rendererDumpLocal.owns;
+ assert.include(globalDumpShared.ownedBy, rendererDumpLocalOwnsLink);
+ assert.strictEqual(rendererDumpLocalOwnsLink.source, rendererDumpLocal);
+ assert.strictEqual(rendererDumpLocalOwnsLink.target, globalDumpShared);
+ assert.strictEqual(rendererDumpLocalOwnsLink.importance, 1);
+
+ // GPU memory allocator dumps.
+ assert.lengthOf(gpuDump.memoryAllocatorDumps, 2);
+
+ const gpuDumpLocal1 = gpuDump.getMemoryAllocatorDumpByFullName('local1');
+ assert.isDefined(gpuDumpLocal1);
+ assert.include(gpuDump.memoryAllocatorDumps, gpuDumpLocal1);
+ checkDumpNumericsAndDiagnostics(gpuDumpLocal1, {
+ 'area': new Scalar(unitlessNumber_smallerIsBetter, 9)
+ }, { 'state': 'ON', 'color': 'blue' });
+ assert.lengthOf(gpuDumpLocal1.children, 0);
+ assert.isUndefined(gpuDumpLocal1.parent);
+
+ assert.isDefined(gpuDumpLocal1.owns);
+ assert.lengthOf(gpuDumpLocal1.ownedBy, 1);
+ assert.lengthOf(gpuDumpLocal1.retains, 1);
+ assert.lengthOf(gpuDumpLocal1.retainedBy, 0);
+
+ const gpuDumpLocal1OwnsLink = gpuDumpLocal1.owns;
+ assert.include(globalDumpShared.ownedBy, gpuDumpLocal1OwnsLink);
+ assert.strictEqual(gpuDumpLocal1OwnsLink.source, gpuDumpLocal1);
+ assert.strictEqual(gpuDumpLocal1OwnsLink.target, globalDumpShared);
+ assert.strictEqual(gpuDumpLocal1OwnsLink.importance, -1);
+
+ const gpuDumpLocal1RetainsLink = gpuDumpLocal1.retains[0];
+ assert.include(rendererDumpLocal.retainedBy, gpuDumpLocal1RetainsLink);
+ assert.strictEqual(gpuDumpLocal1RetainsLink.source, gpuDumpLocal1);
+ assert.strictEqual(gpuDumpLocal1RetainsLink.target, rendererDumpLocal);
+ assert.isUndefined(gpuDumpLocal1RetainsLink.importance);
+
+ const gpuDumpLocal2 = gpuDump.getMemoryAllocatorDumpByFullName('local2');
+ assert.isDefined(gpuDumpLocal2);
+ assert.include(gpuDump.memoryAllocatorDumps, gpuDumpLocal2);
+ checkDumpNumericsAndDiagnostics(gpuDumpLocal2, {
+ 'temperature': new Scalar(unitlessNumber_smallerIsBetter, 100)
+ }, {});
+ assert.lengthOf(gpuDumpLocal2.children, 0);
+ assert.isUndefined(gpuDumpLocal2.parent);
+
+ assert.isDefined(gpuDumpLocal2.owns);
+ assert.lengthOf(gpuDumpLocal2.ownedBy, 0);
+ assert.lengthOf(gpuDumpLocal2.retains, 0);
+ assert.lengthOf(gpuDumpLocal2.retainedBy, 0);
+
+ const gpuDumpLocal2OwnsLink = gpuDumpLocal2.owns;
+ assert.include(gpuDumpLocal1.ownedBy, gpuDumpLocal2OwnsLink);
+ assert.strictEqual(gpuDumpLocal2OwnsLink.source, gpuDumpLocal2);
+ assert.strictEqual(gpuDumpLocal2OwnsLink.target, gpuDumpLocal1);
+ assert.strictEqual(gpuDumpLocal2OwnsLink.importance, 1);
+ }
+
+ const events = [
+ {
+ name: 'browser',
+ pid: 42,
+ ts: 10,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '100'
+ },
+ allocators: {
+ 'local': {
+ guid: '3',
+ attrs: {
+ mood: {type: 'string', units: '', value: 'very good'}
+ }
+ },
+ 'global/shared': {
+ guid: '7',
+ attrs: {
+ color: {type: 'string', units: '', value: 'blue'}
+ }
+ }
+ },
+ allocators_graph: [
+ {
+ source: '3',
+ target: '7',
+ type: 'ownership',
+ importance: 0
+ }
+ ]
+ }
+ }
+ },
+ {
+ name: 'renderer',
+ pid: 43,
+ ts: 11,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '200'
+ },
+ allocators: {
+ 'local': {
+ guid: '4',
+ attrs: {
+ length: {type: 'scalar', units: 'bytes', value: '3'}
+ }
+ },
+ 'global/shared': {
+ guid: '7',
+ attrs: {
+ area: {type: 'scalar', units: 'sq ft', value: '9'}
+ }
+ }
+ },
+ allocators_graph: [
+ {
+ source: '4',
+ target: '7',
+ type: 'ownership',
+ importance: 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ name: 'gpu',
+ pid: 44,
+ ts: 10.5,
+ cat: 'test',
+ tid: 53,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '300'
+ },
+ allocators: {
+ 'local1': {
+ guid: '5',
+ attrs: {
+ state: {type: 'string', units: '', value: 'ON'}
+ }
+ },
+ 'local2': {
+ guid: '6',
+ attrs: {
+ temperature: {type: 'scalar', units: 'C', value: '64'}
+ }
+ }
+ },
+ allocators_graph: [
+ {
+ source: '5',
+ target: '7',
+ type: 'ownership',
+ importance: -1
+ },
+ {
+ source: '6',
+ target: '5',
+ type: 'ownership',
+ importance: 1
+ },
+ {
+ source: '5',
+ target: '4',
+ type: 'retention'
+ }
+ ]
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_memoryAllocatorDumpsMissingFields', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+
+ assert.strictEqual(d.memoryAllocatorDumps.length, 1);
+ const noCrashRoot = d.getMemoryAllocatorDumpByFullName('no_crash');
+ assert.lengthOf(noCrashRoot.children, 0);
+ checkDumpNumericsAndDiagnostics(noCrashRoot, {}, {});
+ assert.isUndefined(noCrashRoot.parent);
+ assert.isUndefined(noCrashRoot.guid);
+ }
+
+ const events = [
+ {
+ name: 'a',
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ allocators: {
+ 'no_crash': {
+ /* Missing GUID and attributes. */
+ }
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_weakMemoryAllocatorDumps', function() {
+ function checkModel(m) {
+ const p = m.getProcess(42);
+ const d = p.memoryDumps[0];
+ const memoryAllocatorDumps = d.memoryAllocatorDumps;
+ assert.lengthOf(memoryAllocatorDumps, 6);
+
+ function checkDump(dump, expectedFullName, expectedGuid, expectedParent,
+ expectedChildCount, expectedOwnsLink, expectedOwnedByLinkCount) {
+ assert.strictEqual(dump.fullName, expectedFullName);
+ assert.strictEqual(dump.guid, expectedGuid);
+ assert.strictEqual(dump.parent, expectedParent);
+ assert.lengthOf(dump.children, expectedChildCount);
+ assert.strictEqual(dump.owns, expectedOwnsLink);
+ assert.lengthOf(dump.ownedBy, expectedOwnedByLinkCount);
+ assert.strictEqual(
+ d.getMemoryAllocatorDumpByFullName(expectedFullName), dump);
+ }
+
+ function checkOwnsLink(ownerDump, expectedTarget) {
+ assert.strictEqual(ownerDump.owns.source, ownerDump);
+ assert.strictEqual(ownerDump.owns.target, expectedTarget);
+ }
+
+ // Check root_sink/* dumps.
+ const rootSink = d.memoryAllocatorDumps[3];
+ checkDump(rootSink, 'root_sink', '100', undefined, 1, undefined, 2);
+ const childSink = rootSink.children[0];
+ checkDump(childSink, 'root_sink/child_sink', '200', rootSink, 1,
+ undefined, 1);
+ const descendantSink = childSink.children[0];
+ checkDump(descendantSink, 'root_sink/child_sink/descendant_sink', '300',
+ childSink, 0, undefined, 2);
+
+ // Check strong_root/* dumps.
+ const strongRoot = d.memoryAllocatorDumps[4];
+ checkDump(strongRoot, 'strong_root', '4', undefined, 0,
+ rootSink.ownedBy[0], 0);
+
+ // Check inferred_strong_root/* dumps.
+ const inferredStrongRoot = d.memoryAllocatorDumps[0];
+ checkDump(inferredStrongRoot, 'inferred_strong_root', undefined,
+ undefined, 1, undefined, 0);
+ const child2Strong = inferredStrongRoot.children[0];
+ checkDump(child2Strong, 'inferred_strong_root/child2_strong', '9',
+ inferredStrongRoot, 0, childSink.ownedBy[0], 0);
+
+ // Check inferred_strong_root2/* dumps.
+ const inferredStrongRoot2 = d.memoryAllocatorDumps[1];
+ checkDump(inferredStrongRoot2, 'inferred_strong_root2', undefined,
+ undefined, 1, undefined, 0);
+ const inferredStrongChild = inferredStrongRoot2.children[0];
+ checkDump(inferredStrongChild,
+ 'inferred_strong_root2/inferred_strong_child', undefined,
+ inferredStrongRoot2, 2, undefined, 0);
+ const desc1Strong = inferredStrongChild.children[0];
+ checkDump(desc1Strong,
+ 'inferred_strong_root2/inferred_strong_child/desc1_strong', '11',
+ inferredStrongChild, 0, descendantSink.ownedBy[0], 0);
+ const desc3Strong = inferredStrongChild.children[1];
+ checkDump(desc3Strong,
+ 'inferred_strong_root2/inferred_strong_child/desc3_strong', '13',
+ inferredStrongChild, 0, descendantSink.ownedBy[1], 0);
+
+ // Check strong_root2/* dumps.
+ const strongRoot2 = d.memoryAllocatorDumps[5];
+ checkDump(strongRoot2, 'strong_root2', '15', undefined, 0,
+ rootSink.ownedBy[1], 0);
+
+ // Check inferred_strong_root3/* dumps.
+ const inferredStrongRoot3 = d.memoryAllocatorDumps[2];
+ checkDump(inferredStrongRoot3, 'inferred_strong_root3', undefined,
+ undefined, 0, undefined, 0);
+
+ // Check the links.
+ checkOwnsLink(strongRoot, rootSink);
+ checkOwnsLink(child2Strong, childSink);
+ checkOwnsLink(desc1Strong, descendantSink);
+ checkOwnsLink(desc3Strong, descendantSink);
+ checkOwnsLink(strongRoot2, rootSink);
+
+ // Check that the removed weak dumps are not indexed.
+ [
+ 'weak_root',
+ 'weak_root/removed_child',
+ 'weak_root/inferred_removed_child',
+ 'weak_root/inferred_removed_child/removed_descendant',
+ 'strong_root/weak_child',
+ 'strong_root/inferred_weak_child/weak_descendant',
+ 'inferred_weak_root',
+ 'inferred_weak_root/inferred_weak_child',
+ 'inferred_weak_root/inferred_weak_child/weak_descendant',
+ 'inferred_strong_root/child1_weak',
+ 'inferred_strong_root/child3_weak',
+ 'inferred_strong_root2/inferred_strong_child/desc2_weak',
+ 'inferred_strong_root2/weak_child',
+ 'strong_root2/weak_child',
+ 'strong_root2/removed_descendant',
+ 'removed_root',
+ 'removed_root/removed_child',
+ 'inferred_strong_root3/removed_child'
+ ].forEach(function(fullName) {
+ assert.isUndefined(d.getMemoryAllocatorDumpByFullName(fullName));
+ });
+ }
+
+ const events = [
+ {
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ allocators: {
+ // Sinks for ownership edges (to check that the correct ownership
+ // edges are removed).
+ 'root_sink': { guid: '100', attrs: {} },
+ 'root_sink/child_sink': { guid: '200', attrs: {} },
+ 'root_sink/child_sink/descendant_sink': {
+ guid: '300', attrs: {}
+ },
+
+ // Note: 'removed' in the name of a dump means that the dump will
+ // be removed despite being non-weak (strong), e.g. due to one of
+ // its ancestors being weak.
+
+ // All descendants of a weak root dump should be removed.
+ 'weak_root': { guid: '1', attrs: {}, flags: 1 },
+ 'weak_root/removed_child': { guid: '2', attrs: {} },
+ 'weak_root/inferred_removed_child/removed_descendant': {
+ guid: '3', attrs: {}, flags: 0
+ },
+
+ // A strong root should be kept even if all its descendants are
+ // weak.
+ 'strong_root': { guid: '4', attrs: {}, flags: 0 },
+ 'strong_root/weak_child': { guid: '5', attrs: {}, flags: 1 },
+ 'strong_root/inferred_weak_child/weak_descendant': {
+ guid: '6', attrs: {}, flags: 1
+ },
+
+ // All inferred ancestors of a weak descendant should be marked
+ // weak and, consequently, removed (provided that they don't have
+ // any non-weak descendants).
+ 'inferred_weak_root/inferred_weak_child/weak_descendant': {
+ guid: '7', attrs: {}, flags: 1
+ },
+
+ // An inferred dump should be marked non-weak if it has at least
+ // one strong descendant.
+ 'inferred_strong_root/child1_weak': {
+ guid: '8', attrs: {}, flags: 1
+ },
+ 'inferred_strong_root/child2_strong': {
+ guid: '9', attrs: {}
+ },
+ 'inferred_strong_root/child3_weak': {
+ guid: '10', attrs: {}, flags: 1
+ },
+ 'inferred_strong_root2/inferred_strong_child/desc1_strong': {
+ guid: '11', attrs: {}
+ },
+ 'inferred_strong_root2/inferred_strong_child/desc2_weak': {
+ guid: '12', attrs: {}, flags: 1
+ },
+ 'inferred_strong_root2/inferred_strong_child/desc3_strong': {
+ guid: '13', attrs: {}
+ },
+ 'inferred_strong_root2/weak_child': {
+ guid: '14', attrs: {}, flags: 1
+ },
+
+ // A desdendant dump should be removed if it has a weak ancestor.
+ 'strong_root2': { guid: '15', attrs: {} },
+ 'strong_root2/weak_child': { guid: '16', attrs: {}, flags: 1 },
+ 'strong_root2/weak_child/removed_descendant': {
+ guid: '17', attrs: {}
+ },
+
+ // Check that "weakness" also propagates across ownership edges.
+ 'removed_root': { guid: '18', attrs: {} },
+ 'removed_root/removed_child': {
+ guid: '19', attrs: {}
+ },
+ 'inferred_strong_root3/removed_child': {
+ guid: '20', attrs: {}
+ },
+ },
+ allocators_graph: [
+ { source: '1', target: '100', type: 'ownership' },
+ { source: '2', target: '200', type: 'ownership' },
+ { source: '3', target: '300', type: 'ownership' },
+
+ { source: '4', target: '100', type: 'ownership' }, // Kept.
+ { source: '5', target: '200', type: 'ownership' },
+ { source: '6', target: '300', type: 'ownership' },
+
+ { source: '7', target: '300', type: 'ownership' },
+
+ { source: '8', target: '200', type: 'ownership' },
+ { source: '9', target: '200', type: 'ownership' }, // Kept.
+ { source: '10', target: '200', type: 'ownership' },
+ { source: '11', target: '300', type: 'ownership' }, // Kept.
+ { source: '12', target: '300', type: 'ownership' },
+ { source: '13', target: '300', type: 'ownership' }, // Kept.
+ { source: '14', target: '200', type: 'ownership' },
+
+ { source: '15', target: '100', type: 'ownership' }, // Kept.
+ { source: '16', target: '200', type: 'ownership' },
+ { source: '17', target: '300', type: 'ownership' },
+
+ { source: '18', target: '3' /* not a sink */, type: 'ownership' },
+ { source: '19', target: '200', type: 'ownership' },
+ { source: '20', target: '19' /* not a sink */, type: 'ownership' }
+ ]
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_levelsOfDetail', function() {
+ function checkLevelsOfDetail(pmdSpecifications, expectedGlobalLevelOfDetail,
+ expectedProcessLevelsOfDetail, expectedHasWarnings) {
+ function checkModel(model) {
+ // Check GlobalMemoryDump level of detail.
+ assert.lengthOf(model.globalMemoryDumps, 1);
+ assert.strictEqual(model.globalMemoryDumps[0].levelOfDetail,
+ expectedGlobalLevelOfDetail);
+
+ // Check ProcessMemoryDumps levels of detail.
+ assert.lengthOf(Object.keys(model.processes),
+ expectedProcessLevelsOfDetail.length);
+ for (let i = 0; i < expectedProcessLevelsOfDetail.length; i++) {
+ const process = model.getProcess(i);
+ assert.lengthOf(process.memoryDumps, 1);
+ assert.strictEqual(process.memoryDumps[0].levelOfDetail,
+ expectedProcessLevelsOfDetail[i]);
+ }
+
+ assert.strictEqual(model.hasImportWarnings, expectedHasWarnings);
+ }
+
+ const events = [];
+ pmdSpecifications.forEach(function(singlePmdSpecifications, pid) {
+ singlePmdSpecifications.forEach(function(singlePmdSpecification) {
+ const dumps = {};
+ if (singlePmdSpecification.levelOfDetail !== undefined) {
+ dumps.level_of_detail = singlePmdSpecification.levelOfDetail;
+ }
+ if (singlePmdSpecification.vmRegions) {
+ dumps.process_mmaps = {
+ vm_regions: [
+ { sa: 'f0', sz: '150', pf: 6, mf: '[stack]', bs: { pss: 'ff'} }
+ ]
+ };
+ }
+ events.push({
+ name: 'process_' + pid,
+ pid,
+ ts: 10,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps
+ }
+ });
+ });
+ });
+ checkParsedAndStreamInput(events, checkModel);
+ }
+
+ // Legacy trace events (without levels of detail).
+ checkLevelsOfDetail([[{}]], LIGHT, [LIGHT], false);
+ checkLevelsOfDetail([[{ vmRegions: true }]], DETAILED, [DETAILED], false);
+ checkLevelsOfDetail(
+ [
+ [{}] /* raw composable PMD1 events */,
+ [{}, {}] /* raw composable PMD2 events */
+ ],
+ LIGHT /* expected GMD level of detail */,
+ [LIGHT, LIGHT] /* expected PMD levels of detail */,
+ false /* no warnings expected */);
+ checkLevelsOfDetail(
+ [
+ [{}, { vmRegions: true }, {}],
+ [{ vmRegions: true }, {}]
+ ],
+ DETAILED, [DETAILED, DETAILED], false);
+
+ // Well-formed traces events (VM regions should be irrelevant).
+ checkLevelsOfDetail(
+ [
+ [{ levelOfDetail: 'light'}],
+ [{ levelOfDetail: 'light', vmRegions: true }]
+ ],
+ LIGHT, [LIGHT, LIGHT], false);
+ checkLevelsOfDetail(
+ [
+ [
+ { levelOfDetail: 'detailed' }, { levelOfDetail: 'detailed' }
+ ],
+ [
+ { levelOfDetail: 'detailed', vmRegions: true }
+ ],
+ [
+ { levelOfDetail: 'detailed' },
+ { levelOfDetail: 'detailed', vmRegions: true },
+ { levelOfDetail: 'detailed' }
+ ]
+ ],
+ DETAILED, [DETAILED, DETAILED, DETAILED], false);
+
+ // Not so well-formed trace events.
+ checkLevelsOfDetail(
+ [
+ [{}, { levelOfDetail: 'detailed'}, {}]
+ ],
+ DETAILED, [DETAILED], true);
+ checkLevelsOfDetail(
+ [
+ [{ levelOfDetail: 'light' }],
+ [{}],
+ [{ levelOfDetail: 'detailed' }],
+ [{ levelOfDetail: 'light' }]],
+ DETAILED, [LIGHT, LIGHT, DETAILED, LIGHT], true);
+ checkLevelsOfDetail(
+ [
+ [{ levelOfDetail: 'light' }, { levelOfDetail: 'detailed' }],
+ [{}],
+ [{ levelOfDetail: 'light' }, {}]],
+ DETAILED, [DETAILED, LIGHT, LIGHT], true);
+ checkLevelsOfDetail(
+ [
+ [{ levelOfDetail: 'invalid' }, { levelOfDetail: 'light' }],
+ [{ levelOfDetail: 'invalid' }]],
+ LIGHT, [LIGHT, LIGHT], true);
+ });
+
+ test('importMemoryDumps_heapDumps_oldFormat', function() {
+ function checkModel(m) {
+ const p1 = m.getProcess(21);
+ const p2 = m.getProcess(42);
+ assert.lengthOf(m.globalMemoryDumps, 2);
+ assert.lengthOf(p1.memoryDumps, 2);
+ assert.lengthOf(p2.memoryDumps, 1);
+
+ // Stack frames.
+ const frameIdToTitle = {};
+ for (const [id, f] of Object.entries(m.stackFrames)) {
+ frameIdToTitle[id] = f.title;
+ }
+ assert.deepEqual(
+ frameIdToTitle,
+ {
+ 'p21:0': 'FrameView::layout',
+ 'p21:0:self': '<self>',
+ 'p21:1': 'MessageLoop::RunTask',
+ 'p42::self': '<self>',
+ 'p42:0': 'MessageLoop::RunTask',
+ 'p42:0:self': '<self>',
+ 'p42:1': 'TimerBase::run',
+ 'p42:TWO': 'ScheduledAction::execute',
+ 'p42:3': 'FunctionCall',
+ 'p42:3:self': '<self>',
+ 'p42:4': 'UpdateLayoutTree',
+ 'p42:4:self': '<self>',
+ 'p42:5': 'MessageLoop::JogTask',
+ 'p42:5:self': '<self>'
+ });
+
+ // 1. Process 21, first dump.
+ const pmd1 = p1.memoryDumps[0];
+ const hds1 = pmd1.heapDumps;
+ assert.sameMembers(Object.keys(hds1), ['partition_alloc']);
+
+ const partitionAllocDump1 = hds1.partition_alloc;
+ assert.strictEqual(partitionAllocDump1.processMemoryDump, pmd1);
+ assert.strictEqual(partitionAllocDump1.allocatorName, 'partition_alloc');
+ const partitionAllocEntries1 = partitionAllocDump1.entries;
+ assert.lengthOf(partitionAllocEntries1, 2);
+ checkHeapEntry(partitionAllocEntries1[0], partitionAllocDump1, 4096,
+ undefined /* root */, undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries1[1], partitionAllocDump1, 2748,
+ ['<self>', 'FrameView::layout', 'MessageLoop::RunTask']);
+
+ // 2. Process 21, second dump.
+ const pmd2 = p1.memoryDumps[1];
+ const hds2 = pmd2.heapDumps;
+ assert.sameMembers(Object.keys(hds2), ['partition_alloc']);
+
+ const partitionAllocDump2 = hds2.partition_alloc;
+ assert.strictEqual(partitionAllocDump2.processMemoryDump, pmd2);
+ assert.strictEqual(partitionAllocDump2.allocatorName, 'partition_alloc');
+ const partitionAllocEntries2 = partitionAllocDump2.entries;
+ assert.lengthOf(partitionAllocEntries2, 2);
+ checkHeapEntry(partitionAllocEntries2[0], partitionAllocDump2, 8192,
+ undefined /* root */, undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries2[1], partitionAllocDump2, 3567,
+ ['<self>', 'FrameView::layout', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */);
+
+ // All heap dumps in Process 21 should use the same stack frames.
+ assert.strictEqual(
+ getFrame(partitionAllocEntries1[1], 0),
+ getFrame(partitionAllocEntries2[1], 0));
+
+ // 3. Process 42.
+ const pmd3 = p2.memoryDumps[0];
+ const hds3 = pmd3.heapDumps;
+ assert.sameMembers(Object.keys(hds3), ['partition_alloc', 'malloc']);
+
+ const partitionAllocDump3 = hds3.partition_alloc;
+ assert.strictEqual(partitionAllocDump3.processMemoryDump, pmd3);
+ assert.strictEqual(partitionAllocDump3.allocatorName, 'partition_alloc');
+ const partitionAllocEntries3 = partitionAllocDump3.entries;
+ assert.lengthOf(partitionAllocEntries3, 6);
+ checkHeapEntry(partitionAllocEntries3[0], partitionAllocDump3, 3043272,
+ undefined /* root */, 'blink::Event');
+ checkHeapEntry(partitionAllocEntries3[1], partitionAllocDump3, 6086545,
+ undefined /* root */, undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries3[2], partitionAllocDump3, 1521636,
+ undefined /* root */, 'blink::ContextLifecycleObserver*');
+ checkHeapEntry(partitionAllocEntries3[3], partitionAllocDump3, 5991638,
+ ['<self>'], undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries3[4], partitionAllocDump3, 6384,
+ ['<self>', 'UpdateLayoutTree', 'TimerBase::run',
+ 'MessageLoop::RunTask'], undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries3[5], partitionAllocDump3, 58280,
+ ['<self>', 'FunctionCall', 'ScheduledAction::execute',
+ 'TimerBase::run', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */);
+
+ const mallocDump3 = hds3.malloc;
+ assert.strictEqual(mallocDump3.processMemoryDump, pmd3);
+ assert.strictEqual(mallocDump3.allocatorName, 'malloc');
+ const mallocEntries3 = mallocDump3.entries;
+ assert.lengthOf(mallocEntries3, 4);
+ checkHeapEntry(mallocEntries3[0], mallocDump3, 1929, undefined /* root */,
+ undefined /* sum over all types */);
+ checkHeapEntry(mallocEntries3[1], mallocDump3, 291,
+ ['<self>', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */);
+ checkHeapEntry(mallocEntries3[2], mallocDump3, 1110,
+ ['<self>', 'MessageLoop::JogTask'],
+ undefined /* sum over all types */);
+ checkHeapEntry(mallocEntries3[3], mallocDump3, 205, undefined /* root */,
+ 'blink::ContextLifecycleObserver*');
+
+ // All heap dumps in Process 42 should use the same stack frames.
+ assert.strictEqual(
+ getFrame(partitionAllocEntries3[5], 3),
+ getFrame(partitionAllocEntries3[4], 2));
+ assert.strictEqual(
+ getFrame(mallocEntries3[1], 1),
+ getFrame(partitionAllocEntries3[4], 3));
+ }
+
+ const events = [ // Intentionally shuffled.
+ {
+ pid: 21,
+ ts: 9,
+ ph: 'v',
+ id: '0123',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ heaps: {
+ partition_alloc: {
+ entries: [
+ { size: '1000' },
+ { bt: '0', size: 'abc' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ pid: 42,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {
+ '0': { name: 'MessageLoop::RunTask' },
+ '1': { name: 'TimerBase::run', parent: '0' },
+ 'TWO': { name: 'ScheduledAction::execute', parent: '1' },
+ '3': { name: 'FunctionCall', parent: 'TWO' },
+ '4': { name: 'UpdateLayoutTree', parent: '1' },
+ '5': { name: 'MessageLoop::JogTask' }
+ }
+ }
+ },
+ {
+ pid: 42,
+ ph: 'M',
+ name: 'typeNames',
+ cat: 'memory-infra',
+ args: {
+ typeNames: {
+ // GCC.
+ '22': '[unknown]',
+ '23': 'testing::ManuallyAnnotatedMockClass',
+ '24': 'const char* WTF::getStringWithTypeName() [with T = ' +
+ 'blink::Event]',
+ '25': 'blink::ContextLifecycleObserver*',
+ '26': 'const char* WTF::getStringWithTypeName() [with T = ' +
+ 'blink::WebFrame*]'
+ }
+ }
+ },
+ {
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0123',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '0'
+ },
+ heaps: {
+ partition_alloc: {
+ entries: [
+ { type: '24', size: '2e6fc8' },
+ { size: '5cdf91' },
+ { type: '25', size: '1737e4' },
+ { bt: '', size: '5b6cd6' },
+ { bt: '4', size: '18f0' },
+ { bt: '3', size: 'e3a8' }
+ ]
+ },
+ malloc: {
+ entries: [
+ { size: '789' },
+ { bt: '0', size: '123' },
+ { bt: '5', size: '456' },
+ { type: '25', size: 'cd' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ pid: 21,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {
+ // Intentionally in reverse order.
+ '0': { name: 'FrameView::layout', parent: '1' },
+ '1': { name: 'MessageLoop::RunTask' }
+ }
+ }
+ },
+ {
+ pid: 21,
+ ts: 12,
+ ph: 'v',
+ id: '0987',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ heaps: {
+ partition_alloc: {
+ entries: [
+ { size: '2000' },
+ { bt: '0', size: 'def' }
+ ]
+ }
+ }
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('importMemoryDumps_heapDumps_newFormat', function() {
+ function checkModel(m) {
+ const p1 = m.getProcess(21);
+ const p2 = m.getProcess(42);
+ const p3 = m.getProcess(63);
+ const p4 = m.getProcess(84);
+ assert.lengthOf(m.globalMemoryDumps, 2);
+ assert.lengthOf(p1.memoryDumps, 2);
+ assert.lengthOf(p2.memoryDumps, 1);
+ assert.lengthOf(p3.memoryDumps, 1);
+ assert.lengthOf(p4.memoryDumps, 1);
+
+ // Stack frames.
+ const frameIdToTitle = {};
+ for (const [id, f] of Object.entries(m.stackFrames)) {
+ frameIdToTitle[id] = f.title;
+ }
+ assert.deepEqual(
+ frameIdToTitle,
+ {
+ 'p21:0': 'FrameView::layout',
+ 'p21:A': '<self>',
+ 'p21:1': 'MessageLoop::RunTask',
+ 'p42:-1': '<self>',
+ 'p42:0': 'MessageLoop::RunTask',
+ 'p42:0.5': '<self>',
+ 'p42:1': 'TimerBase::run',
+ 'p42:TWO': 'ScheduledAction::execute',
+ 'p42:2.72': '<self>',
+ 'p42:3': 'FunctionCall',
+ 'p42:\u03C0': '<self>',
+ 'p42:4': 'UpdateLayoutTree',
+ 'p42:FOUR-AND-A-BIT': '<self>',
+ 'p42:5': 'MessageLoop::JogTask',
+ 'p42:NaN': '<self>',
+ 'p84:5': 'MessageLoop::WalkTask'
+ });
+
+ // 1. Process 21, first dump.
+ const pmd1 = p1.memoryDumps[0];
+ const hds1 = pmd1.heapDumps;
+ assert.sameMembers(Object.keys(hds1), ['partition_alloc']);
+
+ const partitionAllocDump1 = hds1.partition_alloc;
+ assert.strictEqual(partitionAllocDump1.processMemoryDump, pmd1);
+ assert.strictEqual(partitionAllocDump1.allocatorName, 'partition_alloc');
+ const partitionAllocEntries1 = partitionAllocDump1.entries;
+ assert.lengthOf(partitionAllocEntries1, 2);
+ checkHeapEntry(partitionAllocEntries1[0], partitionAllocDump1, 4096,
+ undefined /* root */,
+ 'class v8::FunctionCallbackInfo<class v8::Value>');
+ checkHeapEntry(partitionAllocEntries1[1], partitionAllocDump1, 2748,
+ ['<self>', 'FrameView::layout', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */);
+
+ // 2. Process 21, second dump.
+ const pmd2 = p1.memoryDumps[1];
+ const hds2 = pmd2.heapDumps;
+ assert.sameMembers(Object.keys(hds2), ['partition_alloc']);
+
+ const partitionAllocDump2 = hds2.partition_alloc;
+ assert.strictEqual(partitionAllocDump2.processMemoryDump, pmd2);
+ assert.strictEqual(partitionAllocDump2.allocatorName, 'partition_alloc');
+ const partitionAllocEntries2 = partitionAllocDump2.entries;
+ assert.lengthOf(partitionAllocEntries2, 3);
+ checkHeapEntry(partitionAllocEntries2[0], partitionAllocDump2, 8192,
+ undefined /* root */, undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries2[1], partitionAllocDump2, 3567,
+ ['<self>', 'FrameView::layout', 'MessageLoop::RunTask'],
+ 'class v8::FunctionCallbackInfo<class v8::Value>');
+ checkHeapEntry(partitionAllocEntries2[2], partitionAllocDump2, 4095,
+ ['FrameView::layout', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */);
+
+ // All heap dumps in Process 21 should use the same stack frames.
+ assert.strictEqual(
+ getFrame(partitionAllocEntries1[1], 0),
+ getFrame(partitionAllocEntries2[1], 0));
+ assert.strictEqual(
+ getFrame(partitionAllocEntries2[2], 0),
+ getFrame(partitionAllocEntries2[1], 1));
+
+ // 3. Process 42.
+ const pmd3 = p2.memoryDumps[0];
+ const hds3 = pmd3.heapDumps;
+ assert.sameMembers(Object.keys(hds3), ['partition_alloc', 'malloc']);
+
+ const partitionAllocDump3 = hds3.partition_alloc;
+ assert.strictEqual(partitionAllocDump3.processMemoryDump, pmd3);
+ assert.strictEqual(partitionAllocDump3.allocatorName, 'partition_alloc');
+ const partitionAllocEntries3 = partitionAllocDump3.entries;
+ assert.lengthOf(partitionAllocEntries3, 7);
+ checkHeapEntry(partitionAllocEntries3[0], partitionAllocDump3, 6086545,
+ undefined /* root */, undefined /* sum over all types */);
+ checkHeapEntry(partitionAllocEntries3[1], partitionAllocDump3, 3043272,
+ undefined /* root */, 'blink::Event');
+ checkHeapEntry(partitionAllocEntries3[2], partitionAllocDump3, 1521636,
+ undefined /* root */, 'blink::ContextLifecycleObserver *');
+ checkHeapEntry(partitionAllocEntries3[3], partitionAllocDump3, 5991638,
+ ['<self>'], '[unknown]');
+ checkHeapEntry(partitionAllocEntries3[4], partitionAllocDump3, 6384,
+ ['<self>', 'UpdateLayoutTree', 'TimerBase::run',
+ 'MessageLoop::RunTask'], undefined /* sum over all types */, 256);
+ checkHeapEntry(partitionAllocEntries3[5], partitionAllocDump3, 3192,
+ ['<self>', 'UpdateLayoutTree', 'TimerBase::run',
+ 'MessageLoop::RunTask'], 'blink::WebFrame *');
+ checkHeapEntry(partitionAllocEntries3[6], partitionAllocDump3, 58280,
+ ['<self>', 'FunctionCall', 'ScheduledAction::execute',
+ 'TimerBase::run', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */);
+
+ const mallocDump3 = hds3.malloc;
+ assert.strictEqual(mallocDump3.processMemoryDump, pmd3);
+ assert.strictEqual(mallocDump3.allocatorName, 'malloc');
+ const mallocEntries3 = mallocDump3.entries;
+ assert.lengthOf(mallocEntries3, 4);
+ checkHeapEntry(mallocEntries3[0], mallocDump3, 1929, undefined /* root */,
+ undefined /* sum over all types */, 80);
+ checkHeapEntry(mallocEntries3[1], mallocDump3, 291,
+ ['<self>', 'MessageLoop::RunTask'],
+ undefined /* sum over all types */, 96);
+ checkHeapEntry(mallocEntries3[2], mallocDump3, 1110,
+ ['<self>', 'MessageLoop::JogTask'],
+ undefined /* sum over all types */, 112);
+ checkHeapEntry(mallocEntries3[3], mallocDump3, 205,
+ ['FunctionCall', 'ScheduledAction::execute', 'TimerBase::run',
+ 'MessageLoop::RunTask'], 'blink::ContextLifecycleObserver *', 128);
+
+ // All heap dumps in Process 42 should use the same stack frames.
+ assert.strictEqual(
+ getFrame(partitionAllocEntries3[5], 0),
+ getFrame(partitionAllocEntries3[4], 0));
+ assert.strictEqual(
+ getFrame(partitionAllocEntries3[6], 3),
+ getFrame(partitionAllocEntries3[4], 2));
+ assert.strictEqual(
+ getFrame(mallocEntries3[1], 1),
+ getFrame(partitionAllocEntries3[4], 3));
+ assert.strictEqual(
+ getFrame(mallocEntries3[3], 0),
+ getFrame(partitionAllocEntries3[6], 1));
+
+ // 4. Process 63.
+ const pmd4 = p3.memoryDumps[0];
+ const hds4 = pmd4.heapDumps;
+ assert.sameMembers(Object.keys(hds4), ['winheap']);
+
+ const winheapDump = hds4.winheap;
+ assert.strictEqual(winheapDump.processMemoryDump, pmd4);
+ assert.strictEqual(winheapDump.allocatorName, 'winheap');
+ const winheapEntries = winheapDump.entries;
+ assert.lengthOf(winheapEntries, 1);
+ checkHeapEntry(winheapEntries[0], winheapDump, 65536,
+ undefined /* root */, undefined /* sum over all types */);
+
+ // 5. Process 84.
+ const pmd5 = p4.memoryDumps[0];
+ const hds5 = pmd5.heapDumps;
+ assert.sameMembers(Object.keys(hds5), ['malloc']);
+
+ const mallocDump4 = hds5.malloc;
+ assert.strictEqual(mallocDump4.processMemoryDump, pmd5);
+ assert.strictEqual(mallocDump4.allocatorName, 'malloc');
+ const mallocEntries4 = mallocDump4.entries;
+ assert.lengthOf(mallocEntries4, 1);
+ checkHeapEntry(mallocEntries4[0], mallocDump4, 43981,
+ ['MessageLoop::WalkTask'], 'content::Manually');
+ }
+
+ const events = [ // Intentionally shuffled.
+ {
+ pid: 21,
+ ts: 9,
+ ph: 'v',
+ id: '0123',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ heaps: {
+ partition_alloc: {
+ entries: [
+ { bt: '', type: '25', size: '1000' },
+ { bt: 'A', size: 'abc' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ pid: 42,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {
+ '-1': { name: '<self>' },
+ '0': { name: 'MessageLoop::RunTask' },
+ '0.5': { name: '<self>', parent: '0' },
+ '1': { name: 'TimerBase::run', parent: '0' },
+ 'TWO': { name: 'ScheduledAction::execute', parent: '1' },
+ '2.72': { name: '<self>', parent: 'TWO' },
+ '3': { name: 'FunctionCall', parent: 'TWO' },
+ '\u03C0': { name: '<self>', parent: '3' },
+ '4': { name: 'UpdateLayoutTree', parent: '1' },
+ 'FOUR-AND-A-BIT': { name: '<self>', parent: '4' },
+ '5': { name: 'MessageLoop::JogTask' },
+ 'NaN': { name: '<self>', parent: '5' }
+ }
+ }
+ },
+ {
+ pid: 42,
+ ph: 'M',
+ name: 'typeNames',
+ cat: 'memory-infra',
+ args: {
+ typeNames: {
+ // Clang.
+ '22': '[unknown]',
+ '23': 'testing::ManuallyAnnotatedMockClass',
+ '24': 'const char *WTF::getStringWithTypeName() [T = ' +
+ 'blink::Event]',
+ '25': 'blink::ContextLifecycleObserver *',
+ '26': 'const char *WTF::getStringWithTypeName() [T = ' +
+ 'blink::WebFrame *]'
+ }
+ }
+ },
+ {
+ pid: 42,
+ ts: 10,
+ ph: 'v',
+ id: '0123',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_totals: {
+ resident_set_bytes: '0'
+ },
+ heaps: {
+ partition_alloc: {
+ entries: [
+ { bt: '' /* root */, size: '5cdf91' },
+ { bt: '' /* root */, type: '24', size: '2e6fc8' },
+ { bt: '' /* root */, type: '25', size: '1737e4' },
+ { bt: '-1', type: '22', size: '5b6cd6' },
+ { bt: 'FOUR-AND-A-BIT', size: '18f0', count: '100' },
+ { bt: 'FOUR-AND-A-BIT', type: '26', size: 'c78' },
+ { bt: '\u03C0', size: 'e3a8' }
+ ]
+ },
+ malloc: {
+ entries: [
+ { bt: '', size: '789', count: '50' },
+ { bt: '0.5', size: '123', count: '60' },
+ { bt: 'NaN', size: '456', count: '70' },
+ { bt: '3', type: '25', size: 'cd', count: '80' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ pid: 21,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {
+ // Intentionally in reverse order.
+ 'A': { name: '<self>', parent: '0' },
+ '0': { name: 'FrameView::layout', parent: '1' },
+ '1': { name: 'MessageLoop::RunTask' }
+ }
+ }
+ },
+ {
+ pid: 21,
+ ts: 12,
+ ph: 'v',
+ id: '0987',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ heaps: {
+ winheap: {
+ entries: [] // Intentionally empty.
+ },
+ partition_alloc: {
+ entries: [
+ { bt: '', size: '2000' },
+ { bt: 'A', type: '25', size: 'def' },
+ { bt: '3' /* invalid */, size: 'aaa' },
+ { bt: 'A', type: '24' /* invalid */, size: 'bbb' },
+ { bt: '0', size: 'fff' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ pid: 21,
+ ph: 'M',
+ name: 'typeNames',
+ cat: 'memory-infra',
+ args: {
+ typeNames: {
+ // Microsoft Visual C++.
+ '25': 'const char *__cdecl WTF::getStringWithTypeName<class ' +
+ 'v8::FunctionCallbackInfo<class v8::Value>>(void)'
+ }
+ }
+ },
+ {
+ pid: 63,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {} // Intentionally empty.
+ }
+ },
+ {
+ pid: 63,
+ ph: 'M',
+ name: 'typeNames',
+ cat: 'memory-infra',
+ args: {
+ typeNames: {} // Intentionally empty.
+ }
+ },
+ {
+ pid: 63,
+ ts: 13,
+ ph: 'v',
+ id: '0987',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ heaps: {
+ winheap: {
+ entries: [
+ { bt: '', size: '10000' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ pid: 84,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {
+ '5': { name: 'MessageLoop::WalkTask' }
+ }
+ }
+ },
+ {
+ pid: 84,
+ ph: 'M',
+ name: 'typeNames',
+ cat: 'memory-infra',
+ args: {
+ typeNames: {
+ '0': '[unknown]',
+ '1': 'base::All',
+ '3': 'content::Manually',
+ '4': 'net::Annotated'
+ }
+ }
+ },
+ {
+ pid: 84,
+ ts: 14,
+ ph: 'v',
+ id: '0987',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ heaps: {
+ malloc: {
+ entries: [
+ { bt: '5', type: '3', size: 'abcd' }
+ ]
+ }
+ }
+ }
+ }
+ }
+ ];
+ // TODO(chiniforooshan): checkParsedAndStreamInput uses JSON.stringify and
+ // then stringToUint8Array for converting events to a trace stream. The
+ // current implementation of stringToUint8Array is not Unicode compatible.
+ const m = makeModel(events);
+ checkModel(m);
+ });
+
+ test('importMemoryDumps_composableDumps', function() {
+ function checkModel(m) {
+ function checkDumpTime(dump, expectedStart, expectedDuration) {
+ assert.closeTo(dump.start, expectedStart / 1000, 1e-5);
+ assert.closeTo(dump.duration, expectedDuration / 1000, 1e-5);
+ }
+
+ function checkLinkCounts(allocatorDump, expectedHasOwns,
+ expectedOwnedByCount, expectedRetainsCount, expectedRetainedByCount) {
+ assert.strictEqual(allocatorDump.owns !== undefined, expectedHasOwns);
+ assert.lengthOf(allocatorDump.ownedBy, expectedOwnedByCount);
+ assert.lengthOf(allocatorDump.retains, expectedRetainsCount);
+ assert.lengthOf(allocatorDump.retainedBy, expectedRetainedByCount);
+ }
+
+ // Check the overall structure of the model and memory dumps.
+ assert.sameMembers(Object.keys(m.processes), ['42', '68']);
+ assert.lengthOf(m.globalMemoryDumps, 2);
+ const gmd1 = m.globalMemoryDumps[0];
+ assert.strictEqual(gmd1.model, m);
+ checkDumpTime(gmd1, 9999, 6);
+ const gmd2 = m.globalMemoryDumps[1];
+ assert.strictEqual(gmd2.model, m);
+ checkDumpTime(gmd2, 10003, 0);
+
+ const p1 = m.getProcess(42);
+ assert.lengthOf(p1.memoryDumps, 2);
+ const pmd1 = p1.memoryDumps[0];
+ checkDumpTime(pmd1, 10000, 5);
+ assert.strictEqual(pmd1.globalMemoryDump, gmd1);
+ assert.strictEqual(pmd1.process, p1);
+ const pmd2 = p1.memoryDumps[1];
+ checkDumpTime(pmd2, 10003, 0);
+ assert.strictEqual(pmd2.globalMemoryDump, gmd2);
+ assert.strictEqual(pmd2.process, p1);
+
+ const p2 = m.getProcess(68);
+ assert.lengthOf(p2.memoryDumps, 1);
+ const pmd3 = p2.memoryDumps[0];
+ checkDumpTime(pmd3, 9999, 0);
+ assert.strictEqual(pmd3.globalMemoryDump, gmd1);
+ assert.strictEqual(pmd3.process, p2);
+
+ assert.deepEqual(gmd1.processMemoryDumps, {42: pmd1, 68: pmd3});
+ assert.deepEqual(gmd2.processMemoryDumps, {42: pmd2});
+
+ // Check the composed dump.
+ assert.strictEqual(pmd1.levelOfDetail, LIGHT);
+
+ const totals = pmd1.totals;
+ assert.strictEqual(totals.residentBytes, 256);
+ assert.isUndefined(totals.peakResidentBytes);
+ assert.isUndefined(totals.arePeakResidentBytesResettable);
+ assert.deepEqual(totals.platformSpecific, {private_bytes: 128});
+
+ const vmRegions = pmd1.vmRegions;
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: '[stack:20310]',
+ startAddress: 240,
+ sizeInBytes: 336,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ proportionalResident: 158
+ }
+ }
+ ]);
+
+ const memoryAllocatorDumps = pmd1.memoryAllocatorDumps;
+ assert.lengthOf(memoryAllocatorDumps, 2);
+
+ const local1Dump = pmd1.getMemoryAllocatorDumpByFullName('local1');
+ assert.strictEqual(memoryAllocatorDumps[0], local1Dump);
+ assert.strictEqual(local1Dump.fullName, 'local1');
+ assert.isUndefined(local1Dump.parent);
+ assert.lengthOf(local1Dump.children, 0);
+ checkDumpNumericsAndDiagnostics(local1Dump, {},
+ { 'A': 'blue', 'B': 'red' });
+ checkLinkCounts(local1Dump, true /* owns */, 0 /* owned by */,
+ 0 /* retains */, 0 /* retained by */);
+
+ const local2Dump = pmd1.getMemoryAllocatorDumpByFullName('local2');
+ assert.strictEqual(memoryAllocatorDumps[1], local2Dump);
+ assert.strictEqual(local2Dump.fullName, 'local2');
+ assert.isUndefined(local2Dump.parent);
+ assert.lengthOf(local2Dump.children, 0);
+ checkDumpNumericsAndDiagnostics(local2Dump, {}, { 'B': 'yellow' });
+ checkLinkCounts(local2Dump, false /* owns */, 0 /* owned by */,
+ 1 /* retains */, 0 /* retained by */);
+
+ const heapDumps = pmd1.heapDumps;
+ assert.sameMembers(Object.keys(heapDumps), ['partition_alloc']);
+ const heapDump = heapDumps.partition_alloc;
+ assert.strictEqual(heapDump.processMemoryDump, pmd1);
+ assert.strictEqual(heapDump.allocatorName, 'partition_alloc');
+ const entries = heapDump.entries;
+ assert.lengthOf(entries, 1);
+ assert.strictEqual(entries[0].heapDump, heapDump);
+ assert.strictEqual(
+ entries[0].leafStackFrame.title, 'MessageLoop::RunTask');
+ assert.strictEqual(entries[0].objectTypeName, 'cc::SurfaceFactory');
+ assert.strictEqual(entries[0].size, 1280);
+
+ // Check the other dumps.
+ assert.strictEqual(pmd2.levelOfDetail, DETAILED);
+ assert.isUndefined(pmd2.vmRegions);
+ assert.lengthOf(pmd2.memoryAllocatorDumps, 1);
+ const otherLocal1Dump = pmd2.getMemoryAllocatorDumpByFullName('local1');
+ assert.strictEqual(otherLocal1Dump, pmd2.memoryAllocatorDumps[0]);
+ assert.strictEqual(otherLocal1Dump.fullName, 'local1');
+ assert.isUndefined(otherLocal1Dump.parent);
+ checkDumpNumericsAndDiagnostics(otherLocal1Dump, { 'A': 2989 }, {});
+ assert.isUndefined(pmd2.heapDumps);
+ checkLinkCounts(otherLocal1Dump, false /* owns */, 1 /* owned by */,
+ 0 /* retains */, 0 /* retained by */);
+
+ assert.strictEqual(pmd3.levelOfDetail, DETAILED);
+ const otherVmRegions = pmd3.vmRegions;
+ checkVMRegions(otherVmRegions, [
+ {
+ mappedFile: '/dev/ashmem/dalvik',
+ startAddress: 848,
+ sizeInBytes: 592,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ privateDirtyResident: 205
+ }
+ }
+ ]);
+ assert.lengthOf(pmd3.memoryAllocatorDumps, 0);
+ assert.isUndefined(pmd3.heapDumps);
+
+ // Check the global dumps.
+ assert.lengthOf(gmd1.memoryAllocatorDumps, 2);
+ const shared1Dump = gmd1.getMemoryAllocatorDumpByFullName('shared1');
+ assert.strictEqual(shared1Dump, gmd1.memoryAllocatorDumps[0]);
+ assert.strictEqual(shared1Dump.fullName, 'shared1');
+ assert.isUndefined(shared1Dump.parent);
+ checkDumpNumericsAndDiagnostics(shared1Dump, {},
+ { 'A': 'purple', 'B': 'green' });
+ checkLinkCounts(shared1Dump, false /* owns */, 1 /* owned by */,
+ 0 /* retains */, 0 /* retained by */);
+ const shared2Dump = gmd1.getMemoryAllocatorDumpByFullName('shared2');
+ assert.strictEqual(shared2Dump, gmd1.memoryAllocatorDumps[1]);
+ assert.strictEqual(shared2Dump.fullName, 'shared2');
+ assert.isUndefined(shared2Dump.parent);
+ checkDumpNumericsAndDiagnostics(shared2Dump, {}, { 'A': 'cyan' });
+ checkLinkCounts(shared2Dump, false /* owns */, 0 /* owned by */,
+ 0 /* retains */, 1 /* retained by */);
+
+ assert.lengthOf(gmd2.memoryAllocatorDumps, 1);
+ const otherShared1Dump = gmd2.getMemoryAllocatorDumpByFullName('shared1');
+ assert.strictEqual(otherShared1Dump, gmd2.memoryAllocatorDumps[0]);
+ assert.strictEqual(otherShared1Dump.fullName, 'shared1');
+ assert.isUndefined(otherShared1Dump.parent);
+ checkDumpNumericsAndDiagnostics(otherShared1Dump, {}, { 'A': 'brown' });
+ checkLinkCounts(otherShared1Dump, true /* owns */, 0 /* owned by */,
+ 0 /* retains */, 0 /* retained by */);
+
+ // Check the edges.
+ const ownershipLink = local1Dump.owns;
+ assert.strictEqual(shared1Dump.ownedBy[0], ownershipLink);
+ assert.strictEqual(ownershipLink.source, local1Dump);
+ assert.strictEqual(ownershipLink.target, shared1Dump);
+ assert.strictEqual(ownershipLink.importance, 1);
+
+ const retentionLink = local2Dump.retains[0];
+ assert.strictEqual(shared2Dump.retainedBy[0], retentionLink);
+ assert.strictEqual(retentionLink.source, local2Dump);
+ assert.strictEqual(retentionLink.target, shared2Dump);
+ assert.isUndefined(retentionLink.importance);
+
+ const otherOwnershipLink = otherShared1Dump.owns;
+ assert.strictEqual(otherLocal1Dump.ownedBy[0], otherOwnershipLink);
+ assert.strictEqual(otherOwnershipLink.source, otherShared1Dump);
+ assert.strictEqual(otherOwnershipLink.target, otherLocal1Dump);
+ assert.isUndefined(otherOwnershipLink.importance);
+ }
+
+ // Split PMD1 over multiple PMD trace events, all of which should be merged
+ // by the importer. Also inject some PMD trace events with different PIDs
+ // or dump IDs, which should *not* be merged with the rest.
+ const events = [
+ { // Heap dumps.
+ pid: 42,
+ ts: 10000,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'light',
+ heaps: {
+ partition_alloc: {
+ entries: [
+ { bt: '99', type: '888', size: '500' }
+ ]
+ }
+ }
+ }
+ }
+ },
+ { // PMD2 with a different dump id (should not be merged).
+ pid: 42,
+ ts: 10003, // Same PID, so it will end up in the same process.
+ ph: 'v',
+ id: '0x0002',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'detailed',
+ allocators: {
+ 'local1': {
+ guid: '3',
+ attrs: {
+ A: {type: 'scalar', units: 'bytes', value: '0xBAD'}
+ }
+ },
+ 'global/shared1': {
+ guid: '4',
+ attrs: {
+ A: {type: 'string', units: '', value: 'brown'}
+ }
+ }
+ },
+ allocators_graph: [
+ {
+ source: '4',
+ target: '3',
+ type: 'ownership'
+ }
+ ]
+ }
+ }
+ },
+ { // Stack frames (required for heap dumps).
+ pid: 42,
+ ph: 'M',
+ name: 'stackFrames',
+ cat: 'memory-infra',
+ args: {
+ stackFrames: {
+ '99': { name: 'MessageLoop::RunTask' }
+ }
+ }
+ },
+ { // Allocator dumps.
+ pid: 42,
+ ts: 10001,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'light',
+ allocators: {
+ 'local1': {
+ guid: '3',
+ attrs: {
+ A: {type: 'string', units: '', value: 'blue'}
+ }
+ },
+ 'global/shared1': {
+ guid: '7',
+ attrs: {
+ A: {type: 'string', units: '', value: 'purple'}
+ }
+ },
+ 'global/shared2': {
+ guid: '8',
+ attrs: {
+ A: {type: 'string', units: '', value: 'cyan'}
+ }
+ }
+ }
+ }
+ }
+ },
+ { // PMD3 with a different PID (should not be merged).
+ pid: 68,
+ ts: 9999,
+ ph: 'v',
+ id: '0x0001', // Same dump ID, so it will end up in the same GMD.
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ process_mmaps: {
+ vm_regions: [
+ {
+ sa: '350',
+ sz: '250',
+ pf: 5,
+ mf: '/dev/ashmem/dalvik',
+ bs: {
+ pd: 'cd'
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ { // VM regions.
+ pid: 42,
+ ts: 10005,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'light',
+ process_mmaps: {
+ vm_regions: [
+ {
+ sa: 'f0',
+ sz: '150',
+ pf: 6,
+ mf: '[stack:20310]',
+ bs: {
+ pss: '9e'
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ { // Totals.
+ pid: 42,
+ ts: 10003,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'light',
+ process_totals: {
+ resident_set_bytes: '100',
+ private_bytes: '80' // OS-specific total.
+ }
+ }
+ }
+ },
+ { // Allocator dump edges.
+ pid: 42,
+ ts: 10004,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'light',
+ allocators_graph: [
+ {
+ source: '4',
+ target: '8',
+ type: 'retention'
+ }
+ ]
+ }
+ }
+ },
+ { // Object type names (required for heap dumps).
+ pid: 42,
+ ph: 'M',
+ name: 'typeNames',
+ cat: 'memory-infra',
+ args: {
+ typeNames: {
+ // GCC.
+ '888': 'const char* WTF::getStringWithTypeName() [with T = ' +
+ 'cc::SurfaceFactory]'
+ }
+ }
+ },
+ { // Allocator dumps and dump edges (should be merged).
+ pid: 42,
+ ts: 10002,
+ ph: 'v',
+ id: '0x0001',
+ cat: 'memory-infra',
+ args: {
+ dumps: {
+ level_of_detail: 'light',
+ allocators: {
+ 'local1': {
+ guid: '3',
+ attrs: {
+ B: {type: 'string', units: '', value: 'red'}
+ }
+ },
+ 'local2': {
+ guid: '4',
+ attrs: {
+ B: {type: 'string', units: '', value: 'yellow'}
+ }
+ },
+ 'global/shared1': {
+ guid: '7',
+ attrs: {
+ B: {type: 'string', units: '', value: 'green'}
+ }
+ }
+ },
+ allocators_graph: [
+ {
+ source: '3',
+ target: '7',
+ type: 'ownership',
+ importance: 1
+ }
+ ]
+ }
+ }
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('importThreadInstantSliceWithStackFrame', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.slices.length, 1);
+
+ const s0 = t.sliceGroup.slices[0];
+ assert.strictEqual(s0.startStackFrame.title, 'frame7');
+ assert.isUndefined(s0.endStackFrame);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'I', s: 't', sf: 7 } // @suppress longLineCheck
+ ],
+ stackFrames: {
+ '1': {
+ category: 'm1',
+ name: 'main'
+ },
+ '7': {
+ category: 'm2',
+ name: 'frame7',
+ parent: '1'
+ }
+ }
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importDurationEventsWithStackFrames', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+ assert.isDefined(t);
+ assert.strictEqual(t.sliceGroup.slices.length, 1);
+
+ const s0 = t.sliceGroup.slices[0];
+ assert.strictEqual(s0.startStackFrame.title, 'frame7');
+ assert.strictEqual(s0.endStackFrame.title, 'frame8');
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
+ ],
+ stackFrames: {
+ '1': {
+ category: 'm1',
+ name: 'main'
+ },
+ '7': {
+ category: 'm2',
+ name: 'frame7',
+ parent: '1'
+ },
+ '8': {
+ category: 'm2',
+ name: 'frame8',
+ parent: '1'
+ }
+ }
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('annotationParsing', function() {
+ const yComponents1 = [{stableId: '52.53', yPercentOffset: 0.5},
+ {stableId: '52', yPercentOffset: 0.3}];
+ const yComponents2 = [{stableId: '52.53', yPercentOffset: 0.7},
+ {stableId: '52', yPercentOffset: 0.4}];
+ const location1 = new tr.model.Location(0.1, yComponents1);
+ const location2 = new tr.model.Location(0.2, yComponents2);
+
+ function checkModel(m) {
+ const annotations = m.getAllAnnotations();
+ assert.strictEqual(annotations.length, 3);
+
+ assert.isTrue(annotations[0] instanceof tr.model.XMarkerAnnotation);
+ assert.strictEqual(annotations[0].timestamp, 12);
+
+ assert.isTrue(annotations[1] instanceof tr.model.RectAnnotation);
+ assert.deepEqual(annotations[1].startLocation, location1);
+ assert.deepEqual(annotations[1].endLocation, location2);
+
+ assert.isTrue(
+ annotations[2] instanceof tr.model.CommentBoxAnnotation);
+ assert.strictEqual(annotations[2].text, 'test');
+ assert.deepEqual(annotations[2].location, location1);
+ }
+
+ const eventData = { traceEvents: [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}],
+ traceAnnotations: [
+ {typeName: 'xmarker', args: {timestamp: 12}},
+ {typeName: 'rect', args: {
+ start: location1.toDict(), end: location2.toDict()}},
+ {typeName: 'comment_box', args: {text: 'test',
+ location: location1.toDict()}}
+ ]};
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importDisplayTimeUnit', function() {
+ function checkModel(m) {
+ assert.strictEqual(m.intrinsicTimeUnit, tr.b.TimeDisplayModes.ns);
+ }
+
+ const eventData = {
+ traceEvents: [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}],
+ displayTimeUnit: 'ns'
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ const m = makeModel(JSON.stringify(eventData));
+ checkModel(m);
+ });
+
+ test('extractBattorSubTraces', function() {
+ const battorLog = '# BattOr\n# voltage range [0.000000, 7196.484161] mV\n' +
+ '#current range [11.898481, 2110.900916] mA\n' +
+ '# sample_rate=10000Hz, gain=30.257143x\n' +
+ '# filpot_pos=3, amppot_pos=35, timer_ovf=399, timer_div=4 ovs_bits=0\n' + // @suppress longLineCheck
+ '0.0 1040.3 3984.2\n' +
+ '0.1 1081.3 3987.8\n' +
+ '0.2 1092.6 3987.8\n' +
+ '0.3 1070.0 3987.8\n' +
+ '0.4 1017.7 3994.8\n';
+
+ const eventData = {
+ traceEvents: [
+ { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
+ ],
+ powerTraceAsString: battorLog
+ };
+ let m = new tr.Model();
+ let importer = new tr.e.importer.TraceEventImporter(m, eventData);
+ let subTraces = importer.extractSubtraces();
+ assert.isTrue(subTraces instanceof Array);
+ assert.strictEqual(subTraces.length, 1);
+ assert.strictEqual(subTraces[0], battorLog);
+
+ m = new tr.Model();
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array(JSON.stringify(eventData)), false);
+ importer = new tr.e.importer.TraceEventImporter(m, stream);
+ subTraces = importer.extractSubtraces();
+ assert.isTrue(subTraces instanceof Array);
+ assert.strictEqual(subTraces.length, 1);
+ assert.strictEqual(subTraces[0], battorLog);
+ });
+
+ test('metadataParsing', function() {
+ const metadataValue = {value: {}};
+
+ function checkModel(m) {
+ assert.isTrue(m.metadata instanceof Array);
+ assert.strictEqual(m.metadata.length, 1);
+ assert.strictEqual(m.metadata[0].name, 'metadata');
+ assert.deepEqual(m.metadata[0].value, metadataValue);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
+ { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
+ ],
+ metadata: metadataValue
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importTraceConfigAsMetadata', function() {
+ const traceConfigData = {
+ enable_argument_filter: false,
+ enable_sampling: false,
+ enable_systrace: false,
+ record_mode: 'record-until-full'
+ };
+
+ function checkModel(m) {
+ assert.isTrue(m.metadata instanceof Array);
+ assert.strictEqual(m.metadata.length, 1);
+ assert.strictEqual(m.metadata[0].name, 'TraceConfig');
+ assert.deepEqual(m.metadata[0].value, traceConfigData);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'TraceConfig', args: { value: traceConfigData },
+ pid: 2, ts: 0, tid: 1, ph: 'M', cat: '__metadata' },
+ ],
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('importMarks', function() {
+ function checkModel(m) {
+ const p = m.processes[1];
+ const t = p.threads[2];
+ assert.lengthOf(t.sliceGroup.slices, 2);
+
+ const s0 = t.sliceGroup.slices[0];
+ assert.strictEqual(s0.title, 'mark1');
+ assert.strictEqual(s0.start, 0);
+
+ const s1 = t.sliceGroup.slices[1];
+ assert.strictEqual(s1.title, 'mark2');
+ assert.strictEqual(s1.start, .01);
+ }
+
+ const eventData = {
+ traceEvents: [
+ { name: 'mark1', args: {}, pid: 1, tid: 2, ts: 0, tts: 0, cat: 'blink.user_timing', ph: 'R' }, // @suppress longLineCheck
+ { name: 'mark2', args: {}, pid: 1, tid: 2, ts: 10, tts: 10, cat: 'blink.user_timing', ph: 'R' } // @suppress longLineCheck
+ ]
+ };
+ checkParsedAndStreamInput(eventData, checkModel);
+ });
+
+ test('createNestableAsyncSlicesForUserTimingWithoutArgs', function() {
+ function checkModel(m) {
+ assert(m.numProcesses, 1);
+ const p = m.processes[1];
+ assert.isDefined(p);
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[2];
+ const asyncSliceGroup = t.asyncSliceGroup;
+ assert.strictEqual(asyncSliceGroup.length, 2);
+ for (let i = 0; i < asyncSliceGroup.length; ++i) {
+ assert.isTrue(asyncSliceGroup.slices[i] instanceof MeasureAsyncSlice);
+ }
+
+ const groupA = asyncSliceGroup.slices[0];
+ assert.strictEqual(groupA.viewSubGroupTitle, 'A');
+ assert.strictEqual(groupA.title, 'a1');
+ assert.strictEqual(groupA.subSlices.length, 0);
+ const groupB = asyncSliceGroup.slices[1];
+ assert.strictEqual(groupB.viewSubGroupTitle, 'B');
+ assert.strictEqual(groupB.title, 'b1');
+ assert.strictEqual(groupB.subSlices.length, 2);
+ const groupBSubSlice1 = groupB.subSlices[0];
+ assert.strictEqual(groupBSubSlice1.viewSubGroupTitle, 'B');
+ assert.strictEqual(groupBSubSlice1.title, 'b2');
+ assert.strictEqual(groupBSubSlice1.subSlices.length, 1);
+ assert.strictEqual(groupBSubSlice1.subSlices[0].viewSubGroupTitle, 'B');
+ assert.strictEqual(groupBSubSlice1.subSlices[0].title, 'b3');
+ assert.strictEqual(groupBSubSlice1.subSlices[0].subSlices.length, 0);
+ const groupBSubSlice2 = groupB.subSlices[1];
+ assert.strictEqual(groupBSubSlice2.viewSubGroupTitle, 'B');
+ assert.strictEqual(groupBSubSlice2.title, 'b4');
+ assert.strictEqual(groupBSubSlice2.subSlices.length, 0);
+ }
+
+ /**
+ * Structure of this async slices
+ *
+ * Group A:
+ *
+ * |__________|
+ * a1
+ *
+ * Group B:
+ *
+ * |______________________________|
+ * b1
+ * |__________||_|
+ * b2 b4
+ * |_|
+ * b3
+ **/
+ const events = [
+ {
+ name: 'A:a1',
+ args: {params: ''},
+ pid: 1,
+ ts: 100,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 3,
+ ph: 'b'
+ },
+ {
+ name: 'A:a1',
+ args: {params: ''},
+ pid: 1,
+ ts: 110,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 3,
+ ph: 'e'
+ },
+ {
+ name: 'B:b1',
+ args: {params: ''},
+ pid: 1,
+ ts: 120,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 4,
+ ph: 'b'
+ },
+ {
+ name: 'B:b2',
+ args: {params: ''},
+ pid: 1,
+ ts: 130,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 5,
+ ph: 'b'
+ },
+ {
+ name: 'B:b3',
+ args: {params: ''},
+ pid: 1,
+ ts: 131,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 5,
+ ph: 'b'
+ },
+ {
+ name: 'B:b3',
+ args: {params: ''},
+ pid: 1,
+ ts: 132,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 5,
+ ph: 'e'
+ },
+ {
+ name: 'B:b2',
+ args: {params: ''},
+ pid: 1,
+ ts: 140,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 5,
+ ph: 'e'
+ },
+ {
+ name: 'B:b4',
+ args: {params: ''},
+ pid: 1,
+ ts: 141,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 5,
+ ph: 'b'
+ },
+ {
+ name: 'B:b4',
+ args: {params: ''},
+ pid: 1,
+ ts: 142,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 5,
+ ph: 'e'
+ },
+ {
+ name: 'B:b1',
+ args: {params: ''},
+ pid: 1,
+ ts: 150,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 4,
+ ph: 'e'
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('createNestableAsyncSlicesForUserTimingWithArgs', function() {
+ function checkModel(m) {
+ assert(m.numProcesses, 1);
+ const p = m.processes[1];
+ assert.isDefined(p);
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[2];
+ const asyncSliceGroup = t.asyncSliceGroup;
+ assert.strictEqual(asyncSliceGroup.length, 2);
+ for (let i = 0; i < asyncSliceGroup.length; ++i) {
+ assert.isTrue(asyncSliceGroup.slices[i] instanceof MeasureAsyncSlice);
+ }
+
+ const a1 = asyncSliceGroup.slices[0];
+ assert.strictEqual(a1.viewSubGroupTitle, 'A');
+ assert.strictEqual(a1.title, 'a1');
+ assert.strictEqual(a1.subSlices.length, 0);
+ assert.deepEqual(a1.args, {a: 1});
+ const a2 = asyncSliceGroup.slices[1];
+ assert.strictEqual(a2.viewSubGroupTitle, 'A');
+ assert.strictEqual(a2.title, 'a2');
+ assert.strictEqual(a2.subSlices.length, 0);
+ assert.deepEqual(a2.args, {a: 2, b: 2});
+ }
+
+ /**
+ * Structure of this async slices
+ *
+ * Group A:
+ *
+ * |__________| |__________|
+ * a1 a2
+ *
+ * a1.args = {a: 1}
+ * a2.args = {a: 2, b: 2}
+ **/
+ const events = [
+ {
+ name: 'A:a1/eyJhIjoxfQ==',
+ args: {params: ''},
+ pid: 1,
+ ts: 100,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 3,
+ ph: 'b'
+ },
+ {
+ name: 'A:a1/eyJhIjoxfQ==',
+ args: {params: ''},
+ pid: 1,
+ ts: 110,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 3,
+ ph: 'e'
+ },
+ {
+ name: 'A:a2/eyJhIjoyLCJiIjoyfQ==',
+ args: {params: ''},
+ pid: 1,
+ ts: 120,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 4,
+ ph: 'b'
+ },
+ {
+ name: 'A:a2/eyJhIjoyLCJiIjoyfQ==',
+ args: {params: ''},
+ pid: 1,
+ ts: 130,
+ cat: 'blink.user_timing',
+ tid: 2,
+ id: 4,
+ ph: 'e'
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('UserTimingAsyncSlicesWithNormalAsyncSlices', function() {
+ function checkModel(m) {
+ assert(m.numProcesses, 1);
+ const p = m.processes[1];
+ assert.isDefined(p);
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[2];
+ const asyncSliceGroup = t.asyncSliceGroup;
+ assert.strictEqual(asyncSliceGroup.length, 3);
+ assert.isTrue(asyncSliceGroup.slices[0] instanceof MeasureAsyncSlice);
+ assert.isFalse(asyncSliceGroup.slices[1] instanceof MeasureAsyncSlice);
+ assert.isFalse(asyncSliceGroup.slices[2] instanceof MeasureAsyncSlice);
+
+ const a1 = asyncSliceGroup.slices[0];
+ assert.strictEqual(a1.viewSubGroupTitle, 'A');
+ assert.strictEqual(a1.title, 'a1');
+ assert.strictEqual(a1.subSlices.length, 1);
+ const a2 = a1.subSlices[0];
+ assert.strictEqual(a2.viewSubGroupTitle, 'A');
+ assert.strictEqual(a2.title, 'a2');
+ assert.strictEqual(a2.subSlices.length, 0);
+ const B = asyncSliceGroup.slices[1];
+ assert.strictEqual(B.viewSubGroupTitle, 'B');
+ assert.strictEqual(B.title, 'B');
+ assert.strictEqual(B.subSlices.length, 0);
+ const C = asyncSliceGroup.slices[2];
+ assert.strictEqual(C.viewSubGroupTitle, 'C');
+ assert.strictEqual(C.title, 'C');
+ assert.strictEqual(C.subSlices.length, 0);
+ }
+
+ /**
+ * Structure of user timing async slices
+ *
+ * Group A:
+ *
+ * |__________|
+ * a1
+ * |__|
+ * a2
+ *
+ * B
+ * |__|
+ * B
+ * C
+ * |_|
+ * C
+ **/
+ const events = [
+ {
+ name: 'A:a1', args: {params: ''}, pid: 1, ts: 1,
+ cat: 'blink.user_timing', tid: 2, id: 3, ph: 'b'
+ },
+ {
+ name: 'A:a1', args: {params: ''}, pid: 1, ts: 11,
+ cat: 'blink.user_timing', tid: 2, id: 3, ph: 'e'
+ },
+ {
+ name: 'A:a2', args: {params: ''}, pid: 1, ts: 2,
+ cat: 'blink.user_timing', tid: 2, id: 4, ph: 'b'
+ },
+ {
+ name: 'A:a2', args: {params: ''}, pid: 1, ts: 4,
+ cat: 'blink.user_timing', tid: 2, id: 4, ph: 'e'
+ },
+ {
+ name: 'B', args: {}, pid: 1, ts: 9, cat: 'foo',
+ tid: 2, ph: 'b', id: 5
+ },
+ {
+ name: 'B', args: {}, pid: 1, ts: 11, cat: 'foo',
+ tid: 2, ph: 'e', id: 5
+ },
+ {
+ name: 'C', args: {}, pid: 1, ts: 12, cat: 'foo',
+ tid: 2, ph: 'b', id: 6
+ },
+ {
+ name: 'C', args: {}, pid: 1, ts: 13, cat: 'foo',
+ tid: 2, ph: 'e', id: 6
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('UserTimingAsyncSlicesParsingMeasureNameShouldNotFail', function() {
+ function checkModel(m) {
+ assert(m.numProcesses, 1);
+ const p = m.processes[1];
+ assert.isDefined(p);
+ assert.strictEqual(p.numThreads, 1);
+ const t = p.threads[2];
+ const asyncSliceGroup = t.asyncSliceGroup;
+
+ assert.strictEqual(asyncSliceGroup.length, 1);
+ assert.isTrue(asyncSliceGroup.slices[0] instanceof MeasureAsyncSlice);
+
+ const slice = asyncSliceGroup.slices[0];
+ assert.strictEqual(slice.viewSubGroupTitle, '<extended@component');
+ assert.strictEqual(slice.title,
+ 'shared@dropdown/dropdown-options::ember30> (Rendering: initial)');
+ assert.strictEqual(slice.subSlices.length, 3);
+ assert.strictEqual(slice.subSlices[0].title,
+ 'shared@dropdown/dropdown-options::ember30>:didReceiveAttrs:34');
+ assert.strictEqual(slice.subSlices[1].title,
+ 'shared@dropdown/dropdown-options::ember30>:willRender:35');
+ assert.strictEqual(slice.subSlices[2].title,
+ 'shared@dropdown/dropdown-options::ember30>:willInsertElement:36');
+ }
+
+ // events from ember-perf-timeline addon performance.measure() calls
+ const events = [
+ {
+ pid: 1, tid: 2, ts: 2695561803887, ph: 'b', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:didReceiveAttrs:34', // @suppress longLineCheck
+ args: {}, tts: 10533196, id: '0x9e9a4a'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561803907, ph: 'e', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:didReceiveAttrs:34', // @suppress longLineCheck
+ args: {}, tts: 10533202, id: '0x9e9a4a'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561803987, ph: 'b', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willRender:35', // @suppress longLineCheck
+ args: {}, tts: 10533295, id: '0xd0fc77'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561804002, ph: 'e', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willRender:35', // @suppress longLineCheck
+ args: {}, tts: 10533300, id: '0xd0fc77'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561804252, ph: 'b', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willInsertElement:36', // @suppress longLineCheck
+ args: {}, tts: 10533557, id: '0x5dfb0b'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561804262, ph: 'e', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willInsertElement:36', // @suppress longLineCheck
+ args: {}, tts: 10533561, id: '0x5dfb0b'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561803852, ph: 'b', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30> (Rendering: initial)', // @suppress longLineCheck
+ args: {}, tts: 10533951, id: '0xc824e5'
+ },
+ {
+ pid: 1, tid: 2, ts: 2695561804652, ph: 'e', cat: 'blink.user_timing',
+ name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30> (Rendering: initial)', // @suppress longLineCheck
+ args: {}, tts: 10533956, id: '0xc824e5'
+ }
+ ];
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('clockSync_requesterWithoutMetadata', function() {
+ const events = [{
+ name: 'clock_sync', args: {sync_id: 'abc', issue_ts: 5000},
+ pid: 1, ts: 15000, cat: '__metadata', tid: 2, ph: 'c'
+ }];
+
+ const io = new tr.importer.ImportOptions();
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m, io);
+
+ m.clockSyncManager.addClockSyncMarker(
+ ClockDomainId.LINUX_FTRACE_GLOBAL, 'abc', 15);
+
+ i.importTraces([events]);
+
+ // The clock sync happened in UNKNOWN_CHROME_LEGACY at 10ms and in
+ // LINUX_FTRACE_GLOBAL at 15ms. The transformer should shift the ftrace
+ // timestamps back by 5ms.
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(5),
+ 5);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.LINUX_FTRACE_GLOBAL)(15),
+ 10);
+ assert.isFalse(m.hasImportWarnings);
+ });
+
+ test('clockSync_requesterWithoutMetadata_stream', function() {
+ const events = [{
+ name: 'clock_sync', args: {sync_id: 'abc', issue_ts: 5000},
+ pid: 1, ts: 15000, cat: '__metadata', tid: 2, ph: 'c'
+ }];
+ const stream = new tr.b.InMemoryTraceStream(
+ stringToUint8Array(JSON.stringify(events)), false);
+
+ const io = new tr.importer.ImportOptions();
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m, io);
+
+ m.clockSyncManager.addClockSyncMarker(
+ ClockDomainId.LINUX_FTRACE_GLOBAL, 'abc', 15);
+
+ i.importTraces([stream]);
+
+ // The clock sync happened in UNKNOWN_CHROME_LEGACY at 10ms and in
+ // LINUX_FTRACE_GLOBAL at 15ms. The transformer should shift the ftrace
+ // timestamps back by 5ms.
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(5),
+ 5);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.LINUX_FTRACE_GLOBAL)(15),
+ 10);
+ assert.isFalse(m.hasImportWarnings);
+ });
+
+
+ test('clockSync_requesterWithMetadata', function() {
+ function checkModel(m) {
+ // Make sure that our clock domain was chosen as the model.
+ assert.strictEqual(
+ m.clockSyncManager
+ .getModelTimeTransformer(ClockDomainId.BATTOR)(.005),
+ .005);
+ assert.isFalse(m.hasImportWarnings);
+ }
+
+ const events = {
+ traceEvents: [{
+ name: 'clock_sync', args: {sync_id: 'abc', issue_ts: 5},
+ pid: 1, ts: 15, cat: '__metadata', tid: 2, ph: 'c'
+ }],
+ metadata: {
+ 'clock-domain': ClockDomainId.BATTOR
+ }
+ };
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('clockSync_recipient', function() {
+ function checkModel(m) {
+ m.clockSyncManager.addClockSyncMarker(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC, 'abc', 15);
+
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.TELEMETRY)(10),
+ 10);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC)(10),
+ 10);
+ }
+
+ const events = {
+ traceEvents: [{
+ name: 'clock_sync', args: {sync_id: 'abc'}, pid: 1, ts: 15000,
+ cat: '__metadata', tid: 2, ph: 'c'
+ }],
+ metadata: {
+ 'clock-domain': ClockDomainId.TELEMETRY
+ }
+ };
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('clockSync_missingSyncId', function() {
+ const events = [{
+ name: 'clock_sync', args: {issue_ts: 5},
+ pid: 1, ts: 15, cat: '__metadata', tid: 2, ph: 'c'
+ }];
+ checkParsedAndStreamInput(events, m => assert.isTrue(m.hasImportWarnings));
+ });
+
+ test('clockSync_legacyChrome', function() {
+ function checkModel(m) {
+ m.clockSyncManager.addClockSyncMarker(
+ ClockDomainId.TELEMETRY, 'abc', 15);
+
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(10),
+ 10);
+ assert.strictEqual(
+ m.clockSyncManager.getModelTimeTransformer(
+ ClockDomainId.TELEMETRY)(15),
+ 10);
+ }
+
+ const events = {
+ traceEvents: [
+ {
+ name: 'ClockSyncEvent.abc', ts: 5000,
+ cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'S'
+ },
+ {
+ name: 'ClockSyncEvent.abc', ts: 15000,
+ cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'F'
+ }
+ ]
+ };
+ checkParsedAndStreamInput(events, checkModel);
+ });
+
+ test('clockSync_legacyChromeThrowsWithInconsistentSyncID', function() {
+ const events = {
+ traceEvents: [
+ {
+ name: 'ClockSyncEvent.abc', ts: 5000,
+ cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'S'
+ },
+ {
+ name: 'ClockSyncEvent.def', ts: 15000,
+ cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'F'
+ }
+ ]
+ };
+
+ assert.throws(() => makeModel(events),
+ 'Inconsistent clock sync ID of legacy Chrome clock sync events');
+ // TODO(chiniforooshan): When the input is a trace stream, it is processed
+ // using the Oboe.js library. Oboe.js wraps exceptions thrown from callbacks
+ // and uses setTimeout to throw them in another event loop so that stream
+ // parsing is not interrupted.
+ //
+ // The importer's behaviour should be the same for different types of the
+ // same input. We should either modify Oboe.js to throw right away, or wrap
+ // exceptions in the importer, too.
+ });
+
+ test('importV8SamplesInStreamingFormat', function() {
+ function checkModel(m) {
+ const samples = m.samples;
+ assert.strictEqual(m.samples.length, 4);
+ assert.deepEqual(samples.map(sample => sample.start.toFixed(3)), [
+ '0.011',
+ '0.012',
+ '0.015',
+ '0.018'
+ ]);
+ assert.deepEqual(samples[0].userFriendlyStack, [
+ 'b url: b.js:1:2 Deoptimized reason: a reason',
+ 'a url: a.js:1:2'
+ ]);
+ assert.deepEqual(samples[1].userFriendlyStack, [
+ 'a url: a.js:1:2'
+ ]);
+ assert.deepEqual(samples[2].userFriendlyStack, [
+ 'd url: d.js:2:3 Deoptimized reason: another reason',
+ 'b url: b.js:1:2 Deoptimized reason: a reason',
+ 'a url: a.js:1:2'
+ ]);
+ assert.deepEqual(samples[3].userFriendlyStack, [
+ 'c url: c.js:2:3',
+ 'a url: a.js:1:2'
+ ]);
+ }
+
+ const events = {
+ 'traceEvents': [
+ {'pid': 1, 'tid': 2, 'ts': 3, 'ph': 'P',
+ 'cat': 'disabled-by-default-v8.cpu_profiler',
+ 'name': 'Profile',
+ 'args': {'data': {'startTime': 10}},
+ 'id': 123
+ },
+ {'pid': 1, 'tid': 3, 'ts': 4, 'ph': 'P',
+ 'cat': 'disabled-by-default-v8.cpu_profiler',
+ 'name': 'Profile',
+ 'args': {
+ 'data': {
+ 'timeDeltas': [1, 1],
+ 'cpuProfile': {
+ 'nodes': [
+ // eslint-disable-next-line
+ {'callFrame': {'functionName': '(root)', 'scriptId': 0}, 'id': 1},
+ // eslint-disable-next-line
+ {'callFrame': {'functionName': 'a', 'url': 'a.js', 'scriptId': 1, 'lineNumber': 1, 'columnNumber': 2}, 'id': 2, 'parent': 1},
+ // eslint-disable-next-line
+ {'callFrame': {'functionName': 'b', 'url': 'b.js', 'scriptId': 2, 'lineNumber': 1, 'columnNumber': 2}, 'deoptReason': 'a reason', 'id': 3, 'parent': 2}
+ ],
+ 'samples': [3, 2]
+ }
+ }
+ },
+ 'id': 123
+ },
+ {'pid': 1, 'tid': 3, 'ts': 4, 'ph': 'P',
+ 'cat': 'disabled-by-default-v8.cpu_profiler',
+ 'name': 'Profile',
+ 'args': {
+ 'data': {
+ 'timeDeltas': [3, 3],
+ 'cpuProfile': {
+ 'nodes': [
+ // eslint-disable-next-line
+ {'callFrame': {'functionName': 'c', 'url': 'c.js', 'scriptId': 3, 'lineNumber': 2, 'columnNumber': 3}, 'id': 4, 'parent': 2},
+ // eslint-disable-next-line
+ {'callFrame': {'functionName': 'd', 'url': 'd.js', 'scriptId': 4, 'lineNumber': 2, 'columnNumber': 3}, 'deoptReason': 'another reason', 'id': 5, 'parent': 3}
+ ],
+ 'samples': [5, 4]
+ }
+ }
+ },
+ 'id': 123
+ }
+ ]
+ };
+ checkParsedAndStreamInput(events, checkModel, false);
+ });
+
+ test('scopedIdForEvent_defaultScopeAsyncEvent', function() {
+ const event = {pid: 1, ph: 'b', id: '0x1000'};
+ const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
+ assert.isDefined(id);
+ assert.strictEqual('0x1000', id.id);
+ assert.strictEqual('ptr', id.scope);
+ assert.isUndefined(id.pid);
+ });
+
+ test('scopedIdForEvent_defaultScopeContextEvent', function() {
+ const event = {pid: 1, ph: '(', id: '0x1000'};
+ const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
+ assert.isDefined(id);
+ assert.strictEqual('0x1000', id.id);
+ assert.strictEqual('ptr', id.scope);
+ assert.strictEqual(1, id.pid);
+ });
+
+ test('scopedIdForEvent_defaultScopeProcessLocalId', function() {
+ const event = {pid: 1, ph: 'b', id2: {local: '0x1000'}};
+ const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
+ assert.isDefined(id);
+ assert.strictEqual('0x1000', id.id);
+ assert.strictEqual('ptr', id.scope);
+ assert.strictEqual(1, id.pid);
+ });
+
+ test('scopedIdForEvent_defaultScopeGlobalId', function() {
+ const event = {pid: 1, ph: '(', id2: {global: '0x1000'}};
+ const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
+ assert.isDefined(id);
+ assert.strictEqual('0x1000', id.id);
+ assert.strictEqual('ptr', id.scope);
+ assert.isUndefined(id.pid);
+ });
+
+ test('scopedIdForEvent_explicitScopeGlobalId', function() {
+ const event = {pid: 1, ph: '(', id2: {global: '0x1000'}, scope: 'scope'};
+ const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
+ assert.isDefined(id);
+ assert.strictEqual('0x1000', id.id);
+ assert.strictEqual('scope', id.scope);
+ assert.isUndefined(id.pid);
+ });
+
+ // TODO(nduca): one slice, two threads
+ // TODO(nduca): one slice, two pids
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/codemap.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/codemap.html
new file mode 100644
index 00000000000..23fe39b7e37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/codemap.html
@@ -0,0 +1,383 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/v8/splaytree.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Map addresses to dynamically created functions.
+ */
+tr.exportTo('tr.e.importer.v8', function() {
+ /**
+ * Constructs a mapper that maps addresses into code entries.
+ *
+ * @constructor
+ */
+ function CodeMap() {
+ /**
+ * Dynamic code entries. Used for JIT compiled code.
+ */
+ this.dynamics_ = new tr.e.importer.v8.SplayTree();
+
+ /**
+ * Name generator for entries having duplicate names.
+ */
+ this.dynamicsNameGen_ = new tr.e.importer.v8.CodeMap.NameGenerator();
+
+ /**
+ * Static code entries. Used for statically compiled code.
+ */
+ this.statics_ = new tr.e.importer.v8.SplayTree();
+
+ /**
+ * Libraries entries. Used for the whole static code libraries.
+ */
+ this.libraries_ = new tr.e.importer.v8.SplayTree();
+
+ /**
+ * Map of memory pages occupied with static code.
+ */
+ this.pages_ = [];
+ }
+
+ /**
+ * The number of alignment bits in a page address.
+ */
+ CodeMap.PAGE_ALIGNMENT = 12;
+
+ /**
+ * Page size in bytes.
+ */
+ CodeMap.PAGE_SIZE = 1 << CodeMap.PAGE_ALIGNMENT;
+
+ /**
+ * Adds a dynamic (i.e. moveable and discardable) code entry.
+ *
+ * @param {number} start The starting address.
+ * @param {CodeMap.CodeEntry} codeEntry Code entry object.
+ */
+ CodeMap.prototype.addCode = function(start, codeEntry) {
+ this.deleteAllCoveredNodes_(this.dynamics_, start, start + codeEntry.size);
+ this.dynamics_.insert(start, codeEntry);
+ };
+
+ /**
+ * Moves a dynamic code entry. Throws an exception if there is no dynamic
+ * code entry with the specified starting address.
+ *
+ * @param {number} from The starting address of the entry being moved.
+ * @param {number} to The destination address.
+ */
+ CodeMap.prototype.moveCode = function(from, to) {
+ const removedNode = this.dynamics_.remove(from);
+ this.deleteAllCoveredNodes_(this.dynamics_, to,
+ to + removedNode.value.size);
+ this.dynamics_.insert(to, removedNode.value);
+ };
+
+ /**
+ * Discards a dynamic code entry. Throws an exception if there is no dynamic
+ * code entry with the specified starting address.
+ *
+ * @param {number} start The starting address of the entry being deleted.
+ */
+ CodeMap.prototype.deleteCode = function(start) {
+ const removedNode = this.dynamics_.remove(start);
+ };
+
+ /**
+ * Adds a library entry.
+ *
+ * @param {number} start The starting address.
+ * @param {CodeMap.CodeEntry} codeEntry Code entry object.
+ */
+ CodeMap.prototype.addLibrary = function(
+ start, codeEntry) {
+ this.markPages_(start, start + codeEntry.size);
+ this.libraries_.insert(start, codeEntry);
+ };
+
+ /**
+ * Adds a static code entry.
+ *
+ * @param {number} start The starting address.
+ * @param {CodeMap.CodeEntry} codeEntry Code entry object.
+ */
+ CodeMap.prototype.addStaticCode = function(
+ start, codeEntry) {
+ this.statics_.insert(start, codeEntry);
+ };
+
+ /**
+ * @private
+ */
+ CodeMap.prototype.markPages_ = function(start, end) {
+ for (let addr = start; addr <= end;
+ addr += CodeMap.PAGE_SIZE) {
+ this.pages_[addr >>> CodeMap.PAGE_ALIGNMENT] = 1;
+ }
+ };
+
+ /**
+ * @private
+ */
+ CodeMap.prototype.deleteAllCoveredNodes_ = function(tree, start, end) {
+ const toDelete = [];
+ let addr = end - 1;
+ while (addr >= start) {
+ const node = tree.findGreatestLessThan(addr);
+ if (!node) break;
+ const start2 = node.key;
+ const end2 = start2 + node.value.size;
+ if (start2 < end && start < end2) toDelete.push(start2);
+ addr = start2 - 1;
+ }
+ for (let i = 0, l = toDelete.length; i < l; ++i) tree.remove(toDelete[i]);
+ };
+
+ /**
+ * @private
+ */
+ CodeMap.prototype.isAddressBelongsTo_ = function(addr, node) {
+ return addr >= node.key && addr < (node.key + node.value.size);
+ };
+
+ /**
+ * @private
+ */
+ CodeMap.prototype.findInTree_ = function(tree, addr) {
+ const node = tree.findGreatestLessThan(addr);
+ return node && this.isAddressBelongsTo_(addr, node) ? node.value : null;
+ };
+
+ /**
+ * Finds a code entry that contains the specified address in static libraries.
+ *
+ * @param {number} addr Address.
+ */
+ CodeMap.prototype.findEntryInLibraries = function(addr) {
+ const pageAddr = addr >>> CodeMap.PAGE_ALIGNMENT;
+ if (pageAddr in this.pages_) {
+ return this.findInTree_(this.libraries_, addr);
+ }
+ return undefined;
+ };
+
+ /**
+ * Finds a code entry that contains the specified address. Both static and
+ * dynamic code entries are considered.
+ *
+ * @param {number} addr Address.
+ */
+ CodeMap.prototype.findEntry = function(addr) {
+ const pageAddr = addr >>> CodeMap.PAGE_ALIGNMENT;
+ if (pageAddr in this.pages_) {
+ // Static code entries can contain "holes" of unnamed code.
+ // In this case, the whole library is assigned to this address.
+ return this.findInTree_(this.statics_, addr) ||
+ this.findInTree_(this.libraries_, addr);
+ }
+ const min = this.dynamics_.findMin();
+ const max = this.dynamics_.findMax();
+ if (max !== null && addr < (max.key + max.value.size) && addr >= min.key) {
+ const dynaEntry = this.findInTree_(this.dynamics_, addr);
+ if (dynaEntry === null) return null;
+ // Dedupe entry name.
+ if (!dynaEntry.nameUpdated_) {
+ dynaEntry.name = this.dynamicsNameGen_.getName(dynaEntry.name);
+ dynaEntry.nameUpdated_ = true;
+ }
+ return dynaEntry;
+ }
+ return null;
+ };
+
+ /**
+ * Returns a dynamic code entry using its starting address.
+ *
+ * @param {number} addr Address.
+ */
+ CodeMap.prototype.findDynamicEntryByStartAddress =
+ function(addr) {
+ const node = this.dynamics_.find(addr);
+ return node ? node.value : null;
+ };
+
+ /**
+ * Returns an array of all dynamic code entries.
+ */
+ CodeMap.prototype.getAllDynamicEntries = function() {
+ return this.dynamics_.exportValues();
+ };
+
+ /**
+ * Returns an array of pairs of all dynamic code entries and their addresses.
+ */
+ CodeMap.prototype.getAllDynamicEntriesWithAddresses = function() {
+ return this.dynamics_.exportKeysAndValues();
+ };
+
+ /**
+ * Returns an array of all static code entries.
+ */
+ CodeMap.prototype.getAllStaticEntries = function() {
+ return this.statics_.exportValues();
+ };
+
+ /**
+ * Returns an array of all libraries entries.
+ */
+ CodeMap.prototype.getAllLibrariesEntries = function() {
+ return this.libraries_.exportValues();
+ };
+
+ /**
+ * Enum for code state regarding its dynamic optimization.
+ *
+ * @enum {number}
+ */
+ CodeMap.CodeState = {
+ COMPILED: 0,
+ OPTIMIZABLE: 1,
+ OPTIMIZED: 2
+ };
+
+ /**
+ * Creates a code entry object.
+ *
+ * @param {number} size Code entry size in bytes.
+ * @param {string=} opt_name Code entry name.
+ * @constructor
+ */
+ CodeMap.CodeEntry = function(size, opt_name, opt_type) {
+ this.id = tr.b.GUID.allocateSimple();
+ this.size = size;
+ this.name_ = opt_name || '';
+ this.type = opt_type || '';
+ this.nameUpdated_ = false;
+ };
+
+ CodeMap.CodeEntry.prototype = {
+ __proto__: Object.prototype,
+
+ get name() {
+ return this.name_;
+ },
+
+ set name(value) {
+ this.name_ = value;
+ },
+
+ toString() {
+ this.name_ + ': ' + this.size.toString(16);
+ }
+ };
+
+ CodeMap.CodeEntry.TYPE = {
+ SHARED_LIB: 'SHARED_LIB',
+ CPP: 'CPP'
+ };
+
+ /**
+ * Creates a dynamic code entry.
+ *
+ * @param {number} size Code size.
+ * @param {string} type Code type.
+ * @param {CodeMap.FunctionEntry} func Shared function entry.
+ * @param {CodeMap.CodeState} state Code optimization state.
+ * @constructor
+ */
+ CodeMap.DynamicFuncCodeEntry = function(size, type, func, state) {
+ CodeMap.CodeEntry.call(this, size, '', type);
+ this.func = func;
+ this.state = state;
+ };
+
+ CodeMap.DynamicFuncCodeEntry.STATE_PREFIX = ['', '~', '*'];
+
+ CodeMap.DynamicFuncCodeEntry.prototype = {
+ __proto__: CodeMap.CodeEntry.prototype,
+
+ /**
+ * Returns node name.
+ */
+ get name() {
+ return CodeMap.DynamicFuncCodeEntry.STATE_PREFIX[this.state] +
+ this.func.name;
+ },
+
+ set name(value) {
+ this.name_ = value;
+ },
+
+ /**
+ * Returns raw node name (without type decoration).
+ */
+ getRawName() {
+ return this.func.getName();
+ },
+
+ isJSFunction() {
+ return true;
+ },
+
+ toString() {
+ return this.type + ': ' + this.name + ': ' + this.size.toString(16);
+ }
+ };
+
+ /**
+ * Creates a shared function object entry.
+ *
+ * @param {string} name Function name.
+ * @constructor
+ */
+ CodeMap.FunctionEntry = function(name) {
+ CodeMap.CodeEntry.call(this, 0, name);
+ };
+
+ CodeMap.FunctionEntry.prototype = {
+ __proto__: CodeMap.CodeEntry.prototype,
+
+ /**
+ * Returns node name.
+ */
+ get name() {
+ let name = this.name_;
+ if (name.length === 0) {
+ name = '<anonymous>';
+ } else if (name.charAt(0) === ' ') {
+ // An anonymous function with location: " aaa.js:10".
+ name = '<anonymous>' + name;
+ }
+ return name;
+ },
+
+ set name(value) {
+ this.name_ = value;
+ }
+ };
+
+ CodeMap.NameGenerator = function() {
+ this.knownNames_ = {};
+ };
+
+ CodeMap.NameGenerator.prototype.getName = function(name) {
+ if (!(name in this.knownNames_)) {
+ this.knownNames_[name] = 0;
+ return name;
+ }
+ const count = ++this.knownNames_[name];
+ return name + ' {' + count + '}';
+ };
+ return {
+ CodeMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/log_reader.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/log_reader.html
new file mode 100644
index 00000000000..70ac3349b81
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/log_reader.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Log Reader is used to process log file produced by V8.
+ */
+tr.exportTo('tr.e.importer.v8', function() {
+ /**
+ * Creates a CSV lines parser.
+ */
+ function CsvParser() { }
+
+ /**
+ * A regex for matching a CSV field.
+ * @private
+ */
+ CsvParser.CSV_FIELD_RE_ = /^"((?:[^"]|"")*)"|([^,]*)/;
+
+ /**
+ * A regex for matching a double quote.
+ * @private
+ */
+ CsvParser.DOUBLE_QUOTE_RE_ = /""/g;
+
+ /**
+ * Parses a line of CSV-encoded values. Returns an array of fields.
+ *
+ * @param {string} line Input line.
+ */
+ CsvParser.prototype.parseLine = function(line) {
+ const fieldRe = CsvParser.CSV_FIELD_RE_;
+ const doubleQuoteRe = CsvParser.DOUBLE_QUOTE_RE_;
+ let pos = 0;
+ const endPos = line.length;
+ const fields = [];
+ if (endPos > 0) {
+ do {
+ const fieldMatch = fieldRe.exec(line.substr(pos));
+ if (typeof fieldMatch[1] === 'string') {
+ const field = fieldMatch[1];
+ pos += field.length + 3; // Skip comma and quotes.
+ fields.push(field.replace(doubleQuoteRe, '"'));
+ } else {
+ // The second field pattern will match anything, thus
+ // in the worst case the match will be an empty string.
+ const field = fieldMatch[2];
+ pos += field.length + 1; // Skip comma.
+ fields.push(field);
+ }
+ } while (pos <= endPos);
+ }
+ return fields;
+ };
+
+ /**
+ * Base class for processing log files.
+ *
+ * @param {Array.<Object>} dispatchTable A table used for parsing and
+ * processing log records.
+ *
+ * @constructor
+ */
+ function LogReader(dispatchTable) {
+ /**
+ * @type {Array.<Object>}
+ */
+ this.dispatchTable_ = dispatchTable;
+
+ /**
+ * Current line.
+ * @type {number}
+ */
+ this.lineNum_ = 0;
+
+ /**
+ * CSV lines parser.
+ * @type {CsvParser}
+ */
+ this.csvParser_ = new CsvParser();
+ }
+
+ /**
+ * Used for printing error messages.
+ *
+ * @param {string} str Error message.
+ */
+ LogReader.prototype.printError = function(str) {
+ // Do nothing.
+ };
+
+ /**
+ * Processes a portion of V8 profiler event log.
+ *
+ * @param {string} chunk A portion of log.
+ */
+ LogReader.prototype.processLogChunk = function(chunk) {
+ this.processLog_(chunk.split('\n'));
+ };
+
+ /**
+ * Processes a line of V8 profiler event log.
+ *
+ * @param {string} line A line of log.
+ */
+ LogReader.prototype.processLogLine = function(line) {
+ this.processLog_([line]);
+ };
+
+ /**
+ * Processes stack record.
+ *
+ * @param {number} pc Program counter.
+ * @param {number} func JS Function.
+ * @param {Array.<string>} stack String representation of a stack.
+ * @return {Array.<number>} Processed stack.
+ */
+ LogReader.prototype.processStack = function(pc, func, stack) {
+ const fullStack = func ? [pc, func] : [pc];
+ let prevFrame = pc;
+ for (let i = 0, n = stack.length; i < n; ++i) {
+ const frame = stack[i];
+ const firstChar = frame.charAt(0);
+ if (firstChar === '+' || firstChar === '-') {
+ // An offset from the previous frame.
+ prevFrame += parseInt(frame, 16);
+ fullStack.push(prevFrame);
+ // Filter out possible 'overflow' string.
+ } else if (firstChar !== 'o') {
+ fullStack.push(parseInt(frame, 16));
+ }
+ }
+ return fullStack;
+ };
+
+ /**
+ * Returns whether a particular dispatch must be skipped.
+ *
+ * @param {!Object} dispatch Dispatch record.
+ * @return {boolean} True if dispatch must be skipped.
+ */
+ LogReader.prototype.skipDispatch = function(dispatch) {
+ return false;
+ };
+
+ /**
+ * Does a dispatch of a log record.
+ *
+ * @param {Array.<string>} fields Log record.
+ * @private
+ */
+ LogReader.prototype.dispatchLogRow_ = function(fields) {
+ // Obtain the dispatch.
+ const command = fields[0];
+ if (!(command in this.dispatchTable_)) return;
+
+ const dispatch = this.dispatchTable_[command];
+
+ if (dispatch === null || this.skipDispatch(dispatch)) {
+ return;
+ }
+
+ // Parse fields.
+ const parsedFields = [];
+ for (let i = 0; i < dispatch.parsers.length; ++i) {
+ const parser = dispatch.parsers[i];
+ if (parser === null) {
+ parsedFields.push(fields[1 + i]);
+ } else if (typeof parser === 'function') {
+ parsedFields.push(parser(fields[1 + i]));
+ } else {
+ // var-args
+ parsedFields.push(fields.slice(1 + i));
+ break;
+ }
+ }
+
+ // Run the processor.
+ dispatch.processor.apply(this, parsedFields);
+ };
+
+ /**
+ * Processes log lines.
+ *
+ * @param {Array.<string>} lines Log lines.
+ * @private
+ */
+ LogReader.prototype.processLog_ = function(lines) {
+ for (let i = 0, n = lines.length; i < n; ++i, ++this.lineNum_) {
+ const line = lines[i];
+ if (!line) {
+ continue;
+ }
+ try {
+ const fields = this.csvParser_.parseLine(line);
+ this.dispatchLogRow_(fields);
+ } catch (e) {
+ this.printError('line ' + (this.lineNum_ + 1) + ': ' +
+ (e.message || e));
+ }
+ }
+ };
+ return {
+ LogReader,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/splaytree.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/splaytree.html
new file mode 100644
index 00000000000..3498b15658d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/splaytree.html
@@ -0,0 +1,295 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Splay tree used by CodeMap.
+ */
+tr.exportTo('tr.e.importer.v8', function() {
+ /**
+ * Constructs a Splay tree. A splay tree is a self-balancing binary
+ * search tree with the additional property that recently accessed
+ * elements are quick to access again. It performs basic operations
+ * such as insertion, look-up and removal in O(log(n)) amortized time.
+ *
+ * @constructor
+ */
+ function SplayTree() { }
+
+ /**
+ * Pointer to the root node of the tree.
+ *
+ * @type {SplayTree.Node}
+ * @private
+ */
+ SplayTree.prototype.root_ = null;
+
+ /**
+ * @return {boolean} Whether the tree is empty.
+ */
+ SplayTree.prototype.isEmpty = function() {
+ return !this.root_;
+ };
+
+ /**
+ * Inserts a node into the tree with the specified key and value if
+ * the tree does not already contain a node with the specified key. If
+ * the value is inserted, it becomes the root of the tree.
+ *
+ * @param {number} key Key to insert into the tree.
+ * @param {*} value Value to insert into the tree.
+ */
+ SplayTree.prototype.insert = function(key, value) {
+ if (this.isEmpty()) {
+ this.root_ = new SplayTree.Node(key, value);
+ return;
+ }
+ // Splay on the key to move the last node on the search path for
+ // the key to the root of the tree.
+ this.splay_(key);
+ if (this.root_.key === key) {
+ return;
+ }
+ const node = new SplayTree.Node(key, value);
+ if (key > this.root_.key) {
+ node.left = this.root_;
+ node.right = this.root_.right;
+ this.root_.right = null;
+ } else {
+ node.right = this.root_;
+ node.left = this.root_.left;
+ this.root_.left = null;
+ }
+ this.root_ = node;
+ };
+
+
+ /**
+ * Removes a node with the specified key from the tree if the tree
+ * contains a node with this key. The removed node is returned. If the
+ * key is not found, an exception is thrown.
+ *
+ * @param {number} key Key to find and remove from the tree.
+ * @return {SplayTree.Node} The removed node.
+ */
+ SplayTree.prototype.remove = function(key) {
+ if (this.isEmpty()) {
+ throw Error('Key not found: ' + key);
+ }
+ this.splay_(key);
+ if (this.root_.key !== key) {
+ throw Error('Key not found: ' + key);
+ }
+ const removed = this.root_;
+ if (!this.root_.left) {
+ this.root_ = this.root_.right;
+ } else {
+ const right = this.root_.right;
+ this.root_ = this.root_.left;
+ // Splay to make sure that the new root has an empty right child.
+ this.splay_(key);
+ // Insert the original right child as the right child of the new
+ // root.
+ this.root_.right = right;
+ }
+ return removed;
+ };
+
+
+ /**
+ * Returns the node having the specified key or null if the tree doesn't
+ * contain a node with the specified key.
+ *
+ *
+ * @param {number} key Key to find in the tree.
+ * @return {SplayTree.Node} Node having the specified key.
+ */
+ SplayTree.prototype.find = function(key) {
+ if (this.isEmpty()) return null;
+ this.splay_(key);
+ return this.root_.key === key ? this.root_ : null;
+ };
+
+ /**
+ * @return {SplayTree.Node} Node having the minimum key value.
+ */
+ SplayTree.prototype.findMin = function() {
+ if (this.isEmpty()) return null;
+ let current = this.root_;
+ while (current.left) {
+ current = current.left;
+ }
+ return current;
+ };
+
+ /**
+ * @return {SplayTree.Node} Node having the maximum key value.
+ */
+ SplayTree.prototype.findMax = function(opt_startNode) {
+ if (this.isEmpty()) return null;
+ let current = opt_startNode || this.root_;
+ while (current.right) {
+ current = current.right;
+ }
+ return current;
+ };
+
+ /**
+ * @return {SplayTree.Node} Node having the maximum key value that
+ * is less or equal to the specified key value.
+ */
+ SplayTree.prototype.findGreatestLessThan = function(key) {
+ if (this.isEmpty()) return null;
+ // Splay on the key to move the node with the given key or the last
+ // node on the search path to the top of the tree.
+ this.splay_(key);
+ // Now the result is either the root node or the greatest node in
+ // the left subtree.
+ if (this.root_.key <= key) {
+ return this.root_;
+ }
+ if (this.root_.left) {
+ return this.findMax(this.root_.left);
+ }
+ return null;
+ };
+
+ /**
+ * @return {Array<*>} An array containing all the values of tree's nodes
+ * paired with keys.
+ *
+ */
+ SplayTree.prototype.exportKeysAndValues = function() {
+ const result = [];
+ this.traverse_(function(node) { result.push([node.key, node.value]); });
+ return result;
+ };
+
+ /**
+ * @return {Array<*>} An array containing all the values of tree's nodes.
+ */
+ SplayTree.prototype.exportValues = function() {
+ const result = [];
+ this.traverse_(function(node) { result.push(node.value); });
+ return result;
+ };
+
+ /**
+ * Perform the splay operation for the given key. Moves the node with
+ * the given key to the top of the tree. If no node has the given
+ * key, the last node on the search path is moved to the top of the
+ * tree. This is the simplified top-down splaying algorithm from:
+ * "Self-adjusting Binary Search Trees" by Sleator and Tarjan
+ *
+ * @param {number} key Key to splay the tree on.
+ * @private
+ */
+ SplayTree.prototype.splay_ = function(key) {
+ if (this.isEmpty()) return;
+ // Create a dummy node. The use of the dummy node is a bit
+ // counter-intuitive: The right child of the dummy node will hold
+ // the L tree of the algorithm. The left child of the dummy node
+ // will hold the R tree of the algorithm. Using a dummy node, left
+ // and right will always be nodes and we avoid special cases.
+ const dummy = new SplayTree.Node(null, null);
+ let left = dummy;
+ let right = dummy;
+ let current = this.root_;
+ while (true) {
+ if (key < current.key) {
+ if (!current.left) {
+ break;
+ }
+ if (key < current.left.key) {
+ // Rotate right.
+ const tmp = current.left;
+ current.left = tmp.right;
+ tmp.right = current;
+ current = tmp;
+ if (!current.left) {
+ break;
+ }
+ }
+ // Link right.
+ right.left = current;
+ right = current;
+ current = current.left;
+ } else if (key > current.key) {
+ if (!current.right) {
+ break;
+ }
+ if (key > current.right.key) {
+ // Rotate left.
+ const tmp = current.right;
+ current.right = tmp.left;
+ tmp.left = current;
+ current = tmp;
+ if (!current.right) {
+ break;
+ }
+ }
+ // Link left.
+ left.right = current;
+ left = current;
+ current = current.right;
+ } else {
+ break;
+ }
+ }
+ // Assemble.
+ left.right = current.left;
+ right.left = current.right;
+ current.left = dummy.right;
+ current.right = dummy.left;
+ this.root_ = current;
+ };
+
+ /**
+ * Performs a preorder traversal of the tree.
+ *
+ * @param {function(SplayTree.Node)} f Visitor function.
+ * @private
+ */
+ SplayTree.prototype.traverse_ = function(f) {
+ const nodesToVisit = [this.root_];
+ while (nodesToVisit.length > 0) {
+ const node = nodesToVisit.shift();
+ if (node === null) continue;
+ f(node);
+ nodesToVisit.push(node.left);
+ nodesToVisit.push(node.right);
+ }
+ };
+
+ /**
+ * Constructs a Splay tree node.
+ *
+ * @param {number} key Key.
+ * @param {*} value Value.
+ */
+ SplayTree.Node = function(key, value) {
+ this.key = key;
+ this.value = value;
+ };
+
+ /**
+ * @type {SplayTree.Node}
+ */
+ SplayTree.Node.prototype.left = null;
+
+ /**
+ * @type {SplayTree.Node}
+ */
+ SplayTree.Node.prototype.right = null;
+
+ return {
+ SplayTree,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer.html
new file mode 100644
index 00000000000..74b140beafe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer.html
@@ -0,0 +1,393 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/extras/importer/v8/codemap.html">
+<link rel="import" href="/tracing/extras/importer/v8/log_reader.html">
+<link rel="import" href="/tracing/extras/v8/v8_cpu_profile_node.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/profile_tree.html">
+<link rel="import" href="/tracing/model/slice.html">
+
+<script>
+
+'use strict';
+
+/**
+ * @fileoverview V8LogImporter imports v8.log files into the provided model.
+ */
+tr.exportTo('tr.e.importer.v8', function() {
+ const CodeEntry = tr.e.importer.v8.CodeMap.CodeEntry;
+ const CodeMap = tr.e.importer.v8.CodeMap;
+ const ColorScheme = tr.b.ColorScheme;
+ const DynamicFuncCodeEntry = tr.e.importer.v8.CodeMap.DynamicFuncCodeEntry;
+ const FunctionEntry = tr.e.importer.v8.CodeMap.FunctionEntry;
+ const ProfileNodeType = tr.model.ProfileNode.subTypes.getConstructor(
+ undefined, 'legacySample');
+
+ function V8LogImporter(model, eventData) {
+ this.importPriority = 3;
+ this.model_ = model;
+
+ this.logData_ = eventData;
+
+ this.code_map_ = new CodeMap();
+ this.v8_timer_thread_ = undefined;
+ this.v8_thread_ = undefined;
+
+ this.profileTree_ = new tr.model.ProfileTree();
+ // We predefine a unknown Profile Node so that when a stack address
+ // can't be symbolized, we make it attach to the unknown Profile Node.
+ this.profileTree_.add(new ProfileNodeType(
+ -1, {
+ url: '',
+ functionName: 'unknown'
+ }
+ ));
+
+ // We reconstruct the stack timeline from ticks.
+ this.v8_stack_timeline_ = [];
+ }
+
+ const kV8BinarySuffixes = ['/d8', '/libv8.so'];
+
+
+ const TimerEventDefaultArgs = {
+ 'V8.Execute': { pause: false, no_execution: false},
+ 'V8.External': { pause: false, no_execution: true},
+ 'V8.CompileFullCode': { pause: true, no_execution: true},
+ 'V8.RecompileSynchronous': { pause: true, no_execution: true},
+ 'V8.RecompileParallel': { pause: false, no_execution: false},
+ 'V8.CompileEval': { pause: true, no_execution: true},
+ 'V8.Parse': { pause: true, no_execution: true},
+ 'V8.PreParse': { pause: true, no_execution: true},
+ 'V8.ParseLazy': { pause: true, no_execution: true},
+ 'V8.GCScavenger': { pause: true, no_execution: true},
+ 'V8.GCCompactor': { pause: true, no_execution: true},
+ 'V8.GCContext': { pause: true, no_execution: true}
+ };
+
+ /**
+ * @return {boolean} Whether obj is a V8 log string.
+ */
+ V8LogImporter.canImport = function(eventData) {
+ if (typeof(eventData) !== 'string' && !(eventData instanceof String)) {
+ return false;
+ }
+
+ return eventData.substring(0, 11) === 'v8-version,' ||
+ eventData.substring(0, 12) === 'timer-event,' ||
+ eventData.substring(0, 5) === 'tick,' ||
+ eventData.substring(0, 15) === 'shared-library,' ||
+ eventData.substring(0, 9) === 'profiler,' ||
+ eventData.substring(0, 14) === 'code-creation,';
+ };
+
+ V8LogImporter.prototype = {
+
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'V8LogImporter';
+ },
+
+ processTimerEvent_(name, startInUs, lengthInUs) {
+ const args = TimerEventDefaultArgs[name];
+ if (args === undefined) return;
+ const startInMs = tr.b.convertUnit(startInUs,
+ tr.b.UnitPrefixScale.METRIC.MICRO,
+ tr.b.UnitPrefixScale.METRIC.MILLI);
+ const lengthInMs = tr.b.convertUnit(lengthInUs,
+ tr.b.UnitPrefixScale.METRIC.MICRO,
+ tr.b.UnitPrefixScale.METRIC.MILLI);
+ const colorId = ColorScheme.getColorIdForGeneralPurposeString(name);
+ const slice = new tr.model.ThreadSlice('v8', name, colorId, startInMs,
+ args, lengthInMs);
+ this.v8_timer_thread_.sliceGroup.pushSlice(slice);
+ },
+
+ processTimerEventStart_(name, startInUs) {
+ const args = TimerEventDefaultArgs[name];
+ if (args === undefined) return;
+ const startInMs = tr.b.convertUnit(startInUs,
+ tr.b.UnitPrefixScale.METRIC.MICRO,
+ tr.b.UnitPrefixScale.METRIC.MILLI);
+ this.v8_timer_thread_.sliceGroup.beginSlice('v8', name, startInMs, args);
+ },
+
+ processTimerEventEnd_(name, endInUs) {
+ const endInMs = tr.b.convertUnit(endInUs,
+ tr.b.UnitPrefixScale.METRIC.MICRO,
+ tr.b.UnitPrefixScale.METRIC.MILLI);
+ this.v8_timer_thread_.sliceGroup.endSlice(endInMs);
+ },
+
+ processCodeCreateEvent_(type, kind, address, size, name,
+ maybeFunc) {
+ function parseState(s) {
+ switch (s) {
+ case '': return CodeMap.CodeState.COMPILED;
+ case '~': return CodeMap.CodeState.OPTIMIZABLE;
+ case '*': return CodeMap.CodeState.OPTIMIZED;
+ }
+ throw new Error('unknown code state: ' + s);
+ }
+
+ if (maybeFunc.length) {
+ const funcAddr = parseInt(maybeFunc[0]);
+ const state = parseState(maybeFunc[1]);
+ let func = this.code_map_.findDynamicEntryByStartAddress(funcAddr);
+ if (!func) {
+ func = new FunctionEntry(name);
+ func.kind = kind;
+ this.code_map_.addCode(funcAddr, func);
+ } else if (func.name !== name) {
+ // Function object has been overwritten with a new one.
+ func.name = name;
+ }
+ let entry = this.code_map_.findDynamicEntryByStartAddress(address);
+ if (entry) {
+ if (entry.size === size && entry.func === func) {
+ // Entry state has changed.
+ entry.state = state;
+ }
+ } else {
+ entry = new DynamicFuncCodeEntry(size, type, func, state);
+ entry.kind = kind;
+ this.code_map_.addCode(address, entry);
+ }
+ } else {
+ const codeEntry = new CodeEntry(size, name);
+ codeEntry.kind = kind;
+ this.code_map_.addCode(address, codeEntry);
+ }
+ },
+
+ processCodeMoveEvent_(from, to) {
+ this.code_map_.moveCode(from, to);
+ },
+
+ processCodeDeleteEvent_(address) {
+ this.code_map_.deleteCode(address);
+ },
+
+ processSharedLibrary_(name, start, end) {
+ const codeEntry = new CodeEntry(end - start, name,
+ CodeEntry.TYPE.SHARED_LIB);
+ codeEntry.kind = -3; // External code kind.
+ for (let i = 0; i < kV8BinarySuffixes.length; i++) {
+ const suffix = kV8BinarySuffixes[i];
+ if (name.indexOf(suffix, name.length - suffix.length) >= 0) {
+ codeEntry.kind = -1; // V8 runtime code kind.
+ break;
+ }
+ }
+ this.code_map_.addLibrary(start, codeEntry);
+ },
+
+ processCppSymbol_(address, size, name) {
+ const codeEntry = new CodeEntry(size, name, CodeEntry.TYPE.CPP);
+ codeEntry.kind = -1;
+ this.code_map_.addStaticCode(address, codeEntry);
+ },
+
+ processTickEvent_(pc, startInUs, isExternalCallback,
+ tosOrExternalCallback, vmstate, stack) {
+ const startInMs = tr.b.convertUnit(startInUs,
+ tr.b.UnitPrefixScale.METRIC.MICRO,
+ tr.b.UnitPrefixScale.METRIC.MILLI);
+
+ function findChildWithEntryID(stackFrame, entryID) {
+ for (let i = 0; i < stackFrame.children.length; i++) {
+ if (stackFrame.children[i].entryID === entryID) {
+ return stackFrame.children[i];
+ }
+ }
+ return undefined;
+ }
+
+ function processStack(pc, func, stack) {
+ const fullStack = func ? [pc, func] : [pc];
+ let prevFrame = pc;
+ for (let i = 0, n = stack.length; i < n; ++i) {
+ const frame = stack[i];
+ const firstChar = frame.charAt(0);
+ if (firstChar === '+' || firstChar === '-') {
+ // An offset from the previous frame.
+ prevFrame += parseInt(frame, 16);
+ fullStack.push(prevFrame);
+ // Filter out possible 'overflow' string.
+ } else if (firstChar !== 'o') {
+ fullStack.push(parseInt(frame, 16));
+ }
+ // Otherwise, they will be skipped.
+ }
+ return fullStack;
+ }
+
+ if (isExternalCallback) {
+ // Don't use PC when in external callback code, as it can point
+ // inside callback's code, and we will erroneously report
+ // that a callback calls itself. Instead use tosOrExternalCallback,
+ // as simply resetting PC will produce unaccounted ticks.
+ pc = tosOrExternalCallback;
+ tosOrExternalCallback = 0;
+ } else if (tosOrExternalCallback) {
+ // Find out, if top of stack was pointing inside a JS function
+ // meaning that we have encountered a frameless invocation.
+ const funcEntry = this.code_map_.findEntry(tosOrExternalCallback);
+ if (!funcEntry ||
+ !funcEntry.isJSFunction ||
+ !funcEntry.isJSFunction()) {
+ tosOrExternalCallback = 0;
+ }
+ }
+
+ let processedStack = processStack(pc, tosOrExternalCallback, stack);
+ let node = undefined;
+ let lastNode = undefined;
+ // v8 log stacks are inverted, leaf first and the root at the end.
+ processedStack = processedStack.reverse();
+ for (let i = 0, n = processedStack.length; i < n; i++) {
+ const frame = processedStack[i];
+ if (!frame) break;
+ const entry = this.code_map_.findEntry(frame);
+
+ if (!entry && i !== 0) {
+ continue;
+ }
+
+ let sourceInfo = undefined;
+ if (entry && entry.type === CodeEntry.TYPE.CPP) {
+ const libEntry = this.code_map_.findEntryInLibraries(frame);
+ if (libEntry) {
+ sourceInfo = {
+ file: libEntry.name
+ };
+ }
+ }
+ const entryId = entry ? entry.id : -1;
+ node = this.profileTree_.getNode(entryId);
+ if (node === undefined) {
+ node = this.profileTree_.add(new ProfileNodeType(
+ entryId, {
+ functionName: entry.name,
+ url: sourceInfo ? sourceInfo.file : '',
+ lineNumber: sourceInfo ? sourceInfo.line : undefined,
+ columnNumber: sourceInfo ? sourceInfo.column : undefined,
+ scriptId: sourceInfo ? sourceInfo.scriptId : undefined
+ }, lastNode));
+ }
+ lastNode = node;
+ }
+ this.model_.samples.push(new tr.model.Sample(
+ startInMs, 'V8 PC', node, this.v8_thread_, undefined, 1));
+ },
+
+ processDistortion_(distortionInPicoseconds) {
+ // Do nothing.
+ },
+
+ processPlotRange_(start, end) {
+ // Do nothing.
+ },
+
+ processV8Version_(major, minor, build, patch, candidate) {
+ // Do nothing.
+ },
+
+ /**
+ * Walks through the events_ list and outputs the structures discovered to
+ * model_.
+ */
+ importEvents() {
+ const logreader = new tr.e.importer.v8.LogReader(
+ { 'timer-event': {
+ parsers: [null, parseInt, parseInt],
+ processor: this.processTimerEvent_.bind(this)
+ },
+ 'shared-library': {
+ parsers: [null, parseInt, parseInt],
+ processor: this.processSharedLibrary_.bind(this)
+ },
+ 'timer-event-start': {
+ parsers: [null, parseInt],
+ processor: this.processTimerEventStart_.bind(this)
+ },
+ 'timer-event-end': {
+ parsers: [null, parseInt],
+ processor: this.processTimerEventEnd_.bind(this)
+ },
+ 'code-creation': {
+ parsers: [null, parseInt, parseInt, parseInt, null, 'var-args'],
+ processor: this.processCodeCreateEvent_.bind(this)
+ },
+ 'code-move': {
+ parsers: [parseInt, parseInt],
+ processor: this.processCodeMoveEvent_.bind(this)
+ },
+ 'code-delete': {
+ parsers: [parseInt],
+ processor: this.processCodeDeleteEvent_.bind(this)
+ },
+ 'cpp': {
+ parsers: [parseInt, parseInt, null],
+ processor: this.processCppSymbol_.bind(this)
+ },
+ 'tick': {
+ parsers: [parseInt, parseInt, parseInt, parseInt, parseInt,
+ 'var-args'],
+ processor: this.processTickEvent_.bind(this)
+ },
+ 'distortion': {
+ parsers: [parseInt],
+ processor: this.processDistortion_.bind(this)
+ },
+ 'plot-range': {
+ parsers: [parseInt, parseInt],
+ processor: this.processPlotRange_.bind(this)
+ },
+ 'v8-version': {
+ parsers: [parseInt, parseInt, parseInt, parseInt, parseInt],
+ processor: this.processV8Version_.bind(this)
+ }
+ });
+
+ this.v8_timer_thread_ =
+ this.model_.getOrCreateProcess(-32).getOrCreateThread(1);
+ this.v8_timer_thread_.name = 'V8 Timers';
+ this.v8_thread_ =
+ this.model_.getOrCreateProcess(-32).getOrCreateThread(2);
+ this.v8_thread_.name = 'V8';
+
+ const lines = this.logData_.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ logreader.processLogLine(lines[i]);
+ }
+
+ function addSlices(slices, thread) {
+ for (let i = 0; i < slices.length; i++) {
+ const duration = slices[i].end - slices[i].start;
+ const slice = new tr.model.ThreadSlice('v8', slices[i].name,
+ ColorScheme.getColorIdForGeneralPurposeString(slices[i].name),
+ slices[i].start, {}, duration);
+ thread.sliceGroup.pushSlice(slice);
+ addSlices(slices[i].children, thread);
+ }
+ }
+ addSlices(this.v8_stack_timeline_, this.v8_thread_);
+ }
+ };
+
+ tr.importer.Importer.register(V8LogImporter);
+
+ return {
+ V8LogImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer_test.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer_test.html
new file mode 100644
index 00000000000..e9db2f92334
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/v8/v8_log_importer_test.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/v8/v8_log_importer.html">
+<link rel="import" href="/tracing/extras/v8_config.html">
+
+<script>
+
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const V8LogImporter = tr.e.importer.v8.V8LogImporter;
+
+ function newModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('tickEventInSharedLibrary', function() {
+ const lines = [
+ 'shared-library,"/usr/lib/libc++.1.dylib",0x99d8aae0,0x99dce729',
+ 'tick,0x99d8aae4,12158,0,0x0,5'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const t = p.findAllThreadsNamed('V8')[0];
+ assert.strictEqual(t.samples.length, 1);
+ assert.strictEqual(t.samples[0].title, 'V8 PC');
+ assert.strictEqual(t.samples[0].start, 12158 / 1000);
+ assert.strictEqual(t.samples[0].leafNode.title, '/usr/lib/libc++.1.dylib');
+ });
+
+ test('tickEventInGeneratedCode', function() {
+ const lines = [
+ 'shared-library,"/usr/lib/libc++.1.dylib",0x99d8aae0,0x99dce729',
+ 'code-creation,Stub,2,0x5b60ce80,1259,"StringAddStub"',
+ 'tick,0x5b60ce84,12158,0,0x0,5'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const threads = p.findAllThreadsNamed('V8');
+ const t = threads[0];
+ assert.strictEqual(t.samples.length, 1);
+ assert.strictEqual(t.samples[0].leafNode.title, 'StringAddStub');
+ });
+
+ test('tickEventInUknownCode', function() {
+ const lines = [
+ 'shared-library,"/usr/lib/libc++.1.dylib",0x99d8aae0,0x99dce729',
+ 'code-creation,Stub,2,0x5b60ce80,1259,"StringAddStub"',
+ 'tick,0x4,0xbff02f08,12158,0,0x0,5'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const threads = p.findAllThreadsNamed('V8');
+ const t = threads[0];
+ assert.strictEqual(t.samples.length, 1);
+ assert.strictEqual(t.samples[0].leafNode.title, 'unknown');
+ });
+
+ test('tickEventWithStack', function() {
+ const lines = [
+ 'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+ 'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const t = p.findAllThreadsNamed('V8')[0];
+ assert.strictEqual(t.samples.length, 1);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown'],
+ t.samples[0].userFriendlyStack);
+ });
+
+ test('twoTickEventsWithStack', function() {
+ const lines = [
+ 'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+ 'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304',
+ 'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const t = p.findAllThreadsNamed('V8')[0];
+ assert.strictEqual(t.samples.length, 2);
+ assert.strictEqual(t.samples[0].start, 528674 / 1000);
+ assert.strictEqual(t.samples[1].start, 536213 / 1000);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown'],
+ t.samples[0].userFriendlyStack);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown'],
+ t.samples[1].userFriendlyStack);
+ assert.strictEqual(t.samples[0].leafNode,
+ t.samples[1].leafNode);
+ });
+
+ test('twoTickEventsWithTwoStackFrames', function() {
+ const lines = [
+ 'code-creation,LazyCompile,0,0x2904d560,876,"Instantiate native apinatives.js:9:21",0x56b190c8,~', // @suppress longLineCheck
+ 'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+ 'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304,0x2904d6e8',
+ 'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304,0x2904d6e8'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const t = p.findAllThreadsNamed('V8')[0];
+ assert.strictEqual(t.samples.length, 2);
+
+ assert.strictEqual(t.samples[0].start, 528674 / 1000);
+ assert.strictEqual(t.samples[1].start, 536213 / 1000);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown',
+ '~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[0].userFriendlyStack);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown',
+ '~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[1].userFriendlyStack);
+
+ assert.strictEqual(t.samples[0].leafNode, t.samples[1].leafNode);
+ });
+
+ test('threeTickEventsWithTwoStackFrames', function() {
+ const lines = [
+ 'code-creation,LazyCompile,0,0x2904d560,876,"Instantiate native apinatives.js:9:21",0x56b190c8,~', // @suppress longLineCheck
+ 'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+ 'tick,0x7fd7f75c,518328,0,0x81d86da8,2,0x2904d6e8',
+ 'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304,0x2904d6e8',
+ 'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304,0x2904d6e8'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const threads = p.findAllThreadsNamed('V8');
+
+ const t = threads[0];
+ assert.strictEqual(t.samples.length, 3);
+ assert.strictEqual(t.samples[0].start, 518328 / 1000);
+ assert.strictEqual(t.samples[1].start, 528674 / 1000);
+ assert.strictEqual(t.samples[2].start, 536213 / 1000);
+ assert.deepEqual(
+ ['~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[0].userFriendlyStack);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown',
+ '~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[1].userFriendlyStack);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown',
+ '~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[2].userFriendlyStack);
+
+ const topLevelNode = t.samples[0].leafNode;
+ const leafNode = t.samples[1].leafNode;
+ assert.strictEqual(t.samples[2].leafNode, leafNode);
+ assert.strictEqual(topLevelNode, leafNode.parentNode);
+ });
+
+ test('twoSubStacks', function() {
+ const lines = [
+ 'code-creation,LazyCompile,0,0x2904d560,876,"Instantiate native apinatives.js:9:21",0x56b190c8,~', // @suppress longLineCheck
+ 'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+ 'tick,0x7fd7f75c,518328,0,0x81d86da8,2,0x2904d6e8',
+ 'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304,0x2904d6e8',
+ 'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304,0x2904d6e8',
+ 'code-creation,Script,0,0x2906a7c0,792,"http://www.google.com/",0x5b12fe50,~', // @suppress longLineCheck
+ 'tick,0xb6f51d30,794049,0,0xb6f7b368,2,0x2906a914',
+ 'tick,0xb6f51d30,799145,0,0xb6f7b368,0,0x2906a914'
+ ];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const threads = p.findAllThreadsNamed('V8');
+ const t = threads[0];
+ assert.strictEqual(t.samples.length, 5);
+
+ assert.strictEqual(t.samples[0].start, 518328 / 1000);
+ assert.strictEqual(t.samples[1].start, 528674 / 1000);
+ assert.strictEqual(t.samples[2].start, 536213 / 1000);
+ assert.strictEqual(t.samples[3].start, 794049 / 1000);
+ assert.strictEqual(t.samples[4].start, 799145 / 1000);
+
+ assert.deepEqual(
+ ['~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[0].userFriendlyStack);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown',
+ '~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[1].userFriendlyStack);
+ assert.deepEqual(
+ ['InstantiateFunction native apinatives.js:26:29 url: unknown',
+ '~Instantiate native apinatives.js:9:21 url: unknown'],
+ t.samples[2].userFriendlyStack);
+ assert.deepEqual(['~http://www.google.com/ url: unknown'],
+ t.samples[3].userFriendlyStack);
+ assert.deepEqual(['~http://www.google.com/ url: unknown'],
+ t.samples[4].userFriendlyStack);
+
+ const firstNode = t.samples[0].leafNode;
+ const firstChildNode = t.samples[1].leafNode;
+ assert.strictEqual(firstChildNode, t.samples[2].leafNode);
+ assert.strictEqual(firstNode, firstChildNode.parentNode);
+
+ const secondNode = t.samples[3].leafNode;
+ assert.strictEqual(secondNode, t.samples[4].leafNode);
+ assert.isUndefined(secondNode.parentNode);
+ });
+
+ test('timerEventSliceCreation', function() {
+ const lines = ['timer-event,"V8.External",38189483,3'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const threads = p.findAllThreadsNamed('V8 Timers');
+ assert.isDefined(threads);
+ assert.strictEqual(threads.length, 1);
+ const t = threads[0];
+ assert.strictEqual(t.sliceGroup.length, 1);
+ });
+
+ test('processThreadCreation', function() {
+ const lines = ['timer-event,"V8.External",38189483,3'];
+ const m = newModel(lines.join('\n'));
+ assert.isDefined(m);
+ const p = m.processes[-32];
+ assert.isDefined(p);
+ const threads = p.findAllThreadsNamed('V8 Timers');
+ assert.isDefined(threads);
+ assert.strictEqual(1, threads.length);
+ const t = threads[0];
+ assert.strictEqual('V8 Timers', t.name);
+ });
+
+ test('canImport', function() {
+ assert.isTrue(V8LogImporter.canImport(
+ 'timer-event,"V8.External",38189483,3'));
+ assert.isTrue(V8LogImporter.canImport('v8-version,4,3,66,0,0'));
+ assert.isFalse(V8LogImporter.canImport(''));
+ assert.isFalse(V8LogImporter.canImport([]));
+ });
+
+ test('CppSymbolsProcess', function() {
+ const lines = [
+ 'shared-library,"/usr/loca/v8/out/native/d8",0x00400000, 0x01860000',
+ 'cpp,0x00600000,100,"v8::internal::Heap::AllocateByteArray"',
+ 'cpp,0x00700000,100,"v8::internal::Utf8StringKey::AsHandle"',
+ 'tick,0x00600010,121,0,0x0,5',
+ 'tick,0x00700010,122,0,0x0,5',
+ 'tick,0x00800010,123,0,0x0,5'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const t = p.findAllThreadsNamed('V8')[0];
+ assert.strictEqual(t.samples.length, 3);
+ assert.strictEqual(t.samples[0].leafNode.title,
+ 'v8::internal::Heap::AllocateByteArray');
+ assert.strictEqual(t.samples[1].leafNode.title,
+ 'v8::internal::Utf8StringKey::AsHandle');
+ assert.strictEqual(t.samples[2].leafNode.title,
+ '/usr/loca/v8/out/native/d8');
+ });
+
+ test('CppSymbolsWithJsStack', function() {
+ const lines = [
+ 'shared-library,"/usr/loca/v8/out/native/d8",0x00400000, 0x01860000',
+ 'cpp,0x00600000,100,"v8::internal::Heap::AllocateByteArray"',
+ 'cpp,0x00700000,100,"v8::internal::Utf8StringKey::AsHandle"',
+ 'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+ 'tick,0x00700010,1,0,0x3,0,0x2905d304',
+ 'tick,0x00600010,3,0,0x3,0,0x2905d306',
+ 'tick,0x00800010,5,0,0x3,0,0x2905d306',
+ 'tick,0x01860010,5,0,0x3,0,0x2905d306'];
+ const m = newModel(lines.join('\n'));
+ const p = m.processes[-32];
+ const t = p.findAllThreadsNamed('V8')[0];
+ assert.strictEqual(t.samples.length, 4);
+ assert.deepEqual(t.samples[0].userFriendlyStack, [
+ 'v8::internal::Utf8StringKey::AsHandle url: /usr/loca/v8/out/native/d8',
+ 'InstantiateFunction native apinatives.js:26:29 url: unknown'
+ ]);
+ assert.deepEqual(t.samples[1].userFriendlyStack, [
+ 'v8::internal::Heap::AllocateByteArray url: /usr/loca/v8/out/native/d8',
+ 'InstantiateFunction native apinatives.js:26:29 url: unknown'
+ ]);
+ assert.deepEqual(t.samples[2].userFriendlyStack, [
+ '/usr/loca/v8/out/native/d8 url: unknown',
+ 'InstantiateFunction native apinatives.js:26:29 url: unknown'
+ ]);
+ assert.deepEqual(t.samples[3].userFriendlyStack, [
+ 'InstantiateFunction native apinatives.js:26:29 url: unknown'
+ ]);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/importer/zip_importer.html b/chromium/third_party/catapult/tracing/tracing/extras/importer/zip_importer.html
new file mode 100644
index 00000000000..db16134a838
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/importer/zip_importer.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/importer/gzip_importer.html">
+<link rel="import" href="/tracing/extras/importer/jszip.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview ZipImporter inflates zip compressed data and passes it along
+ * to an actual importer.
+ */
+tr.exportTo('tr.e.importer', function() {
+ function ZipImporter(model, eventData) {
+ if (eventData instanceof ArrayBuffer) {
+ eventData = new Uint8Array(eventData);
+ }
+ this.model_ = model;
+ this.eventData_ = eventData;
+ }
+
+ /**
+ * @param {eventData} string Possibly zip compressed data.
+ * @return {boolean} Whether eventData looks like zip compressed data.
+ */
+ ZipImporter.canImport = function(eventData) {
+ let header;
+ if (eventData instanceof ArrayBuffer) {
+ header = new Uint8Array(eventData.slice(0, 2));
+ } else if (typeof(eventData) === 'string' || eventData instanceof String) {
+ header = [eventData.charCodeAt(0), eventData.charCodeAt(1)];
+ } else {
+ return false;
+ }
+ return header[0] === 'P'.charCodeAt(0) && header[1] === 'K'.charCodeAt(0);
+ };
+
+ ZipImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'ZipImporter';
+ },
+
+ isTraceDataContainer() {
+ return true;
+ },
+
+ extractSubtraces() {
+ const zip = new JSZip(this.eventData_);
+ const subtraces = [];
+ for (const idx in zip.files) {
+ subtraces.push(zip.files[idx].asBinary());
+ }
+ return subtraces;
+ }
+ };
+
+ tr.importer.Importer.register(ZipImporter);
+
+ return {
+ ZipImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/lean_config.html b/chromium/third_party/catapult/tracing/tracing/extras/lean_config.html
new file mode 100644
index 00000000000..b39e51073d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/lean_config.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!--
+The lean config is just enough to import uncompressed, trace-event-formatted
+json blobs.
+-->
+
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/measure/measure.html b/chromium/third_party/catapult/tracing/tracing/extras/measure/measure.html
new file mode 100644
index 00000000000..05836554eda
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/measure/measure.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/measure/measure_async_slice.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice.html
new file mode 100644
index 00000000000..d05d25999e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/model/async_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.measure', function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+
+ // Split on first : with an optional `/` + base64 suffix
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice_test.html
new file mode 100644
index 00000000000..cd5de5db878
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/measure/measure_async_slice_test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/measure/measure.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const MeasureAsyncSlice = tr.e.measure.MeasureAsyncSlice;
+
+ test('basic', function() {
+ const s = new MeasureAsyncSlice(
+ 'blink.user_timing', 'createImports', 7, 0, {});
+ s.duration = 100;
+
+ assert.strictEqual(AsyncSlice.subTypes.getConstructor(
+ 'blink.user_timing', 'createImports'),
+ MeasureAsyncSlice);
+ assert.strictEqual(s.viewSubGroupTitle, 'Ungrouped Measure');
+ });
+
+ test('import', function() {
+ const events = [
+ {name: 'createImports', args: {}, pid: 1, ts: 100,
+ cat: 'blink.user_timing', tid: 2, ph: 'b', id: 71},
+ {name: 'createImports', args: {}, pid: 1, ts: 200,
+ cat: 'blink.user_timing', tid: 2, ph: 'e', id: 71}
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([events]);
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ assert.strictEqual(t2.asyncSliceGroup.length, 1);
+ assert.instanceOf(t2.asyncSliceGroup.slices[0], MeasureAsyncSlice);
+ });
+
+ test('groupMeasureAsyncSlice', function() {
+ const events = [
+ {name: 'createImports', args: {}, pid: 1, ts: 100,
+ cat: 'blink.user_timing', tid: 2, ph: 'b', id: 71},
+ {name: 'createImports', args: {}, pid: 1, ts: 200,
+ cat: 'blink.user_timing', tid: 2, ph: 'e', id: 71},
+ {name: 'runAutoClosers', args: {}, pid: 1, ts: 210,
+ cat: 'blink.user_timing', tid: 2, ph: 'b', id: 72},
+ {name: 'runAutoClosers', args: {}, pid: 1, ts: 310,
+ cat: 'blink.user_timing', tid: 2, ph: 'e', id: 72}
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([events]);
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ assert.strictEqual(t2.asyncSliceGroup.length, 2);
+ assert.strictEqual(t2.asyncSliceGroup.slices[0].title, 'createImports');
+ assert.strictEqual(t2.asyncSliceGroup.slices[1].title, 'runAutoClosers');
+ assert.instanceOf(t2.asyncSliceGroup.slices[0], MeasureAsyncSlice);
+ assert.instanceOf(t2.asyncSliceGroup.slices[1], MeasureAsyncSlice);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/memory/lowmemory_auditor.html b/chromium/third_party/catapult/tracing/tracing/extras/memory/lowmemory_auditor.html
new file mode 100644
index 00000000000..678393e9d43
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/memory/lowmemory_auditor.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/model/alert.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.e.audits', function() {
+ /**
+ * Auditor that analyzes the model and annotates low memory events.
+ */
+ class LowMemoryAuditor extends tr.c.Auditor {
+ constructor(model) {
+ super();
+ this.model_ = model;
+ }
+
+ runAnnotate() {
+ this.model_.device.lowMemoryEvents = this.getLowMemoryEvents_();
+ }
+
+ /**
+ * Returns a list of low memory killer events.
+ */
+ getLowMemoryEvents_() {
+ const model = this.model_;
+ const result = [];
+ for (const process of model.getAllProcesses()) {
+ for (const e of process.getDescendantEvents()) {
+ // low memory killer events are 0-duration events.
+ if (!(e instanceof tr.model.ThreadSlice) || e.duration !== 0) {
+ continue;
+ }
+
+ if (e.category !== 'lowmemory') {
+ continue;
+ }
+
+ result.push(e);
+ }
+ }
+ return result;
+ }
+ }
+
+ tr.c.Auditor.register(LowMemoryAuditor);
+
+ return {
+ LowMemoryAuditor
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/net/net.html b/chromium/third_party/catapult/tracing/tracing/extras/net/net.html
new file mode 100644
index 00000000000..3a246c930c0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/net/net.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/net/net_async_slice.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice.html
new file mode 100644
index 00000000000..fe3008bf55f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/model/async_slice.html">
+
+<script>
+'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;
+ // Boolean variables indicating whether we have computed corresponding
+ // fields. Computing these fields needs iteration through all sub-slices and
+ // so recomputation will be costly.
+ 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) {
+ // Set the title so we do not have to recompute when it is redrawn.
+ this.title_ = this.url;
+ } else if (this.args !== undefined &&
+ this.args.source_type !== undefined) {
+ // We do not have a URL, use the source type as the title.
+ this.title_ = this.args.source_type;
+ }
+ this.isTitleComputed_ = true;
+ return this.title_;
+ },
+
+ set title(title) {
+ this.title_ = title;
+ },
+
+ // A recursive helper function that gets the url param of a slice or its
+ // nested subslices if there is one.
+ get url() {
+ if (this.isUrlComputed_) {
+ return this.url_;
+ }
+ if (this.args !== undefined && this.args.params !== undefined &&
+ this.args.params.url !== undefined) {
+ this.url_ = this.args.params.url;
+ } else if (this.subSlices !== undefined && this.subSlices.length > 0) {
+ for (let i = 0; i < this.subSlices.length && ! this.url_; i++) {
+ if (this.subSlices[i].url !== undefined) {
+ this.url_ = this.subSlices[i].url;
+ }
+ }
+ }
+ this.isUrlComputed_ = true;
+ return this.url_;
+ },
+
+ get byteCount() {
+ if (this.byteCount_ !== undefined) {
+ return this.byteCount_;
+ }
+
+ this.byteCount_ = 0;
+ if ((this.originalTitle === 'URL_REQUEST_JOB_FILTERED_BYTES_READ' ||
+ this.originalTitle === 'URL_REQUEST_JOB_BYTES_READ') &&
+ this.args !== undefined && this.args.params !== undefined &&
+ this.args.params.byte_count !== undefined) {
+ this.byteCount_ = this.args.params.byte_count;
+ }
+ for (let i = 0; i < this.subSlices.length; i++) {
+ this.byteCount_ += this.subSlices[i].byteCount;
+ }
+ return this.byteCount_;
+ }
+ };
+
+ AsyncSlice.subTypes.register(
+ NetAsyncSlice,
+ {
+ categoryParts: ['netlog', 'disabled-by-default-netlog']
+ });
+
+ return {
+ NetAsyncSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice_test.html
new file mode 100644
index 00000000000..13f2a938189
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/net/net_async_slice_test.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/extras/net/net.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const NetAsyncSlice = tr.e.net.NetAsyncSlice;
+
+ test('basic', function() {
+ const s = new NetAsyncSlice('netlog', 'HTTP_STREAM_JOB', 7, 0, {});
+ s.duration = 100;
+
+ assert.strictEqual(
+ AsyncSlice.subTypes.getConstructor('netlog', 'HTTP_STREAM_JOB'),
+ NetAsyncSlice);
+ assert.strictEqual(s.viewSubGroupTitle, 'NetLog');
+ });
+
+ test('import', function() {
+ const events = [
+ {name: 'HTTP_STREAM_JOB', args: {}, pid: 1, ts: 100, cat: 'netlog', tid: 2, ph: 'b', id: 71}, // @suppress longLineCheck
+ {name: 'HTTP_STREAM_JOB', args: {}, pid: 1, ts: 200, cat: 'netlog', tid: 2, ph: 'e', id: 71} // @suppress longLineCheck
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents([events]);
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ assert.strictEqual(t2.asyncSliceGroup.length, 1);
+ assert.instanceOf(t2.asyncSliceGroup.slices[0], NetAsyncSlice);
+ });
+
+ test('ExposeURLBasic', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1,
+ {params: {url: 'https://google.com'},
+ source_type: 'b'}, 0, true);
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ // URL is exposed as the title of the parent slice.
+ assert.strictEqual(slice.title, 'https://google.com');
+ assert.strictEqual(slice.url, 'https://google.com');
+ });
+
+ test('ExposeURLNested', function() {
+ const slice = new NetAsyncSlice(
+ '', 'a', 0, 1, {params: {}, source_type: 'HELLO'}, 1, true);
+ const childSlice = new NetAsyncSlice('', 'b', 0, 1,
+ {params: {url: 'http://test.url'}});
+ slice.subSlices = [childSlice];
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ assert.isFalse(childSlice.isTopLevel);
+ // URL is exposed as the title of the parent slice.
+ assert.strictEqual(slice.title, 'http://test.url');
+ assert.strictEqual(slice.title, 'http://test.url');
+ assert.strictEqual(childSlice.title, 'b');
+ assert.strictEqual(childSlice.url, 'http://test.url');
+ });
+
+ test('ExposeURLNestedNoURL', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1, {params: {}}, 1, true);
+ const childSlice = new NetAsyncSlice('', 'b', 0, 1, {params: {}});
+ slice.subSlices = [childSlice];
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ assert.isFalse(childSlice.isTopLevel);
+ // URL is exposed as the title of the parent slice.
+ assert.strictEqual(slice.title, 'a');
+ assert.strictEqual(slice.url, undefined);
+ assert.strictEqual(childSlice.title, 'b');
+ assert.strictEqual(childSlice.url, undefined);
+ });
+
+ test('ExposeURLNestedBothChildrenHaveURL', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1, {params: {}}, 1, true);
+ const childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+ {params: {url: 'http://test.url.net'}});
+ const childSlice2 = new NetAsyncSlice('', 'c', 0, 1,
+ {params: {url: 'http://test.url.com'}});
+ slice.subSlices = [childSlice1, childSlice2];
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ assert.isFalse(childSlice1.isTopLevel);
+ assert.isFalse(childSlice2.isTopLevel);
+ // Parent should take the first url param that it finds.
+ assert.strictEqual(slice.title, 'http://test.url.net');
+ assert.strictEqual(childSlice1.title, 'b');
+ assert.strictEqual(childSlice2.title, 'c');
+ });
+
+ test('ExposeURLNestedBothParentAndChildHaveURL', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1,
+ {params: {url: 'parent123.url.com'}}, 1,
+ true);
+ const childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+ {params: {url: 'http://test.url.net'}});
+ const childSlice2 = new NetAsyncSlice('', 'c', 0, 1);
+
+ slice.subSlices = [childSlice1, childSlice2];
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ assert.isFalse(childSlice1.isTopLevel);
+ assert.isFalse(childSlice2.isTopLevel);
+ // Parent should take its own url param if there is one.
+ assert.strictEqual(slice.title, 'parent123.url.com');
+ assert.strictEqual(childSlice1.title, 'b');
+ assert.strictEqual(childSlice2.title, 'c');
+ });
+
+ test('ExposeURLDoNotComputeUrlTwice', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1, {params: {}}, 1, true);
+ const childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+ {params: {url: 'http://test.url.net'}});
+ const childSlice2 = new NetAsyncSlice('', 'c', 0, 1);
+
+ slice.subSlices = [childSlice1, childSlice2];
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ assert.isFalse(childSlice1.isTopLevel);
+ assert.isFalse(childSlice2.isTopLevel);
+ // Parent should take its child's url param.
+ assert.strictEqual(slice.title, 'http://test.url.net');
+ assert.strictEqual(childSlice1.title, 'b');
+ assert.strictEqual(childSlice2.title, 'c');
+ // Now if we change the url param of the child, the parent's title should
+ // remain the same. We do not recompute.
+ childSlice1.args.params.url = 'example.com';
+ assert.strictEqual(slice.title, 'http://test.url.net');
+ assert.strictEqual(childSlice1.title, 'b');
+ assert.strictEqual(childSlice2.title, 'c');
+ });
+
+ test('ExposeSourceTypeAsTitle', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1,
+ {params: {}, source_type: 'UDP_SOCKET'}, 1,
+ true);
+ const childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+ {params: {}, source_type: 'SOCKET'});
+ const childSlice2 = new NetAsyncSlice('', 'c', 0, 1);
+
+ slice.subSlices = [childSlice1, childSlice2];
+ // Make sure isTopLevel is populated in the constructor.
+ assert.isTrue(slice.isTopLevel);
+ assert.isFalse(childSlice1.isTopLevel);
+ assert.isFalse(childSlice2.isTopLevel);
+ // Parent should take its own source_type.
+ assert.strictEqual(slice.title, 'UDP_SOCKET');
+ assert.strictEqual(childSlice1.title, 'b');
+ assert.strictEqual(childSlice2.title, 'c');
+ });
+
+ test('ByteCountBasic', function() {
+ const slice = new NetAsyncSlice('', 'URL_REQUEST_JOB_BYTES_READ', 0, 1,
+ {params: {byte_count: 12}}, 0, true);
+ assert.strictEqual(slice.byteCount, 12);
+ });
+
+ test('NoByteCount', function() {
+ const slice = new NetAsyncSlice('', 'a', 0, 1, {}, 0, true);
+ assert.strictEqual(slice.byteCount, 0);
+ });
+
+ test('ByteCountNested', function() {
+ const slice = new NetAsyncSlice('', 'URL_REQUEST_JOB_FILTERED_BYTES_READ',
+ 0, 1, {params: {byte_count: 12}}, 0, true);
+ const childSlice = new NetAsyncSlice('', 'URL_REQUEST_JOB_BYTES_READ', 0, 1,
+ {params: {byte_count: 50}});
+ slice.subSlices = [childSlice];
+ assert.strictEqual(slice.byteCount, 62);
+ assert.strictEqual(childSlice.byteCount, 50);
+ });
+
+ test('ByteCountTwoChildren', function() {
+ const slice = new NetAsyncSlice(
+ '', 'URL_REQUEST_JOB_FILTERED_BYTES_READ', 0, 1,
+ {params: {byte_count: 12}}, 0, true);
+ const childSlice1 = new NetAsyncSlice(
+ '', 'URL_REQUEST_JOB_BYTES_READ', 0, 1, {params: {byte_count: 50}});
+ const childSlice2 = new NetAsyncSlice(
+ '', 'URL_REQUEST_JOB_BYTES_READ', 0, 1, {params: {byte_count: 20}});
+ slice.subSlices = [childSlice1, childSlice2];
+ assert.strictEqual(slice.byteCount, 82);
+ assert.strictEqual(childSlice1.byteCount, 50);
+ assert.strictEqual(childSlice2.byteCount, 20);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/OWNERS b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/OWNERS
new file mode 100644
index 00000000000..25b1b8fc5e8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/OWNERS
@@ -0,0 +1,2 @@
+dskiba@chromium.org
+erikchen@chromium.org
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/__init__.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/__init__.py
new file mode 100644
index 00000000000..a22a6ee39a9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2017 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.
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/addr2line-pdb.exe.sha1 b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/addr2line-pdb.exe.sha1
new file mode 100644
index 00000000000..7b6ab903155
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/addr2line-pdb.exe.sha1
@@ -0,0 +1 @@
+4e236e6ea0d9f95067eafcb6a49fa85960253cd7 \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/linux_trace_v2_presymbolization.json.gz.sha1 b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/linux_trace_v2_presymbolization.json.gz.sha1
new file mode 100644
index 00000000000..a9a23ea8f9a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/linux_trace_v2_presymbolization.json.gz.sha1
@@ -0,0 +1 @@
+cf9861aae9dc507e779fe7b34a810848d1c1b288 \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/mac_trace_v2_presymbolization.json.gz.sha1 b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/mac_trace_v2_presymbolization.json.gz.sha1
new file mode 100644
index 00000000000..bdd769b990d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/mac_trace_v2_presymbolization.json.gz.sha1
@@ -0,0 +1 @@
+4bbc69a25b711b5a9488936382298063e240ffa7 \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v1_presymbolization.json.gz.sha1 b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v1_presymbolization.json.gz.sha1
new file mode 100644
index 00000000000..0243761389e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v1_presymbolization.json.gz.sha1
@@ -0,0 +1 @@
+4f63b242a7bab2266b76bee3e528c7d7c1e3882f \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v2_presymbolization.json.gz.sha1 b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v2_presymbolization.json.gz.sha1
new file mode 100644
index 00000000000..29027e5dd72
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/data/windows_trace_v2_presymbolization.json.gz.sha1
@@ -0,0 +1 @@
+520410cd56479aa54321eee65be5676acc2e8ed7 \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace.py
new file mode 100755
index 00000000000..e90b9ce5e30
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace.py
@@ -0,0 +1,1707 @@
+#!/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.
+# pylint: disable=too-many-lines
+
+"""
+This script processes trace files and symbolizes stack frames generated by
+Chrome's native heap profiler. This script assumes that the Chrome binary
+referenced in the trace contains symbols, and is the same binary used to emit
+the trace.
+
+=== Overview ===
+
+Trace file is essentially a giant JSON array of dictionaries (events).
+Events have some predefined keys (e.g. 'pid'), but otherwise are free to
+have anything inside. Trace file contains events from all Chrome processes
+that were sampled during tracing period.
+
+This script cares only about memory dump events generated with memory-infra
+category enabled.
+
+When Chrome native heap profiling is enabled, some memory dump events
+include the following extra information:
+
+* (Per allocator) Information about live allocations at the moment of the
+ memory dump (the information includes backtraces, types / categories,
+ sizes, and counts of allocations). There are several allocators in
+ Chrome: e.g. malloc, blink_gc, partition_alloc.
+
+* (Per process) Stack frame tree of all functions that called allocators
+ above.
+
+This script does the following:
+
+1. Parses the given trace file (loads JSON).
+2. Finds memory dump events and parses stack frame tree for each process.
+3. Finds stack frames that have PC addresses instead of function names.
+4. Symbolizes PCs and modifies loaded JSON.
+5. Writes modified JSON back to the file.
+
+The script supports trace files from the following platforms:
+ * Android (the script itself must be run on Linux)
+ * Linux
+ * macOS
+ * Windows
+
+Important note - the script doesn't check that it symbolizes same binaries
+that were used at the time trace was taken. I.e. if you take a trace, change
+and rebuild Chrome binaries, the script will blindly use the new binaries.
+
+=== Details ===
+
+There are two formats of heap profiler information: legacy and modern. The
+main differences relevant to this script are:
+
+* In the modern format the stack frame tree, type name mapping, and string
+ mapping nodes are dumped incrementally. These nodes are dumped in each
+ memory dump event and carry updates that occurred since the last event.
+
+ For example, let's say that when the first memory dump event is generated
+ we only know about a function foo() (called from main()) allocating objects
+ of type "int":
+
+ {
+ "args": {
+ "dumps": {
+ "heaps_v2": {
+ "maps": {
+ "nodes": [
+ { "id": 1, "name_sid": 1 },
+ { "id": 2, "parent": 1, "name_sid": 3 },
+ ],
+ "types": [
+ { "id": 1, "name_sid": 2 },
+ ],
+ "strings": [
+ { "id": 1, "string": "main()" },
+ { "id": 2, "string": "int" },
+ { "id": 3, "string": "foo()" },
+ ]
+ },
+ "allocators": { ...live allocations per allocator... },
+ ...
+ },
+ ...
+ }
+ },
+ ...
+ }
+
+ Here:
+ * 'nodes' node encodes stack frame tree
+ * 'types' node encodes type name mappings
+ * 'strings' node encodes string mapping (explained below)
+
+ Then, by the time second memory dump even is generated, we learn about
+ bar() (called from main()), which also allocated "int" objects. Only the
+ new information is dumped, i.e. bar() stack frame:
+
+ {
+ "args": {
+ "dumps": {
+ "heaps_v2": {
+ "maps": {
+ "nodes": [
+ { "id": 2, "parent": 1, "name_sid": 4 },
+ ],
+ "types": [],
+ "strings": [
+ { "id": 4, "string": "bar()" },
+ ]
+ },
+ "allocators": { ...live allocations per allocator... },
+ ...
+ },
+ ...
+ }
+ },
+ ...
+ }
+
+ Note that 'types' node is empty, since there were no updates. All three
+ nodes ('nodes', types', and 'strings') can be empty if there were no updates
+ to them.
+
+ For simplicity, when the script updates incremental nodes, it puts updated
+ content in the first node, and clears all others. I.e. the following stack
+ frame nodes:
+
+ 'nodes': [
+ { "id": 1, "name_sid": 1 },
+ { "id": 2, "parent": 1, "name_sid": 2 },
+ ]
+ 'nodes': [
+ { "id": 3, "parent": 2, "name_sid": 3 },
+ ]
+ 'nodes': [
+ { "id": 4, "parent": 3, "name_sid": 4 },
+ { "id": 5, "parent": 1, "name_sid": 5 },
+ ]
+
+ After symbolization are written as:
+
+ 'nodes': [
+ { "id": 1, "name_sid": 1 },
+ { "id": 2, "parent": 1, "name_sid": 2 },
+ { "id": 3, "parent": 2, "name_sid": 3 },
+ { "id": 4, "parent": 3, "name_sid": 4 },
+ { "id": 5, "parent": 1, "name_sid": 5 },
+ ]
+ 'nodes': []
+ 'nodes': []
+
+
+* In contrast, in the legacy format stack frame tree and type mappings are
+ dumped separately from memory dump events, once per process.
+
+ Here is how trace file with two memory dump events looks like in the
+ legacy format:
+
+ {
+ "args": {
+ "dumps": {
+ "heaps": { ...live allocations per allocator... },
+ ...
+ }
+ },
+ ...
+ }
+
+ {
+ "args": {
+ "dumps": {
+ "heaps": { ...live allocations per allocator... },
+ ...
+ }
+ },
+ ...
+ }
+
+ {
+ "args": {
+ "typeNames": {
+ 1: "int",
+ }
+ },
+ "cat": "__metadata",
+ "name": "typeNames",
+ ...
+ }
+
+ {
+ "args": {
+ "stackFrames": {
+ 1: { "name": "main" },
+ 2: { "name": "foo", "parent": 1 },
+ 3: { "name": "bar", "parent": 1 },
+ }
+ },
+ "cat": "__metadata",
+ "name": "stackFrames",
+ ...
+ }
+
+
+* Another change in the modern format is 'strings' node, which was added
+ to deduplicate stack frame names (mainly for trace file size reduction).
+ For consistency 'types' node also uses string mappings.
+
+
+See crbug.com/708930 for more information about the modern format.
+"""
+
+from __future__ import print_function
+
+import argparse
+import bisect
+import collections
+import gzip
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tarfile
+import zipfile
+import tempfile
+
+import symbols.elf_symbolizer as elf_symbolizer
+
+from tracing.extras.symbolizer import symbolize_trace_atos_regex
+from tracing.extras.symbolizer import symbolize_trace_macho_reader
+
+import py_utils.cloud_storage as cloud_storage
+
+_UNNAMED_FILE = 'unnamed'
+
+
+class NodeWrapper(object):
+ """Wraps an event data node(s).
+
+ A node is a reference into a trace event JSON. Wrappers parse nodes to
+ provide convenient APIs and update nodes when asked to propagate changes
+ back (see ApplyModifications() below).
+
+ Here is an example of legacy metadata event that contains stack frame tree:
+
+ {
+ "args": {
+ "stackFrames": { ... }
+ },
+ "cat": "__metadata",
+ "name": "stackFrames",
+ "ph": "M",
+ ...
+ }
+
+ When this event is encountered, a reference to the "stackFrames" dictionary
+ is obtained and passed down to a specific wrapped class, which knows how to
+ parse / update the dictionary.
+
+ There are two parsing patterns depending on whether node is serialized
+ incrementally:
+
+ * If node is not incremental, then parsing is done by __init__(),
+ see MemoryMap for an example.
+
+ * If node is incremental, then __init__() does nothing, and instead
+ ParseNext() method is called when next node (from a next event) is
+ encountered.
+
+ Some wrappers can also modify nodes they parsed. In such cases they have
+ additional APIs:
+
+ * 'modified' flag, which indicates whether the wrapper was changed.
+
+ * 'ApplyModifications' method, which propagates changes made to the wrapper
+ back to nodes. Successful invocation of ApplyModifications() resets
+ 'modified' flag.
+
+ """
+ pass
+
+
+class MemoryMap(NodeWrapper):
+ """Wraps 'process_mmaps' node.
+
+ 'process_mmaps' node contains information about file mappings.
+
+ "process_mmaps": {
+ "vm_regions": [
+ {
+ "mf": "<file_path>",
+ "sa": "<start_address>",
+ "sz": "<size>",
+ ...
+ },
+ ...
+ ]
+ }
+ """
+
+ class Region(object):
+ def __init__(self, start_address, size, file_path, file_offset):
+ self._start_address = start_address
+ self._size = size
+ self._file_path = file_path if file_path else _UNNAMED_FILE
+ self._file_offset = file_offset
+ self._code_id = None
+
+ @property
+ def start_address(self):
+ return self._start_address
+
+ @property
+ def end_address(self):
+ return self._start_address + self._size
+
+ @property
+ def size(self):
+ return self._size
+
+ @property
+ def code_id(self):
+ return self._code_id
+
+ @property
+ def file_path(self):
+ return self._file_path
+
+ @property
+ def file_offset(self):
+ return self._file_offset
+
+ @file_offset.setter
+ def file_offset(self, value):
+ self._file_offset = value
+
+ def __cmp__(self, other):
+ if isinstance(other, type(self)):
+ other_start_address = other._start_address
+ elif isinstance(other, (long, int)):
+ other_start_address = other
+ else:
+ raise Exception('Cannot compare with %s' % type(other))
+ if self._start_address < other_start_address:
+ return -1
+ elif self._start_address > other_start_address:
+ return 1
+ else:
+ return 0
+
+ def __repr__(self):
+ return 'Region(0x{:X} - 0x{:X}, {})'.format(
+ self.start_address, self.end_address, self.file_path)
+
+ def __init__(self, process_mmaps_node):
+ regions = []
+ for region_node in process_mmaps_node['vm_regions']:
+ file_offset = long(region_node['fo'], 16) if 'fo' in region_node else 0
+ file_path = region_node['mf'].replace(" (deleted)", "")
+ region = self.Region(long(region_node['sa'], 16),
+ long(region_node['sz'], 16),
+ file_path,
+ file_offset)
+ # Keep track of code-identifier when present.
+ if 'ts' in region_node and 'sz' in region_node:
+ region._code_id = "%08X%X" % (long(region_node['ts'], 16), region.size)
+ regions.append(region)
+
+ regions.sort()
+
+ # Iterate through the regions in order. If two regions border each other,
+ # and have the same file_path [or at least one of them is unnamed], but the
+ # latter region has file_offset == 0, then set the file_offset of the latter
+ # region to be former_region.file_offset + former_region.size.
+ #
+ # Rationale: Semantically, we want file_offset to be the distance between
+ # the base address of the region and the base address of the module [which
+ # breakpad symbols use as a relative-zero]. Technically, this is called
+ # slide (macOS) and load bias (ELF). See
+ # https://chromium-review.googlesource.com/c/chromium/src/+/568413#message-01cf829007882eea8c9d3403871814c4f336d16d
+ # for more details.
+ # Chrome does not emit slide or load bias. This usually doesn't make a
+ # difference because the TEXT segment usually has a slide or load bias of 0.
+ # In the rare cases that it doesn't [observed on Chrome Linux official
+ # builds], this heuristic correctly computes it.
+ #
+ # This hack relies on the assumption that all regions with the same name are
+ # mapped from the same file. Each region's file_offset should be computed
+ # based on the first region's base address.
+ last_region_with_file_path = {}
+ for region in regions:
+ if (region.file_path in last_region_with_file_path and
+ region.file_offset == 0):
+ region.file_offset = (
+ region.start_address -
+ last_region_with_file_path[region.file_path].start_address)
+ if (region.file_path and
+ region.file_path not in last_region_with_file_path):
+ last_region_with_file_path[region.file_path] = region
+
+ # Copy regions without duplicates and check for overlaps.
+ self._regions = []
+ previous_region = None
+ for region in regions:
+ if previous_region is not None:
+ if region == previous_region:
+ continue
+ if region.start_address < previous_region.end_address:
+ print('Regions {} and {} overlap.'.format(previous_region, region))
+ previous_region = region
+ self._regions.append(region)
+
+ @property
+ def regions(self):
+ return self._regions
+
+ def FindRegion(self, address):
+ """Finds region containing |address|. Returns None if none found."""
+
+ region_index = bisect.bisect_right(self._regions, address) - 1
+ if region_index >= 0:
+ region = self._regions[region_index]
+ if address >= region.start_address and address < region.end_address:
+ return region
+ return None
+
+
+class UnsupportedHeapDumpVersionError(Exception):
+ """Helper exception class to signal unsupported heap dump version."""
+
+ def __init__(self, version):
+ message = 'Unsupported heap dump version: {}'.format(version)
+ super(UnsupportedHeapDumpVersionError, self).__init__(message)
+
+
+class StringMap(NodeWrapper):
+ """Wraps all 'strings' nodes for a process.
+
+ 'strings' node contains incremental mappings between integer ids and strings.
+
+ "strings": [
+ {
+ "id": <string_id>,
+ "string": <string>
+ },
+ ...
+ ]
+ """
+
+ def __init__(self):
+ self._modified = False
+ self._strings_nodes = []
+ self._string_by_id = {}
+ self._id_by_string = {}
+ self._max_string_id = 0
+
+ @property
+ def modified(self):
+ """Returns True if the wrapper was modified (see NodeWrapper)."""
+ return self._modified
+
+ @property
+ def string_by_id(self):
+ return self._string_by_id
+
+ def ParseNext(self, heap_dump_version, strings_node):
+ """Parses and interns next node (see NodeWrapper)."""
+
+ if heap_dump_version != Trace.HEAP_DUMP_VERSION_1:
+ raise UnsupportedHeapDumpVersionError(heap_dump_version)
+
+ self._strings_nodes.append(strings_node)
+ for string_node in strings_node:
+ self._Insert(string_node['id'], string_node['string'])
+
+ def Clear(self):
+ """Clears all string mappings."""
+ if self._string_by_id:
+ self._modified = True
+
+ self._string_by_id = {}
+ self._id_by_string = {}
+ self._max_string_id = 0
+
+ def AddString(self, string):
+ """Adds a string (if it doesn't exist) and returns its integer id."""
+ string_id = self._id_by_string.get(string)
+ if string_id is None:
+ string_id = self._max_string_id + 1
+ self._Insert(string_id, string)
+ self._modified = True
+ return string_id
+
+ def ApplyModifications(self):
+ """Propagates modifications back to nodes (see NodeWrapper)."""
+ if not self.modified:
+ return
+
+ assert self._strings_nodes, 'no nodes'
+
+ # Serialize into the first node, and clear all others.
+
+ for strings_node in self._strings_nodes:
+ del strings_node[:]
+ strings_node = self._strings_nodes[0]
+ for string_id, string in self._string_by_id.items():
+ strings_node.append({'id': string_id, 'string': string})
+
+ self._modified = False
+
+ def _Insert(self, string_id, string):
+ self._id_by_string[string] = string_id
+ self._string_by_id[string_id] = string
+ self._max_string_id = max(self._max_string_id, string_id)
+
+
+class TypeNameMap(NodeWrapper):
+ """Wraps all 'types' nodes for a process.
+
+ 'types' nodes encode mappings between integer type ids and integer
+ string ids (from 'strings' nodes).
+
+ "types": [
+ {
+ "id": <type_id>,
+ "name_sid": <name_string_id>
+ }
+ ...
+ ]
+
+ For simplicity string ids are translated into strings during parsing,
+ and then translated back to ids in ApplyModifications().
+ """
+ def __init__(self):
+ self._modified = False
+ self._type_name_nodes = []
+ self._name_by_id = {}
+ self._id_by_name = {}
+ self._max_type_id = 0
+
+ @property
+ def modified(self):
+ """Returns True if the wrapper was modified (see NodeWrapper)."""
+ return self._modified
+
+ @property
+ def name_by_id(self):
+ """Returns {id -> name} dict (must not be changed directly)."""
+ return self._name_by_id
+
+ def ParseNext(self, heap_dump_version, type_name_node, string_map):
+ """Parses and interns next node (see NodeWrapper).
+
+ |string_map| - A StringMap object to use to translate string ids
+ to strings.
+ """
+ if heap_dump_version != Trace.HEAP_DUMP_VERSION_1:
+ raise UnsupportedHeapDumpVersionError(heap_dump_version)
+
+ self._type_name_nodes.append(type_name_node)
+ for type_node in type_name_node:
+ self._Insert(type_node['id'],
+ string_map.string_by_id[type_node['name_sid']])
+
+ def AddType(self, type_name):
+ """Adds a type name (if it doesn't exist) and returns its id."""
+ type_id = self._id_by_name.get(type_name)
+ if type_id is None:
+ type_id = self._max_type_id + 1
+ self._Insert(type_id, type_name)
+ self._modified = True
+ return type_id
+
+ def ApplyModifications(self, string_map, force=False):
+ """Propagates modifications back to nodes.
+
+ |string_map| - A StringMap object to use to translate strings to ids.
+ |force| - Whether to propagate changes regardless of 'modified' flag.
+ """
+ if not self.modified and not force:
+ return
+
+ assert self._type_name_nodes, 'no nodes'
+
+ # Serialize into the first node, and clear all others.
+
+ for types_node in self._type_name_nodes:
+ del types_node[:]
+ types_node = self._type_name_nodes[0]
+ for type_id, type_name in self._name_by_id.items():
+ types_node.append({
+ 'id': type_id,
+ 'name_sid': string_map.AddString(type_name)})
+
+ self._modified = False
+
+ def _Insert(self, type_id, type_name):
+ self._id_by_name[type_name] = type_id
+ self._name_by_id[type_id] = type_name
+ self._max_type_id = max(self._max_type_id, type_id)
+
+
+class StackFrameMap(NodeWrapper):
+ """ Wraps stack frame tree nodes for a process.
+
+ For the legacy format this wrapper expects a single 'stackFrames' node
+ (which comes from metadata event):
+
+ "stackFrames": {
+ "<frame_id>": {
+ "name": "<frame_name>"
+ "parent": "<parent_frame_id>"
+ },
+ ...
+ }
+
+ For the modern format this wrapper expects several 'nodes' nodes:
+
+ "nodes": [
+ {
+ "id": <frame_id>,
+ "parent": <parent_frame_id>,
+ "name_sid": <name_string_id>
+ },
+ ...
+ ]
+
+ In both formats frame name is a string. Native heap profiler generates
+ specially formatted frame names (e.g. "pc:10eb78dba") for function
+ addresses (PCs). Inner Frame class below parses name and extracts PC,
+ if it's there.
+ """
+ class Frame(object):
+ def __init__(self, frame_id, name, parent_frame_id):
+ self._modified = False
+ self._id = frame_id
+ self._name = name
+ self._pc = self._ParsePC(name)
+ self._parent_id = parent_frame_id
+ self._ext = None
+
+ @property
+ def modified(self):
+ """Returns True if the frame was modified.
+
+ For example changing frame's name sets this flag (since the change
+ needs to be propagated back to nodes).
+ """
+ return self._modified
+
+ @property
+ def id(self):
+ """Frame id (integer)."""
+ return self._id
+
+ @property
+ def pc(self):
+ """Parsed (integer) PC of the frame, or None."""
+ return self._pc
+
+ @property
+ def name(self):
+ """Name of the frame (see above)."""
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ """Changes the name. Doesn't affect value of |pc|."""
+ self._modified = True
+ self._name = value
+
+ @property
+ def parent_id(self):
+ """Parent frame id (integer)."""
+ return self._parent_id
+
+ _PC_TAG = 'pc:'
+
+ def _ParsePC(self, name):
+ if not name.startswith(self._PC_TAG):
+ return None
+ return long(name[len(self._PC_TAG):], 16)
+
+ def _ClearModified(self):
+ self._modified = False
+
+ def __init__(self):
+ self._modified = False
+ self._heap_dump_version = None
+ self._stack_frames_nodes = []
+ self._frame_by_id = {}
+
+ @property
+ def modified(self):
+ """Returns True if the wrapper or any of its frames were modified."""
+ return (self._modified or
+ any(f.modified for f in self._frame_by_id.values()))
+
+ @property
+ def frame_by_id(self):
+ """Returns {id -> frame} dict (must not be modified directly)."""
+ return self._frame_by_id
+
+ def ParseNext(self, heap_dump_version, stack_frames_node, string_map):
+ """Parses the next stack frames node (see NodeWrapper).
+
+ For the modern format |string_map| is used to translate string ids
+ to strings.
+ """
+
+ frame_by_id = {}
+ if heap_dump_version == Trace.HEAP_DUMP_VERSION_LEGACY:
+ if self._stack_frames_nodes:
+ raise Exception('Legacy stack frames node is expected only once.')
+ for frame_id, frame_node in stack_frames_node.items():
+ frame = self.Frame(frame_id,
+ frame_node['name'],
+ frame_node.get('parent'))
+ frame_by_id[frame.id] = frame
+ else:
+ if heap_dump_version != Trace.HEAP_DUMP_VERSION_1:
+ raise UnsupportedHeapDumpVersionError(heap_dump_version)
+ for frame_node in stack_frames_node:
+ frame = self.Frame(frame_node['id'],
+ string_map.string_by_id[frame_node['name_sid']],
+ frame_node.get('parent'))
+ frame_by_id[frame.id] = frame
+
+ self._heap_dump_version = heap_dump_version
+ self._stack_frames_nodes.append(stack_frames_node)
+
+ self._frame_by_id.update(frame_by_id)
+
+ def ApplyModifications(self, string_map, force=False):
+ """Applies modifications back to nodes (see NodeWrapper)."""
+
+ if not self.modified and not force:
+ return
+
+ assert self._stack_frames_nodes, 'no nodes'
+ if self._heap_dump_version == Trace.HEAP_DUMP_VERSION_LEGACY:
+ assert string_map is None, \
+ 'string_map should not be used with the legacy format'
+
+ # Serialize frames into the first node, clear all others.
+
+ for frames_node in self._stack_frames_nodes:
+ if self._heap_dump_version == Trace.HEAP_DUMP_VERSION_LEGACY:
+ frames_node.clear()
+ else:
+ del frames_node[:]
+
+ frames_node = self._stack_frames_nodes[0]
+ for frame in self._frame_by_id.values():
+ if self._heap_dump_version == Trace.HEAP_DUMP_VERSION_LEGACY:
+ frame_node = {'name': frame.name}
+ frames_node[frame.id] = frame_node
+ else:
+ frame_node = {
+ 'id': frame.id,
+ 'name_sid': string_map.AddString(frame.name)
+ }
+ frames_node.append(frame_node)
+ if frame.parent_id is not None:
+ frame_node['parent'] = frame.parent_id
+ frame._ClearModified()
+
+ self._modified = False
+
+
+class Trace(NodeWrapper):
+ """Wrapper for the root trace node (i.e. the trace JSON itself).
+
+ This wrapper parses select nodes from memory-infra events and groups
+ parsed data per-process (see inner Process class below).
+ """
+
+ # Indicates legacy heap dump format.
+ HEAP_DUMP_VERSION_LEGACY = 'Legacy'
+
+ # Indicates variation of a modern heap dump format.
+ HEAP_DUMP_VERSION_1 = 1
+
+ class Process(object):
+ """Collection of per-process data and wrappers."""
+
+ def __init__(self, pid):
+ self._pid = pid
+ self._name = None
+ self._memory_map = None
+ self._stack_frame_map = StackFrameMap()
+ self._type_name_map = TypeNameMap()
+ self._string_map = StringMap()
+ self._heap_dump_version = None
+
+ @property
+ def modified(self):
+ return self._stack_frame_map.modified or self._type_name_map.modified
+
+ @property
+ def pid(self):
+ return self._pid
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def unique_name(self):
+ """Returns string that includes both process name and its pid."""
+ name = self._name if self._name else 'UnnamedProcess'
+ return '{}({})'.format(name, self._pid)
+
+ @property
+ def memory_map(self):
+ return self._memory_map
+
+ @property
+ def stack_frame_map(self):
+ return self._stack_frame_map
+
+ @property
+ def type_name_map(self):
+ return self._type_name_map
+
+ def ApplyModifications(self):
+ """Calls ApplyModifications() on contained wrappers."""
+ if self._heap_dump_version == Trace.HEAP_DUMP_VERSION_LEGACY:
+ self._stack_frame_map.ApplyModifications(None)
+ else:
+ if self._stack_frame_map.modified or self._type_name_map.modified:
+ self._string_map.Clear()
+ self._stack_frame_map.ApplyModifications(self._string_map, force=True)
+ self._type_name_map.ApplyModifications(self._string_map, force=True)
+ self._string_map.ApplyModifications()
+
+ def __init__(self, trace_node):
+ self._trace_node = trace_node
+ self._processes = []
+ self._heap_dump_version = None
+ self._os = None
+ self._version = None
+ self._is_chromium = True
+ self._is_64bit = False
+ self._is_win = False
+ self._is_mac = False
+ self._is_linux = False
+ self._is_cros = False
+ self._is_android = False
+
+ # Misc per-process information needed only during parsing.
+ class ProcessExt(object):
+ def __init__(self, pid):
+ self.process = Trace.Process(pid)
+ self.mapped_entry_names = set()
+ self.process_mmaps_node = None
+ self.seen_strings_node = False
+
+ process_ext_by_pid = {}
+
+ if isinstance(trace_node, dict):
+ metadata = trace_node['metadata']
+ product_version = metadata['product-version']
+ # product-version has the form "Chrome/60.0.3103.0"
+ self._version = product_version.split('/', 1)[-1]
+ self._os = metadata['os-name']
+
+ self._is_win = re.search('windows', metadata['os-name'], re.IGNORECASE)
+ self._is_mac = re.search('mac', metadata['os-name'], re.IGNORECASE)
+ self._is_linux = re.search('linux', metadata['os-name'], re.IGNORECASE)
+ self._is_cros = re.search('cros', metadata['os-name'], re.IGNORECASE)
+ self._is_android = re.search(
+ 'android', metadata['os-name'], re.IGNORECASE)
+
+ self._is_64bit = (
+ re.search('x86_64', metadata['os-arch'], re.IGNORECASE) and
+ not re.search('WOW64', metadata['user-agent'], re.IGNORECASE))
+
+ # Android traces produced via 'chrome://inspect/?tracing#devices' are
+ # just list of events.
+ events = trace_node if isinstance(trace_node, list) \
+ else trace_node['traceEvents']
+ for event in events:
+ name = event.get('name')
+ if not name:
+ continue
+
+ pid = event['pid']
+ process_ext = process_ext_by_pid.get(pid)
+ if process_ext is None:
+ process_ext = ProcessExt(pid)
+ process_ext_by_pid[pid] = process_ext
+ process = process_ext.process
+
+ phase = event['ph']
+ if phase == self._EVENT_PHASE_METADATA:
+ if name == 'process_name':
+ process._name = event['args']['name']
+ elif name == 'stackFrames':
+ process._stack_frame_map.ParseNext(
+ self._UseHeapDumpVersion(self.HEAP_DUMP_VERSION_LEGACY),
+ event['args']['stackFrames'],
+ process._string_map)
+ elif phase == self._EVENT_PHASE_MEMORY_DUMP:
+ dumps = event['args']['dumps']
+ process_mmaps = dumps.get('process_mmaps')
+ if process_mmaps:
+ # We want the most recent memory map, so parsing happens later
+ # once we finished reading all events.
+ process_ext.process_mmaps_node = process_mmaps
+ heaps = dumps.get('heaps_v2')
+ if heaps:
+ version = self._UseHeapDumpVersion(heaps['version'])
+ maps = heaps.get('maps')
+ if maps:
+ process_ext.mapped_entry_names.update(maps.keys())
+ types = maps.get('types')
+ stack_frames = maps.get('nodes')
+ strings = maps.get('strings')
+ if (strings is None and (types or stack_frames)
+ and not process_ext.seen_strings_node):
+ # ApplyModifications() for TypeNameMap and StackFrameMap puts
+ # everything into the first node and depends on StringMap. So
+ # we need to make sure that 'strings' node is there if any of
+ # other two nodes present.
+ strings = []
+ maps['strings'] = strings
+ if strings is not None:
+ process_ext.seen_strings_node = True
+ process._string_map.ParseNext(version, strings)
+ if types:
+ process._type_name_map.ParseNext(
+ version, types, process._string_map)
+ if stack_frames:
+ process._stack_frame_map.ParseNext(
+ version, stack_frames, process._string_map)
+
+ self._processes = []
+ for pe in process_ext_by_pid.values():
+ pe.process._heap_dump_version = self._heap_dump_version
+ if pe.process_mmaps_node:
+ # Now parse the most recent memory map.
+ pe.process._memory_map = MemoryMap(pe.process_mmaps_node)
+ self._processes.append(pe.process)
+
+ @property
+ def node(self):
+ """Root node (that was passed to the __init__)."""
+ return self._trace_node
+
+ @property
+ def modified(self):
+ """Returns True if trace file needs to be updated.
+
+ Before writing trace JSON back to a file ApplyModifications() needs
+ to be called.
+ """
+ return any(p.modified for p in self._processes)
+
+ @property
+ def processes(self):
+ return self._processes
+
+ @property
+ def heap_dump_version(self):
+ return self._heap_dump_version
+
+ @property
+ def version(self):
+ return self._version
+
+ @property
+ def os(self):
+ return self._os
+
+ @property
+ def is_chromium(self):
+ return self._is_chromium
+
+ @is_chromium.setter
+ def is_chromium(self, new_value):
+ self._is_chromium = new_value
+
+ @property
+ def is_mac(self):
+ return self._is_mac
+
+ @property
+ def is_win(self):
+ return self._is_win
+
+ @property
+ def is_linux(self):
+ return self._is_linux
+
+ @property
+ def is_android(self):
+ return self._is_android
+
+ @property
+ def is_64bit(self):
+ return self._is_64bit
+
+ @property
+ def library_name(self):
+ return self._trace_node['metadata'].get('chrome-library-name')
+
+ def ApplyModifications(self):
+ """Propagates modifications back to the trace JSON."""
+ for process in self._processes:
+ process.ApplyModifications()
+ assert not self.modified, 'still modified'
+
+ # Relevant trace event phases from Chromium's
+ # src/base/trace_event/common/trace_event_common.h.
+ _EVENT_PHASE_METADATA = 'M'
+ _EVENT_PHASE_MEMORY_DUMP = 'v'
+
+ def _UseHeapDumpVersion(self, version):
+ if self._heap_dump_version is None:
+ self._heap_dump_version = version
+ return version
+ elif self._heap_dump_version != version:
+ raise Exception(
+ ("Inconsistent trace file: first saw '{}' heap dump version, "
+ "then '{}'.").format(self._heap_dump_version, version))
+ else:
+ return version
+
+
+class SymbolizableFile(object):
+ """Holds file path, addresses to symbolize and stack frames to update.
+
+ This class is a link between ELFSymbolizer and a trace file: it specifies
+ what to symbolize (addresses) and what to update with the symbolization
+ result (frames).
+ """
+ def __init__(self, file_path, code_id):
+ self.path = file_path
+ self.symbolizable_path = file_path # path to use for symbolization
+ self.code_id = code_id
+ self.frames_by_address = collections.defaultdict(list)
+ self.skip_symbolization = False
+ self.has_breakpad_symbols = False
+
+
+def ResolveSymbolizableFiles(processes):
+ """Resolves and groups PCs into list of SymbolizableFiles.
+
+ As part of the grouping process, this function resolves PC from each stack
+ frame to the corresponding mmap region. Stack frames that failed to resolve
+ are symbolized with '<unresolved>'.
+ """
+ symfile_by_path = {}
+ for process in processes:
+ if not process.memory_map:
+ continue
+ for frame in process.stack_frame_map.frame_by_id.values():
+ if frame.pc is None:
+ continue
+ region = process.memory_map.FindRegion(frame.pc)
+ if region is None:
+ frame.name = '<unresolved>'
+ continue
+
+ symfile = symfile_by_path.get(region.file_path)
+ if symfile is None:
+ file_path = region.file_path
+ symfile = SymbolizableFile(file_path, region.code_id)
+ symfile_by_path[symfile.path] = symfile
+
+ relative_pc = frame.pc - region.start_address + region.file_offset
+ symfile.frames_by_address[relative_pc].append(frame)
+
+ return symfile_by_path.values()
+
+
+def FindInSystemPath(binary_name):
+ paths = os.environ['PATH'].split(os.pathsep)
+ for path in paths:
+ binary_path = os.path.join(path, binary_name)
+ if os.path.isfile(binary_path):
+ return binary_path
+ return None
+
+
+class BreakpadSymbolsModule(object):
+ """Encapsulates Breakpad logic for symbols of a specific module."""
+
+ def __init__(self, filename):
+ super(BreakpadSymbolsModule, self).__init__()
+ self.filename = filename
+ self.files = []
+ self.symbols = {}
+ self.arch = None
+ self.debug_id = None
+ self.code_id = None
+ self.binary = None
+
+ def Parse(self):
+ # see: https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md
+ with open(self.filename) as fp:
+ for line in fp:
+ fragments = line.rstrip().split()
+ if fragments[0] == 'MODULE':
+ # MODULE mac x86_64 A7001116478B33F18FF9BEDE9F615F190 t
+ self.arch = fragments[2]
+ self.debug_id = fragments[3]
+ self.binary = ' '.join(fragments[4:])
+ elif fragments[0] == 'INFO' and fragments[1] == 'CODE_ID':
+ # INFO CODE_ID 595D00BD31F0000 chrome.dll
+ self.code_id = fragments[2]
+ elif fragments[0] == 'FILE':
+ # FILE 0 /b/c/b/mac64/src/out/Release/../../base/at_exit.cc
+ self.files.append(' '.join(fragments[2:]))
+ elif fragments[0] == 'PUBLIC':
+ # PUBLIC db60 0 base::mac::CallWithEHFrame(void () block_pointer)
+ self.symbols[int(fragments[1], 16)] = ' '.join(fragments[3:])
+ elif fragments[0] == 'FUNC':
+ # FUNC 567e0 264 0 Cr_z_fill_window_sse
+ self.symbols[int(fragments[1], 16)] = ' '.join(fragments[4:])
+
+
+class Symbolizer(object):
+ """Encapsulates platform-specific symbolization logic."""
+
+ def __init__(self, addr2line_executable):
+ self.is_mac = sys.platform == 'darwin'
+ self.is_win = sys.platform == 'win32'
+ if self.is_mac:
+ self.binary = 'atos'
+ self._matcher = symbolize_trace_atos_regex.AtosRegexMatcher()
+ elif self.is_win:
+ self.binary = 'addr2line-pdb.exe'
+ else:
+ self.binary = 'addr2line'
+
+ if addr2line_executable and os.path.isfile(addr2line_executable):
+ self.symbolizer_path = addr2line_executable
+ else:
+ self.symbolizer_path = FindInSystemPath(self.binary)
+
+ self.breakpad_modules = {}
+
+ def _SymbolizeLinuxAndAndroid(self, symfile):
+ def _SymbolizerCallback(sym_info, frames):
+ # Unwind inline chain to the top.
+ while sym_info.inlined_by:
+ sym_info = sym_info.inlined_by
+
+ symbolized_name = (sym_info.name if sym_info.name else
+ '<{}>'.format(symfile.path))
+ for frame in frames:
+ frame.name = symbolized_name
+
+ symbolizer = elf_symbolizer.ELFSymbolizer(symfile.symbolizable_path,
+ self.symbolizer_path,
+ _SymbolizerCallback,
+ inlines=True)
+
+ for address, frames in symfile.frames_by_address.items():
+ # SymbolizeAsync() asserts that the type of address is int. We operate
+ # on longs (since they are raw pointers possibly from 64-bit processes).
+ # It's OK to cast here because we're passing relative PC, which should
+ # always fit into int.
+ symbolizer.SymbolizeAsync(int(address), frames)
+
+ symbolizer.Join()
+
+ def _SymbolizeMac(self, symfile):
+ load_address = (symbolize_trace_macho_reader.
+ ReadMachOTextLoadAddress(symfile.symbolizable_path))
+ assert load_address is not None
+
+ address_os_file, address_file_path = tempfile.mkstemp()
+ try:
+ with os.fdopen(address_os_file, 'w') as address_file:
+ for address in symfile.frames_by_address.keys():
+ address_file.write('{:x} '.format(address + load_address))
+
+ cmd = [self.symbolizer_path, '-arch', 'x86_64', '-l',
+ '0x%x' % load_address, '-o', symfile.symbolizable_path,
+ '-f', address_file_path]
+ output_array = subprocess.check_output(cmd).split('\n')
+
+ for i, frames in enumerate(symfile.frames_by_address.values()):
+ symbolized_name = self._matcher.Match(output_array[i])
+ for frame in frames:
+ frame.name = symbolized_name
+ finally:
+ os.remove(address_file_path)
+
+ def _SymbolizeWin(self, symfile):
+ """Invoke symbolizer binary on windows and write all input in one go.
+
+ Unlike linux, on windows, symbolization talks through a shared system
+ service that handles communication with the NT symbol servers. This
+ creates an explicit serialization (and therefor lock contention) of
+ any process using the symbol API for files do not have a local PDB.
+
+ Thus, even though the windows symbolizer binary can be make command line
+ compatible with the POSIX addr2line interface, parallelizing the
+ symbolization does not yield the same performance effects. Running
+ just one symbolizer seems good enough for now. Can optimize later
+ if this becomes a bottleneck.
+ """
+ cmd = [self.symbolizer_path, '--functions', '--demangle', '--exe',
+ symfile.symbolizable_path]
+
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
+ stderr=None)
+ addrs = ["%x" % relative_pc for relative_pc in
+ symfile.frames_by_address.keys()]
+ (stdout_data, _) = proc.communicate('\n'.join(addrs))
+ # On windows, lines may contain '\r' character: e.g. "RtlUserThreadStart\r".
+ stdout_data.replace('\r', '')
+ stdout_data = stdout_data.split('\n')
+
+ # This is known to be in the same order as stderr_data.
+ for i, addr in enumerate(addrs):
+ for frame in symfile.frames_by_address[int(addr, 16)]:
+ # Output of addr2line with --functions is always 2 outputs per
+ # symbol, function name followed by source line number. Only grab
+ # the function name as line info is not always available.
+ frame.name = stdout_data[i * 2]
+
+ def _SymbolizeBreakpad(self, symfile):
+ module_filename = symfile.symbolizable_path
+ module = BreakpadSymbolsModule(module_filename)
+ module.Parse()
+
+ if module.code_id and symfile.code_id and module.code_id != symfile.code_id:
+ print("Warning: Code identifiers do not match for %s" % symfile.path)
+ print(" from trace file: %s" % symfile.code_id)
+ print(" from debug file: %s" % module.code_id)
+ return
+
+ addresses = symfile.frames_by_address.keys()
+ addresses.sort()
+
+ symbols_addresses = module.symbols.keys()
+ symbols_addresses.sort()
+ symbols_addresses.append(float('inf'))
+
+ offset = 0
+ skipped_addresses = 0
+ for symbol_offset in range(1, len(symbols_addresses)):
+ symbol_address_start = symbols_addresses[symbol_offset - 1]
+ symbol_address_end = symbols_addresses[symbol_offset]
+ resolved_symbol = module.symbols[symbol_address_start]
+ while (offset < len(addresses) and
+ addresses[offset] < symbol_address_end):
+ if addresses[offset] >= symbol_address_start:
+ for frame in symfile.frames_by_address[addresses[offset]]:
+ frame.name = resolved_symbol
+ else:
+ skipped_addresses = skipped_addresses + 1
+ offset = offset + 1
+
+ if skipped_addresses:
+ print("warning: %d unsymbolized symbols!" % skipped_addresses)
+
+ def SymbolizeSymfile(self, symfile):
+ if symfile.skip_symbolization:
+ for address, frames in symfile.frames_by_address.items():
+ unsymbolized_name = ('<' + os.path.basename(symfile.symbolizable_path)
+ + '>')
+ # Only append the address if there's a library.
+ if symfile.symbolizable_path != _UNNAMED_FILE:
+ unsymbolized_name += ' + ' + str(hex(address))
+
+ for frame in frames:
+ frame.name = unsymbolized_name
+ return
+
+ if symfile.has_breakpad_symbols:
+ self._SymbolizeBreakpad(symfile)
+ elif self.is_mac:
+ self._SymbolizeMac(symfile)
+ elif self.is_win:
+ self._SymbolizeWin(symfile)
+ else:
+ self._SymbolizeLinuxAndAndroid(symfile)
+
+ def IsSymbolizableFile(self, file_path):
+ if self.is_win:
+ extension = os.path.splitext(file_path)[1].lower()
+ return extension in ['.dll', '.exe']
+ else:
+ result = subprocess.check_output(['file', '-0', file_path])
+ type_string = result[result.find('\0') + 1:]
+ return bool(re.match(r'.*(ELF|Mach-O) (32|64)-bit\b.*',
+ type_string, re.DOTALL))
+
+
+def SymbolizeFiles(symfiles, symbolizer):
+ """Symbolizes each file in the given list of SymbolizableFiles
+ and updates stack frames with symbolization results."""
+
+ if not symfiles:
+ print('Nothing to symbolize.')
+ return
+
+ print('Symbolizing...')
+
+ def _SubPrintf(message, *args):
+ print((' ' + message).format(*args))
+
+ for symfile in symfiles:
+ problem = None
+ if symfile.skip_symbolization:
+ pass
+ elif (symfile.has_breakpad_symbols and
+ os.path.isabs(symfile.symbolizable_path) and
+ os.path.isfile(symfile.symbolizable_path)):
+ pass
+ elif not os.path.isabs(symfile.symbolizable_path):
+ problem = 'not a file'
+ elif not os.path.isfile(symfile.symbolizable_path):
+ problem = "file doesn't exist"
+ elif not symbolizer.IsSymbolizableFile(symfile.symbolizable_path):
+ problem = 'file is not symbolizable'
+ if problem:
+ _SubPrintf("Problem with '{}': {}.",
+ symfile.symbolizable_path,
+ problem)
+ symfile.skip_symbolization = True
+
+ _SubPrintf('Symbolizing {} PCs from {}...',
+ len(symfile.frames_by_address),
+ symfile.symbolizable_path)
+
+ symbolizer.SymbolizeSymfile(symfile)
+
+
+# Subpath of output path where unstripped libraries are stored.
+ANDROID_UNSTRIPPED_SUBPATH = 'lib.unstripped'
+
+
+def RemapAndroidFiles(symfiles, output_path, chrome_soname):
+ for symfile in symfiles:
+ filename = os.path.basename(symfile.path)
+ if os.path.splitext(filename)[1] == '.so':
+ symfile.symbolizable_path = os.path.join(
+ output_path, ANDROID_UNSTRIPPED_SUBPATH, filename)
+ elif os.path.splitext(filename)[1] == '.apk' and chrome_soname:
+ # If there is any pc in .apk memory map, then just assume it is from
+ # chroms.so since we memory map libraries from apk directly. This does
+ # not work for component builds.
+ symfile.symbolizable_path = os.path.join(
+ output_path, ANDROID_UNSTRIPPED_SUBPATH, chrome_soname)
+ else:
+ # Clobber file path to trigger "not a file" problem in SymbolizeFiles().
+ # Without this, files won't be symbolized with "file not found" problem,
+ # which is not accurate.
+ symfile.symbolizable_path = 'android://{}'.format(symfile.path)
+
+
+def RemapMacFiles(symfiles, symbol_base_directory, version,
+ only_symbolize_chrome_symbols):
+ suffix = ("Google Chrome Framework.dSYM/Contents/Resources/DWARF/"
+ "Google Chrome Framework")
+ symbol_sub_dir = os.path.join(symbol_base_directory, version)
+ symbolizable_path = os.path.join(symbol_sub_dir, suffix)
+
+ for symfile in symfiles:
+ if symfile.path.endswith("Google Chrome Framework"):
+ symfile.symbolizable_path = symbolizable_path
+ elif only_symbolize_chrome_symbols:
+ symfile.skip_symbolization = True
+
+
+def RemapWinFiles(symfiles, symbol_base_directory, version, is64bit,
+ only_symbolize_chrome_symbols):
+ folder = "win64" if is64bit else "win"
+ symbol_sub_dir = os.path.join(symbol_base_directory,
+ "chrome-" + folder + "-" + version)
+ for symfile in symfiles:
+ image = os.path.join(symbol_sub_dir, os.path.basename(symfile.path))
+ symbols = image + ".pdb"
+ if os.path.isfile(image) and os.path.isfile(symbols):
+ symfile.symbolizable_path = image
+ elif only_symbolize_chrome_symbols:
+ symfile.skip_symbolization = True
+
+
+def RemapBreakpadModules(symfiles, symbolizer, only_symbolize_chrome_symbols):
+ for symfile in symfiles:
+ image = os.path.basename(symfile.path).lower()
+ # Looked if the image has Breakpad symbols. Breakpad symbols are generated
+ # for Chrome modules for official builds.
+ if image in symbolizer.breakpad_modules:
+ symfile.symbolizable_path = symbolizer.breakpad_modules[image]
+ symfile.has_breakpad_symbols = True
+ elif only_symbolize_chrome_symbols:
+ symfile.skip_symbolization = True
+
+
+def SymbolizeTrace(options, trace, symbolizer):
+ symfiles = ResolveSymbolizableFiles(trace.processes)
+
+ if options.use_breakpad_symbols:
+ RemapBreakpadModules(symfiles, symbolizer,
+ options.only_symbolize_chrome_symbols)
+ else:
+ if trace.is_android:
+ if not options.output_directory:
+ sys.exit('The trace file appears to be from Android. Please '
+ 'specify output directory to properly symbolize it.')
+ RemapAndroidFiles(symfiles, os.path.abspath(options.output_directory),
+ trace.library_name)
+
+ if not trace.is_chromium:
+ if symbolizer.is_mac:
+ RemapMacFiles(symfiles, options.symbol_base_directory, trace.version,
+ options.only_symbolize_chrome_symbols)
+ if symbolizer.is_win:
+ RemapWinFiles(symfiles, options.symbol_base_directory, trace.version,
+ trace.is_64bit, options.only_symbolize_chrome_symbols)
+
+ SymbolizeFiles(symfiles, symbolizer)
+
+
+def FetchAndExtractBreakpadSymbols(symbol_base_directory,
+ breakpad_info_folder,
+ trace,
+ symbolizer,
+ cloud_storage_bucket):
+
+ if breakpad_info_folder:
+ # Using local symbols from |breakpad_info_folder|.
+ symbol_sub_dir = breakpad_info_folder
+ else:
+ # Fetching the symbols from GCS (OS dependent).
+ if trace.is_win:
+ folder = 'win64-pgo' if trace.is_64bit else 'win-pgo'
+ elif trace.is_mac:
+ folder = 'mac64'
+ elif trace.is_linux:
+ folder = 'linux64'
+ else:
+ raise Exception('OS not supported for Breakpad symbolization (%s/%s)' %
+ (trace.os, trace.version))
+
+ gsc_folder = 'desktop-*/' + trace.version + '/' + folder
+ gcs_file = gsc_folder + '/breakpad-info'
+
+ symbol_sub_dir = os.path.join(symbol_base_directory,
+ 'breakpad-info_' + trace.version + '_' + folder)
+ zip_path = symbol_sub_dir + '/breakpad-info.zip'
+
+ # Check whether symbols are already downloaded and extracted.
+ if not os.path.isdir(symbol_sub_dir):
+ if cloud_storage.Exists(cloud_storage_bucket, gcs_file + '.zip'):
+ # Some version, like mac, doesn't have the .zip extension.
+ gcs_file = gcs_file + '.zip'
+ elif not cloud_storage.Exists(cloud_storage_bucket, gcs_file):
+ print("Can't find symbols on GCS " + gcs_file + ".")
+ return False
+ print('Downloading symbols files from GCS, please wait.')
+ cloud_storage.Get(cloud_storage_bucket, gcs_file, zip_path)
+
+ with zipfile.ZipFile(zip_path, 'r') as zip_file:
+ zip_file.extractall(symbol_sub_dir)
+ os.remove(zip_path)
+
+ # Parse breakpad module header (first line) and register known modules.
+ for root, _, filenames in os.walk(symbol_sub_dir):
+ for filename in filenames:
+ full_filename = os.path.abspath(os.path.join(root, filename))
+ with open(full_filename, 'r') as file_handle:
+ first_line = file_handle.readline()
+ fragments = first_line.rstrip().split()
+ if fragments[0] == 'MODULE':
+ binary = ' '.join(fragments[4:]).lower()
+ module_name, extension = os.path.splitext(binary)
+ if extension == ".pdb":
+ binary = module_name
+ symbolizer.breakpad_modules[binary] = full_filename
+
+ return True
+
+
+def OpenTraceFile(file_path, mode):
+ if file_path.endswith('.gz'):
+ return gzip.open(file_path, mode + 'b')
+ else:
+ return open(file_path, mode + 't')
+
+
+def FetchAndExtractSymbolsMac(symbol_base_directory, version,
+ cloud_storage_bucket):
+ def GetLocalPath(base_dir, version):
+ return os.path.join(base_dir, version + ".tar.bz2")
+ def GetSymbolsPath(version):
+ return "desktop-*/" + version + "/mac64/Google Chrome.dSYM.tar.bz2"
+ def ExtractSymbolTarFile(symbol_sub_dir, symbol_tar_file):
+ os.makedirs(symbol_sub_dir)
+ with tarfile.open(os.path.expanduser(symbol_tar_file), "r:bz2") as tar:
+ tar.extractall(symbol_sub_dir)
+
+ symbol_sub_dir = os.path.join(symbol_base_directory, version)
+ if os.path.isdir(symbol_sub_dir):
+ return True
+
+ bzip_path = GetLocalPath(symbol_base_directory, version)
+ if not os.path.isfile(bzip_path):
+ if not cloud_storage.Exists(cloud_storage_bucket, GetSymbolsPath(version)):
+ print("Can't find symbols on GCS '%s'." % version)
+ return False
+ print("Downloading symbols files from GCS, please wait.")
+ cloud_storage.Get(cloud_storage_bucket, GetSymbolsPath(version), bzip_path)
+
+ ExtractSymbolTarFile(symbol_sub_dir, bzip_path)
+ os.remove(bzip_path)
+ return True
+
+
+def FetchAndExtractSymbolsWin(symbol_base_directory, version, is64bit,
+ cloud_storage_bucket):
+ def DownloadAndExtractZipFile(zip_path, source, destination):
+ if not os.path.isfile(zip_path):
+ if not cloud_storage.Exists(cloud_storage_bucket, source):
+ print("Can't find symbols on GCS '%s'." % version)
+ return False
+ print("Downloading symbols files from GCS, please wait.")
+ cloud_storage.Get(cloud_storage_bucket, source, zip_path)
+ if not os.path.isfile(zip_path):
+ print("Can't download symbols on GCS.")
+ return False
+ with zipfile.ZipFile(zip_path, "r") as zip_file:
+ for member in zip_file.namelist():
+ filename = os.path.basename(member)
+ # Skip directories.
+ if not filename:
+ continue
+ # Extract archived files.
+ source = zip_file.open(member)
+ target = file(os.path.join(destination, filename), "wb")
+ with source, target:
+ shutil.copyfileobj(source, target)
+
+ folder = "win64" if is64bit else "win"
+ # Clang build (M61+)
+ folder_suffix = "-clang"
+ gcs_folder = "desktop-*/" + version + "/" + folder + folder_suffix + "/"
+ if not cloud_storage.Exists(cloud_storage_bucket, gcs_folder):
+ # MSVC build (before M61)
+ folder_suffix = "-pgo"
+ gcs_folder = "desktop-*/" + version + "/" + folder + folder_suffix + "/"
+
+ symbol_sub_dir = os.path.join(symbol_base_directory,
+ "chrome-" + folder + "-" + version)
+ if os.path.isdir(symbol_sub_dir):
+ return True
+
+ os.makedirs(symbol_sub_dir)
+ DownloadAndExtractZipFile(
+ os.path.join(symbol_base_directory,
+ "chrome-" + folder + "-" + version + "-syms.zip"),
+ gcs_folder + "chrome-win32-syms.zip",
+ symbol_sub_dir)
+ DownloadAndExtractZipFile(
+ os.path.join(symbol_base_directory,
+ "chrome-" + folder + "-" + version + ".zip"),
+ gcs_folder + "chrome-" + folder + folder_suffix + ".zip",
+ symbol_sub_dir)
+
+ return True
+
+# Suffix used for backup files.
+BACKUP_FILE_TAG = '.BACKUP'
+
+def main(args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'file',
+ help='Trace file to symbolize (.json or .json.gz)')
+
+ parser.add_argument(
+ '--no-backup', dest='backup', action='store_false',
+ help="Don't create {} files".format(BACKUP_FILE_TAG))
+
+ parser.add_argument(
+ '--is-local-build', action='store_true',
+ help="Indicate that the memlog trace is from a local build of Chromium.")
+
+ parser.add_argument(
+ '--output-directory',
+ help='The path to the build output directory, such as out/Debug.')
+
+ parser.add_argument(
+ '--only-symbolize-chrome-symbols',
+ action='store_true',
+ help='Prevents symbolization of non-Chrome [system] symbols.')
+
+ parser.add_argument(
+ '--cloud-storage-bucket', default='chrome-unsigned',
+ help="Bucket that holds symbols for official Chrome builds. "
+ "Used by tests, which don't have access to the default bucket.")
+
+ parser.add_argument(
+ '--addr2line-executable', default=None,
+ help="The path to the executable used to convert address to line."
+ "Default uses the executable found in the PATH environment variable."
+ "Used by tests, which don't have the executable.")
+
+ parser.add_argument(
+ '--use-breakpad-symbols',
+ action='store_true',
+ help='Use breakpad symbols files for symbolisation.')
+
+ parser.add_argument(
+ '--breakpad-symbols-directory', default=None,
+ help='A path to a directory containing breakpad symbols.')
+
+ home_dir = os.path.expanduser('~')
+ default_dir = os.path.join(home_dir, "symbols")
+ parser.add_argument(
+ '--symbol-base-directory',
+ default=default_dir,
+ help='Directory where symbols are downloaded and cached.')
+
+ options = parser.parse_args(args)
+
+ symbolizer = Symbolizer(options.addr2line_executable)
+ if (symbolizer.symbolizer_path is None and
+ not options.use_breakpad_symbols):
+ sys.exit("Can't symbolize - no %s in PATH." % symbolizer.binary)
+
+ trace_file_path = options.file
+
+ print('Reading trace file...')
+ with OpenTraceFile(trace_file_path, 'r') as trace_file:
+ trace = Trace(json.load(trace_file))
+ print('Trace loaded for %s/%s' % (trace.os, trace.version))
+
+ trace.is_chromium = options.is_local_build
+
+ # Perform some sanity checks.
+ if (trace.is_win and sys.platform != 'win32' and
+ not options.use_breakpad_symbols):
+ print("Cannot symbolize a windows trace on this architecture!")
+ return False
+
+ # If the trace is from Chromium, assume that symbols are already present.
+ # Otherwise the trace is from Google Chrome. Assume that this is not a local
+ # build of Google Chrome with symbols, and that we need to fetch symbols
+ # from gcs.
+ if trace.is_chromium or options.output_directory:
+ if options.use_breakpad_symbols and options.breakpad_symbols_directory:
+ # Local build with local symbols.
+ FetchAndExtractBreakpadSymbols(
+ options.symbol_base_directory,
+ options.breakpad_symbols_directory,
+ trace, symbolizer,
+ options.cloud_storage_bucket)
+ else:
+ has_symbols = False
+ if options.use_breakpad_symbols:
+ # Official build, using Breakpad symbolization.
+ has_symbols = FetchAndExtractBreakpadSymbols(
+ options.symbol_base_directory,
+ options.breakpad_symbols_directory,
+ trace, symbolizer,
+ options.cloud_storage_bucket)
+ else:
+ # Official build, using native symbolization.
+ if symbolizer.is_mac:
+ has_symbols = FetchAndExtractSymbolsMac(options.symbol_base_directory,
+ trace.version,
+ options.cloud_storage_bucket)
+ elif symbolizer.is_win:
+ has_symbols = FetchAndExtractSymbolsWin(options.symbol_base_directory,
+ trace.version, trace.is_64bit,
+ options.cloud_storage_bucket)
+ else:
+ raise Exception('OS not supported for native symbolization (%s/%s)' %
+ (trace.os, trace.version))
+ if not has_symbols:
+ print('Cannot fetch symbols from GCS')
+ return False
+
+ SymbolizeTrace(options, trace, symbolizer)
+
+ if trace.modified:
+ trace.ApplyModifications()
+
+ if options.backup:
+ backup_file_path = trace_file_path + BACKUP_FILE_TAG
+ print('Backing up trace file to {}'.format(backup_file_path))
+ os.rename(trace_file_path, backup_file_path)
+
+ print('Updating the trace file...')
+ with OpenTraceFile(trace_file_path, 'w') as trace_file:
+ trace_file.write(json.dumps(trace.node))
+ else:
+ print('No modifications were made - not updating the trace file.')
+ return True
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex.py
new file mode 100644
index 00000000000..65356c3efa3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex.py
@@ -0,0 +1,36 @@
+# Copyright 2017 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
+
+
+class AtosRegexMatcher(object):
+ def __init__(self):
+ """
+ Atos output has two useful forms:
+ 1. <name> (in <library name>) (<filename>:<linenumber>)
+ 2. <name> (in <library name>) + <symbol offset>
+ And two less useful forms:
+ 3. <address>
+ 4. <address> (in <library name>)
+ e.g.
+ 1. -[SKTGraphicView drawRect:] (in Sketch) (SKTGraphicView.m:445)
+ 2. malloc (in libsystem_malloc.dylib) + 42
+ 3. 0x4a12
+ 4. 0x00000d9a (in Chromium)
+
+ We don't bother checking for the latter two, and just return the full
+ output.
+ """
+ self._regex1 = re.compile(r"(.*) \(in (.+)\) \((.+):(\d+)\)")
+ self._regex2 = re.compile(r"(.*) \(in (.+)\) \+ (\d+)")
+
+ def Match(self, text):
+ result = self._regex1.match(text)
+ if result:
+ return result.group(1)
+ result = self._regex2.match(text)
+ if result:
+ return result.group(1)
+ return text
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex_unittest.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex_unittest.py
new file mode 100644
index 00000000000..28b488badde
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_atos_regex_unittest.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# Copyright 2017 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 sys
+import unittest
+
+from . import symbolize_trace_atos_regex
+
+
+class AtosRegexTest(unittest.TestCase):
+ def testRegex(self):
+ if sys.platform != "darwin":
+ return
+ matcher = symbolize_trace_atos_regex.AtosRegexMatcher()
+ text = "-[SKTGraphicView drawRect:] (in Sketch) (SKTGraphicView.m:445)"
+ output = matcher.Match(text)
+ self.assertEqual("-[SKTGraphicView drawRect:]", output)
+
+ text = "malloc (in libsystem_malloc.dylib) + 42"
+ output = matcher.Match(text)
+ self.assertEqual("malloc", output)
+
+ expected_output = (
+ "content::CacheStorage::MatchAllCaches(std::__1::unique_ptr<content::Se"
+ "rviceWorkerFetchRequest, std::__1::default_delete<content::ServiceWork"
+ "erFetchRequest> >, content::CacheStorageCacheQueryParams const&, base:"
+ ":Callback<void (content::CacheStorageError, std::__1::unique_ptr<conte"
+ "nt::ServiceWorkerResponse, std::__1::default_delete<content::ServiceWo"
+ "rkerResponse> >, std::__1::unique_ptr<storage::BlobDataHandle, std::__"
+ "1::default_delete<storage::BlobDataHandle> >), (base::internal::CopyMo"
+ "de)1, (base::internal::RepeatMode)1> const&)"
+ )
+ text = expected_output + " (in Chromium Framework) (ref_counted.h:322)"
+ output = matcher.Match(text)
+ self.assertEqual(expected_output, output)
+
+ text = "0x4a12"
+ output = matcher.Match(text)
+ self.assertEqual(text, output)
+
+ text = "0x00000d9a (in Chromium)"
+ output = matcher.Match(text)
+ self.assertEqual(text, output)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_end_to_end_test_slow.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_end_to_end_test_slow.py
new file mode 100755
index 00000000000..0a416800596
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_end_to_end_test_slow.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+# Copyright 2017 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.
+#
+# pylint: disable=line-too-long
+# To upload test files, use the command:
+# <path_to_depot_tools>/upload_to_google_storage.py --bucket chrome-partner-telemetry <path_to_data_dir>/linux_trace_v2_breakpad_postsymbolization.json.gz
+#
+# To run this test suite, use ./tracing/bin/run_symbolizer_tests
+
+from __future__ import print_function
+
+import json
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+from tracing.extras.symbolizer import symbolize_trace
+
+_THIS_DIR_PATH = os.path.abspath(os.path.dirname(__file__))
+_TRACING_DIR = os.path.abspath(
+ os.path.join(_THIS_DIR_PATH,
+ os.path.pardir,
+ os.path.pardir,
+ os.path.pardir))
+_PY_UTILS_PATH = os.path.abspath(os.path.join(
+ _TRACING_DIR,
+ os.path.pardir,
+ 'common',
+ 'py_utils'))
+sys.path.append(_PY_UTILS_PATH)
+import py_utils.cloud_storage as cloud_storage # pylint: disable=wrong-import-position
+
+
+def _DownloadFromCloudStorage(path):
+ print('Downloading %s from gcs.' % (path))
+ cloud_storage.GetIfChanged(path, cloud_storage.PARTNER_BUCKET)
+
+
+class SymbolizeTraceEndToEndTest(unittest.TestCase):
+ def _ValidateTrace(self, trace_path, expectations):
+ with symbolize_trace.OpenTraceFile(trace_path, 'r') as trace_file:
+ trace = symbolize_trace.Trace(json.load(trace_file))
+ # Find the browser process.
+ browser = None
+ for process in trace.processes:
+ if process.name == expectations['process']:
+ browser = process
+ self.assertTrue(browser)
+
+ # Look for a frame with a symbolize name, and check that it has the right
+ # parent.
+ frames = browser.stack_frame_map.frame_by_id
+ exact = expectations['frame_exact']
+ found = False
+ for _, frame in frames.items():
+ if frame.name.strip() == exact['frame_name']:
+ parent_id = frame.parent_id
+ if frames[parent_id].name.strip() == exact['parent_name']:
+ found = True
+ break
+
+ self.assertTrue(found)
+
+
+ def _RunSymbolizationOnTrace(self, pre_symbolization, expectations,
+ extra_options):
+ trace_presymbolization_path = os.path.join(
+ _THIS_DIR_PATH, 'data', pre_symbolization)
+ _DownloadFromCloudStorage(trace_presymbolization_path)
+ self.assertTrue(os.path.exists(trace_presymbolization_path))
+
+ temporary_fd, temporary_trace = tempfile.mkstemp(suffix='.json.gz')
+
+ symbolization_options = ['--only-symbolize-chrome-symbols',
+ '--no-backup',
+ '--cloud-storage-bucket',
+ cloud_storage.PARTNER_BUCKET,
+ temporary_trace]
+
+ symbolization_options.extend(extra_options)
+
+ # On windows, a pre-built version of addr2line-pdb is provided.
+ if sys.platform == 'win32':
+ addr2line_path = os.path.join(
+ _THIS_DIR_PATH, 'data', 'addr2line-pdb.exe')
+ _DownloadFromCloudStorage(addr2line_path)
+ self.assertTrue(os.path.exists(addr2line_path))
+ symbolization_options += ['--addr2line-executable', addr2line_path]
+
+ # Execute symbolization and compare results with the expected trace.
+ try:
+ shutil.copy(trace_presymbolization_path, temporary_trace)
+ self.assertTrue(symbolize_trace.main(symbolization_options))
+ self._ValidateTrace(temporary_trace, expectations)
+ finally:
+ os.close(temporary_fd)
+ if os.path.exists(temporary_trace):
+ os.remove(temporary_trace)
+
+
+ def testMacv2(self):
+ if sys.platform != 'darwin':
+ return
+ # The corresponding macOS Chrome symbols must be uploaded to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/66.0.3334.0/mac64/Google Chrome.dSYM.tar.bz2"
+ # since the waterfall bots do not have access to the chrome-unsigned bucket.
+ expectations = {}
+ expectations['process'] = 'Browser'
+ expectations['frame_exact'] = {
+ 'parent_name': 'ProfileImpl::OnPrefsLoaded(Profile::CreateMode, bool)',
+ 'frame_name': 'ProfileImpl::OnLocaleReady()'
+ }
+ self._RunSymbolizationOnTrace(
+ 'mac_trace_v2_presymbolization.json.gz',
+ expectations, [])
+
+ def testMacv2Breakpad(self):
+ # The corresponding macOS Chrome symbols must be uploaded to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/66.0.3334.0/mac64/breakpad-info"
+ # since the waterfall bots do not have access to the chrome-unsigned bucket.
+ expectations = {}
+ expectations['process'] = 'Browser'
+ expectations['frame_exact'] = {
+ 'parent_name': 'ProfileImpl::OnPrefsLoaded(Profile::CreateMode, bool)',
+ 'frame_name': 'ProfileImpl::OnLocaleReady()'
+ }
+ self._RunSymbolizationOnTrace(
+ 'mac_trace_v2_presymbolization.json.gz',
+ expectations, ['--use-breakpad-symbols'])
+
+ def testWin64v1(self):
+ if sys.platform != 'win32':
+ return
+
+ expectations = {}
+ expectations['process'] = 'Browser'
+ expectations['frame_exact'] = {
+ 'parent_name': 'ChromeMain',
+ 'frame_name': 'content::ContentMain'
+ }
+ # The corresponding Win64 Chrome symbols must be uploaded to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/61.0.3130.0/"
+ # "win64-pgo/chrome-win32-syms.zip"
+ # and the corresponding executables to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/61.0.3130.0/"
+ # "win64-pgo/chrome-win64-pgo.zip"
+ # since the waterfall bots do not have access to the chrome-unsigned bucket.
+ self._RunSymbolizationOnTrace('windows_trace_v1_presymbolization.json.gz',
+ expectations,
+ [])
+
+ def testWin64v2(self):
+ if sys.platform != 'win32':
+ return
+
+ expectations = {}
+ expectations['process'] = 'Browser'
+ expectations['frame_exact'] = {
+ 'parent_name': 'base::MessagePumpWin::Run',
+ 'frame_name': 'base::MessagePumpForUI::DoRunLoop'
+ }
+ # The corresponding Win64 Chrome symbols must be uploaded to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/61.0.3142.0/"
+ # "win64-pgo/chrome-win32-syms.zip"
+ # and the corresponding executables to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/61.0.3142.0/"
+ # "win64-pgo/chrome-win64-pgo.zip"
+ # since the waterfall bots do not have access to the chrome-unsigned bucket.
+ self._RunSymbolizationOnTrace('windows_trace_v2_presymbolization.json.gz',
+ expectations,
+ [])
+
+
+ def testLinuxv2(self):
+ # The corresponding Linux breakpad symbols must be uploaded to
+ # "gs://chrome-partner-telemetry/desktop-symbolizer-test/64.0.3282.24/linux64/breakpad-info.zip"
+ # since the waterfall bots do not have access to the chrome-unsigned bucket.
+ expectations = {}
+ expectations['process'] = 'Renderer'
+ expectations['frame_exact'] = {
+ 'parent_name': 'cc::LayerTreeSettings::LayerTreeSettings(cc::LayerTreeSettings const&)',
+ 'frame_name': 'viz::ResourceSettings::ResourceSettings(viz::ResourceSettings const&)'
+ }
+ self._RunSymbolizationOnTrace(
+ 'linux_trace_v2_presymbolization.json.gz',
+ expectations,
+ ['--use-breakpad-symbols'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader.py
new file mode 100644
index 00000000000..ea8a6366671
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader.py
@@ -0,0 +1,26 @@
+# Copyright 2017 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 subprocess
+
+
+def ReadMachOTextLoadAddress(file_name):
+ """
+ This function returns the load address of the TEXT segment of a Mach-O file.
+ """
+ regex = re.compile(r".* vmaddr 0x([\dabcdef]*)")
+ cmd = ["otool", "-l", file_name]
+ output = subprocess.check_output(cmd).split('\n')
+ for i in range(len(output) - 3):
+ # It's possible to use a regex here instead, but these conditionals are much
+ # clearer.
+ if ("cmd LC_SEGMENT_64" in output[i] and
+ "cmdsize" in output[i + 1] and
+ "segname __TEXT" in output[i + 2] and
+ "vmaddr" in output[i + 3]):
+ result = regex.match(output[i + 3])
+ assert result
+ return int(result.group(1), 16)
+ return None
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader_unittest.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader_unittest.py
new file mode 100644
index 00000000000..f0058abda51
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_macho_reader_unittest.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# Copyright 2017 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 sys
+import unittest
+
+from . import symbolize_trace_macho_reader
+
+
+class AtosRegexTest(unittest.TestCase):
+ def testRegex(self):
+ if sys.platform != "darwin":
+ return
+ file_name = "/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit"
+ result = symbolize_trace_macho_reader.ReadMachOTextLoadAddress(file_name)
+ self.assertNotEqual(None, result)
+
+ file_name = "/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa"
+ result = symbolize_trace_macho_reader.ReadMachOTextLoadAddress(file_name)
+ self.assertNotEqual(None, result)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_unittest.py b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_unittest.py
new file mode 100755
index 00000000000..1b9b7b912c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/symbolizer/symbolize_trace_unittest.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+# Copyright 2017 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 . import symbolize_trace
+
+class StackFrameMapTest(unittest.TestCase):
+
+ def assertStackFrame(self, stack_frame_map, frame_id, name, parent_id=None):
+ self.assertTrue(frame_id in stack_frame_map.frame_by_id)
+ frame = stack_frame_map.frame_by_id[frame_id]
+ self.assertEqual(name, frame.name)
+ self.assertEqual(parent_id, frame.parent_id)
+
+ def testParseNext(self):
+ string_map = symbolize_trace.StringMap()
+ stack_frame_map = symbolize_trace.StackFrameMap()
+
+ # Check that ParseNext() actually parses anything.
+ stack_frame_map.ParseNext(
+ symbolize_trace.Trace.HEAP_DUMP_VERSION_1,
+ [
+ {'id': 1, 'name_sid': string_map.AddString('main')},
+ {'id': 45, 'name_sid': string_map.AddString('foo'), 'parent': 1},
+ ],
+ string_map)
+ self.assertStackFrame(stack_frame_map, 1, 'main')
+ self.assertStackFrame(stack_frame_map, 45, 'foo', parent_id=1)
+
+ # Check that ParseNext() retains all previously parsed frames.
+ stack_frame_map.ParseNext(
+ symbolize_trace.Trace.HEAP_DUMP_VERSION_1,
+ [
+ {'id': 33, 'name_sid': string_map.AddString('bar')},
+ ],
+ string_map)
+ self.assertStackFrame(stack_frame_map, 1, 'main')
+ self.assertStackFrame(stack_frame_map, 45, 'foo', parent_id=1)
+ self.assertStackFrame(stack_frame_map, 33, 'bar')
+
+ def testParseNextLegacy(self):
+ stack_frame_map = symbolize_trace.StackFrameMap()
+
+ stack_frame_map.ParseNext(
+ symbolize_trace.Trace.HEAP_DUMP_VERSION_LEGACY,
+ {
+ 1: {'name': 'main'},
+ 45: {'name': 'foo', 'parent': 1},
+ },
+ string_map=None)
+ self.assertStackFrame(stack_frame_map, 1, 'main')
+ self.assertStackFrame(stack_frame_map, 45, 'foo', parent_id=1)
+
+ # When parsing legacy format, ParseNext() is expected to be called once.
+ self.assertRaises(
+ Exception,
+ stack_frame_map.ParseNext,
+ symbolize_trace.Trace.HEAP_DUMP_VERSION_LEGACY,
+ {
+ 33: {'name': 'bar'},
+ },
+ string_map=None)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/system_stats/system_stats_snapshot.html b/chromium/third_party/catapult/tracing/tracing/extras/system_stats/system_stats_snapshot.html
new file mode 100644
index 00000000000..4b23769967e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/system_stats/system_stats_snapshot.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.system_stats', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * @constructor
+ */
+ function SystemStatsSnapshot(objectInstance, ts, args) {
+ ObjectSnapshot.apply(this, arguments);
+ this.objectInstance = objectInstance;
+ this.ts = ts;
+ this.args = args;
+ this.stats_ = args;
+ }
+
+ SystemStatsSnapshot.prototype = {
+ __proto__: ObjectSnapshot.prototype,
+
+ initialize() {
+ if (this.args.length === 0) {
+ throw new Error('No system stats snapshot data.');
+ }
+ this.stats_ = this.args;
+ },
+
+ getStats() {
+ return this.stats_;
+ },
+
+ setStats(stats) {
+ this.stats_ = stats;
+ }
+ };
+
+ ObjectSnapshot.subTypes.register(
+ SystemStatsSnapshot,
+ {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+ return {
+ SystemStatsSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/systrace_config.html b/chromium/third_party/catapult/tracing/tracing/extras/systrace_config.html
new file mode 100644
index 00000000000..7711959fcc7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/systrace_config.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/android/android_auditor.html">
+<link rel="import" href="/tracing/extras/cpu/cpu_usage_auditor.html">
+<link rel="import" href="/tracing/extras/importer/android/atrace_process_dump_importer.html">
+<link rel="import" href="/tracing/extras/importer/android/event_log_importer.html">
+<link rel="import" href="/tracing/extras/importer/android/process_data_importer.html">
+<link rel="import" href="/tracing/extras/importer/battor_importer.html">
+<link rel="import" href="/tracing/extras/importer/ddms_importer.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/extras/memory/lowmemory_auditor.html">
+<link rel="import" href="/tracing/extras/vsync/vsync_auditor.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/model.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/context.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/context.html
new file mode 100644
index 00000000000..8c893206543
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/context.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function Context() {
+ this.event = undefined;
+ this.ancestors = [];
+ }
+
+ Context.prototype = {
+ push(event) {
+ const ctx = new Context();
+ ctx.ancestors = this.ancestors.slice();
+ ctx.ancestors.push(event);
+ return ctx;
+ },
+
+ pop(event) {
+ const ctx = new Context();
+ ctx.event = this.ancestors[this.ancestors.length - 1];
+ ctx.ancestors = this.ancestors.slice(0, this.ancestors.length - 1);
+ return ctx;
+ }
+ };
+
+ return {
+ Context,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter.html
new file mode 100644
index 00000000000..62126e3fc0c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_object.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function Filter() {
+ tr.c.ScriptingObject.call(this);
+ }
+
+ Filter.normalizeFilterExpression = function(filterExpression) {
+ // Shortcut: naked strings and regexps can be used to match against slice
+ // titles.
+ if (filterExpression instanceof String ||
+ typeof(filterExpression) === 'string' ||
+ filterExpression instanceof RegExp) {
+ const filter = new tr.e.tquery.FilterHasTitle(filterExpression);
+ return filter;
+ }
+ return filterExpression;
+ };
+
+ Filter.prototype = {
+ __proto__: tr.c.ScriptingObject.prototype,
+
+ evaluate(context) {
+ throw new Error('Not implemented');
+ },
+
+ matchValue_(value, expected) {
+ if (expected instanceof RegExp) {
+ return expected.test(value);
+ } else if (expected instanceof Function) {
+ return expected(value);
+ }
+ return value === expected;
+ }
+ };
+
+ return {
+ Filter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_all_of.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_all_of.html
new file mode 100644
index 00000000000..89f187100cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_all_of.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterAllOf(opt_subExpressions) {
+ tr.e.tquery.Filter.call(this);
+ this.subExpressions = opt_subExpressions || [];
+ }
+
+ FilterAllOf.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ set subExpressions(exprs) {
+ this.subExpressions_ = [];
+ for (let i = 0; i < exprs.length; i++) {
+ this.subExpressions_.push(
+ tr.e.tquery.Filter.normalizeFilterExpression(exprs[i]));
+ }
+ },
+
+ get subExpressions() {
+ return this.subExpressions_;
+ },
+
+ evaluate(context) {
+ if (!this.subExpressions.length) return true;
+ for (let i = 0; i < this.subExpressions.length; i++) {
+ if (!this.subExpressions[i].evaluate(context)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function() {
+ const exprs = [];
+ for (let i = 0; i < arguments.length; i++) {
+ exprs.push(arguments[i]);
+ }
+ return new FilterAllOf(exprs);
+ },
+ {
+ name: 'allOf'
+ }
+ );
+ return {
+ FilterAllOf,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_any_of.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_any_of.html
new file mode 100644
index 00000000000..3303adc53c9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_any_of.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+<link rel="import" href="/tracing/extras/tquery/filter_not.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterAnyOf(opt_subExpressions) {
+ tr.e.tquery.Filter.call(this);
+ this.subExpressions = opt_subExpressions || [];
+ }
+
+ FilterAnyOf.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ set subExpressions(exprs) {
+ this.subExpressions_ = [];
+ for (let i = 0; i < exprs.length; i++) {
+ this.subExpressions_.push(
+ tr.e.tquery.Filter.normalizeFilterExpression(exprs[i]));
+ }
+ },
+
+ get subExpressions() {
+ return this.subExpressions_;
+ },
+
+ evaluate(context) {
+ if (!this.subExpressions.length) return true;
+ for (let i = 0; i < this.subExpressions.length; i++) {
+ if (this.subExpressions[i].evaluate(context)) return true;
+ }
+ return false;
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function() {
+ const exprs = Array.prototype.slice.call(arguments);
+ return new FilterAnyOf(exprs);
+ },
+ {
+ name: 'anyOf'
+ }
+ );
+ tr.c.ScriptingObjectRegistry.register(
+ function() {
+ const exprs = Array.prototype.slice.call(arguments);
+ return new tr.e.tquery.FilterNot(new FilterAnyOf(exprs));
+ },
+ {
+ name: 'noneOf'
+ }
+ );
+ return {
+ FilterAnyOf,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_ancestor.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_ancestor.html
new file mode 100644
index 00000000000..98597ccb2da
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_ancestor.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterHasAncestor(opt_subExpression) {
+ this.subExpression = opt_subExpression;
+ }
+
+ FilterHasAncestor.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ set subExpression(expr) {
+ this.subExpression_ = tr.e.tquery.Filter.normalizeFilterExpression(expr);
+ },
+
+ get subExpression() {
+ return this.subExpression_;
+ },
+
+ evaluate(context) {
+ if (!this.subExpression) {
+ return context.ancestors.length > 0;
+ }
+ while (context.ancestors.length) {
+ context = context.pop();
+ if (this.subExpression.evaluate(context)) return true;
+ }
+ return false;
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function(subExpression) {
+ return new FilterHasAncestor(subExpression);
+ },
+ {
+ name: 'hasAncestor'
+ }
+ );
+ return {
+ FilterHasAncestor,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_duration.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_duration.html
new file mode 100644
index 00000000000..ab5828e2dda
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_duration.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterHasDuration(minValueOrExpected, opt_maxValue) {
+ if (minValueOrExpected !== undefined && opt_maxValue !== undefined) {
+ this.minValue = minValueOrExpected;
+ this.maxValue = opt_maxValue;
+ } else {
+ this.expected = minValueOrExpected;
+ }
+ }
+
+ FilterHasDuration.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ evaluate(context) {
+ if (context.event.duration === undefined) return false;
+ if (this.minValue !== undefined && this.maxValue !== undefined) {
+ return context.event.duration >= this.minValue &&
+ context.event.duration <= this.maxValue;
+ }
+ return this.matchValue_(context.event.duration, this.expected);
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function(minValueOrExpected, opt_maxValue) {
+ return new FilterHasDuration(minValueOrExpected, opt_maxValue);
+ },
+ {
+ name: 'hasDuration'
+ }
+ );
+ return {
+ FilterHasDuration,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_title.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_title.html
new file mode 100644
index 00000000000..dcd4c8ac79b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_has_title.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterHasTitle(expected) {
+ tr.e.tquery.Filter.call(this);
+ this.expected = expected;
+ }
+
+ FilterHasTitle.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ evaluate(context) {
+ return this.matchValue_(context.event.title, this.expected);
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function(expected) {
+ const filter = new tr.e.tquery.FilterHasTitle(expected);
+ return filter;
+ },
+ {
+ name: 'hasTitle'
+ }
+ );
+
+ return {
+ FilterHasTitle,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_is_top_level.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_is_top_level.html
new file mode 100644
index 00000000000..24fd66d8aa3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_is_top_level.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterIsTopLevel(opt_subExpression) {
+ this.subExpression = opt_subExpression;
+ }
+
+ FilterIsTopLevel.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ set subExpression(expr) {
+ this.subExpression_ = tr.e.tquery.Filter.normalizeFilterExpression(expr);
+ },
+
+ get subExpression() {
+ return this.subExpression_;
+ },
+
+ evaluate(context) {
+ if (context.ancestors.length > 0) return false;
+ if (!this.subExpression) return true;
+ return this.subExpression.evaluate(context);
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function(subExpression) {
+ return new FilterIsTopLevel(subExpression);
+ },
+ {
+ name: 'isTopLevel'
+ }
+ );
+ return {
+ FilterIsTopLevel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_not.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_not.html
new file mode 100644
index 00000000000..d3014376113
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/filter_not.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/extras/tquery/filter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function FilterNot(subExpression) {
+ tr.e.tquery.Filter.call(this);
+ this.subExpression = subExpression;
+ }
+
+ FilterNot.prototype = {
+ __proto__: tr.e.tquery.Filter.prototype,
+
+ set subExpression(expr) {
+ this.subExpression_ = tr.e.tquery.Filter.normalizeFilterExpression(expr);
+ },
+
+ get subExpression() {
+ return this.subExpression_;
+ },
+
+ evaluate(context) {
+ return !this.subExpression.evaluate(context);
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ function() {
+ const exprs = Array.prototype.slice.call(arguments);
+ if (exprs.length !== 1) {
+ throw new Error('not() must have exactly one subexpression');
+ }
+ return new FilterNot(exprs[0]);
+ },
+ {
+ name: 'not'
+ }
+ );
+ return {
+ FilterNot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery.html
new file mode 100644
index 00000000000..c4b5d19e995
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/core/scripting_object.html">
+<link rel="import" href="/tracing/extras/tquery/context.html">
+<link rel="import" href="/tracing/extras/tquery/filter_all_of.html">
+<link rel="import" href="/tracing/extras/tquery/filter_any_of.html">
+<link rel="import" href="/tracing/extras/tquery/filter_has_ancestor.html">
+<link rel="import" href="/tracing/extras/tquery/filter_has_duration.html">
+<link rel="import" href="/tracing/extras/tquery/filter_has_title.html">
+<link rel="import" href="/tracing/extras/tquery/filter_is_top_level.html">
+<link rel="import" href="/tracing/extras/tquery/filter_not.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.tquery', function() {
+ function addEventTreeToSelection(selection, event) {
+ selection.push(event);
+ if (!event.subSlices) return;
+ event.subSlices.forEach(
+ addEventTreeToSelection.bind(undefined, selection));
+ }
+
+ function TQuery(model) {
+ tr.c.ScriptingObject.call(this);
+
+ this.model_ = model;
+ this.parent_ = undefined;
+ this.filterExpression_ = undefined;
+ // Memoized filtering result.
+ this.selection_ = undefined;
+ }
+
+ TQuery.prototype = {
+ __proto__: tr.c.ScriptingObject.prototype,
+
+ onModelChanged(model) {
+ this.model_ = model;
+ this.selection_ = undefined;
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ // Append a new filter expression to this query and return a query node
+ // that represents the result.
+ filter(filterExpression) {
+ const result = new TQuery(this.model_);
+ result.parent_ = this;
+ result.filterExpression_ =
+ tr.e.tquery.Filter.normalizeFilterExpression(filterExpression);
+ return result;
+ },
+
+ // Creates a graph of {Task} objects which will compute the selections for
+ // this filter object and all of its parents. The return value is an object
+ // with the following fields:
+ // - rootTask: {Task} which should be executed to kick off processing for
+ // the entire task graph.
+ // - lastTask: The final {Task} of the graph. Can be used by the caller to
+ // enqueue additional processing at the end.
+ // - lastNode: The last filter object in the task. It's selection property
+ // will contain the filtering result once |finalTask|
+ // completes.
+ createFilterTaskGraph_() {
+ // List of nodes in order from the current one to the root.
+ const nodes = [this];
+ while (nodes[nodes.length - 1].parent_) {
+ nodes.push(nodes[nodes.length - 1].parent_);
+ }
+
+ const rootTask = new tr.b.Task();
+ let lastTask = rootTask;
+ let node;
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ node = nodes[i];
+ // Reuse any memoized result.
+ if (node.selection_ !== undefined) continue;
+ node.selection_ = new tr.model.EventSet();
+ if (node.parent_ === undefined) {
+ // If this is the root, start by collecting all objects from the
+ // model.
+ lastTask = lastTask.after(
+ this.selectEverythingAsTask_(node.selection_));
+ } else {
+ // Otherwise execute the filter expression for this node and fill
+ // in its selection.
+ const prevNode = nodes[i + 1];
+ lastTask = this.createFilterTaskForNode_(lastTask, node, prevNode);
+ }
+ }
+ return {rootTask, lastTask, lastNode: node};
+ },
+
+ createFilterTaskForNode_(lastTask, node, prevNode) {
+ return lastTask.after(function() {
+ // TODO(skyostil): Break into subtasks.
+ node.evaluateFilterExpression_(
+ prevNode.selection_, node.selection_);
+ }, this);
+ },
+
+ // Applies the result of a filter expression for a given event and all
+ // of its subslices and adds the matching events to an output selection.
+ evaluateFilterExpression_(inputSelection, outputSelection) {
+ const seenEvents = {};
+ inputSelection.forEach(function(event) {
+ const context = new tr.e.tquery.Context();
+ context.event = event;
+ this.evaluateFilterExpressionForEvent_(
+ context, inputSelection, outputSelection, seenEvents);
+ }.bind(this));
+ },
+
+ evaluateFilterExpressionForEvent_(
+ context, inputSelection, outputSelection, seenEvents) {
+ const event = context.event;
+ if (inputSelection.contains(event) && !seenEvents[event.guid]) {
+ seenEvents[event.guid] = true;
+ if (!this.filterExpression_ ||
+ this.filterExpression_.evaluate(context)) {
+ outputSelection.push(event);
+ }
+ }
+ if (!event.subSlices) return;
+ context = context.push(event);
+ for (let i = 0; i < event.subSlices.length; i++) {
+ context.event = event.subSlices[i];
+ this.evaluateFilterExpressionForEvent_(
+ context, inputSelection, outputSelection, seenEvents);
+ }
+ },
+
+ // Returns a task that fills the given selection with everything in the
+ // model.
+ selectEverythingAsTask_(selection) {
+ const filterTask = new tr.b.Task();
+ for (const container of this.model_.getDescendantEventContainers()) {
+ filterTask.subTask(() => {
+ for (const event of container.childEvents()) {
+ addEventTreeToSelection(selection, event);
+ }
+ }, this);
+ }
+ return filterTask;
+ },
+
+ // Returns a promise which will resolve into a {EventSet} representing the
+ // result of this query.
+ ready() {
+ return new Promise(function(resolve, reject) {
+ const graph = this.createFilterTaskGraph_();
+ graph.lastTask = graph.lastTask.after(function() {
+ resolve(this.selection_);
+ }, this);
+ tr.b.Task.RunWhenIdle(graph.rootTask);
+ }.bind(this));
+ },
+
+ get selection() {
+ if (this.selection_ === undefined) {
+ const graph = this.createFilterTaskGraph_();
+ tr.b.Task.RunSynchronously(graph.rootTask);
+ }
+ return this.selection_;
+ }
+ };
+ tr.c.ScriptingObjectRegistry.register(
+ new TQuery(),
+ {
+ name: '$t'
+ }
+ );
+
+ return {
+ TQuery,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery_test.html b/chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery_test.html
new file mode 100644
index 00000000000..1ecd7e6255a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/tquery/tquery_test.html
@@ -0,0 +1,247 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/tquery/tquery.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createTestModel(sliceCount) {
+ const slices = [];
+ for (let i = 0; i < sliceCount; i++) {
+ slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 2}));
+ }
+ return createTestModelWithSlices(slices);
+ }
+
+ function createTestModelWithSlices(slices) {
+ const model = new tr.Model();
+ const cpu = model.kernel.getOrCreateCpu(1);
+ for (let i = 0; i < slices.length; i++) {
+ cpu.slices.push(slices[i]);
+ }
+ return model;
+ }
+
+ function getScriptObject(name) {
+ const typeInfos = tr.c.ScriptingObjectRegistry.getAllRegisteredTypeInfos();
+ for (let i = 0; i < typeInfos.length; i++) {
+ if (typeInfos[i].metadata.name === name) {
+ return typeInfos[i].constructor;
+ }
+ }
+ }
+
+ test('tqueryAsyncSelection', function() {
+ const model = createTestModel(3);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ return tquery.ready().then(function(selection) {
+ assert.strictEqual(selection.length, 3);
+ });
+ });
+
+ test('tquerySyncSelection', function() {
+ let model = createTestModel(3);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ assert.strictEqual(tquery.selection.length, 3);
+
+ // Selection should get reset when the model changes.
+ model = createTestModel(5);
+ tquery.onModelChanged(model);
+ assert.strictEqual(tquery.selection.length, 5);
+ });
+
+ test('tqueryPassThroughFiltering', function() {
+ const model = new createTestModel(3);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ const result = tquery.filter().filter().selection;
+ assert.strictEqual(result.length, 3);
+ });
+
+ test('tqueryFilterHasTitle', function() {
+ const hasTitle = getScriptObject('hasTitle');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a'},
+ {guid: 2, title: 'b'},
+ {guid: 3, title: 'c'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ let result = tquery.filter(hasTitle('a')).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 1);
+
+ result = tquery.filter('b').selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 2);
+
+ result = tquery.filter(/^c$/).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 3);
+ });
+
+ test('tqueryFilterHasAncestor', function() {
+ const hasAncestor = getScriptObject('hasAncestor');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a'},
+ {guid: 2, title: 'b', subSlices: [{guid: 4}]},
+ {guid: 3, title: 'c'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ let result = tquery.filter(hasAncestor('b')).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 4);
+
+ result = tquery.filter(hasAncestor()).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 4);
+
+ result = tquery.filter(hasAncestor('a')).selection;
+ assert.strictEqual(result.length, 0);
+ });
+
+ test('tqueryFilterAllOf', function() {
+ const allOf = getScriptObject('allOf');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a1'},
+ {guid: 2, title: 'b1'},
+ {guid: 3, title: 'c1'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ let result = tquery.filter(allOf('a1')).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 1);
+
+ result = tquery.filter(allOf('a1', /1/)).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 1);
+
+ result = tquery.filter(allOf()).selection;
+ assert.strictEqual(result.length, 3);
+ });
+
+ test('tqueryFilterAnyOf', function() {
+ const anyOf = getScriptObject('anyOf');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a'},
+ {guid: 2, title: 'b'},
+ {guid: 3, title: 'c'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ let result = tquery.filter(anyOf('a', 'b')).selection;
+ assert.strictEqual(result.length, 2);
+ let nextGuid = 1;
+ for (const event of result) {
+ assert.strictEqual(event.guid, nextGuid++);
+ }
+
+ result = tquery.filter(anyOf('not there', 'a')).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 1);
+
+ result = tquery.filter(anyOf()).selection;
+ assert.strictEqual(result.length, 3);
+ });
+
+ test('tqueryFilterIsTopLevel', function() {
+ const isTopLevel = getScriptObject('isTopLevel');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a'},
+ {guid: 2, title: 'b', subSlices: [{guid: 4}]},
+ {guid: 3, title: 'c'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ let result = tquery.filter(isTopLevel()).selection;
+ assert.strictEqual(result.length, 3);
+ let nextGuid = 1;
+ for (const event of result) {
+ assert.strictEqual(event.guid, nextGuid++);
+ }
+
+ result = tquery.filter(isTopLevel('a')).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 1);
+ });
+
+ test('tqueryFilterHasDuration', function() {
+ const hasDuration = getScriptObject('hasDuration');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a', duration: 1},
+ {guid: 2, title: 'b', duration: 2},
+ {guid: 3, title: 'c', duration: 3},
+ {guid: 4, title: 'no duration'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ let result = tquery.filter(hasDuration(1.5, 2.5)).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 2);
+
+ result = tquery.filter(hasDuration(3, Infinity)).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 3);
+
+ result = tquery.filter(hasDuration(-1, 0)).selection;
+ assert.strictEqual(result.length, 0);
+
+ result = tquery.filter(hasDuration(function(d) {
+ return d > 2;
+ })).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 3);
+ });
+
+ test('tqueryFilterNot', function() {
+ const not = getScriptObject('not');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a'},
+ {guid: 2, title: 'b'},
+ {guid: 3, title: 'c'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ const result = tquery.filter(not(/a/)).selection;
+ assert.strictEqual(result.length, 2);
+ let nextGuid = 2;
+ for (const event of result) {
+ assert.strictEqual(event.guid, nextGuid++);
+ }
+
+ // Test a not() without any subexpressions.
+ assert.throws(function() { not(); });
+
+ // Test a not() with too many subexpressions.
+ assert.throws(function() { not(/a/, /b/); });
+ });
+
+ test('tqueryFilterNoneOf', function() {
+ const noneOf = getScriptObject('noneOf');
+ const model = createTestModelWithSlices([
+ {guid: 1, title: 'a'},
+ {guid: 2, title: 'b'},
+ {guid: 3, title: 'c'}
+ ]);
+ const tquery = new tr.e.tquery.TQuery(model);
+
+ const result = tquery.filter(noneOf(/a/, /b/)).selection;
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(result).guid, 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry.html
new file mode 100644
index 00000000000..6717b80302d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry.html
@@ -0,0 +1,209 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/v8/v8_ic_stats_thread_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.v8', function() {
+ const IC_STATS_PROPERTIES = [
+ 'type', 'category', 'scriptName', 'filePosition', 'state', 'isNative',
+ 'map', 'propertiesMode', 'numberOfOwnProperties', 'instanceType'
+ ];
+
+ class ICStatsEntry {
+ constructor(obj) {
+ this.type_ = obj.type;
+ if (this.type_.includes('Store')) {
+ this.category_ = 'Store';
+ } else if (this.type_.includes('Load')) {
+ this.category_ = 'Load';
+ }
+ this.state_ = obj.state;
+ if (obj.functionName) {
+ this.functionName_ = obj.optimized ? '*' : '~';
+ this.functionName_ += obj.functionName.length === 0 ?
+ '(anonymous function)' : obj.functionName;
+ }
+ this.offset_ = obj.offset;
+ this.scriptName_ = obj.scriptName ? obj.scriptName : 'unknown';
+ this.isNative_ = obj.scriptName && obj.scriptName.includes('native');
+ this.lineNum_ = obj.lineNum ? obj.lineNum : 'unknown';
+ this.filePosition_ = this.scriptName_ + ':' + this.lineNum_;
+ if (this.functionName_) {
+ this.filePosition_ += ' ' + this.functionName_ + '+' + this.offset_;
+ }
+ this.constructor_ = obj.constructor ? false : true;
+ this.map_ = obj.map;
+ if (this.map_) {
+ this.propertiesMode_ = obj.dict === 1 ? 'slow' : 'fast';
+ } else {
+ this.propertiesMode_ = 'unknown';
+ }
+ this.numberOfOwnProperties_ = obj.own;
+ this.instanceType_ = obj.instanceType;
+ this.key_ = obj.key;
+ }
+
+ get type() {
+ return this.type_;
+ }
+
+ get category() {
+ return this.category_;
+ }
+
+ get state() {
+ return this.state_;
+ }
+
+ get functionName() {
+ return this.functionName_;
+ }
+
+ get offset() {
+ return this.offset_;
+ }
+
+ get scriptName() {
+ return this.scriptName_;
+ }
+
+ get isNative() {
+ return this.isNative_;
+ }
+
+ get lineNumber() {
+ return this.lineNum_;
+ }
+
+ get isConstructor() {
+ return this.constructor_;
+ }
+
+ get map() {
+ return this.map_;
+ }
+
+ get propertiesMode() {
+ return this.propertiesMode_;
+ }
+
+ get numberOfOwnProperties() {
+ return this.numberOfOwnProperties_;
+ }
+
+ get instanceType() {
+ return this.instanceType_;
+ }
+
+ get filePosition() {
+ return this.filePosition_;
+ }
+ }
+
+ class ICStatsEntryGroup {
+ constructor(property, key) {
+ this.property_ = property;
+ this.key_ = key;
+ this.percentage_ = 0;
+ this.entries_ = [];
+ // This is map used to cache all sub grouping result
+ this.subGroup_ = undefined;
+ }
+
+ static groupBy(groups, entries, property) {
+ for (const entry of entries) {
+ const key = entry[property];
+ let group = groups.get(key);
+ if (!group) {
+ group = new ICStatsEntryGroup(property, key);
+ groups.set(key, group);
+ }
+ group.add(entry);
+ }
+ for (const group of groups.values()) {
+ group.percentage = group.length / entries.length;
+ }
+ }
+
+ add(entry) {
+ this.entries_.push(entry);
+ }
+
+ createSubGroup() {
+ if (this.subGroup_) return this.subGroup_;
+ this.subGroup_ = new Map();
+ for (const property of IC_STATS_PROPERTIES) {
+ if (property === this.property_) continue;
+ const groups = new Map();
+ this.subGroup_.set(property, groups);
+ ICStatsEntryGroup.groupBy(groups, this.entries_, property);
+ }
+ return this.subGroup_;
+ }
+
+ get entries() {
+ return this.entries_;
+ }
+
+ get key() {
+ return this.key_;
+ }
+
+ get length() {
+ return this.entries_.length;
+ }
+
+ get percentage() {
+ return this.percentage_;
+ }
+
+ set percentage(value) {
+ this.percentage_ = value;
+ }
+ }
+
+ class ICStatsCollection {
+ constructor() {
+ this.entries_ = [];
+ // Used to cache property grouping result.
+ this.groupedEntries_ = new Map();
+ }
+
+ add(entry) {
+ this.entries_.push(entry);
+ }
+
+ groupBy(property) {
+ if (this.groupedEntries_.has(property)) {
+ return Array.from(this.groupedEntries_.get(property).values());
+ }
+ const groups = new Map();
+ this.groupedEntries_.set(property, groups);
+ ICStatsEntryGroup.groupBy(groups, this.entries_, property);
+ return Array.from(groups.values());
+ }
+
+ get entries() {
+ return this.entries_;
+ }
+
+ get length() {
+ return this.entries_.length;
+ }
+ }
+
+ return {
+ IC_STATS_PROPERTIES,
+ ICStatsEntry,
+ ICStatsEntryGroup,
+ ICStatsCollection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry_test.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry_test.html
new file mode 100644
index 00000000000..1cb0f851552
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/ic_stats_entry_test.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/ic_stats_entry.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const obj1 = {
+ type: 'StoreIC',
+ functionName: 'f',
+ offset: 10,
+ scriptName: 'a.js',
+ lineNum: 1,
+ state: '(0->.)',
+ map: '0x000000000A',
+ dict: 0,
+ own: 3
+ };
+ const obj2 = {
+ type: 'LoadIC',
+ functionName: 'saveBuiltin',
+ offset: 11,
+ scriptName: 'extensions::SafeBuiltins',
+ lineNum: 2,
+ state: '(0->.)',
+ map: '0x00000000AA',
+ dict: 1,
+ own: 0
+ };
+ const obj3 = {
+ type: 'CompareIC',
+ functionName: 'InnerArrayForEach',
+ offset: 12,
+ scriptName: 'native array.js',
+ lineNum: 3,
+ state: '((UNINITIALIZED+UNINITIALIZED=UNINITIALIZED)->(SMI+SMI=SMI))#LT',
+ map: '0x0000000AAA'
+ };
+ const obj4 = {
+ type: 'StoreIC',
+ functionName: 'bar',
+ offset: 13,
+ scriptName: 'a.js',
+ lineNum: 4,
+ state: '(0->.)',
+ map: '0x000000AAAA',
+ dict: 1,
+ own: 7
+ };
+
+ test('ICStatsEntry', function() {
+ const entry1 = new tr.e.v8.ICStatsEntry(obj1);
+ assert.strictEqual(entry1.type, 'StoreIC');
+ assert.strictEqual(entry1.category, 'Store');
+ assert.strictEqual(entry1.state, '(0->.)');
+ assert.strictEqual(entry1.filePosition, 'a.js:1 ~f+10');
+ assert.strictEqual(entry1.isNative, false);
+ assert.strictEqual(entry1.propertiesMode, 'fast');
+ assert.strictEqual(entry1.numberOfOwnProperties, 3);
+
+ const entry2 = new tr.e.v8.ICStatsEntry(obj4);
+ assert.strictEqual(entry2.type, 'StoreIC');
+ assert.strictEqual(entry2.category, 'Store');
+ assert.strictEqual(entry2.state, '(0->.)');
+ assert.strictEqual(entry2.filePosition, 'a.js:4 ~bar+13');
+ assert.strictEqual(entry2.isNative, false);
+ assert.strictEqual(entry2.propertiesMode, 'slow');
+ assert.strictEqual(entry2.numberOfOwnProperties, 7);
+
+ const entry3 = new tr.e.v8.ICStatsEntry(obj3);
+ assert.strictEqual(entry3.isNative, true);
+ });
+
+ test('ICStatsEntryCollection', function() {
+ const entryCollection = new tr.e.v8.ICStatsCollection();
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj1));
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj2));
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj3));
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj4));
+
+ assert.strictEqual(entryCollection.length, 4);
+ });
+
+ test('ICStatsEntryGroup', function() {
+ const entryCollection = new tr.e.v8.ICStatsCollection();
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj1));
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj2));
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj3));
+ entryCollection.add(new tr.e.v8.ICStatsEntry(obj4));
+
+ const entryGroups = entryCollection.groupBy('type');
+ assert.strictEqual(entryGroups[0].length, 2);
+ assert.strictEqual(entryGroups[0].key, 'StoreIC');
+ assert.strictEqual(entryGroups[1].key, 'LoadIC');
+ assert.strictEqual(entryGroups[2].key, 'CompareIC');
+
+ const subGroups = entryGroups[0].createSubGroup();
+ assert.strictEqual(subGroups.size, 9);
+ const filePositionGroup = subGroups.get('filePosition');
+ assert.deepEqual(Array.from(filePositionGroup.keys()), [
+ 'a.js:1 ~f+10', 'a.js:4 ~bar+13'
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry.html
new file mode 100644
index 00000000000..9895f33096f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.v8', function() {
+ class RuntimeStatsEntry {
+ /**
+ * @param time is in microseconds.
+ */
+ constructor(name, count, time) {
+ this.name_ = name;
+ this.count_ = count;
+ this.time_ = time;
+ }
+
+ get name() {
+ return this.name_;
+ }
+
+ get count() {
+ return this.count_;
+ }
+
+ get time() {
+ return this.time_;
+ }
+
+ addSample(count, time) {
+ this.count_ += count;
+ this.time_ += time;
+ }
+ }
+
+ class RuntimeStatsGroup extends RuntimeStatsEntry {
+ constructor(name, matchRegex) {
+ super(name, 0, 0);
+ this.regex_ = matchRegex;
+ this.entries_ = new Map();
+ }
+
+ match(name) {
+ return this.regex_ && name.match(this.regex_);
+ }
+
+ add(entry) {
+ const value = this.entries_.get(entry.name);
+ if (value !== undefined) {
+ value.addSample(entry.count, entry.time);
+ } else {
+ this.entries_.set(entry.name, entry);
+ }
+ this.count_ += entry.count;
+ this.time_ += entry.time;
+ }
+
+ get values() {
+ return Array.from(this.entries_.values());
+ }
+ }
+
+ class RuntimeStatsGroupCollection {
+ constructor() {
+ this.blink_cpp_group_ =
+ new RuntimeStatsGroup('Blink C++', /.*Callback.*/);
+ this.api_group_ = new RuntimeStatsGroup('API', /.*API.*/);
+
+ this.groups_ = [
+ new RuntimeStatsGroup('Total'),
+ new RuntimeStatsGroup('IC', /.*IC_.*/),
+ new RuntimeStatsGroup('Optimize-Background',
+ /(.*OptimizeConcurrent.*)|RecompileConcurrent.*/),
+ new RuntimeStatsGroup('Optimize',
+ /StackGuard|.*Optimize.*|.*Deoptimize.*|Recompile.*/),
+ new RuntimeStatsGroup('Compile-Background',
+ /(.*CompileBackground.*)/),
+ new RuntimeStatsGroup('Compile', /(^Compile.*)|(.*_Compile.*)/),
+ new RuntimeStatsGroup('Parse-Background', /.*ParseBackground.*/),
+ new RuntimeStatsGroup('Parse', /.*Parse.*/),
+ this.blink_cpp_group_,
+ this.api_group_,
+ new RuntimeStatsGroup('GC-Background-Marking',
+ /.*GC.MC.BACKGROUND.*MARKING.*/),
+ new RuntimeStatsGroup('GC-Background-Sweeping',
+ /.*GC.MC.BACKGROUND.*SWEEPING.*/),
+ new RuntimeStatsGroup('GC-Background-Scavenger',
+ /.*GC.SCAVENGER.BACKGROUND.*/),
+ new RuntimeStatsGroup('GC-Background-MinorMC',
+ /.*GC.MINOR_MC.BACKGROUND.*/),
+ new RuntimeStatsGroup('GC-Background-MajorMC',
+ /.*GC.MC.BACKGROUND.*/),
+ new RuntimeStatsGroup('GC-Background-Other', /.*GC.*BACKGROUND.*/),
+ new RuntimeStatsGroup('GC', /GC|AllocateInTargetSpace/),
+ new RuntimeStatsGroup('JavaScript', /JS_Execution/),
+ new RuntimeStatsGroup('V8 C++', /.*/)
+ ];
+
+ this.blink_group_collection_ = null;
+ }
+
+ addSlices(slices) {
+ const blinkEntries = [];
+ for (const slice of slices) {
+ if (!(slice instanceof tr.e.v8.V8ThreadSlice)) return;
+ let runtimeCallStats;
+ try {
+ runtimeCallStats = JSON.parse(slice.runtimeCallStats);
+ } catch (e) {
+ runtimeCallStats = slice.runtimeCallStats;
+ }
+ if (runtimeCallStats === undefined) continue;
+ for (const [name, stat] of Object.entries(runtimeCallStats)) {
+ // Blink RCS stats go to a separate table
+ if (name.match(/Blink_.*/)) {
+ // This counter is used to avoid counting time spent in V8 as time
+ // spent doing Blink C++, and interferes with total and percentage
+ // calculations, so we skip this counter.
+ if (name === 'Blink_V8') continue;
+ const entry = new RuntimeStatsEntry(name, stat[0], stat[1]);
+ blinkEntries.push(entry);
+ continue;
+ }
+
+ // Skip the 'Total' group
+ for (let i = 1; i < this.groups_.length; ++i) {
+ if (this.groups_[i].match(name)) {
+ if (stat.length !== 2) break;
+ const entry = new RuntimeStatsEntry(name, stat[0], stat[1]);
+ this.groups_[0].addSample(stat[0], stat[1]);
+ this.groups_[i].add(entry);
+ break;
+ }
+ }
+ }
+ }
+
+ this.blink_group_collection_ =
+ new BlinkRuntimeStatsGroupCollection(blinkEntries);
+ }
+
+ get totalTime() {
+ return this.groups_[0].time;
+ }
+
+ get totalCount() {
+ return this.groups_[0].count;
+ }
+
+ get runtimeGroups() {
+ return this.groups_;
+ }
+
+ get blinkRCSGroupCollection() {
+ return this.blink_group_collection_;
+ }
+
+ get blinkCppTotalTime() {
+ // Include API time because Blink RCS times also include V8 API times
+ return this.blink_cpp_group_.time + this.api_group_.time;
+ }
+ }
+
+ class BlinkRuntimeStatsGroupCollection {
+ constructor(entries) {
+ this.groups_ = [
+ new RuntimeStatsGroup('Blink_Bindings', /^Blink_Bindings_(.*)/),
+ new RuntimeStatsGroup('Blink_GC', /^Blink_GC_(.*)/),
+ new RuntimeStatsGroup('Blink_Layout', /^Blink_Layout_(.*)/),
+ new RuntimeStatsGroup('Blink_Parsing', /^Blink_Parsing_(.*)/),
+ new RuntimeStatsGroup('Blink_Style', /^Blink_Style_(.*)/),
+ new RuntimeStatsGroup('Blink_Callbacks', /^Blink_(.*)/)
+ ];
+ this.total_group_ = new RuntimeStatsGroup('Blink_Total', /.*/);
+
+ for (const entry of entries) {
+ for (const group of this.groups_) {
+ if (group.match(entry.name)) {
+ // Strip out category prefix
+ const newEntry = new RuntimeStatsEntry(
+ 'Blink_' + group.match(entry.name)[1], entry.count, entry.time);
+ group.add(newEntry);
+ this.total_group_.addSample(entry.count, entry.time);
+ break;
+ }
+ }
+ }
+ }
+
+ get runtimeGroups() {
+ return this.groups_.concat(this.total_group_);
+ }
+
+ get values() {
+ return this.groups_.reduce(
+ (values, group) => values.concat(group.values), []);
+ }
+
+ get totalTime() {
+ return this.total_group_.time;
+ }
+
+ get totalCount() {
+ return this.total_group_.count;
+ }
+ }
+
+ return {
+ BlinkRuntimeStatsGroupCollection,
+ RuntimeStatsEntry,
+ RuntimeStatsGroup,
+ RuntimeStatsGroupCollection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry_test.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry_test.html
new file mode 100644
index 00000000000..aa5aa39ee55
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/runtime_stats_entry_test.html
@@ -0,0 +1,322 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('RuntimeStatsEntry', function() {
+ const entry = new tr.e.v8.RuntimeStatsEntry('IC_Entry', 5, 1234);
+ assert.strictEqual(entry.name, 'IC_Entry');
+ assert.strictEqual(entry.count, 5);
+ assert.strictEqual(entry.time, 1234);
+
+ entry.addSample(37, 8766);
+ assert.strictEqual(entry.name, 'IC_Entry');
+ assert.strictEqual(entry.count, 42);
+ assert.strictEqual(entry.time, 10000);
+
+ entry.addSample(58, 1);
+ assert.strictEqual(entry.name, 'IC_Entry');
+ assert.strictEqual(entry.count, 100);
+ assert.strictEqual(entry.time, 10001);
+ });
+
+ test('RuntimeStatsGroup', function() {
+ const group = new tr.e.v8.RuntimeStatsGroup('IC', /.*IC_.*/);
+ assert.notEqual(group.match('IC_Entry'), undefined);
+ assert.notEqual(group.match('CallbackIC_Entry'), undefined);
+ assert.strictEqual(group.match('Callback_Entry'), null);
+
+ const entry1a = new tr.e.v8.RuntimeStatsEntry('IC_Entry', 5, 1234);
+ group.add(entry1a);
+ assert.deepEqual(group.values, [entry1a]);
+ const entry2 = new tr.e.v8.RuntimeStatsEntry('IC_Entry2', 2, 2048);
+ group.add(entry2);
+ assert.deepEqual(group.values, [entry1a, entry2]);
+ const entry1b = new tr.e.v8.RuntimeStatsEntry('IC_Entry', 37, 8766);
+ group.add(entry1b);
+ const entry1sum = new tr.e.v8.RuntimeStatsEntry('IC_Entry', 42, 10000);
+ assert.deepEqual(group.values, [entry1sum, entry2]);
+ });
+
+ function checkRuntimeGroup_(group, name, count, time) {
+ assert.strictEqual(group.name, name);
+ assert.strictEqual(group.count, count);
+ assert.strictEqual(group.time, time);
+ }
+
+ test('RuntimeStatsGroupCollection', function() {
+ const slices = [];
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99],
+ RecompileConcurrent: [10, 100],
+ }
+ }
+ }));
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'V8.newInstance',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [2, 22],
+ HandleApiCall: [3, 33],
+ CompileFullCode: [4, 44],
+ LoadIC_Miss: [5, 55],
+ ParseLazy: [6, 66],
+ OptimizeCode: [7, 77],
+ FunctionCallback: [8, 88],
+ AllocateInTargetSpace: [9, 99],
+ API_Object_Get: [1, 11],
+ ParseBackgroundFunctionLiteral: [2, 22],
+ CompileBackgroundIgnition: [3, 33],
+ RecompileConcurrent: [5, 50],
+ }
+ }
+ }));
+
+ const groupCollection = new tr.e.v8.RuntimeStatsGroupCollection();
+ groupCollection.addSlices(slices);
+ assert.strictEqual(groupCollection.totalTime, 1195);
+
+ const groups = groupCollection.runtimeGroups;
+ assert.deepEqual(groups.length, 19);
+ checkRuntimeGroup_(groups[0], 'Total', 110, 1195);
+ checkRuntimeGroup_(groups[1], 'IC', 9, 99);
+ checkRuntimeGroup_(groups[2], 'Optimize-Background', 15, 150);
+ checkRuntimeGroup_(groups[3], 'Optimize', 13, 143);
+ checkRuntimeGroup_(groups[4], 'Compile-Background', 3, 33);
+ checkRuntimeGroup_(groups[5], 'Compile', 7, 77);
+ checkRuntimeGroup_(groups[6], 'Parse-Background', 2, 22);
+ checkRuntimeGroup_(groups[7], 'Parse', 11, 121);
+ checkRuntimeGroup_(groups[8], 'Blink C++', 15, 165);
+ checkRuntimeGroup_(groups[9], 'API', 10, 110);
+ checkRuntimeGroup_(groups[10], 'GC-Background-Marking', 0, 0);
+ checkRuntimeGroup_(groups[11], 'GC-Background-Sweeping', 0, 0);
+ checkRuntimeGroup_(groups[12], 'GC-Background-Scavenger', 0, 0);
+ checkRuntimeGroup_(groups[13], 'GC-Background-MinorMC', 0, 0);
+ checkRuntimeGroup_(groups[14], 'GC-Background-MajorMC', 0, 0);
+ checkRuntimeGroup_(groups[15], 'GC-Background-Other', 0, 0);
+ checkRuntimeGroup_(groups[16], 'GC', 17, 187);
+ checkRuntimeGroup_(groups[17], 'JavaScript', 3, 33);
+ checkRuntimeGroup_(groups[18], 'V8 C++', 5, 55);
+ });
+
+ test('BlinkRuntimeStatsGroupCollection', function() {
+ const slices = [];
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99],
+ RecompileConcurrent: [10, 100],
+ }
+ }
+ }));
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'V8.newInstance',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [2, 22],
+ HandleApiCall: [3, 33],
+ CompileFullCode: [4, 44],
+ LoadIC_Miss: [5, 55],
+ ParseLazy: [6, 66],
+ OptimizeCode: [7, 77],
+ FunctionCallback: [8, 88],
+ AllocateInTargetSpace: [9, 99],
+ API_Object_Get: [1, 11],
+ ParseBackgroundFunctionLiteral: [2, 22],
+ CompileBackgroundIgnition: [3, 33],
+ RecompileConcurrent: [5, 50],
+ }
+ }
+ }));
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'BlinkRuntimeCallStats',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'disabled-by-default-v8.runtime_stats',
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [10, 25],
+ Blink_GC_CollectGarbage: [10, 20],
+ Blink_Style_UpdateStyle: [15, 30],
+ Blink_Parsing_ParseHTML: [5, 60],
+ Blink_WindowSetTimeout: [10, 15],
+ Blink_V8: [5, 100],
+ }
+ }
+ }));
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'BlinkRuntimeCallStats',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'disabled-by-default-v8.runtime_stats',
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [5, 10],
+ Blink_GC_CollectGarbage: [20, 50],
+ Blink_Parsing_ParseHTML: [10, 60],
+ Blink_Bindings_CreateWrapper: [100, 5],
+ Blink_NodeAppendChild: [20, 20],
+ Blink_V8: [2, 50],
+ }
+ }
+ }));
+
+ const groupCollection = new tr.e.v8.RuntimeStatsGroupCollection();
+ groupCollection.addSlices(slices);
+ assert.strictEqual(groupCollection.totalTime, 1195);
+
+ const groups = groupCollection.runtimeGroups;
+ assert.deepEqual(groups.length, 19);
+ checkRuntimeGroup_(groups[0], 'Total', 110, 1195);
+ checkRuntimeGroup_(groups[1], 'IC', 9, 99);
+ checkRuntimeGroup_(groups[2], 'Optimize-Background', 15, 150);
+ checkRuntimeGroup_(groups[3], 'Optimize', 13, 143);
+ checkRuntimeGroup_(groups[4], 'Compile-Background', 3, 33);
+ checkRuntimeGroup_(groups[5], 'Compile', 7, 77);
+ checkRuntimeGroup_(groups[6], 'Parse-Background', 2, 22);
+ checkRuntimeGroup_(groups[7], 'Parse', 11, 121);
+ checkRuntimeGroup_(groups[8], 'Blink C++', 15, 165);
+ checkRuntimeGroup_(groups[9], 'API', 10, 110);
+ checkRuntimeGroup_(groups[10], 'GC-Background-Marking', 0, 0);
+ checkRuntimeGroup_(groups[11], 'GC-Background-Sweeping', 0, 0);
+ checkRuntimeGroup_(groups[12], 'GC-Background-Scavenger', 0, 0);
+ checkRuntimeGroup_(groups[13], 'GC-Background-MinorMC', 0, 0);
+ checkRuntimeGroup_(groups[14], 'GC-Background-MajorMC', 0, 0);
+ checkRuntimeGroup_(groups[15], 'GC-Background-Other', 0, 0);
+ checkRuntimeGroup_(groups[16], 'GC', 17, 187);
+ checkRuntimeGroup_(groups[17], 'JavaScript', 3, 33);
+ checkRuntimeGroup_(groups[18], 'V8 C++', 5, 55);
+
+ const blinkGroupCollection = groupCollection.blinkRCSGroupCollection;
+ const blinkGroups = blinkGroupCollection.runtimeGroups;
+ checkRuntimeGroup_(blinkGroups[0], 'Blink_Bindings', 100, 5);
+ checkRuntimeGroup_(blinkGroups[1], 'Blink_GC', 30, 70);
+ checkRuntimeGroup_(blinkGroups[2], 'Blink_Layout', 15, 35);
+ checkRuntimeGroup_(blinkGroups[3], 'Blink_Parsing', 15, 120);
+ checkRuntimeGroup_(blinkGroups[4], 'Blink_Style', 15, 30);
+ checkRuntimeGroup_(blinkGroups[5], 'Blink_Callbacks', 30, 35);
+
+ assert.strictEqual(205, blinkGroupCollection.totalCount);
+ assert.strictEqual(295, blinkGroupCollection.totalTime);
+
+ assert.strictEqual('Blink_UpdateLayout', blinkGroups[2].values[0].name);
+ assert.strictEqual('Blink_CollectGarbage', blinkGroups[1].values[0].name);
+ });
+
+ test('RuntimeStatsBackgroundGC', function() {
+ const slices = [];
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'V8.Execute',
+ start: 0,
+ end: 700,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {
+ 'runtime-call-stats': {
+ GC_BACKGROUND_UNMAPPER: [1, 10],
+ GC_SCAVENGER_BACKGROUND_SCAVENGE_PARALLEL: [10, 100],
+ GC_MINOR_MC_BACKGROUND_MARKING: [20, 200],
+ GC_MINOR_MC_BACKGROUND_EVACUATE_COPY: [30, 300],
+ GC_MC_BACKGROUND_MARKING: [40, 400],
+ GC_MC_BACKGROUND_SWEEPING: [50, 500],
+ GC_MC_BACKGROUND_EVACUATE_COPY: [60, 600],
+ GC_MC_BACKGROUND_EVACUATE_UPDATE_POINTERS: [70, 700]
+ }
+ }
+ }));
+ slices.push(tr.c.TestUtils.newSliceEx({
+ title: 'V8.newInstance',
+ start: 1000,
+ end: 8000,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {
+ 'runtime-call-stats': {
+ GC_SCAVENGER_BACKGROUND_SCAVENGE_PARALLEL: [100, 1000],
+ GC_MINOR_MC_BACKGROUND_MARKING: [200, 2000],
+ GC_MINOR_MC_BACKGROUND_EVACUATE_COPY: [300, 3000],
+ GC_MC_BACKGROUND_MARKING: [400, 4000],
+ GC_MC_BACKGROUND_SWEEPING: [500, 5000],
+ GC_MC_BACKGROUND_EVACUATE_COPY: [600, 6000],
+ GC_MC_BACKGROUND_EVACUATE_UPDATE_POINTERS: [700, 7000]
+ }
+ }
+ }));
+ const groupCollection = new tr.e.v8.RuntimeStatsGroupCollection();
+ groupCollection.addSlices(slices);
+ assert.strictEqual(groupCollection.totalTime, 28000 + 2800 + 10);
+
+ const groups = groupCollection.runtimeGroups;
+ assert.deepEqual(groups.length, 19);
+ checkRuntimeGroup_(groups[0], 'Total', 2800 + 280 + 1, 28000 + 2800 + 10);
+ checkRuntimeGroup_(groups[1], 'IC', 0, 0);
+ checkRuntimeGroup_(groups[2], 'Optimize-Background', 0, 0);
+ checkRuntimeGroup_(groups[3], 'Optimize', 0, 0);
+ checkRuntimeGroup_(groups[4], 'Compile-Background', 0, 0);
+ checkRuntimeGroup_(groups[5], 'Compile', 0, 0);
+ checkRuntimeGroup_(groups[6], 'Parse-Background', 0, 0);
+ checkRuntimeGroup_(groups[7], 'Parse', 0, 0);
+ checkRuntimeGroup_(groups[8], 'Blink C++', 0, 0);
+ checkRuntimeGroup_(groups[9], 'API', 0, 0);
+ checkRuntimeGroup_(groups[10], 'GC-Background-Marking', 440, 4400);
+ checkRuntimeGroup_(groups[11], 'GC-Background-Sweeping', 550, 5500);
+ checkRuntimeGroup_(groups[12], 'GC-Background-Scavenger', 110, 1100);
+ checkRuntimeGroup_(groups[13], 'GC-Background-MinorMC',
+ 300 + 200 + 30 + 20, 3000 + 2000 + 300 + 200);
+ checkRuntimeGroup_(groups[14], 'GC-Background-MajorMC',
+ 700 + 600 + 70 + 60, 7000 + 6000 + 700 + 600);
+ checkRuntimeGroup_(groups[15], 'GC-Background-Other', 1, 10);
+ checkRuntimeGroup_(groups[16], 'GC', 0, 0);
+ checkRuntimeGroup_(groups[17], 'JavaScript', 0, 0);
+ checkRuntimeGroup_(groups[18], 'V8 C++', 0, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_cpu_profile_node.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_cpu_profile_node.html
new file mode 100644
index 00000000000..b5a1855fdca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_cpu_profile_node.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/profile_node.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the V8CpuProfileNode class.
+ */
+tr.exportTo('tr.e.v8', function() {
+ const ProfileNode = tr.model.ProfileNode;
+ /**
+ * A V8CpuProfileNode represents a node in the v8 cpu profile tree,
+ * it is essentially a frame in the stack when the sample gets recorded.
+ */
+ function V8CpuProfileNode(id, callFrame, parentNode) {
+ ProfileNode.call(this, id, callFrame.functionName, parentNode);
+ this.callFrame_ = tr.b.deepCopy(callFrame);
+ this.deoptReason_ = '';
+ this.colorId_ = tr.b.ColorScheme.getColorIdForGeneralPurposeString(
+ callFrame.functionName);
+ }
+
+ V8CpuProfileNode.prototype = {
+ __proto__: ProfileNode.prototype,
+
+ get functionName() {
+ return this.callFrame_.functionName;
+ },
+
+ get scriptId() {
+ return this.callFrame_.scriptId;
+ },
+
+ get url() {
+ if (!this.callFrame_.url) {
+ return 'unknown';
+ }
+ let url = this.callFrame_.url;
+ if (this.callFrame_.lineNumber === undefined) {
+ return url;
+ }
+ url = url + ':' + this.callFrame_.lineNumber;
+ if (this.callFrame_.columnNumber === undefined) {
+ return url;
+ }
+ url = url + ':' + this.callFrame_.columnNumber;
+ return url;
+ },
+
+ get deoptReason() {
+ return this.deoptReason_;
+ },
+
+ set deoptReason(value) {
+ this.deoptReason_ = value;
+ },
+
+ get userFriendlyName() {
+ const name = this.functionName + ' url: ' + this.url;
+ return !this.deoptReason_ ?
+ name : name + ' Deoptimized reason: ' + this.deoptReason_;
+ },
+
+ get sampleTitle() {
+ return 'V8 Sample';
+ }
+ };
+
+ V8CpuProfileNode.constructFromObject = function(profileTree, node) {
+ const nodeId = node.id;
+ if (nodeId === 1) {
+ // Ignore fake root.
+ // The node with id 1 is a fake root for the profile tree,
+ // since we are using map, we don't really need it.
+ return undefined;
+ }
+ const parentNode = profileTree.getNode(node.parent);
+ const profileNode = new V8CpuProfileNode(nodeId, node.callFrame,
+ parentNode);
+ if (node.deoptReason !== undefined) {
+ profileNode.deoptReason = node.deoptReason;
+ }
+ return profileNode;
+ };
+
+ ProfileNode.subTypes.register(
+ V8CpuProfileNode,
+ {
+ typeName: 'cpuProfile',
+ name: 'v8 cpu profile node',
+ pluralName: 'v8 cpu profile nodes'
+ }
+ );
+
+ ProfileNode.subTypes.register(
+ V8CpuProfileNode,
+ {
+ typeName: 'legacySample',
+ name: 'v8 cpu profile node',
+ pluralName: 'v8 cpu profile nodes'
+ }
+ );
+
+ return {
+ ProfileNode,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_gc_stats_thread_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_gc_stats_thread_slice.html
new file mode 100644
index 00000000000..8e29b5fdf3a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_gc_stats_thread_slice.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.v8', function() {
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ function V8GCStatsThreadSlice() {
+ ThreadSlice.apply(this, arguments);
+ this.liveObjects_ = JSON.parse(this.args.live);
+ delete this.args.live;
+ this.deadObjects_ = JSON.parse(this.args.dead);
+ delete this.args.dead;
+ }
+
+ V8GCStatsThreadSlice.prototype = {
+ __proto__: ThreadSlice.prototype,
+
+ get liveObjects() {
+ return this.liveObjects_;
+ },
+
+ get deadObjects() {
+ return this.deadObjects_;
+ }
+ };
+
+ ThreadSlice.subTypes.register(
+ V8GCStatsThreadSlice,
+ {
+ categoryParts: ['disabled-by-default-v8.gc_stats'],
+ name: 'v8 gc stats slice',
+ pluralName: 'v8 gc stats slices'
+ }
+ );
+
+ return {
+ V8GCStatsThreadSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_ic_stats_thread_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_ic_stats_thread_slice.html
new file mode 100644
index 00000000000..7ee3f0c527c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_ic_stats_thread_slice.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.v8', function() {
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ function V8ICStatsThreadSlice() {
+ ThreadSlice.apply(this, arguments);
+ this.icStats_ = undefined;
+ if (this.args['ic-stats']) {
+ this.icStats_ = this.args['ic-stats'].data;
+ delete this.args['ic-stats'];
+ }
+ }
+
+ V8ICStatsThreadSlice.prototype = {
+ __proto__: ThreadSlice.prototype,
+
+ get icStats() {
+ return this.icStats_;
+ }
+ };
+
+ ThreadSlice.subTypes.register(
+ V8ICStatsThreadSlice,
+ {
+ categoryParts: ['disabled-by-default-v8.ic_stats'],
+ name: 'v8 ic stats slice',
+ pluralName: 'v8 ic stats slices'
+ }
+ );
+
+ return {
+ V8ICStatsThreadSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_thread_slice.html b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_thread_slice.html
new file mode 100644
index 00000000000..4aaee2eeca5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8/v8_thread_slice.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.e.v8', function() {
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ function V8ThreadSlice() {
+ ThreadSlice.apply(this, arguments);
+ this.runtimeCallStats_ = undefined;
+ }
+
+ V8ThreadSlice.prototype = {
+ __proto__: ThreadSlice.prototype,
+
+ get runtimeCallStats() {
+ if ('runtime-call-stats' in this.args) {
+ this.runtimeCallStats_ = this.args['runtime-call-stats'];
+ delete this.args['runtime-call-stats'];
+ }
+ return this.runtimeCallStats_;
+ }
+ };
+
+ ThreadSlice.subTypes.register(
+ V8ThreadSlice,
+ {
+ categoryParts: ['v8', 'disabled-by-default-v8.runtime_stats'],
+ name: 'v8 slice',
+ pluralName: 'v8 slices'
+ }
+ );
+
+ return {
+ V8ThreadSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/v8_config.html b/chromium/third_party/catapult/tracing/tracing/extras/v8_config.html
new file mode 100644
index 00000000000..7729e6c1905
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/v8_config.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/v8/v8_cpu_profile_node.html">
+<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html">
+<link rel="import" href="/tracing/extras/v8/v8_ic_stats_thread_slice.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor.html b/chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor.html
new file mode 100644
index 00000000000..df56592cde5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/auditor.html">
+
+<script>
+'use strict';
+
+function filterDuplicateTimestamps(timestamps) {
+ const dedupedTimestamps = [];
+ let lastTs = 0;
+ for (const ts of timestamps) {
+ if (ts - lastTs >= 1) {
+ dedupedTimestamps.push(ts);
+ lastTs = ts;
+ }
+ }
+ return dedupedTimestamps;
+}
+
+tr.exportTo('tr.e.audits', function() {
+ const VSYNC_COUNTER_PRECISIONS = {
+ // Android. Some versions have VSYNC split out into VSYNC-app and VSYNC-sf.
+ // Requires "gfx" systrace category to be enabled.
+ 'android.VSYNC-app': 15,
+ 'android.VSYNC': 15
+ };
+
+ const VSYNC_SLICE_PRECISIONS = {
+ // Android.
+ 'RenderWidgetHostViewAndroid::OnVSync': 5,
+ // Android. Very precise. Requires "gfx" systrace category to be enabled.
+ 'VSYNC': 10,
+ // Linux. Very precise. Requires "gpu" tracing category to be enabled.
+ 'vblank': 10,
+ // Mac. Derived from a Mac callback (CVDisplayLinkSetOutputCallback).
+ 'DisplayLinkMac::GetVSyncParameters': 5
+ };
+
+ const BEGIN_FRAME_SLICE_PRECISION = {
+ 'DisplayScheduler::BeginFrame': 10
+ };
+
+ /**
+ * Auditor that analyzes the model and, if possible, adds data to it
+ * indicating when vertical sync events took place.
+ *
+ * @constructor
+ * @extends {tr.c.Auditor}
+ */
+ function VSyncAuditor(model) {
+ tr.c.Auditor.call(this, model);
+ }
+
+ VSyncAuditor.prototype = {
+ __proto__: tr.c.Auditor.prototype,
+
+ runAnnotate() {
+ this.model.device.vSyncTimestamps = this.findVSyncTimestamps(this.model);
+ },
+
+ /**
+ * Returns an array of the most accurate VSync times available in the model.
+ */
+ findVSyncTimestamps(model) {
+ let times = [];
+
+ // Only keep the most precise VSync data.
+ let maxPrecision = Number.NEGATIVE_INFINITY;
+ let maxTitle = undefined;
+
+ function useInstead(title, precisions) {
+ const precision = precisions[title];
+ if (precision === undefined) return false;
+
+ if (title === maxTitle) return true;
+
+ if (precision <= maxPrecision) {
+ if (precision === maxPrecision) {
+ model.importWarning({
+ type: 'VSyncAuditor',
+ message: 'Encountered two different VSync events (' +
+ maxTitle + ', ' + title + ') with the same precision, ' +
+ 'ignoring the newer one (' + title + ')',
+ showToUser: false,
+ });
+ }
+ return false;
+ }
+ maxPrecision = precision;
+ maxTitle = title;
+ times = [];
+
+ return true;
+ }
+
+ for (const pid in model.processes) {
+ const process = model.processes[pid];
+ // Traverse process counters.
+ for (const cid in process.counters) {
+ if (useInstead(cid, VSYNC_COUNTER_PRECISIONS)) {
+ const counter = process.counters[cid];
+ for (let i = 0; i < counter.series.length; i++) {
+ const series = counter.series[i];
+ Array.prototype.push.apply(times, series.timestamps);
+ }
+ }
+ }
+
+ // Traverse thread slices.
+ for (const tid in process.threads) {
+ const thread = process.threads[tid];
+ for (let i = 0; i < thread.sliceGroup.slices.length; i++) {
+ const slice = thread.sliceGroup.slices[i];
+ if (useInstead(slice.title, VSYNC_SLICE_PRECISIONS)) {
+ times.push(slice.start);
+ } else if (useInstead(slice.title, BEGIN_FRAME_SLICE_PRECISION) &&
+ slice.args.args && slice.args.args.frame_time_us) {
+ // We need to check not only that we have a Scheduler::BeginFrame
+ // event, but also that we have one that has a frame time
+ // associated with it.
+ // Older versions of Scheduler::BeginFrame don't have one.
+ times.push(slice.args.args.frame_time_us / 1000.0);
+ }
+ }
+ }
+ }
+ times.sort(function(x, y) { return x - y; });
+ return filterDuplicateTimestamps(times);
+ }
+ };
+
+ tr.c.Auditor.register(VSyncAuditor);
+
+ return {
+ VSyncAuditor,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor_test.html b/chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor_test.html
new file mode 100644
index 00000000000..69aea99fd9e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/extras/vsync/vsync_auditor_test.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/vsync/vsync_auditor.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const CounterSeries = tr.model.CounterSeries;
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const VSyncAuditor = tr.e.audits.VSyncAuditor;
+
+ function testFindVSyncTimestamps(slices, counters, expectedTimes) {
+ const model = buildModel(slices, counters);
+ const auditor = new VSyncAuditor(model);
+ assert.deepEqual(auditor.findVSyncTimestamps(model), expectedTimes);
+ }
+
+ function buildModel(slices, counters) {
+ const model = new Model();
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ for (let j = 0; j < slices[i].length; j++) {
+ thread.sliceGroup.pushSlice(slices[i][j]);
+ }
+ }
+ for (let i = 0; i < counters.length; i++) {
+ const counter = process.getOrCreateCounter(
+ counters[i].category,
+ counters[i].name);
+ counter.addSeries(counters[i].series);
+ }
+ return model;
+ }
+
+ function buildSlice(title, time) {
+ return new ThreadSlice('', title, 0, time, {});
+ }
+
+ function buildCounterSeries(name, timestamps) {
+ const series = new CounterSeries(name, '');
+ for (let i = 0; i < timestamps.length; i++) {
+ series.addCounterSample(timestamps[i], 1);
+ }
+ return series;
+ }
+
+ test('findEmpty', function() {
+ testFindVSyncTimestamps([], [], []);
+ });
+
+ test('findNoVsync', function() {
+ testFindVSyncTimestamps([
+ [buildSlice('MessageLoop::RunTask', 10),
+ buildSlice('MessageLoop::RunTask', 20)],
+ [buildSlice('MessageLoop::RunTask', 15)]
+ ], [], []);
+ });
+
+ test('findOneVsync', function() {
+ testFindVSyncTimestamps([[buildSlice('vblank', 42)]], [], [42]);
+ });
+
+ test('findMultipleVsyncs', function() {
+ testFindVSyncTimestamps([
+ [buildSlice('VSYNC', 1), buildSlice('MessageLoop::RunTask', 2)],
+ [buildSlice('MessageLoop::RunTask', 3)],
+ [buildSlice('MessageLoop::RunTask', 4), buildSlice('VSYNC', 5)],
+ [buildSlice('VSYNC', 6), buildSlice('VSYNC', 7)]
+ ], [], [1, 5, 6, 7]);
+ });
+
+ test('filterNearDuplicates', function() {
+ testFindVSyncTimestamps([
+ [buildSlice('VSYNC', 1), buildSlice('VSYNC', 1)],
+ [buildSlice('VSYNC', 2), buildSlice('VSYNC', 2.1)],
+ [buildSlice('VSYNC', 3), buildSlice('VSYNC', 4)],
+ ], [], [1, 2, 3, 4]);
+ });
+
+ test('findMultipleAndroidVsyncs', function() {
+ testFindVSyncTimestamps([
+ [buildSlice('MessageLoop::RunTask', 2)],
+ [buildSlice('MessageLoop::RunTask', 3)],
+ [buildSlice('MessageLoop::RunTask', 4)]
+ ],
+ [
+ {
+ category: 'android',
+ name: 'VSYNC-app',
+ series: buildCounterSeries('VSYNC-app', [1, 5, 6, 7])
+ }
+ ], [1, 5, 6, 7]);
+ });
+
+ test('findUnsorted', function() {
+ testFindVSyncTimestamps([
+ [buildSlice('RenderWidgetHostViewAndroid::OnVSync', 4),
+ buildSlice('MessageLoop::RunTask', 2)],
+ [buildSlice('RenderWidgetHostViewAndroid::OnVSync', 1),
+ buildSlice('RenderWidgetHostViewAndroid::OnVSync', 3)]
+ ], [], [1, 3, 4]);
+ });
+
+ test('findDifferentPrecisions', function() {
+ // vblank has higher precision than RenderWidgetHostViewAndroid::OnVSync.
+ testFindVSyncTimestamps([
+ [buildSlice('RenderWidgetHostViewAndroid::OnVSync', 1),
+ buildSlice('vblank', 2),
+ buildSlice('RenderWidgetHostViewAndroid::OnVSync', 3)]
+ ], [], [2]);
+ });
+
+ test('findBeginFrame', function() {
+ const title = 'DisplayScheduler::BeginFrame';
+ testFindVSyncTimestamps([[
+ new ThreadSlice('', title, 0, 2, { args: { frame_time_us: 1000 } }),
+ new ThreadSlice('', title, 0, 4, { args: { frame_time_us: 3000 } })
+ ]], [], [1, 3]);
+ });
+
+ test('findBeginFrame_noFrameTime', function() {
+ const title = 'DisplayScheduler::BeginFrame';
+ testFindVSyncTimestamps([[
+ new ThreadSlice('', title, 0, 2, {}),
+ new ThreadSlice('', title, 0, 4, { args: {} })
+ ]], [], []);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html b/chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html
new file mode 100644
index 00000000000..ce164eb881b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/battor_importer.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/clock_sync_manager.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains end-to-end clock sync tests that ensure
+ * clock sync behavior works as expected across traces. There are too many
+ * possible combinations of trace types to test all of them, but we aim to test
+ * many of the important ones in this file.
+ */
+tr.b.unittest.testSuite(function() {
+ test('import_noClockDomains', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([]);
+
+ assert.isFalse(m.hasImportWarnings);
+ });
+
+ test('import_traceEvent', function() {
+ const trace = JSON.stringify({
+ traceEvents: [
+ {ts: 0, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}},
+ {ts: 1000, pid: 0, tid: 0, ph: 'c', cat: 'metadata',
+ args: { issue_ts: 500, sync_id: 'abc' }},
+ {ts: 2000, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}}
+ ]});
+ const m = tr.c.TestUtils.newModelWithEvents([trace]);
+
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[0].start, 0);
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[1].start, 2);
+ });
+
+
+ test('import_ftrace', function() {
+ const ftrace =
+ 'SurfaceFlinger-0 [001] ...1 0.001: 0: B|1|taskA\n' +
+ ' chrome-3 [001] ...1 0.010: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0.020\n';
+
+ const m = tr.c.TestUtils.newModelWithEvents([ftrace]);
+
+ assert.isFalse(m.hasImportWarnings);
+ assert.strictEqual(m.processes[1].threads[0].sliceGroup.slices[0].start, 0);
+ });
+
+ test('import_traceEventWithNoClockDomainAndFtrace', function() {
+ // Include a clock sync marker that indicates the LINUX_CLOCK_MONOTONIC time
+ // of 20ms is equal to the LINUX_FTRACE_GLOBAL time of 10ms, effectively
+ // shifting all ftrace timestamps forward by 10ms.
+ const ftrace =
+ 'SurfaceFlinger-0 [001] ...1 0.001: 0: B|1|taskA\n' +
+ ' chrome-3 [001] ...1 0.010: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0.020\n';
+
+ const trace = JSON.stringify({
+ traceEvents: [
+ {ts: 0, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}},
+ {ts: 1000, pid: 0, tid: 0, ph: 'c', cat: 'metadata',
+ args: { issue_ts: 500, sync_id: 'abc' }},
+ {ts: 2000, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}}
+ ],
+ systemTraceEvents: ftrace
+ });
+ const m = tr.c.TestUtils.newModelWithEvents([trace]);
+
+ assert.isFalse(m.hasImportWarnings);
+
+ // Chrome events shouldn't be shifted.
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[0].start, 0);
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[1].start, 2);
+
+ // Ftrace events should be shifted forward by 10ms.
+ assert.strictEqual(
+ m.processes[1].threads[0].sliceGroup.slices[0].start, 11);
+ });
+
+ test('import_traceEventWithClockDomainAndFtrace', function() {
+ // Include a clock sync marker that indicates the LINUX_CLOCK_MONOTONIC time
+ // of 20ms is equal to the LINUX_FTRACE_GLOBAL time of 10ms, effectively
+ // shifting all ftrace timestamps forward by 10ms.
+ const ftrace =
+ 'SurfaceFlinger-0 [001] ...1 0.001: 0: B|1|taskA\n' +
+ ' chrome-3 [001] ...1 0.010: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0.020\n';
+
+ const trace = JSON.stringify({
+ traceEvents: [
+ {ts: 0, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}},
+ {ts: 1000, pid: 0, tid: 0, ph: 'c', cat: 'metadata',
+ args: { issue_ts: 500, sync_id: 'abc' }},
+ {ts: 2000, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}}
+ ],
+ metadata: {
+ 'clock-domain': 'LINUX_CLOCK_MONOTONIC'
+ },
+ systemTraceEvents: ftrace
+ });
+ const m = tr.c.TestUtils.newModelWithEvents([trace]);
+
+ assert.isFalse(m.hasImportWarnings);
+
+ // Chrome events shouldn't be shifted.
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[0].start, 0);
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[1].start, 2);
+
+ // Ftrace events should be shifted forward by 10ms.
+ assert.strictEqual(
+ m.processes[1].threads[0].sliceGroup.slices[0].start, 11);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/context_processor.html b/chromium/third_party/catapult/tracing/tracing/importer/context_processor.html
new file mode 100644
index 00000000000..c82e80645a5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/context_processor.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ /**
+ * The context processor consumes context events and maintains a set of
+ * active contexts for a single thread.
+ *
+ * @constructor
+ */
+ function ContextProcessor(model) {
+ this.model_ = model;
+ this.activeContexts_ = [];
+ this.stackPerType_ = {};
+ // Cache of unique context objects.
+ this.contextCache_ = {};
+ // Cache of unique context object sets.
+ this.contextSetCache_ = {};
+ this.cachedEntryForActiveContexts_ = undefined;
+ // All seen context object snapshots.
+ this.seenSnapshots_ = {};
+ }
+
+ ContextProcessor.prototype = {
+ enterContext(contextType, scopedId) {
+ const newActiveContexts = [
+ this.getOrCreateContext_(contextType, scopedId),
+ ];
+ for (const oldContext of this.activeContexts_) {
+ if (oldContext.type === contextType) {
+ // If a previous context of the same type is active, it is removed
+ // and pushed onto the stack for this type.
+ this.pushContext_(oldContext);
+ } else {
+ // Otherwise the old context is it is still active.
+ 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) {
+ // Remove all matching contexts from stacks.
+ for (const stack of Object.values(this.stackPerType_)) {
+ // Perform in-place filtering instead of Array.prototype.filter to
+ // prevent creating a new array.
+ 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;
+ }
+
+ // Remove all matching contexts from active context set.
+ this.leaveContextImpl_(context =>
+ context.snapshot.scope === scopedId.scope &&
+ context.snapshot.idRef === scopedId.id);
+ },
+
+ leaveContextImpl_(predicate) {
+ const newActiveContexts = [];
+ for (const oldContext of this.activeContexts_) {
+ if (predicate(oldContext)) {
+ // If we left this context, remove it from the active set and
+ // restore any previous context of the same type.
+ const previousContext = this.popContext_(oldContext.type);
+ if (previousContext) {
+ newActiveContexts.push(previousContext);
+ }
+ } else {
+ newActiveContexts.push(oldContext);
+ }
+ }
+ this.activeContexts_ = newActiveContexts;
+ this.cachedEntryForActiveContexts_ = undefined;
+ },
+
+ getOrCreateContext_(contextType, scopedId) {
+ const context = {
+ type: contextType,
+ snapshot: {
+ scope: scopedId.scope,
+ idRef: scopedId.id
+ }
+ };
+ const key = this.getContextKey_(context);
+ if (key in this.contextCache_) {
+ return this.contextCache_[key];
+ }
+ this.contextCache_[key] = context;
+ const snapshotKey = this.getSnapshotKey_(scopedId);
+ this.seenSnapshots_[snapshotKey] = true;
+ return context;
+ },
+
+ pushContext_(context) {
+ if (!(context.type in this.stackPerType_)) {
+ this.stackPerType_[context.type] = [];
+ }
+ this.stackPerType_[context.type].push(context);
+ },
+
+ popContext_(contextType) {
+ if (!(contextType in this.stackPerType_)) {
+ return undefined;
+ }
+ return this.stackPerType_[contextType].pop();
+ },
+
+ getContextKey_(context) {
+ return [
+ context.type,
+ context.snapshot.scope,
+ context.snapshot.idRef
+ ].join('\x00');
+ },
+
+ getSnapshotKey_(scopedId) {
+ return [
+ scopedId.scope,
+ scopedId.idRef
+ ].join('\x00');
+ },
+
+ get activeContexts() {
+ // Keep a single instance for each unique set of active contexts to
+ // reduce memory usage.
+ if (this.cachedEntryForActiveContexts_ === undefined) {
+ let key = [];
+ for (const context of this.activeContexts_) {
+ key.push(this.getContextKey_(context));
+ }
+ key.sort();
+ key = key.join('\x00');
+ if (key in this.contextSetCache_) {
+ this.cachedEntryForActiveContexts_ = this.contextSetCache_[key];
+ } else {
+ this.activeContexts_.sort(function(a, b) {
+ const keyA = this.getContextKey_(a);
+ const keyB = this.getContextKey_(b);
+ if (keyA < keyB) {
+ return -1;
+ }
+ if (keyA > keyB) {
+ return 1;
+ }
+ return 0;
+ }.bind(this));
+ this.contextSetCache_[key] = Object.freeze(this.activeContexts_);
+ this.cachedEntryForActiveContexts_ = this.contextSetCache_[key];
+ }
+ }
+ return this.cachedEntryForActiveContexts_;
+ },
+
+ invalidateContextCacheForSnapshot(scopedId) {
+ const snapshotKey = this.getSnapshotKey_(scopedId);
+ if (!(snapshotKey in this.seenSnapshots_)) return;
+
+ this.contextCache_ = {};
+ this.contextSetCache_ = {};
+ this.cachedEntryForActiveContexts_ = undefined;
+ this.activeContexts_ = this.activeContexts_.map(function(context) {
+ // Do not alter unrelated contexts.
+ if (context.snapshot.scope !== scopedId.scope ||
+ context.snapshot.idRef !== scopedId.id) {
+ return context;
+ }
+ // Replace the invalidated context by a deep copy.
+ return {
+ type: context.type,
+ snapshot: {
+ scope: context.snapshot.scope,
+ idRef: context.snapshot.idRef
+ }
+ };
+ });
+ this.seenSnapshots_ = {};
+ },
+ };
+
+ return {
+ ContextProcessor,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html b/chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html
new file mode 100644
index 00000000000..c033f7c28ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html
@@ -0,0 +1,297 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/importer/context_processor.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ContextProcessor = tr.importer.ContextProcessor;
+
+ test('empty', function() {
+ const processor = new ContextProcessor();
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('enterAndLeave', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+ processor.enterContext('type', id);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.leaveContext('type', id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('parallelContexts', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {type: 'A', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {type: 'B', snapshot: {scope: 'idx', idRef: 456}};
+
+ // Entering and leaving in order.
+ processor.enterContext('A', idA);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.enterContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA,
+ expectedContextB]);
+ processor.leaveContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.leaveContext('A', idA);
+ assert.deepEqual(processor.activeContexts, []);
+
+ // Entering and leaving out of order.
+ processor.enterContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextB]);
+ processor.enterContext('A', idA);
+ assert.deepEqual(processor.activeContexts, [expectedContextA,
+ expectedContextB]);
+ processor.leaveContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.leaveContext('A', idA);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('contextStack', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {
+ type: 'type', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {
+ type: 'type', snapshot: {scope: 'idx', idRef: 456}};
+
+ // Entering and leaving the same context type.
+ processor.enterContext('type', idA);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.enterContext('type', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextB]);
+ processor.leaveContext('type', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.leaveContext('type', idA);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('contextCached', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {
+ type: 'A', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {
+ type: 'B', snapshot: {scope: 'idx', idRef: 456}};
+
+ processor.enterContext('A', idA);
+ const firstSet = processor.activeContexts;
+ processor.enterContext('B', idB);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('B', idB);
+ processor.leaveContext('A', idA);
+
+ assert.deepEqual(firstSet, [expectedContextA]);
+ assert.deepEqual(secondSet, [expectedContextA, expectedContextB]);
+
+ // Identical context objects should be the same instance.
+ assert(Object.is(firstSet[0], secondSet[0]));
+ });
+
+ test('contextSetCached', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ processor.enterContext('type', id);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert(Object.is(firstSet, secondSet));
+ });
+
+ test('contextSetIsOrdered', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {type: 'A', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {type: 'B', snapshot: {scope: 'idx', idRef: 456}};
+
+ processor.enterContext('A', idA);
+ processor.enterContext('B', idB);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('B', idB);
+ processor.leaveContext('A', idA);
+
+ processor.enterContext('B', idB);
+ processor.enterContext('A', idA);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('A', idA);
+ processor.leaveContext('B', idB);
+
+ assert.deepEqual(firstSet, [expectedContextA, expectedContextB]);
+ assert(Object.is(firstSet, secondSet));
+ });
+
+ test('contextSetIsFrozen', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ assert(Object.isFrozen(processor.activeContexts));
+ processor.enterContext('type', id);
+ assert(Object.isFrozen(processor.activeContexts));
+ processor.leaveContext('type', id);
+ assert(Object.isFrozen(processor.activeContexts));
+ });
+
+ test('cacheInvalidation', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ processor.invalidateContextCacheForSnapshot(id);
+
+ processor.enterContext('type', id);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert.deepEqual(secondSet, [expectedContext]);
+ assert(!Object.is(firstSet, secondSet));
+ assert(!Object.is(firstSet[0], secondSet[0]));
+ assert(!Object.is(firstSet[0].snapshot, secondSet[0].snapshot));
+ });
+
+ test('cacheInvalidationOfAnActiveContext', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+
+ processor.invalidateContextCacheForSnapshot(id);
+
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert.deepEqual(secondSet, [expectedContext]);
+ assert(!Object.is(firstSet, secondSet));
+ assert(!Object.is(firstSet[0], secondSet[0]));
+ assert(!Object.is(firstSet[0].snapshot, secondSet[0].snapshot));
+ });
+
+ test('cacheInvalidationForUnrelatedSnapshot', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const unrelatedId = new tr.model.ScopedId('ofs', 789);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ processor.invalidateContextCacheForSnapshot(unrelatedId);
+
+ processor.enterContext('type', id);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert.deepEqual(secondSet, [expectedContext]);
+ assert(Object.is(firstSet, secondSet));
+ });
+
+ test('destroyBasic', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+ processor.enterContext('type', id);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.destroyContext(id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyActiveContextWithNonEmptyStack', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+ processor.enterContext('type', idA);
+ processor.enterContext('type', idB);
+ processor.destroyContext(idB);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.leaveContext('type', idA);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyInactiveContextInStack', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'idx', idRef: 456},
+ };
+ processor.enterContext('type', idA);
+ processor.enterContext('type', idB);
+ processor.destroyContext(idA);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.leaveContext('type', idB);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyContextEnteredWithMultipleTypes', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ processor.enterContext('A', id);
+ processor.enterContext('B', id);
+ processor.destroyContext(id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyReenteredContext', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ processor.enterContext('type', id);
+ processor.enterContext('type', id);
+ processor.destroyContext(id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html b/chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html
new file mode 100644
index 00000000000..2f876b3708e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/importer/importer.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data importers.
+ */
+tr.exportTo('tr.importer', function() {
+ /**
+ * Importer for empty strings and arrays.
+ * @constructor
+ */
+ function EmptyImporter(events) {
+ this.importPriority = 0;
+ }
+
+ EmptyImporter.canImport = function(eventData) {
+ if (eventData instanceof Array && eventData.length === 0) {
+ return true;
+ }
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ return eventData.length === 0;
+ }
+ return false;
+ };
+
+ EmptyImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'EmptyImporter';
+ }
+ };
+
+ tr.importer.Importer.register(EmptyImporter);
+
+ return {
+ EmptyImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
new file mode 100644
index 00000000000..464a41853d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
@@ -0,0 +1,1409 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/importer/proto_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'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',
+ ];
+ /* 1s is a bit arbitrary, but it reliably avoids all the jank caused by
+ * VR entry.
+ */
+ const VR_RESPONSE_MS = 1000;
+
+ /**
+ * If there's less than this much time between the end of one event and the
+ * start of the next, then they might be merged.
+ * There was not enough thought given to this value, so if you have any slight
+ * reason to change it, then please do so. It might also be good to split this
+ * into multiple values.
+ */
+ const INPUT_MERGE_THRESHOLD_MS = 200;
+ const ANIMATION_MERGE_THRESHOLD_MS = 32; // 2x 60FPS frames
+
+ /**
+ * If two MouseWheel events begin this close together, then they're an
+ * Animation, not two responses.
+ */
+ const MOUSE_WHEEL_THRESHOLD_MS = 40;
+
+ /**
+ * If two MouseMoves are more than this far apart, then they're two Responses,
+ * not Animation.
+ */
+ const MOUSE_MOVE_THRESHOLD_MS = 40;
+
+ // TODO(#3813) Move this.
+ 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;
+ }
+
+ function forEventTypesIn(events, typeNames, cb, opt_this) {
+ events.forEach(function(event) {
+ if (typeNames.indexOf(event.typeName) >= 0) {
+ cb.call(opt_this, event);
+ }
+ });
+ }
+
+ function causedFrame(event) {
+ return event.associatedEvents.some(isImplFrameEvent);
+ }
+
+ function getSortedFrameEventsByProcess(modelHelper) {
+ const frameEventsByPid = {};
+ for (const [pid, rendererHelper] of
+ Object.entries(modelHelper.rendererHelpers)) {
+ frameEventsByPid[pid] = rendererHelper.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds);
+ }
+ return frameEventsByPid;
+ }
+
+ function getSortedInputEvents(modelHelper) {
+ const inputEvents = [];
+
+ const browserProcess = modelHelper.browserHelper.process;
+ const mainThread = browserProcess.findAtMostOneThreadNamed(
+ 'CrBrowserMain');
+ for (const slice of mainThread.asyncSliceGroup.getDescendantEvents()) {
+ if (!slice.isTopLevel) continue;
+
+ if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice)) continue;
+
+ if (isNaN(slice.start) ||
+ isNaN(slice.duration) ||
+ isNaN(slice.end)) {
+ continue;
+ }
+
+ inputEvents.push(slice);
+ }
+
+ return inputEvents.sort(compareEvents);
+ }
+
+ function findProtoExpectations(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ // This order is not important. Handlers are independent.
+ const handlers = [
+ handleKeyboardEvents,
+ handleMouseResponseEvents,
+ handleMouseWheelEvents,
+ handleMouseDragEvents,
+ handleTapResponseEvents,
+ handlePinchEvents,
+ handleFlingEvents,
+ handleTouchEvents,
+ handleScrollEvents,
+ handleCSSAnimations,
+ handleWebGLAnimations,
+ handleVideoAnimations,
+ handleVrAnimations,
+ ];
+ handlers.forEach(function(handler) {
+ protoExpectations.push.apply(protoExpectations, handler(
+ modelHelper, sortedInputEvents, warn));
+ });
+ protoExpectations.sort(compareEvents);
+ return protoExpectations;
+ }
+
+ /**
+ * Every keyboard event is a Response.
+ */
+ function handleKeyboardEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ forEventTypesIn(sortedInputEvents, KEYBOARD_TYPE_NAMES, function(event) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.KEYBOARD);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Some mouse events can be translated directly into Responses.
+ */
+ function handleMouseResponseEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_RESPONSE_TYPE_NAMES, function(event) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ });
+ return protoExpectations;
+ }
+ /**
+ * MouseWheel events are caused either by a physical wheel on a physical
+ * mouse, or by a touch-drag gesture on a track-pad. The physical wheel
+ * causes MouseWheel events that are much more spaced out, and have no
+ * chance of hitting 60fps, so they are each turned into separate Response
+ * UEs. The track-pad causes MouseWheel events that are much closer
+ * together, and are expected to be 60fps, so the first event in a sequence
+ * is turned into a Response, and the rest are merged into an Animation.
+ * NB this threshold uses the two events' start times, unlike
+ * ProtoExpectation.isNear, which compares the end time of the previous event
+ * with the start time of the next.
+ */
+ function handleMouseWheelEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let prevEvent_ = undefined;
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_WHEEL_TYPE_NAMES, function(event) {
+ // Switch prevEvent in one place so that we can early-return later.
+ const prevEvent = prevEvent_;
+ prevEvent_ = event;
+
+ if (currentPE &&
+ (prevEvent.start + MOUSE_WHEEL_THRESHOLD_MS) >= event.start) {
+ if (currentPE.type === ProtoExpectation.ANIMATION_TYPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.MOUSE_WHEEL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ return;
+ }
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE_WHEEL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Down events followed closely by Up events are click Responses, but the
+ * Response doesn't start until the Up event.
+ *
+ * RRR
+ * DDD UUU
+ *
+ * If there are any Move events in between a Down and an Up, then the Down
+ * and the first Move are a Response, then the rest of the Moves are an
+ * Animation:
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * DDD MMM MMM MMM MMM MMM UUU
+ */
+ function handleMouseDragEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let mouseDownEvent = undefined;
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_DRAG_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.MOUSE_DOWN:
+ if (causedFrame(event)) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ } else {
+ // Responses typically don't start until the mouse up event.
+ // Add this MouseDown to the Response that starts at the
+ // MouseUp.
+ mouseDownEvent = event;
+ }
+ break;
+
+ // There may be more than 100ms between the start of the mouse
+ // down and the start of the mouse up. Chrome and the web don't
+ // start to respond until the mouse up. Responses start deducting
+ // comfort at 100ms duration. If more than that 100ms duration is
+ // burned through while waiting for the user to release the mouse
+ // button, then ResponseExpectation will unfairly start deducting
+ // comfort before Chrome even has a mouse up to respond to. It is
+ // technically possible for a site to afford one response on mouse
+ // down and another on mouse up, but that is an edge case. The
+ // vast majority of mouse downs are not responses.
+
+ case INPUT_TYPE.MOUSE_MOVE:
+ if (!causedFrame(event)) {
+ // Ignore MouseMoves that do not affect the screen. They are not
+ // part of an interaction record by definition.
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ } else if (!currentPE ||
+ !currentPE.isNear(event, MOUSE_MOVE_THRESHOLD_MS)) {
+ // The first MouseMove after a MouseDown or after a while is a
+ // Response.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ currentPE.pushEvent(event);
+ if (mouseDownEvent) {
+ currentPE.associatedEvents.push(mouseDownEvent);
+ mouseDownEvent = undefined;
+ }
+ protoExpectations.push(currentPE);
+ } else {
+ // Merge this event into an Animation.
+ if (currentPE.type === ProtoExpectation.ANIMATION_TYPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.MOUSE);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ }
+ break;
+
+ case INPUT_TYPE.MOUSE_UP:
+ if (!mouseDownEvent) {
+ const pe = new ProtoExpectation(
+ causedFrame(event) ? ProtoExpectation.RESPONSE_TYPE :
+ ProtoExpectation.IGNORED_TYPE,
+ INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ if (mouseDownEvent) {
+ currentPE.associatedEvents.push(mouseDownEvent);
+ }
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ mouseDownEvent = undefined;
+ currentPE = undefined;
+ break;
+ }
+ });
+ if (mouseDownEvent) {
+ currentPE = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ currentPE.pushEvent(mouseDownEvent);
+ protoExpectations.push(currentPE);
+ }
+ return protoExpectations;
+ }
+
+ /**
+ * Solitary Tap events are simple Responses:
+ *
+ * RRR
+ * TTT
+ *
+ * TapDowns are part of Responses.
+ *
+ * RRRRRRR
+ * DDD TTT
+ *
+ * TapCancels are part of Responses, which seems strange. They always go
+ * with scrolls, so they'll probably be merged with scroll Responses.
+ * TapCancels can take a significant amount of time and account for a
+ * significant amount of work, which should be grouped with the scroll UEs
+ * if possible.
+ *
+ * RRRRRRR
+ * DDD CCC
+ **/
+ function handleTapResponseEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ forEventTypesIn(sortedInputEvents, TAP_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.TAP_DOWN:
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ break;
+
+ case INPUT_TYPE.TAP:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ // Sometimes we get Tap events with no TapDown, sometimes we get
+ // TapDown events. Handle both.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ currentPE = undefined;
+ break;
+
+ case INPUT_TYPE.TAP_CANCEL:
+ if (!currentPE) {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * The PinchBegin and the first PinchUpdate comprise a Response, then the
+ * rest of the PinchUpdates comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * BBB UUU UUU UUU UUU UUU EEE
+ */
+ function handlePinchEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstUpdate = false;
+ const modelBounds = modelHelper.model.bounds;
+ forEventTypesIn(sortedInputEvents, PINCH_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.PINCH_BEGIN:
+ if (currentPE &&
+ currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ break;
+ }
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.PINCH);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstUpdate = false;
+ break;
+
+ case INPUT_TYPE.PINCH_UPDATE:
+ // Like ScrollUpdates, the Begin and the first Update constitute a
+ // Response, then the rest of the Updates constitute an Animation
+ // that begins when the Response ends. If the user pauses in the
+ // middle of an extended pinch gesture, then multiple Animations
+ // will be created.
+ if (!currentPE ||
+ ((currentPE.type === ProtoExpectation.RESPONSE_TYPE) &&
+ sawFirstUpdate) ||
+ !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.PINCH);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ sawFirstUpdate = true;
+ }
+ break;
+
+ case INPUT_TYPE.PINCH_END:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Flings are defined by 3 types of events: FlingStart, FlingCancel, and the
+ * renderer fling event. Flings do not begin with a Response. Flings end
+ * either at the beginning of a FlingCancel, or at the end of the renderer
+ * fling event.
+ *
+ * AAAAAAAAAAAAAAAAAAAAAAAAAA
+ * SSS
+ * RRRRRRRRRRRRRRRRRRRRRR
+ *
+ *
+ * AAAAAAAAAAA
+ * SSS CCC
+ */
+ function handleFlingEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+
+ function isRendererFling(event) {
+ return event.title === RENDERER_FLING_TITLE;
+ }
+ const browserHelper = modelHelper.browserHelper;
+ const flingEvents = browserHelper.getAllAsyncSlicesMatching(
+ isRendererFling);
+
+ forEventTypesIn(sortedInputEvents, FLING_TYPE_NAMES, function(event) {
+ flingEvents.push(event);
+ });
+ flingEvents.sort(compareEvents);
+
+ flingEvents.forEach(function(event) {
+ if (event.title === RENDERER_FLING_TITLE) {
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.FLING);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ return;
+ }
+
+ switch (event.typeName) {
+ case INPUT_TYPE.FLING_START:
+ if (currentPE) {
+ warn({
+ type: 'UserModelBuilder',
+ message: 'Unexpected FlingStart',
+ showToUser: false,
+ });
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.FLING);
+ currentPE.pushEvent(event);
+ // Set end to an invalid value so that it can be noticed and fixed
+ // later.
+ currentPE.end = 0;
+ protoExpectations.push(currentPE);
+ }
+ break;
+
+ case INPUT_TYPE.FLING_CANCEL:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ // FlingCancel events start when TouchStart events start, which is
+ // typically when a Response starts. FlingCancel events end when
+ // chrome acknowledges them, not when they update the screen. So
+ // there might be one more frame during the FlingCancel, after
+ // this Animation ends. That won't affect the scoring algorithms,
+ // and it will make the UEs look more correct if they don't
+ // overlap unnecessarily.
+ currentPE.end = event.start;
+ currentPE = undefined;
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ break;
+ }
+ });
+ // If there was neither a FLING_CANCEL nor a renderer fling after the
+ // FLING_START, then assume that it ends at the end of the model, so set
+ // the end of currentPE to the end of the model.
+ if (currentPE && !currentPE.end) {
+ currentPE.end = modelHelper.model.bounds.max;
+ }
+ return protoExpectations;
+ }
+
+ /**
+ * The TouchStart and the first TouchMove comprise a Response, then the
+ * rest of the TouchMoves comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * SSS MMM MMM MMM MMM MMM EEE
+ *
+ * If there are no TouchMove events in between a TouchStart and a TouchEnd,
+ * then it's just a Response.
+ *
+ * RRRRRRR
+ * SSS EEE
+ */
+ function handleTouchEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstMove = false;
+ forEventTypesIn(sortedInputEvents, TOUCH_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.TOUCH_START:
+ if (currentPE) {
+ // NB: currentPE will probably be merged with something from
+ // handlePinchEvents(). Multiple TouchStart events without an
+ // intervening TouchEnd logically implies that multiple fingers
+ // are on the screen, so this is probably a pinch gesture.
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstMove = false;
+ }
+ break;
+
+ case INPUT_TYPE.TOUCH_MOVE:
+ if (!currentPE) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ break;
+ }
+
+ // Like Scrolls and Pinches, the Response is defined to be the
+ // TouchStart plus the first TouchMove, then the rest of the
+ // TouchMoves constitute an Animation.
+ if ((sawFirstMove &&
+ (currentPE.type === ProtoExpectation.RESPONSE_TYPE)) ||
+ !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ // If there's already a touchmove in the currentPE or it's not
+ // near event, then finish it and start a new animation.
+ const prevEnd = currentPE.end;
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ // It's possible for there to be a gap between TouchMoves, but
+ // that doesn't mean that there should be an Idle UE there.
+ currentPE.start = prevEnd;
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ sawFirstMove = true;
+ }
+ break;
+
+ case INPUT_TYPE.TOUCH_END:
+ if (!currentPE) {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * The first ScrollBegin and the first ScrollUpdate comprise a Response,
+ * then the rest comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * BBB UUU UUU UUU UUU UUU EEE
+ */
+ function handleScrollEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstUpdate = false;
+ forEventTypesIn(sortedInputEvents, SCROLL_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.SCROLL_BEGIN:
+ // Always begin a new PE even if there already is one, unlike
+ // PinchBegin.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstUpdate = false;
+ break;
+
+ case INPUT_TYPE.SCROLL_UPDATE:
+ if (currentPE) {
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS) &&
+ ((currentPE.type === ProtoExpectation.ANIMATION_TYPE) ||
+ !sawFirstUpdate)) {
+ currentPE.pushEvent(event);
+ sawFirstUpdate = true;
+ } else {
+ currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ } else {
+ // ScrollUpdate without ScrollBegin.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ break;
+
+ case INPUT_TYPE.SCROLL_END:
+ if (!currentPE) {
+ warn({
+ type: 'UserModelBuilder',
+ message: 'Unexpected ScrollEnd',
+ showToUser: false,
+ });
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+ currentPE.pushEvent(event);
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Returns proto expectations for video animation events.
+ *
+ * Video animations represent video playback, and are based on
+ * VideoPlayback async events (going from the VideoFrameCompositor::Start
+ * to VideoFrameCompositor::Stop calls)
+ */
+ function handleVideoAnimations(modelHelper, sortedInputEvents, warn) {
+ const events = [];
+ for (const pid in modelHelper.rendererHelpers) {
+ for (const tid in modelHelper.rendererHelpers[pid].process.threads) {
+ for (const asyncSlice of
+ modelHelper.rendererHelpers[pid].process.threads[tid]
+ .asyncSliceGroup.slices) {
+ if (asyncSlice.title === PLAYBACK_EVENT_TITLE) {
+ events.push(asyncSlice);
+ }
+ }
+ }
+ }
+
+ events.sort(tr.importer.compareEvents);
+
+ const protoExpectations = [];
+ for (const event of events) {
+ const currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.VIDEO);
+ currentPE.start = event.start;
+ currentPE.end = event.end;
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+
+ return protoExpectations;
+ }
+
+ /**
+ * Returns proto expectations for VR animation events.
+ */
+ function handleVrAnimations(modelHelper, sortedInputEvents, warn) {
+ const events = [];
+
+ // Find all the processes we should check
+ 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);
+ }
+
+ // Add all counter samples to the list of events we care about
+ 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);
+ }
+ }
+ }
+ }
+ }
+
+ 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) +
+ VR_RESPONSE_MS;
+ const responsePE = new ProtoExpectation(ProtoExpectation.RESPONSE_TYPE,
+ INITIATOR_TYPE.VR);
+ const animationPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.VR);
+ let lastResponseEvent;
+
+ for (const event of events) {
+ // Categorize the first 1s of VR time as entry/response
+ // TODO(bsheedy): Make this smarter by basing response duration off trace
+ // data instead of a fixed duration
+ if (event.range.min < vrAnimationStart) {
+ if (event instanceof tr.model.CounterSample) {
+ responsePE.pushSample(event);
+ } else {
+ responsePE.pushEvent(event);
+ }
+ lastResponseEvent = event;
+ } else {
+ if (event instanceof tr.model.CounterSample) {
+ animationPE.pushSample(event);
+ } else {
+ animationPE.pushEvent(event);
+ }
+ }
+ }
+
+ // Make sure that there isn't a gap between the two expectations
+ if (lastResponseEvent instanceof tr.model.CounterSample) {
+ animationPE.pushSample(lastResponseEvent);
+ } else {
+ animationPE.pushEvent(lastResponseEvent);
+ }
+ return [responsePE, animationPE];
+ }
+
+ /**
+ * CSS Animations are merged into AnimationExpectations when they intersect.
+ */
+ function handleCSSAnimations(modelHelper, sortedInputEvents, warn) {
+ // First find all the top-level CSS Animation async events.
+ const animationEvents = modelHelper.browserHelper.
+ getAllAsyncSlicesMatching(function(event) {
+ return ((event.title === CSS_ANIMATION_TITLE) &&
+ event.isTopLevel &&
+ (event.duration > 0));
+ });
+
+
+ // Time ranges where animations are actually running will be collected here.
+ // Each element will contain {min, max, animation}.
+ const animationRanges = [];
+
+ // This helper function will be called when a time range is found
+ // during which the animation is actually running.
+ function pushAnimationRange(start, end, animation) {
+ const range = tr.b.math.Range.fromExplicitRange(start, end);
+ range.animation = animation;
+ animationRanges.push(range);
+ }
+
+ animationEvents.forEach(function(animation) {
+ if (animation.subSlices.length === 0) {
+ pushAnimationRange(animation.start, animation.end, animation);
+ } else {
+ // Now run a state machine over the animation's subSlices, which
+ // indicate the animations running/paused/finished states, in order to
+ // find ranges where the animation was actually running.
+ let start = undefined;
+ animation.subSlices.forEach(function(sub) {
+ if ((sub.args.data.state === 'running') &&
+ (start === undefined)) {
+ // It's possible for the state to alternate between running and
+ // pending, but the animation is still running in that case,
+ // so only set start if the state is changing from one of the halted
+ // states.
+ start = sub.start;
+ } else if ((sub.args.data.state === 'paused') ||
+ (sub.args.data.state === 'idle') ||
+ (sub.args.data.state === 'finished')) {
+ if (start === undefined) {
+ // An animation was already running when the trace started.
+ // (Actually, it's possible that the animation was in the 'idle'
+ // state when tracing started, but that should be rare, and will
+ // be fixed when async events are buffered.)
+ // http: //crbug.com/565627
+ start = modelHelper.model.bounds.min;
+ }
+
+ pushAnimationRange(start, sub.start, animation);
+ start = undefined;
+ }
+ });
+
+ // An animation was still running when the
+ // top-level animation event ended.
+ if (start !== undefined) {
+ pushAnimationRange(start, animation.end, animation);
+ }
+ }
+ });
+
+ // Now we have a set of time ranges when css animations were actually
+ // running.
+ // Leave merging intersecting animations to mergeIntersectingAnimations(),
+ // after findFrameEventsForAnimations removes frame-less animations.
+
+ return animationRanges.map(function(range) {
+ const protoExpectation = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.CSS);
+ protoExpectation.start = range.min;
+ protoExpectation.end = range.max;
+ protoExpectation.associatedEvents.push(range.animation);
+ return protoExpectation;
+ });
+ }
+
+ /**
+ * Get all the events (prepareMailbox and serviceScriptedAnimations)
+ * relevant to WebGL. Note that modelHelper is the helper object containing
+ * the model, and mailboxEvents and animationEvents are arrays where the
+ * events are being pushed into (DrawingBuffer::prepareMailbox events go
+ * into mailboxEvents; PageAnimator::serviceScriptedAnimations events go
+ * into animationEvents). The function does not return anything but
+ * modifies mailboxEvents and animationEvents.
+ */
+ function findWebGLEvents(modelHelper, mailboxEvents, animationEvents) {
+ for (const event of modelHelper.model.getDescendantEvents()) {
+ if (event.title === 'DrawingBuffer::prepareMailbox') {
+ mailboxEvents.push(event);
+ } else if (event.title === 'PageAnimator::serviceScriptedAnimations') {
+ animationEvents.push(event);
+ }
+ }
+ }
+
+ /**
+ * Returns a list of events in mailboxEvents that have an event in
+ * animationEvents close by (within ANIMATION_MERGE_THRESHOLD_MS).
+ */
+ function findMailboxEventsNearAnimationEvents(
+ mailboxEvents, animationEvents) {
+ if (animationEvents.length === 0) return [];
+
+ mailboxEvents.sort(compareEvents);
+ animationEvents.sort(compareEvents);
+ const animationIterator = animationEvents[Symbol.iterator]();
+ let animationEvent = animationIterator.next().value;
+
+ const filteredEvents = [];
+
+ // We iterate through the mailboxEvents. With each event, we check if
+ // there is a animationEvent near it, and if so, add it to the result.
+ for (const event of mailboxEvents) {
+ // If the current animationEvent is too far before the mailboxEvent,
+ // we advance until we get to the next animationEvent that is not too
+ // far before the animationEvent.
+ while (animationEvent &&
+ (animationEvent.start < (
+ event.start - ANIMATION_MERGE_THRESHOLD_MS))) {
+ animationEvent = animationIterator.next().value;
+ }
+
+ // If there aren't any more animationEvents, then that means all the
+ // remaining mailboxEvents are too far after the animationEvents, so
+ // we can quit now.
+ if (!animationEvent) break;
+
+ // If there's a animationEvent close to the mailboxEvent, then we push
+ // the current mailboxEvent onto the stack.
+ if (animationEvent.start < (event.start + ANIMATION_MERGE_THRESHOLD_MS)) {
+ filteredEvents.push(event);
+ }
+ }
+ return filteredEvents;
+ }
+
+ /**
+ * Merge consecutive mailbox events into a ProtoExpectation. Note: Only
+ * the drawingBuffer::prepareMailbox events will end up in the
+ * associatedEvents. The PageAnimator::serviceScriptedAnimations events
+ * will not end up in the associatedEvents.
+ */
+ function createProtoExpectationsFromMailboxEvents(mailboxEvents) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ for (const event of mailboxEvents) {
+ if (currentPE === undefined || !currentPE.isNear(
+ event, ANIMATION_MERGE_THRESHOLD_MS)) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.WEBGL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ }
+ }
+ return protoExpectations;
+ }
+
+ // WebGL animations are identified by the DrawingBuffer::prepareMailbox
+ // and PageAnimator::serviceScriptedAnimations events (one of each per frame)
+ // and consecutive frames are merged into the same animation.
+ function handleWebGLAnimations(modelHelper, sortedInputEvents, warn) {
+ // Get the prepareMailbox and scriptedAnimation events.
+ const prepareMailboxEvents = [];
+ const scriptedAnimationEvents = [];
+
+ findWebGLEvents(modelHelper, prepareMailboxEvents, scriptedAnimationEvents);
+ const webGLMailboxEvents = findMailboxEventsNearAnimationEvents(
+ prepareMailboxEvents, scriptedAnimationEvents);
+
+ return createProtoExpectationsFromMailboxEvents(webGLMailboxEvents);
+ }
+
+
+ function postProcessProtoExpectations(modelHelper, protoExpectations) {
+ // protoExpectations is input only. Returns a modified set of
+ // ProtoExpectations. The order is important.
+ protoExpectations = findFrameEventsForAnimations(
+ modelHelper, protoExpectations);
+ protoExpectations = mergeIntersectingResponses(protoExpectations);
+ protoExpectations = mergeIntersectingAnimations(protoExpectations);
+ protoExpectations = fixResponseAnimationStarts(protoExpectations);
+ protoExpectations = fixTapResponseTouchAnimations(protoExpectations);
+ return protoExpectations;
+ }
+
+ /**
+ * TouchStarts happen at the same time as ScrollBegins.
+ * It's easier to let multiple handlers create multiple overlapping
+ * Responses and then merge them, rather than make the handlers aware of the
+ * other handlers' PEs.
+ *
+ * For example:
+ * RR
+ * RRR -> RRRRR
+ * RR
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function mergeIntersectingResponses(protoExpectations) {
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // Only consider Responses for now.
+ if (pe.type !== ProtoExpectation.RESPONSE_TYPE) continue;
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (otherPE.type !== pe.type) continue;
+
+ if (!otherPE.intersects(pe)) continue;
+
+ // Don't merge together Responses of the same type.
+ // If handleTouchEvents wanted two of its Responses to be merged, then
+ // it would have made them that way to begin with.
+ const typeNames = pe.associatedEvents.map(function(event) {
+ return event.typeName;
+ });
+ if (otherPE.containsTypeNames(typeNames)) continue;
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ /**
+ * An animation is simply an expectation of 60fps between start and end.
+ * If two animations overlap, then merge them.
+ *
+ * For example:
+ * AA
+ * AAA -> AAAAA
+ * AA
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function mergeIntersectingAnimations(protoExpectations) {
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // Only consider Animations for now.
+ if (pe.type !== ProtoExpectation.ANIMATION_TYPE) continue;
+
+ const isCSS = pe.initiatorType === INITIATOR_TYPE.CSS;
+ const isFling = pe.containsTypeNames([INPUT_TYPE.FLING_START]);
+ const isVideo = pe.initiatorType === INITIATOR_TYPE.VIDEO;
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (otherPE.type !== pe.type) continue;
+
+ // Don't merge some animation types with others.
+ if ((isCSS && otherPE.initiatorType !== INITIATOR_TYPE.CSS) ||
+ isFling !== otherPE.containsTypeNames([INPUT_TYPE.FLING_START]) ||
+ isVideo && otherPE.initiatorType !== INITIATOR_TYPE.VIDEO ||
+ otherPE.initiatorType === INITIATOR_TYPE.VR) {
+ continue;
+ }
+
+ if (isCSS) {
+ if (!pe.isNear(otherPE, ANIMATION_MERGE_THRESHOLD_MS)) {
+ continue;
+ }
+ } else if (!otherPE.intersects(pe)) {
+ continue;
+ }
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ /**
+ * The ends of responses frequently overlap the starts of animations.
+ * Fix the animations to reflect the fact that the user can only start to
+ * expect 60fps after the response.
+ *
+ * For example:
+ * RRR -> RRRAA
+ * AAAA
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function fixResponseAnimationStarts(protoExpectations) {
+ protoExpectations.forEach(function(ape) {
+ // Only consider animations for now.
+ if (ape.type !== ProtoExpectation.ANIMATION_TYPE) {
+ return;
+ }
+
+ protoExpectations.forEach(function(rpe) {
+ // Only consider responses for now.
+ if (rpe.type !== ProtoExpectation.RESPONSE_TYPE) {
+ return;
+ }
+
+ // Only consider responses that end during the animation.
+ if (!ape.containsTimestampInclusive(rpe.end)) {
+ return;
+ }
+
+ // Ignore Responses that are entirely contained by the animation.
+ if (ape.containsTimestampInclusive(rpe.start)) {
+ return;
+ }
+
+ // Move the animation start to the response end.
+ ape.start = rpe.end;
+ // Remove any frames that were part of the animation but are now before
+ // the animation.
+ if (ape.associatedEvents !== undefined) {
+ ape.associatedEvents = ape.associatedEvents.filter(
+ e => (!isImplFrameEvent(e) || e.start >= ape.start));
+ }
+ });
+ });
+ return protoExpectations;
+ }
+
+ function isImplFrameEvent(event) {
+ return event.title === tr.model.helpers.IMPL_RENDERING_STATS;
+ }
+
+ /**
+ * Merge Tap Responses that overlap Touch-only Animations.
+ * https: *github.com/catapult-project/catapult/issues/1431
+ */
+ function fixTapResponseTouchAnimations(protoExpectations) {
+ function isTapResponse(pe) {
+ return (pe.type === ProtoExpectation.RESPONSE_TYPE) &&
+ pe.containsTypeNames([INPUT_TYPE.TAP]);
+ }
+ function isTouchAnimation(pe) {
+ return (pe.type === ProtoExpectation.ANIMATION_TYPE) &&
+ pe.containsTypeNames([INPUT_TYPE.TOUCH_MOVE]) &&
+ !pe.containsTypeNames([
+ INPUT_TYPE.SCROLL_UPDATE, INPUT_TYPE.PINCH_UPDATE]);
+ }
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // protoExpectations are sorted by start time, and we don't know whether
+ // the Tap Response or the Touch Animation will be first
+ const peIsTapResponse = isTapResponse(pe);
+ const peIsTouchAnimation = isTouchAnimation(pe);
+ if (!peIsTapResponse && !peIsTouchAnimation) {
+ continue;
+ }
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (!otherPE.intersects(pe)) continue;
+
+ if (peIsTapResponse && !isTouchAnimation(otherPE)) continue;
+
+ if (peIsTouchAnimation && !isTapResponse(otherPE)) continue;
+
+ // pe might be the Touch Animation, but the merged ProtoExpectation
+ // should be a Response.
+ pe.type = ProtoExpectation.RESPONSE_TYPE;
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ function findFrameEventsForAnimations(modelHelper, protoExpectations) {
+ const newPEs = [];
+ const frameEventsByPid = getSortedFrameEventsByProcess(modelHelper);
+
+ for (const pe of protoExpectations) {
+ if (pe.type !== ProtoExpectation.ANIMATION_TYPE) {
+ newPEs.push(pe);
+ continue;
+ }
+
+ const frameEvents = [];
+ for (const pid of Object.keys(modelHelper.rendererHelpers)) {
+ const range = tr.b.math.Range.fromExplicitRange(pe.start, pe.end);
+ frameEvents.push.apply(frameEvents,
+ range.filterArray(frameEventsByPid[pid], e => e.start));
+ }
+
+ // If a tree falls in a forest...
+ // If there were not actually any frames while the animation was
+ // running, then it wasn't really an animation, now, was it?
+ // Philosophy aside, the system_health Animation metrics fail hard if
+ // there are no frames in an AnimationExpectation.
+ // Since WebGL and VR animations don't generate this type of frame
+ // event, don't remove them if it's a WebGL or VR animation.
+ if (frameEvents.length === 0 &&
+ !(pe.initiatorType === INITIATOR_TYPE.WEBGL ||
+ pe.initiatorType === INITIATOR_TYPE.VR)) {
+ pe.type = ProtoExpectation.IGNORED_TYPE;
+ newPEs.push(pe);
+ continue;
+ }
+
+ pe.associatedEvents.addEventSet(frameEvents);
+ newPEs.push(pe);
+ }
+
+ return newPEs;
+ }
+
+ /**
+ * Check that none of the handlers accidentally ignored an input event.
+ */
+ function checkAllInputEventsHandled(
+ modelHelper, sortedInputEvents, protoExpectations, warn) {
+ const handledEvents = [];
+ protoExpectations.forEach(function(protoExpectation) {
+ protoExpectation.associatedEvents.forEach(function(event) {
+ // Ignore CSS Animations that might have multiple active ranges.
+ if ((event.title === CSS_ANIMATION_TITLE) &&
+ (event.subSlices.length > 0)) {
+ return;
+ }
+
+ if ((handledEvents.indexOf(event) >= 0) &&
+ (!isImplFrameEvent(event))) {
+ warn({
+ type: 'UserModelBuilder',
+ message: `double-handled event: ${event.typeName} @ ${event.start}`,
+ showToUser: false,
+ });
+ return;
+ }
+ 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,
+ });
+ }
+ });
+ }
+
+ /**
+ * Find ProtoExpectations, post-process them, convert them to real UEs.
+ */
+ function findInputExpectations(modelHelper) {
+ // Prevent helper functions from producing too many import warnings.
+ let warning;
+ function warn(w) {
+ // Keep only the first warning.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html
new file mode 100644
index 00000000000..a5a3265e0b8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html
@@ -0,0 +1,325 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/extras/chrome/time_to_interactive.html">
+<link rel="import" href="/tracing/model/user_model/load_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const LONG_TASK_THRESHOLD_MS = 50;
+
+ const IGNORE_URLS = [
+ // Blank URLs correspond to initial empty loads and we want to ignore
+ // them.
+ '',
+ 'about:blank',
+ ];
+
+
+ /**
+ * @param {!tr.model.Process} process
+ * @param {!tr.b.math.Range} range
+ * @return {Array.<tr.model.Event>} An array of network events of a process
+ * and that are intersecting a range.
+ */
+ 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;
+ }
+
+ 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;
+ }
+
+ /**
+ * Returns Time to Interactive and First CPU Idle for the
+ * given parameters. See the time_to_interactive.html module for detailed
+ * description and implementation of these metrics. The two metrics are
+ * computed together in the same function because almost all the computed
+ * parameters, for example list of relevant long tasks, are same for these two
+ * metrics, and this helps avoid duplicate computation.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper - Renderer
+ * helper for the renderer of interest.
+ * @param {tr.model.ThreadSlice} navigationStart - The navigation start
+ * event for which loading metrics is being computed.
+ * @param {tr.model.ThreadSlice} fmpEvent - The first meaningful paint
+ * event for which loading metrics is being computed.
+ * @param {tr.model.ThreadSlice} domContentLoadedEndEvent - Event
+ * corresponding to finish of dom content loading
+ * @param {number} searchWindowEnd - Time till when to search for a TTI. This
+ * value is either the start of next navigation or the end of the trace.
+ * @returns {interactiveSample: {number}|undefined,
+ * firstCpuIdleTime: {number}|undefined}
+ */
+ function computeInteractivityMetricSample_(rendererHelper, navigationStart,
+ fmpEvent, domContentLoadedEndEvent, searchWindowEnd) {
+ // Cannot determine TTI if DomContentLoadedEnd was never reached or if
+ // there is no corresponding fmpEvent.
+ 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);
+
+ // If we did not find any resource load events, interactiveTime should not
+ // be computed to avoid reporting misleading values.
+ const interactiveTime = resourceLoadEvents.length > 0 ?
+ tr.e.chrome.findInteractiveTime(
+ firstMeaningfulPaintTime, searchWindowEnd,
+ domContentLoadedEndEvent.start, longTasksInWindow,
+ resourceLoadEvents) : undefined;
+
+ return {interactiveTime, firstCpuIdleTime};
+ }
+
+ /* Constructs a loading metrics for the specified navigation start event and
+ * the corresponding fmpEvent and returns a sample including the metrics and
+ * navigationStartEvent, fmpEvent, url and the frameId.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper - Renderer
+ * helper for the renderer of interest.
+ * @param {Map.<string, Array<!tr.model.ThreadSlice>>} frameToNavStartEvents -
+ * Map from frame ids to sorted array of navigation start events.
+ * @param {Map.<string, Array<!tr.model.ThreadSlice>>}
+ * frameToDomContentLoadedEndEvents - Map from frame ids to sorted array
+ * of DOMContentLoadedEnd events.
+ * @param {tr.model.ThreadSlice} navigationStart - The navigation start
+ * event for which loading metrics is being computed.
+ * @param {tr.model.ThreadSlice} fmpEvent - The first meaningful paint
+ * event for which loading metrics is being computed.
+ * @param {number} searchWindowEnd - The end of the current navigation either
+ * because new navigation has started or the trace has ended.
+ * @param {string} url - URL of the current main frame document.
+ * @param {number} frameId - fameId.
+ * @returns {{start: {number}, duration: {number},
+ * fmpEvent: {tr.model.ThreadSlice}, navStart: {tr.model.ThreadSlice},
+ * dclEndTime: {tr.model.ThreadSlice}, firstCpuIdleTime: {number}|undefined,
+ * interactiveSample: {number}|undefined, url: {string}, frameId: {number}}}
+ */
+ function constructLoadingExpectation_(rendererHelper,
+ frameToDomContentLoadedEndEvents, navigationStart, fmpEvent,
+ searchWindowEnd, url, frameId) {
+ // Find when dom content has loaded.
+ 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) {
+ // TODO(catapult:#3796): Ideally a frame should reach DomContentLoadedEnd
+ // at most once within two navigationStarts, but sometimes there is a
+ // strange DclEnd event immediately following the navigationStart, and
+ // then the 'real' dclEnd happens later. It is not clear how to best
+ // determine the correct dclEnd value. For now, if there are multiple
+ // DclEnd events in the search window, we just pick the last one.
+ 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);
+ }
+
+ /**
+ * Computes the loading expectations for a renderer represented by
+ * |rendererHelper| and returns a list of samples. The loading
+ * expectation is the time between navigation start and the time to
+ * be interactive. There will be one load expectation corresponding
+ * to each navigation start for loading main frames.
+ *
+ * Also, computes Time to First Meaningful Paint (TTFMP), and
+ * Time to First CPU Idle (TTFCI) along with time to interactive (TTI)
+ * and returns them along with the load expectation.
+ *
+ * First meaningful paint is the paint following the layout with the highest
+ * "Layout Significance". The Layout Significance is computed inside Blink,
+ * by FirstMeaningfulPaintDetector class. It logs
+ * "firstMeaningfulPaintCandidate" event every time the Layout Significance
+ * marks a record. TTFMP is the time between NavigationStart and the last
+ * firstMeaningfulPaintCandidate event.
+ *
+ * Design doc: https://goo.gl/vpaxv6
+ *
+ * Time to Interactive and Time to First CPU Idle is based on heuristics
+ * involving main thread and network activity, as well as First Meaningful
+ * Paint and DOMContentLoadedEnd event. See time_to_interactive.html module
+ * for detailed description and implementation of these two metrics.
+ */
+ 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) {
+ // Don't use fmpCandidate if it is not corresponding this navigation.
+ fmpMarkerEvent = undefined;
+ }
+ samples.push(constructLoadingExpectation_(
+ rendererHelper, frameToDomContentLoadedEndEvents, navigationStart,
+ fmpMarkerEvent, searchWindowEnd, url, frameIdRef));
+ }
+
+ const candidatesForFrameId =
+ findFirstMeaningfulPaintCandidates(rendererHelper);
+
+ for (const [frameIdRef, navStartEvents] of frameToNavStartEvents) {
+ const fmpCandidateEvents = candidatesForFrameId[frameIdRef] || [];
+ let prevNavigation = {navigationEvent: undefined, url: undefined};
+
+ for (let index = 0; index < navStartEvents.length; index++) {
+ const currNavigation = navStartEvents[index];
+ let url;
+ let isLoadingMainFrame = false;
+
+ if (currNavigation.args.data) {
+ url = currNavigation.args.data.documentLoaderURL;
+ isLoadingMainFrame = currNavigation.args.data.isLoadingMainFrame;
+ } else {
+ // TODO(#4358): Delete old path of obtaining URL.
+ const snapshot = findFrameLoaderSnapshotAt(
+ rendererHelper, frameIdRef, currNavigation.start);
+ if (snapshot) {
+ url = snapshot.args.documentLoaderURL;
+ isLoadingMainFrame = snapshot.args.isLoadingMainFrame;
+ }
+ }
+
+ // Filter navigationStartEvents that do not correspond to a loading main
+ // frame, or has a URL that we do not care about.
+ if (!isLoadingMainFrame) continue;
+ if (url === undefined || IGNORE_URLS.includes(url)) continue;
+
+ if (prevNavigation.navigationEvent !== undefined) {
+ // Add a LoadExpectation for the previous navigation ending on or
+ // before current navigation.
+ addSamples(frameIdRef, prevNavigation.navigationEvent,
+ fmpCandidateEvents, currNavigation.start, prevNavigation.url);
+ }
+
+ prevNavigation = {navigationEvent: currNavigation, url};
+ }
+
+ // Handle the last navigation here.
+ if (prevNavigation.navigationEvent !== undefined) {
+ addSamples(frameIdRef, prevNavigation.navigationEvent,
+ fmpCandidateEvents, rendererHelper.modelHelper.chromeBounds.max,
+ prevNavigation.url);
+ }
+ }
+ return samples;
+ }
+
+
+ function findLoadExpectations(modelHelper) {
+ const loads = [];
+
+ const chromeHelper = modelHelper.model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ for (const pid in chromeHelper.rendererHelpers) {
+ const rendererHelper = chromeHelper.rendererHelpers[pid];
+ if (rendererHelper.isChromeTracingUI) continue;
+
+ loads.push.apply(loads,
+ collectLoadExpectationsForRenderer(rendererHelper));
+ }
+ return loads;
+ }
+
+ return {
+ findLoadExpectations,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html
new file mode 100644
index 00000000000..72415a62604
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/startup_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ function getAllFrameEvents(modelHelper) {
+ const frameEvents = [];
+ frameEvents.push.apply(frameEvents,
+ modelHelper.browserHelper.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds));
+
+ for (const renderer of Object.values(modelHelper.rendererHelpers)) {
+ frameEvents.push.apply(frameEvents, renderer.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds));
+ }
+ return frameEvents.sort(tr.importer.compareEvents);
+ }
+
+ // If a thread contains a typical initialization slice, then the first event
+ // on that thread is a startup event.
+ function getStartupEvents(modelHelper) {
+ function isStartupSlice(slice) {
+ return slice.title === 'BrowserMainLoop::CreateThreads';
+ }
+ const events = modelHelper.browserHelper.getAllAsyncSlicesMatching(
+ isStartupSlice);
+ const deduper = new tr.model.EventSet();
+ events.forEach(function(event) {
+ const sliceGroup = event.parentContainer.sliceGroup;
+ const slice = sliceGroup && sliceGroup.findFirstSlice();
+ if (slice) {
+ deduper.push(slice);
+ }
+ });
+ return deduper.toArray();
+ }
+
+ // Match every event in |openingEvents| to the first following event from
+ // |closingEvents| and return an array containing a load interaction record
+ // for each pair.
+ function findStartupExpectations(modelHelper) {
+ const openingEvents = getStartupEvents(modelHelper);
+ const closingEvents = getAllFrameEvents(modelHelper);
+ const startups = [];
+ openingEvents.forEach(function(openingEvent) {
+ closingEvents.forEach(function(closingEvent) {
+ // Ignore opening event that already have a closing event.
+ if (openingEvent.closingEvent) return;
+
+ // Ignore closing events that already belong to an opening event.
+ if (closingEvent.openingEvent) return;
+
+ // Ignore closing events before |openingEvent|.
+ if (closingEvent.start <= openingEvent.start) return;
+
+ // Ignore events from different threads.
+ if (openingEvent.parentContainer.parent.pid !==
+ closingEvent.parentContainer.parent.pid) {
+ return;
+ }
+
+ // This is the first closing event for this opening event, record it.
+ openingEvent.closingEvent = closingEvent;
+ closingEvent.openingEvent = openingEvent;
+ const se = new tr.model.um.StartupExpectation(
+ modelHelper.model, openingEvent.start,
+ closingEvent.end - openingEvent.start);
+ se.associatedEvents.push(openingEvent);
+ se.associatedEvents.push(closingEvent);
+ startups.push(se);
+ });
+ });
+ return startups;
+ }
+
+ return {
+ findStartupExpectations,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/import.html b/chromium/third_party/catapult/tracing/tracing/importer/import.html
new file mode 100644
index 00000000000..8893120c2c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/import.html
@@ -0,0 +1,339 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/importer/empty_importer.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/importer/user_model_builder.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const Timing = tr.b.Timing;
+
+ function ImportOptions() {
+ this.shiftWorldToZero = true;
+ this.pruneEmptyContainers = true;
+ this.showImportWarnings = true;
+ this.trackDetailedModelStats = false;
+
+ // Callback called after
+ // importers run in which more data can be added to the model, before it is
+ // finalized.
+ this.customizeModelCallback = undefined;
+
+ const auditorTypes = tr.c.Auditor.getAllRegisteredTypeInfos();
+ this.auditorConstructors = auditorTypes.map(function(typeInfo) {
+ return typeInfo.constructor;
+ });
+ }
+
+ function Import(model, opt_options) {
+ if (model === undefined) {
+ throw new Error('Must provide model to import into.');
+ }
+
+ // TODO(dsinclair): Check the model is empty.
+
+ this.importing_ = false;
+ this.importOptions_ = opt_options || new ImportOptions();
+
+ this.model_ = model;
+ this.model_.importOptions = this.importOptions_;
+ }
+
+ Import.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Imports the provided traces into the model. The eventData type
+ * is undefined and will be passed to all the importers registered
+ * via Importer.register. The first importer that returns true
+ * for canImport(events) will be used to import the events.
+ *
+ * The primary trace is provided via the eventData variable. If multiple
+ * traces are to be imported, specify the first one as events, and the
+ * remainder in the opt_additionalEventData array.
+ *
+ * @param {Array} traces An array of eventData to be imported. Each
+ * eventData should correspond to a single trace file and will be handled by
+ * a separate importer.
+ */
+ importTraces(traces) {
+ const progressMeter = {
+ update(msg) {}
+ };
+
+ tr.b.Task.RunSynchronously(
+ this.createImportTracesTask(progressMeter, traces));
+ },
+
+ /**
+ * Imports a trace with the usual options from importTraces, but
+ * does so using idle callbacks, putting up an import dialog
+ * during the import process.
+ */
+ importTracesWithProgressDialog(traces) {
+ if (tr.isHeadless) {
+ throw new Error('Cannot use this method in headless mode.');
+ }
+
+ const overlay = tr.ui.b.Overlay();
+ overlay.title = 'Importing...';
+ overlay.userCanClose = false;
+ overlay.msgEl = document.createElement('div');
+ Polymer.dom(overlay).appendChild(overlay.msgEl);
+ overlay.msgEl.style.margin = '20px';
+ overlay.update = function(msg) {
+ Polymer.dom(this.msgEl).textContent = msg;
+ };
+ overlay.visible = true;
+
+ const promise =
+ tr.b.Task.RunWhenIdle(this.createImportTracesTask(overlay, traces));
+ promise.then(
+ function() { overlay.visible = false; },
+ function(err) { overlay.visible = false; }
+ );
+ return promise;
+ },
+
+ /**
+ * Creates a task that will import the provided traces into the model,
+ * updating the progressMeter as it goes. Parameters are as defined in
+ * importTraces.
+ */
+ createImportTracesTask(progressMeter, traces) {
+ const importStartTimeMs = tr.b.Timing.getCurrentTimeMs();
+
+ if (this.importing_) {
+ throw new Error('Already importing.');
+ }
+ this.importing_ = true;
+
+ // Just some simple setup. It is useful to have a no-op first
+ // task so that we can set up the lastTask = lastTask.after()
+ // pattern that follows.
+ const importTask = new tr.b.Task(function prepareImport() {
+ progressMeter.update('I will now import your traces for you...');
+ }, this);
+ let lastTask = importTask;
+
+ const importers = [];
+
+ function addImportStage(title, callback) {
+ lastTask = lastTask.after(() => progressMeter.update(title));
+ lastTask.updatesUi = true;
+ lastTask = lastTask.after(callback);
+ }
+
+ function addStageForEachImporter(title, callback) {
+ lastTask = lastTask.after((task) => {
+ importers.forEach((importer, index) => {
+ const uiSubTask = task.subTask(() => {
+ progressMeter.update(
+ `${title} ${index + 1} of ${importers.length}`);
+ });
+ uiSubTask.updatesUi = true;
+ task.subTask(() => callback(importer));
+ });
+ });
+ }
+
+ addImportStage('Creating importers...', () => {
+ // Copy the traces array, we may mutate it.
+ traces = traces.slice(0);
+ progressMeter.update('Creating importers...');
+ // Figure out which importers to use.
+ for (let i = 0; i < traces.length; ++i) {
+ importers.push(this.createImporter_(traces[i]));
+ }
+
+ // Some traces have other traces inside them. Before doing the full
+ // import, ask the importer if it has any subtraces, and if so, create
+ // importers for them, also.
+ for (let i = 0; i < importers.length; i++) {
+ const subtraces = importers[i].extractSubtraces();
+ for (let j = 0; j < subtraces.length; j++) {
+ try {
+ traces.push(subtraces[j]);
+ importers.push(this.createImporter_(subtraces[j]));
+ } catch (error) {
+ this.model_.importWarning({
+ type: error.name,
+ message: error.message,
+ showToUser: true,
+ });
+ continue;
+ }
+ }
+ }
+
+ if (traces.length && !this.hasEventDataDecoder_(importers)) {
+ throw new Error(
+ 'Could not find an importer for the provided eventData.');
+ }
+
+ // Sort them on priority. This ensures importing happens in a
+ // predictable order, e.g. ftrace_importer before
+ // trace_event_importer.
+ importers.sort(function(x, y) {
+ return x.importPriority - y.importPriority;
+ });
+ });
+
+ // We import clock sync markers before all other events. This is necessary
+ // because we need the clock sync markers in order to know by how much we
+ // need to shift the timestamps of other events.
+ addStageForEachImporter('Importing clock sync markers',
+ importer => importer.importClockSyncMarkers());
+
+ addStageForEachImporter('Importing', importer => importer.importEvents());
+
+ // Run the cusomizeModelCallback if needed.
+ if (this.importOptions_.customizeModelCallback) {
+ addImportStage('Customizing', () => {
+ this.importOptions_.customizeModelCallback(this.model_);
+ });
+ }
+
+ // Import sample data.
+ addStageForEachImporter('Importing sample data',
+ importer => importer.importSampleData());
+
+ // Autoclose open slices and create subSlices.
+ addImportStage('Autoclosing open slices...', () => {
+ this.model_.autoCloseOpenSlices();
+ this.model_.createSubSlices();
+ });
+
+ // Finalize import.
+ addStageForEachImporter('Finalizing import',
+ importer => importer.finalizeImport());
+
+ // Run preinit.
+ addImportStage('Initializing objects (step 1/2)...',
+ () => this.model_.preInitializeObjects());
+
+ // Prune empty containers.
+ if (this.importOptions_.pruneEmptyContainers) {
+ addImportStage('Pruning empty containers...',
+ () => this.model_.pruneEmptyContainers());
+ }
+
+ // Merge kernel and userland slices on each thread.
+ addImportStage('Merging kernel with userland...',
+ () => this.model_.mergeKernelWithUserland());
+
+ // Create auditors
+ let auditors = [];
+ addImportStage('Adding arbitrary data to model...', () => {
+ auditors = this.importOptions_.auditorConstructors.map(
+ auditorConstructor => new auditorConstructor(this.model_));
+ auditors.forEach((auditor) => {
+ auditor.runAnnotate();
+ auditor.installUserFriendlyCategoryDriverIfNeeded();
+ });
+ });
+
+ addImportStage('Computing final world bounds...', () => {
+ this.model_.computeWorldBounds(this.importOptions_.shiftWorldToZero);
+ });
+
+ addImportStage('Building flow event map...',
+ () => this.model_.buildFlowEventIntervalTree());
+
+ // Join refs.
+ addImportStage('Joining object refs...', () => this.model_.joinRefs());
+
+ // Delete any undeleted objects.
+ addImportStage('Cleaning up undeleted objects...',
+ () => this.model_.cleanupUndeletedObjects());
+
+ // Sort global and process memory dumps.
+ addImportStage('Sorting memory dumps...',
+ () => this.model_.sortMemoryDumps());
+
+ // Finalize memory dump graphs.
+ addImportStage('Finalizing memory dump graphs...',
+ () => this.model_.finalizeMemoryGraphs());
+
+ // Run initializers.
+ addImportStage('Initializing objects (step 2/2)...',
+ () => this.model_.initializeObjects());
+
+ // Build event indices mapping from an event id to all flow events.
+ addImportStage('Building event indices...',
+ () => this.model_.buildEventIndices());
+
+ // Build the UserModel.
+ addImportStage('Building UserModel...', () => {
+ const userModelBuilder = new tr.importer.UserModelBuilder(this.model_);
+ userModelBuilder.buildUserModel();
+ });
+
+ // Sort Expectations.
+ addImportStage('Sorting user expectations...',
+ () => this.model_.userModel.sortExpectations());
+
+ // Run audits.
+ addImportStage('Running auditors...', () => {
+ auditors.forEach(auditor => auditor.runAudit());
+ });
+
+ addImportStage('Updating alerts...', () => this.model_.sortAlerts());
+
+ addImportStage('Update bounds...', () => this.model_.updateBounds());
+
+ addImportStage('Looking for warnings...', () => {
+ // Log an import warning if the clock is low resolution.
+ if (!this.model_.isTimeHighResolution) {
+ this.model_.importWarning({
+ type: 'low_resolution_timer',
+ message: 'Trace time is low resolution, trace may be unusable.',
+ showToUser: true
+ });
+ }
+ });
+
+ // Cleanup.
+ lastTask.after(() => {
+ this.importing_ = false;
+ this.model_.stats.traceImportDurationMs =
+ tr.b.Timing.getCurrentTimeMs() - importStartTimeMs;
+ });
+ return importTask;
+ },
+
+ createImporter_(eventData) {
+ const importerConstructor = tr.importer.Importer.findImporterFor(
+ eventData);
+ if (!importerConstructor) {
+ throw new Error('Couldn\'t create an importer for the provided ' +
+ 'eventData.');
+ }
+ return new importerConstructor(this.model_, eventData);
+ },
+
+ hasEventDataDecoder_(importers) {
+ for (let i = 0; i < importers.length; ++i) {
+ if (!importers[i].isTraceDataContainer()) return true;
+ }
+
+ return false;
+ }
+ };
+
+ return {
+ ImportOptions,
+ Import,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/import_test.html b/chromium/third_party/catapult/tracing/tracing/importer/import_test.html
new file mode 100644
index 00000000000..77787402719
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/import_test.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/extras/importer/v8/v8_log_importer.html">
+<link rel="import" href="/tracing/extras/importer/zip_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+
+ test('canImportEmpty', function() {
+ let m = tr.c.TestUtils.newModelWithEvents([]);
+ assert.isDefined(m.modelIndices);
+ m = new tr.Model('');
+ });
+
+ test('canImportSubtraces', function() {
+ const systraceLines = [
+ 'SurfaceFlinger-2 [001] ...1 1000.0: 0: B|1|taskA',
+ 'SurfaceFlinger-2 [001] ...1 2000.0: 0: E',
+ ' chrome-3 [001] ...1 2000.0: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0'
+ ];
+ const traceEvents = [
+ {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+ my_object: {id_ref: '0x1000'}
+ }},
+ {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+ ];
+
+ const combined = JSON.stringify({
+ traceEvents,
+ systemTraceEvents: systraceLines.join('\n')
+ });
+
+ const m = tr.c.TestUtils.newModelWithEvents([combined]);
+ assert.strictEqual(Object.values(m.processes).length, 1);
+
+ const p1 = m.processes[1];
+ assert.isDefined(p1);
+
+ const t2 = p1.threads[2];
+ const t3 = p1.threads[3];
+ assert.isDefined(t2);
+ assert.isDefined(t3);
+
+ assert.strictEqual(1, 1, t2.sliceGroup.length);
+ assert.strictEqual(t2.sliceGroup.slices[0].title, 'taskA');
+
+ assert.strictEqual(t3.sliceGroup.length, 1);
+ assert.strictEqual(t3.sliceGroup.slices[0].title, 'taskB');
+ });
+
+ test('canImportCompressedSingleSubtrace', function() {
+ const compressedTrace = Base64.atob(
+ 'H4sIACKfFVUC/wsuLUpLTE51y8nMS08t0jVSUIg2MDCMV' +
+ 'dDT0zNUMDQwMNAzsFIAIqcaw5qSxOJsR65gfDqMEDpcATiC61ZbAAAA');
+ const m = tr.c.TestUtils.newModelWithEvents([compressedTrace]);
+ assert.strictEqual(1, Object.values(m.processes).length);
+
+ const p1 = m.processes[1];
+ assert.isDefined(p1);
+
+ const t2 = p1.threads[2];
+ assert.isDefined(t2);
+
+ assert.strictEqual(1, t2.sliceGroup.length, 1);
+ assert.strictEqual('taskA', t2.sliceGroup.slices[0].title);
+ });
+
+ test('canImportSubtracesRecursively', function() {
+ const systraceLines = [
+ 'SurfaceFlinger-2 [001] ...1 1000.0: 0: B|1|taskA',
+ 'SurfaceFlinger-2 [001] ...1 2000.0: 0: E',
+ ' chrome-3 [001] ...1 2000.0: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0'
+ ];
+ const outerTraceEvents = [
+ {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+ my_object: {id_ref: '0x1000'}
+ }}
+ ];
+
+ const innerTraceEvents = [
+ {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+ ];
+
+ const innerTrace = JSON.stringify({
+ traceEvents: innerTraceEvents,
+ systemTraceEvents: systraceLines.join('\n')
+ });
+
+ const outerTrace = JSON.stringify({
+ traceEvents: outerTraceEvents,
+ systemTraceEvents: innerTrace
+ });
+
+ const m = tr.c.TestUtils.newModelWithEvents([outerTrace]);
+ assert.strictEqual(Object.values(m.processes).length, 1);
+
+ const p1 = m.processes[1];
+ assert.isDefined(p1);
+
+ const t2 = p1.threads[2];
+ const t3 = p1.threads[3];
+ assert.isDefined(t2);
+ assert.isDefined(t3);
+
+ assert.strictEqual(1, 1, t2.sliceGroup.length);
+ assert.strictEqual(t2.sliceGroup.slices[0].title, 'taskA');
+
+ assert.strictEqual(t3.sliceGroup.length, 1);
+ assert.strictEqual(t3.sliceGroup.slices[0].title, 'taskB');
+ });
+
+ test('withImportFailure', function() {
+ assert.throw(function() {
+ tr.c.TestUtils.newModelWithEvents([malformed]);
+ });
+ });
+
+ test('customizeCallback', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ const browserProcess = m.getOrCreateProcess(1);
+ const browserMain = browserProcess.getOrCreateThread(2);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+ browserMain.sliceGroup.beginSlice('cat', 'SubTask', 1);
+ browserMain.sliceGroup.endSlice(9);
+ browserMain.sliceGroup.endSlice(10);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+ browserMain.sliceGroup.endSlice(30);
+ }
+ });
+ const t2 = m.processes[1].threads[2];
+ assert.strictEqual(t2.sliceGroup.length, 3);
+ assert.strictEqual(t2.sliceGroup.topLevelSlices.length, 2);
+ });
+
+ test('sortsSamples', function() {
+ // The 184, 0 and 185 are the tick-times
+ // and irrespective of the order
+ // in which the lines appear in the trace,
+ // the samples should always be sorted by sampling time.
+ const m = tr.c.TestUtils.newModelWithEvents([
+ 'tick,0x9a,184,0,0x0,5',
+ 'tick,0x9b,0,0,0x0,5',
+ 'tick,0x9c,185,0,0x0,5']);
+ assert.strictEqual(m.samples[0].start, 0);
+ assert.strictEqual(m.samples[1].start, 0.184);
+ assert.strictEqual(m.samples[2].start, 0.185);
+ });
+
+ test('sortsGlobalMemoryDumps', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 1));
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 5));
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 3));
+ }
+ });
+ assert.strictEqual(m.globalMemoryDumps[0].start, 0);
+ assert.strictEqual(m.globalMemoryDumps[1].start, 2);
+ assert.strictEqual(m.globalMemoryDumps[2].start, 4);
+ });
+
+ test('finalizesProcessMemoryDumps', function() {
+ let p;
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ p = m.getOrCreateProcess(1);
+
+ const g = new tr.model.GlobalMemoryDump(m, -1);
+ m.globalMemoryDumps.push(g);
+
+ const pmd1 = new tr.model.ProcessMemoryDump(g, p, 1);
+ p.memoryDumps.push(pmd1);
+
+ const pmd2 = new tr.model.ProcessMemoryDump(g, p, 5);
+ p.memoryDumps.push(pmd2);
+
+ const pmd3 = new tr.model.ProcessMemoryDump(g, p, 3);
+ p.memoryDumps.push(pmd3);
+ pmd3.vmRegions = [];
+ }
+ });
+
+ // Check the sort order.
+ assert.strictEqual(p.memoryDumps[0].start, 2);
+ assert.strictEqual(p.memoryDumps[1].start, 4);
+ assert.strictEqual(p.memoryDumps[2].start, 6);
+
+ // Check that the most recent VM regions are linked correctly.
+ assert.isUndefined(p.memoryDumps[0].mostRecentVmRegions);
+ assert.lengthOf(p.memoryDumps[1].mostRecentVmRegions, 0);
+ assert.strictEqual(
+ p.memoryDumps[1].mostRecentVmRegions,
+ p.memoryDumps[2].mostRecentVmRegions);
+ });
+
+ test('setsModelStatsTraceImportDurationMs', function() {
+ const traceEvents = [
+ {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+ my_object: {id_ref: '0x1000'}
+ }},
+ {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents(JSON.stringify({traceEvents}));
+
+ assert.isAbove(m.stats.traceImportDurationMs, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/importer.html b/chromium/third_party/catapult/tracing/tracing/importer/importer.html
new file mode 100644
index 00000000000..66accd2b840
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/importer.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data importers.
+ */
+tr.exportTo('tr.importer', function() {
+ function Importer() { }
+
+ Importer.prototype = {
+ __proto__: Object.prototype,
+
+ get importerName() {
+ return 'Importer';
+ },
+
+ /**
+ * Called by the Model to check whether the importer type stores the actual
+ * trace data or just holds it as container for further extraction.
+ */
+ isTraceDataContainer() {
+ return false;
+ },
+
+ /**
+ * Called by the Model to extract one or more subtraces from the event data.
+ */
+ extractSubtraces() {
+ return [];
+ },
+
+ /**
+ * Called to import clock sync markers into the Model.
+ */
+ importClockSyncMarkers() {
+ },
+
+ /**
+ * Called to import events into the Model.
+ */
+ importEvents() {
+ },
+
+ /**
+ * Called to import sample data into the Model.
+ */
+ importSampleData() {
+ },
+
+ /**
+ * Called by the Model after all other importers have imported their
+ * events.
+ */
+ finalizeImport() {
+ }
+ };
+
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.defaultMetadata = {};
+ options.mandatoryBaseClass = Importer;
+ tr.b.decorateExtensionRegistry(Importer, options);
+
+ Importer.findImporterFor = function(eventData) {
+ const typeInfo = Importer.findTypeInfoMatching(function(ti) {
+ return ti.constructor.canImport(eventData);
+ });
+ if (typeInfo) {
+ return typeInfo.constructor;
+ }
+ return undefined;
+ };
+
+ return {
+ Importer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html b/chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html
new file mode 100644
index 00000000000..3deaac7d934
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/user_model/animation_expectation.html">
+<link rel="import" href="/tracing/model/user_model/response_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ // This is an intermediate data format between InputLatencyAsyncSlices and
+ // Responses and Animations.
+ function ProtoExpectation(type, initiatorType) {
+ this.type = type;
+ this.initiatorType = initiatorType;
+ this.start = Infinity;
+ this.end = -Infinity;
+ this.associatedEvents = new tr.model.EventSet();
+ this.isAnimationBegin = false;
+ }
+
+ ProtoExpectation.RESPONSE_TYPE = 'r';
+ ProtoExpectation.ANIMATION_TYPE = 'a';
+
+ // Explicitly ignore some input events to allow
+ // UserModelBuilder.checkAllInputEventsHandled() to determine which events
+ // were unintentionally ignored due to a bug.
+ ProtoExpectation.IGNORED_TYPE = 'ignored';
+
+ /**
+ * Combine initiator titles by selecting the initiator title first in a
+ * hard-coded hierarchy. Higher up in the hierarchy are more "specific"
+ * initiator titles (e.g. a scroll is higher than a touch, because a
+ * touch could mean many different things, of which a scroll is one)
+ */
+ const INITIATOR_HIERARCHY = [
+ tr.model.um.INITIATOR_TYPE.PINCH,
+ tr.model.um.INITIATOR_TYPE.FLING,
+ tr.model.um.INITIATOR_TYPE.MOUSE_WHEEL,
+ tr.model.um.INITIATOR_TYPE.SCROLL,
+ tr.model.um.INITIATOR_TYPE.VR,
+ tr.model.um.INITIATOR_TYPE.VIDEO,
+ tr.model.um.INITIATOR_TYPE.WEBGL,
+ tr.model.um.INITIATOR_TYPE.CSS,
+ tr.model.um.INITIATOR_TYPE.MOUSE,
+ tr.model.um.INITIATOR_TYPE.KEYBOARD,
+ tr.model.um.INITIATOR_TYPE.TAP,
+ tr.model.um.INITIATOR_TYPE.TOUCH
+ ];
+
+ function combineInitiatorTypes(title1, title2) {
+ for (const item of INITIATOR_HIERARCHY) {
+ if (title1 === item || title2 === item) return item;
+ }
+ throw new Error('Invalid titles in combineInitiatorTypes');
+ }
+
+ ProtoExpectation.prototype = {
+ get isValid() {
+ return this.end > this.start;
+ },
+
+ // Return true if any associatedEvent's typeName is in typeNames.
+ 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);
+
+ // |event| is either an InputLatencyAsyncSlice (which collects all of
+ // its associated events transitively) or a CSS Animation (which doesn't
+ // have any associated events). So this does not need to recurse.
+ if (event.associatedEvents) {
+ ir.associatedEvents.addEventSet(event.associatedEvents);
+ }
+ }
+
+ this.associatedEvents.forEach(function(event) {
+ pushAssociatedEvents(event);
+
+ // Old-style InputLatencyAsyncSlices have subSlices.
+ if (event.subSlices) {
+ event.subSlices.forEach(pushAssociatedEvents);
+ }
+ });
+
+ return ir;
+ },
+
+ // Merge the other ProtoExpectation into this one.
+ // The types need not match: ignored ProtoExpectations might be merged
+ // into overlapping ProtoExpectations, and Touch-only Animations are merged
+ // into Tap Responses.
+ merge(other) {
+ this.initiatorType = combineInitiatorTypes(
+ this.initiatorType, other.initiatorType);
+
+ // Don't use pushEvent(), which would lose special start, end.
+ 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;
+ }
+ },
+
+ // Include |event| in this ProtoExpectation, expanding start/end to include
+ // it.
+ pushEvent(event) {
+ // Usually, this method will be called while iterating over a list of
+ // events sorted by start time, so this method won't usually change
+ // this.start. However, this will sometimes be called for
+ // ProtoExpectations created by previous handlers, in which case
+ // event.start could possibly be before this.start.
+ this.start = Math.min(this.start, event.start);
+ this.end = Math.max(this.end, event.end);
+ this.associatedEvents.push(event);
+ },
+
+ // Include |sample| in this ProtoExpectation, expanding start/end to
+ // include it.
+ pushSample(sample) {
+ this.start = Math.min(this.start, sample.timestamp);
+ this.end = Math.max(this.end, sample.timestamp);
+ this.associatedEvents.push(sample);
+ },
+
+ // Returns true if timestamp is contained in this ProtoExpectation.
+ containsTimestampInclusive(timestamp) {
+ return (this.start <= timestamp) && (timestamp <= this.end);
+ },
+
+ // Return true if the other event intersects this ProtoExpectation.
+ intersects(other) {
+ // http://stackoverflow.com/questions/325933
+ return (other.start < this.end) && (other.end > this.start);
+ },
+
+ isNear(event, threshold) {
+ return (this.end + threshold) > event.start;
+ },
+
+ // Return a string describing this ProtoExpectation for debugging.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html b/chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html
new file mode 100644
index 00000000000..ff94c9d74ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ class SimpleLineReader {
+ constructor(text) {
+ this.data_ = text instanceof tr.b.TraceStream ?
+ text : text.split(new RegExp('\r?\n'));
+ this.curLine_ = 0;
+ this.readLastLine_ = false;
+ this.savedLines_ = undefined;
+ }
+
+ * [Symbol.iterator]() {
+ let lastLine = undefined;
+ while (this.hasData_) {
+ if (this.readLastLine_) {
+ this.curLine_++;
+ this.readLastLine_ = false;
+ } else if (this.data_ instanceof tr.b.TraceStream) {
+ this.curLine_++;
+ const line = this.data_.readUntilDelimiter('\n');
+ if (line.endsWith('\r\n')) {
+ lastLine = line.slice(0, -2);
+ } else if (line.endsWith('\n')) {
+ lastLine = line.slice(0, -1);
+ } else {
+ lastLine = line;
+ }
+ } else {
+ this.curLine_++;
+ lastLine = this.data_[this.curLine_ - 1];
+ }
+ yield lastLine;
+ }
+ }
+
+ get curLineNumber() {
+ return this.curLine_;
+ }
+
+ get hasData_() {
+ if (this.data_ instanceof tr.b.TraceStream) return this.data_.hasData;
+ return this.curLine_ < this.data_.length;
+ }
+
+ advanceToLineMatching(regex) {
+ for (const line of this) {
+ if (this.savedLines_ !== undefined) this.savedLines_.push(line);
+ if (regex.test(line)) {
+ this.goBack_();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ goBack_() {
+ if (this.readLastLine_) {
+ throw new Error('There should be at least one nextLine call between ' +
+ 'any two goBack calls.');
+ }
+ if (this.curLine_ === 0) {
+ throw new Error('There should be at least one nextLine call before ' +
+ 'the first goBack call.');
+ }
+ this.readLastLine_ = true;
+ this.curLine_--;
+ }
+
+ beginSavingLines() {
+ this.savedLines_ = [];
+ }
+
+ endSavingLinesAndGetResult() {
+ const tmp = this.savedLines_;
+ this.savedLines_ = undefined;
+ return tmp;
+ }
+ }
+
+ return {
+ SimpleLineReader,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html b/chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html
new file mode 100644
index 00000000000..ccaef12de51
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/importer/user_model_builder.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.importer', function() {
+ function compareEvents(x, y) {
+ if (x.start !== y.start) return x.start - y.start;
+ return x.guid - y.guid;
+ }
+
+ class UserExpectationVerifier {
+ constructor() {
+ this.customizeModelCallback_ = undefined;
+ this.expectedUEs_ = undefined;
+ this.expectedSegments_ = undefined;
+ }
+
+ set customizeModelCallback(cmc) {
+ this.customizeModelCallback_ = cmc;
+ }
+
+ /**
+ * @param {!Array.<!Object>} ues must be sorted by start time.
+ */
+ set expectedUEs(ues) {
+ this.expectedUEs_ = ues;
+ }
+
+ get expectedUEs() {
+ return this.expectedUEs_;
+ }
+
+ /**
+ * @param {!Array.<!Object>} segments must be sorted by start time.
+ */
+ set expectedSegments(segments) {
+ this.expectedSegments_ = segments;
+ }
+
+ verify() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(
+ this.customizeModelCallback_);
+
+ let failure = undefined;
+ try {
+ this.verifyExpectations_([...model.userModel.expectations]);
+ this.verifySegments_(model.userModel.segments);
+ } catch (e) {
+ failure = e;
+ }
+
+ if (failure) throw failure;
+ }
+
+ verifyExpectations_(expectations) {
+ assert.lengthOf(expectations, this.expectedUEs.length);
+ for (let i = 0; i < this.expectedUEs.length; ++i) {
+ this.verifyExpectation_(
+ this.expectedUEs[i], expectations[i], `UEs[${i}]`);
+ }
+ }
+
+ verifySegments_(segments) {
+ assert.lengthOf(segments, this.expectedSegments_.length);
+ for (let i = 0; i < this.expectedSegments_.length; ++i) {
+ this.verifySegment_(
+ this.expectedSegments_[i], segments[i], `segments[${i}].`);
+ }
+ }
+
+ verifyExpectation_(expected, actual, at) {
+ assert.strictEqual(expected.title, actual.title, at + 'title');
+ if (expected.name !== undefined) {
+ assert.strictEqual(expected.name, actual.name, at + 'name');
+ }
+ assert.strictEqual(expected.start, actual.start, at + 'start');
+ assert.strictEqual(expected.end, actual.end, at + 'end');
+ assert.strictEqual(expected.eventCount,
+ actual.associatedEvents.length, at + 'eventCount');
+ if (actual instanceof tr.model.um.ResponseExpectation) {
+ assert.strictEqual(expected.isAnimationBegin || false,
+ actual.isAnimationBegin, at + 'isAnimationBegin');
+ }
+ }
+
+ verifySegment_(expected, actual, at) {
+ assert.strictEqual(expected.start, actual.start, at + 'start');
+ assert.strictEqual(expected.end, actual.end, at + 'end');
+ assert.lengthOf(actual.expectations, expected.expectations.length,
+ at + 'expectations.length');
+ for (let i = 0; i < expected.expectations.length; ++i) {
+ this.verifyExpectation_(
+ expected.expectations[i], actual.expectations[i],
+ at + `expectations[${i}].`);
+ }
+ }
+ }
+
+ return {
+ UserExpectationVerifier,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html
new file mode 100644
index 00000000000..c5cf14137f0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html
@@ -0,0 +1,277 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/importer/find_input_expectations.html">
+<link rel="import" href="/tracing/importer/find_load_expectations.html">
+<link rel="import" href="/tracing/importer/find_startup_expectations.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/ir_coverage.html">
+<link rel="import" href="/tracing/model/user_model/idle_expectation.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const INSIGNIFICANT_MS = 1;
+
+ class UserModelBuilder {
+ constructor(model) {
+ this.model = model;
+ this.modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ }
+
+ static supportsModelHelper(modelHelper) {
+ return modelHelper.browserHelper !== undefined;
+ }
+
+ /**
+ * This is called during the trace model import process.
+ */
+ buildUserModel() {
+ if (!this.modelHelper || !this.modelHelper.browserHelper) return;
+
+ try {
+ for (const ue of this.findUserExpectations()) {
+ // This is an EventSet, not an Array, so it can't use push(...).
+ // https://github.com/catapult-project/catapult/issues/3157
+ this.model.userModel.expectations.push(ue);
+ }
+ this.model.userModel.segments.push(...this.findSegments());
+ // There are not currently any known cases when this could throw,
+ // but there have been in the past and there could be again, so
+ // keep handling exceptions here to be friendly to the future.
+ } catch (error) {
+ this.model.importWarning({
+ type: 'UserModelBuilder',
+ message: error,
+ showToUser: true
+ });
+ }
+ }
+
+ /**
+ * Returns an array of Segments covering the trace Model. A Segment
+ * represents a range of time during which the set of active
+ * UserExpectations does not change. Because of this, segments are
+ * guaranteed to not overlap, whereas UserExpectations can.
+ *
+ * @return {!Array.<!tr.model.um.Segment>}
+ */
+ findSegments() {
+ let timestamps = new Set();
+ for (const expectation of this.model.userModel.expectations) {
+ timestamps.add(expectation.start);
+ timestamps.add(expectation.end);
+ }
+ timestamps = [...timestamps];
+ timestamps.sort((x, y) => x - y);
+ const segments = [];
+ for (let i = 0; i < timestamps.length - 1; ++i) {
+ const segment = new tr.model.um.Segment(
+ timestamps[i], timestamps[i + 1] - timestamps[i]);
+ segments.push(segment);
+ const segmentRange = tr.b.math.Range.fromExplicitRange(
+ segment.start, segment.end);
+ for (const expectation of this.model.userModel.expectations) {
+ const expectationRange = tr.b.math.Range.fromExplicitRange(
+ expectation.start, expectation.end);
+ if (segmentRange.intersectsRangeExclusive(expectationRange)) {
+ segment.expectations.push(expectation);
+ }
+ }
+ }
+ return segments;
+ }
+
+ /**
+ * Returns an array of UserExpectations covering the trace Model. A
+ * UserExpectation represents a range of time during which the user is
+ * expecting something from Chrome, either to startup or load a page or
+ * respond to input or play an animation, or just sit there idle. Users can
+ * have multiple expectations at any given time, so UserExpectations can
+ * overlap.
+ *
+ * @return {!Array.<!tr.model.um.UserExpectation>}
+ */
+ findUserExpectations() {
+ const expectations = [];
+ expectations.push.apply(expectations, tr.importer.findStartupExpectations(
+ this.modelHelper));
+ expectations.push.apply(expectations, tr.importer.findLoadExpectations(
+ this.modelHelper));
+ expectations.push.apply(expectations, tr.importer.findInputExpectations(
+ this.modelHelper));
+ // findIdleExpectations must be called last!
+ expectations.push.apply(
+ expectations, this.findIdleExpectations(expectations));
+ this.collectUnassociatedEvents_(expectations);
+ return expectations;
+ }
+
+ // Find all unassociated top-level ThreadSlices. If they start during an
+ // Idle or Load UE, then add their entire hierarchy to that UE.
+ collectUnassociatedEvents_(expectations) {
+ const vacuumUEs = [];
+ for (const expectation of expectations) {
+ if (expectation instanceof tr.model.um.IdleExpectation ||
+ expectation instanceof tr.model.um.LoadExpectation ||
+ expectation instanceof tr.model.um.StartupExpectation) {
+ vacuumUEs.push(expectation);
+ }
+ }
+ if (vacuumUEs.length === 0) return;
+
+ const allAssociatedEvents = tr.model.getAssociatedEvents(expectations);
+ const unassociatedEvents = tr.model.getUnassociatedEvents(
+ this.model, allAssociatedEvents);
+
+ for (const event of unassociatedEvents) {
+ if (!(event instanceof tr.model.ThreadSlice)) continue;
+
+ if (!event.isTopLevel) continue;
+
+ for (let index = 0; index < vacuumUEs.length; ++index) {
+ const expectation = vacuumUEs[index];
+
+ if ((event.start >= expectation.start) &&
+ (event.start < expectation.end)) {
+ expectation.associatedEvents.addEventSet(event.entireHierarchy);
+ break;
+ }
+ }
+ }
+ }
+
+ // Fill in the empty space between UEs with IdleUEs.
+ findIdleExpectations(otherUEs) {
+ if (this.model.bounds.isEmpty) return;
+
+ const emptyRanges = tr.b.math.findEmptyRangesBetweenRanges(
+ tr.b.math.convertEventsToRanges(otherUEs),
+ this.model.bounds);
+ const expectations = [];
+ const model = this.model;
+ for (const range of emptyRanges) {
+ // Ignore insignificantly tiny idle ranges.
+ if (range.max < (range.min + INSIGNIFICANT_MS)) continue;
+
+ expectations.push(new tr.model.um.IdleExpectation(
+ model, range.min, range.max - range.min));
+ }
+ return expectations;
+ }
+ }
+
+ function createCustomizeModelLinesFromModel(model) {
+ const modelLines = [];
+ modelLines.push(' audits.addEvent(model.browserMain,');
+ modelLines.push(' {title: \'model start\', start: 0, end: 1});');
+
+ const typeNames = {};
+ for (const typeName in tr.e.cc.INPUT_EVENT_TYPE_NAMES) {
+ typeNames[tr.e.cc.INPUT_EVENT_TYPE_NAMES[typeName]] = typeName;
+ }
+
+ let modelEvents = new tr.model.EventSet();
+ for (const ue of model.userModel.expectations) {
+ modelEvents.addEventSet(ue.sourceEvents);
+ }
+ modelEvents = modelEvents.toArray();
+ modelEvents.sort(tr.importer.compareEvents);
+
+ for (const event of modelEvents) {
+ const startAndEnd = 'start: ' + parseInt(event.start) + ', ' +
+ 'end: ' + parseInt(event.end) + '});';
+ if (event instanceof tr.e.cc.InputLatencyAsyncSlice) {
+ modelLines.push(' audits.addInputEvent(model, INPUT_TYPE.' +
+ typeNames[event.typeName] + ',');
+ } else if (event.title === 'RenderFrameImpl::didCommitProvisionalLoad') {
+ modelLines.push(' audits.addCommitLoadEvent(model,');
+ } else if (event.title ===
+ 'InputHandlerProxy::HandleGestureFling::started') {
+ modelLines.push(' audits.addFlingAnimationEvent(model,');
+ } else if (event.title === tr.model.helpers.IMPL_RENDERING_STATS) {
+ modelLines.push(' audits.addFrameEvent(model,');
+ } else if (event.title === tr.importer.CSS_ANIMATION_TITLE) {
+ modelLines.push(' audits.addEvent(model.rendererMain, {');
+ modelLines.push(' title: \'Animation\', ' + startAndEnd);
+ return;
+ } else {
+ throw new Error(
+ 'You must extend createCustomizeModelLinesFromModel()' +
+ 'to support this event:\n' + event.title + '\n');
+ }
+ modelLines.push(' {' + startAndEnd);
+ }
+
+ modelLines.push(' audits.addEvent(model.browserMain,');
+ modelLines.push(' {' +
+ 'title: \'model end\', ' +
+ 'start: ' + (parseInt(model.bounds.max) - 1) + ', ' +
+ 'end: ' + parseInt(model.bounds.max) + '});');
+ return modelLines;
+ }
+
+ function createExpectedUELinesFromModel(model) {
+ const expectedLines = [];
+ const ueCount = model.userModel.expectations.length;
+ for (let index = 0; index < ueCount; ++index) {
+ const expectation = model.userModel.expectations[index];
+ let ueString = ' {';
+ ueString += 'title: \'' + expectation.title + '\', ';
+ ueString += 'start: ' + parseInt(expectation.start) + ', ';
+ ueString += 'end: ' + parseInt(expectation.end) + ', ';
+ ueString += 'eventCount: ' + expectation.sourceEvents.length;
+ ueString += '}';
+ if (index < (ueCount - 1)) ueString += ',';
+ expectedLines.push(ueString);
+ }
+ return expectedLines;
+ }
+
+ function createUEFinderTestCaseStringFromModel(model) {
+ const filename = window.location.hash.substr(1);
+ let testName = filename.substr(filename.lastIndexOf('/') + 1);
+ testName = testName.substr(0, testName.indexOf('.'));
+
+ // createCustomizeModelLinesFromModel() throws an error if there's an
+ // unsupported event.
+ try {
+ const testLines = [];
+ testLines.push(' /*');
+ testLines.push(' This test was generated from');
+ testLines.push(' ' + filename + '');
+ testLines.push(' */');
+ testLines.push(' test(\'' + testName + '\', function() {');
+ testLines.push(' const verifier = new UserExpectationVerifier();');
+ testLines.push(' verifier.customizeModelCallback = function(model) {');
+ testLines.push.apply(testLines,
+ createCustomizeModelLinesFromModel(model));
+ testLines.push(' };');
+ testLines.push(' verifier.expectedUEs = [');
+ testLines.push.apply(testLines, createExpectedUELinesFromModel(model));
+ testLines.push(' ];');
+ testLines.push(' verifier.verify();');
+ testLines.push(' });');
+ return testLines.join('\n');
+ } catch (error) {
+ return error;
+ }
+ }
+
+ return {
+ UserModelBuilder,
+ createUEFinderTestCaseStringFromModel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html
new file mode 100644
index 00000000000..4379316e8ae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html
@@ -0,0 +1,1649 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/importer/user_expectation_verifier.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
+ const ChromeTestUtils = tr.e.chrome.ChromeTestUtils;
+ const UserExpectationVerifier = tr.importer.UserExpectationVerifier;
+
+ function addFrameEventForInput(model, event) {
+ const frame = ChromeTestUtils.addFrameEvent(model,
+ {start: event.start, end: event.end, isTopLevel: true});
+ model.flowEvents.push(tr.c.TestUtils.newFlowEventEx({
+ id: event.id,
+ start: event.start,
+ end: event.end,
+ startSlice: frame,
+ endSlice: frame
+ }));
+ }
+
+ test('empty', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ };
+ verifier.expectedUEs = [];
+ verifier.expectedSegments = [];
+ verifier.verify();
+ });
+
+ test('vrExpectations', function() {
+ const verifier = new UserExpectationVerifier();
+
+ verifier.customizeModelCallback = function(model) {
+ model.gpuProcess = model.getOrCreateProcess(3);
+ model.gpuMain = model.gpuProcess.getOrCreateThread(6);
+ model.gpuMain.name = 'CrGpuMain';
+
+ const series = new tr.model.CounterSeries('gpu.WebVR FPS');
+ series.addCounterSample(0, 1);
+ series.addCounterSample(990, 2);
+ series.addCounterSample(1005, 3);
+ series.addCounterSample(1500, 4);
+ model.gpuProcess.getOrCreateCounter('gpu',
+ 'WebVR FPS').addSeries(series);
+
+ model.gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'VrShellGl::DrawFrame', start: 5, end: 10,
+ type: tr.model.ThreadSlice}));
+ model.gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'VrShellGl::DrawFrame', start: 995, end: 1000,
+ type: tr.model.ThreadSlice}));
+ model.gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'VrShellGl::DrawFrame', start: 1010, end: 1050,
+ type: tr.model.ThreadSlice}));
+ };
+
+ verifier.expectedUEs = [
+ {title: 'VR Response', start: 0, end: 1000, eventCount: 4},
+ {title: 'VR Animation', start: 1000, end: 1500, eventCount: 4},
+ ];
+
+ verifier.expectedSegments = [
+ {start: 0, end: 1000, expectations: [verifier.expectedUEs[0]]},
+ {start: 1000, end: 1500, expectations: [verifier.expectedUEs[1]]},
+ ];
+
+ verifier.verify();
+ });
+
+ test('videoExpectations_gapInMiddle', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 100, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 200, end: 300, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 210, end: 220});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 100, eventCount: 2},
+ {title: 'Idle', start: 100, end: 200, eventCount: 0},
+ {title: 'Video Animation', start: 200, end: 300, eventCount: 2},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('videoExpectations_overlapping', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 200, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 100, end: 300, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 210, end: 220});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 300, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 300, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('videoExpectations_oneInTheOther', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 300, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 100, end: 200, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 110, end: 120});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 300, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 300, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('videoExpectations_dontMergeWithOtherAnimations', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = model => {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 100, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20}),
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 90, end: 190, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 110, end: 120});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 100, eventCount: 2},
+ {title: 'CSS Animation', start: 90, end: 190, eventCount: 2},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 90, expectations: [verifier.expectedUEs[0]]},
+ {
+ start: 90,
+ end: 100,
+ expectations: [verifier.expectedUEs[0], verifier.expectedUEs[1]]
+ },
+ {start: 100, end: 190, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('slowMouseMoveResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_DOWN, {start: 0, end: 10});
+ let mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 10, end: 20, id: '0x100'});
+ addFrameEventForInput(model, mouseMove);
+
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 70, end: 80, id: '0x101'});
+ addFrameEventForInput(model, mouseMove);
+
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 130, end: 140, id: '0x102'});
+ addFrameEventForInput(model, mouseMove);
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 0},
+ {title: 'Mouse Response', start: 10, end: 20, eventCount: 4},
+ {title: 'Idle', start: 20, end: 70, eventCount: 0},
+ {title: 'Mouse Response', start: 70, end: 80, eventCount: 3},
+ {title: 'Idle', start: 80, end: 130, eventCount: 0},
+ {title: 'Mouse Response', start: 130, end: 140, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]},
+ {start: 10, end: 20, expectations: [verifier.expectedUEs[1]]},
+ {start: 20, end: 70, expectations: [verifier.expectedUEs[2]]},
+ {start: 70, end: 80, expectations: [verifier.expectedUEs[3]]},
+ {start: 80, end: 130, expectations: [verifier.expectedUEs[4]]},
+ {start: 130, end: 140, expectations: [verifier.expectedUEs[5]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseEventResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ const mouseDown = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_DOWN, {start: 0, end: 50, id: '0x100'});
+ addFrameEventForInput(model, mouseDown);
+
+ const mouseUp = ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_UP,
+ {start: 50, end: 100, id: '0x101'});
+ addFrameEventForInput(model, mouseUp);
+
+ const mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 200, end: 250, id: '0x102'});
+ addFrameEventForInput(model, mouseMove);
+ };
+ verifier.expectedUEs = [
+ {title: 'Mouse Response', start: 0, end: 50, eventCount: 3},
+ {title: 'Mouse Response', start: 50, end: 100, eventCount: 3},
+ {title: 'Idle', start: 100, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 250, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]},
+ {start: 50, end: 100, expectations: [verifier.expectedUEs[1]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[2]]},
+ {start: 200, end: 250, expectations: [verifier.expectedUEs[3]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseEventsIgnored', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_MOVE,
+ {start: 0, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_DOWN,
+ {start: 50, end: 100});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 0}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('unassociatedEvents', function() {
+ // Unassociated ThreadSlices that start during an Idle should be associated
+ // with it. Expect the IdleExpectation to have 2 associated events: both of
+ // the ThreadSlices in the model.
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ const start = tr.c.TestUtils.newSliceEx(
+ {title: 'model start', start: 0, end: 1, type: tr.model.ThreadSlice});
+ start.isTopLevel = true;
+ model.browserMain.sliceGroup.pushSlice(start);
+
+ const end = tr.c.TestUtils.newSliceEx(
+ {title: 'model end', start: 9, end: 10, type: tr.model.ThreadSlice});
+ end.isTopLevel = true;
+ model.browserMain.sliceGroup.pushSlice(end);
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('stillLoading', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 25, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 11,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 100, end: 130});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 11, eventCount: 1},
+ {title: 'Successful Load', start: 11, end: 130, eventCount: 4}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 11, expectations: [verifier.expectedUEs[0]]},
+ {start: 11, end: 130, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('overlappingIdleAndLoadCollectUnassociatedEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 15, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ ChromeTestUtils.addFrameEvent(model, {start: 20, end: 40});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 20,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 20,
+ duration: 5.0,
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 5000, end: 5050});
+ // 3 Idle events.
+ ChromeTestUtils.addRenderingEvent(model, {start: 5, end: 15});
+ ChromeTestUtils.addRenderingEvent(model, {start: 11, end: 15});
+ ChromeTestUtils.addRenderingEvent(model, {start: 13, end: 15});
+ // 1 Idle event.
+ ChromeTestUtils.addRenderingEvent(model, {start: 5045, end: 5046});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 20, eventCount: 4},
+ {title: 'Successful Load', start: 20, end: 40, eventCount: 4},
+ {title: 'Idle', start: 40, end: 5050, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 40, expectations: [verifier.expectedUEs[1]]},
+ {start: 40, end: 5050, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingFlingFling', function() {
+ // This trace gave me so many different kinds of trouble that I'm just going
+ // to copy it straight in here, without trying to clarify it at all.
+ // measurmt-traces/mobile/cnet_fling_up_fling_down_motox_2013.json
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 919, end: 998});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 919, end: 1001});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 919, end: 1001});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 974, end: 1020});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 974, end: 1020});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 974, end: 1040});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 1039, end: 1040, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 974, end: 1054});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 990, end: 1021});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 990, end: 1052});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 1051, end: 1052, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1006, end: 1021});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1022, end: 1036});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1022, end: 1052});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1038, end: 1049});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1038, end: 1068});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 1067, end: 1068, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 1046, end: 1050});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 1046, end: 1077});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 1432, end: 2238});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 1432, end: 2241});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1516, end: 2605});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 1532, end: 2274});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1532, end: 2294});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 2293, end: 2294, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1549, end: 2310});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 2309, end: 2310, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 1627, end: 2275});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 1627, end: 2310});
+ ChromeTestUtils.addFrameEvent(model, {start: 2990, end: 3000});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 919, eventCount: 1},
+ {title: 'Scroll Response', start: 919, end: 1054,
+ eventCount: 6, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 1054, end: 1068,
+ eventCount: 9},
+ {title: 'Fling Animation', start: 1054, end: 1432,
+ eventCount: 3},
+ {title: 'Scroll Response', start: 1432, end: 2605,
+ eventCount: 5, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 1549, end: 2310,
+ eventCount: 3},
+ {title: 'Fling Animation', start: 2605, end: 3000,
+ eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 919, expectations: [verifier.expectedUEs[0]]},
+ {start: 919, end: 1054, expectations: [verifier.expectedUEs[1]]},
+ {start: 1054, end: 1068, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 1068, end: 1432, expectations: [verifier.expectedUEs[3]]},
+ {start: 1432, end: 1549, expectations: [verifier.expectedUEs[4]]},
+ {start: 1549, end: 2310, expectations: [
+ verifier.expectedUEs[4],
+ verifier.expectedUEs[5],
+ ]},
+ {start: 2310, end: 2605, expectations: [verifier.expectedUEs[4]]},
+ {start: 2605, end: 3000, expectations: [verifier.expectedUEs[6]]},
+ ];
+ verifier.verify();
+ });
+
+ test('keyboardEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.KEY_DOWN_RAW,
+ {start: 0, end: 45});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.CHAR,
+ {start: 10, end: 50});
+ };
+ verifier.expectedUEs = [
+ {title: 'Keyboard Response', start: 0, end: 50, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.CLICK,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.CONTEXT_MENU,
+ {start: 200, end: 300});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 400, end: 500});
+ };
+ verifier.expectedUEs = [
+ {title: 'Mouse Response', start: 0, end: 100, eventCount: 1},
+ {title: 'Idle', start: 100, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 300, eventCount: 1},
+ {title: 'Idle', start: 300, end: 400, eventCount: 0},
+ {title: 'MouseWheel Response', start: 400, end: 500,
+ eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[2]]},
+ {start: 300, end: 400, expectations: [verifier.expectedUEs[3]]},
+ {start: 400, end: 500, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('mouseWheelAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 0, end: 20});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 19, end: 20, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 16, end: 36});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 35, end: 36, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 55, end: 75});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 74, end: 75, isTopLevel: true});
+
+ // This threshold uses both events' start times, not end...start.
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 100, end: 150});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 149, end: 150, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 141, end: 191});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 190, end: 191, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 182, end: 200});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 199, end: 200, isTopLevel: true});
+ };
+ verifier.expectedUEs = [
+ {title: 'MouseWheel Response', start: 0, end: 20, eventCount: 1},
+ {title: 'MouseWheel Animation', start: 20, end: 75,
+ eventCount: 4},
+ {title: 'Idle', start: 75, end: 100, eventCount: 0},
+ {title: 'MouseWheel Response', start: 100, end: 150,
+ eventCount: 1},
+ {title: 'MouseWheel Response', start: 141, end: 191,
+ eventCount: 1},
+ {title: 'MouseWheel Response', start: 182, end: 200,
+ eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 75, expectations: [verifier.expectedUEs[1]]},
+ {start: 75, end: 100, expectations: [verifier.expectedUEs[2]]},
+ {start: 100, end: 141, expectations: [verifier.expectedUEs[3]]},
+ {start: 141, end: 150, expectations: [
+ verifier.expectedUEs[3],
+ verifier.expectedUEs[4],
+ ]},
+ {start: 150, end: 182, expectations: [verifier.expectedUEs[4]]},
+ {start: 182, end: 191, expectations: [
+ verifier.expectedUEs[4],
+ verifier.expectedUEs[5],
+ ]},
+ {start: 191, end: 200, expectations: [verifier.expectedUEs[5]]},
+ ];
+ verifier.verify();
+ });
+
+ test('mouseDownUpResponse', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_DOWN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_UP,
+ {start: 200, end: 210});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 210, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 200, expectations: [verifier.expectedUEs[0]]},
+ {start: 200, end: 210, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('ignoreLoneMouseMoves', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_MOVE,
+ {start: 0, end: 100});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 0}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseDrags', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_DOWN, {start: 0, end: 100});
+ let mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 200, end: 215});
+ addFrameEventForInput(model, mouseMove);
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 210, end: 220});
+ addFrameEventForInput(model, mouseMove);
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 221, end: 240});
+ addFrameEventForInput(model, mouseMove);
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 215, eventCount: 4},
+ {title: 'Mouse Animation', start: 215, end: 240, eventCount: 6}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 200, expectations: [verifier.expectedUEs[0]]},
+ {start: 200, end: 215, expectations: [verifier.expectedUEs[1]]},
+ {start: 215, end: 240, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('twoScrollsNoFling', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 20, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 40, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 60, end: 150});
+ ChromeTestUtils.addFrameEvent(model, {start: 149, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 70, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_END,
+ {start: 80, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 300, end: 400});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 320, end: 400});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 330, end: 450});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 340, end: 450});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 350, end: 500});
+ ChromeTestUtils.addFrameEvent(model, {start: 499, end: 500});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_END,
+ {start: 360, end: 500});
+ };
+ verifier.expectedUEs = [
+ {title: 'Scroll Response', start: 0, end: 100, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 100, end: 150, eventCount: 5},
+ {title: 'Idle', start: 150, end: 300, eventCount: 0},
+ {title: 'Scroll Response', start: 300, end: 400, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 400, end: 500, eventCount: 5}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 150, expectations: [verifier.expectedUEs[1]]},
+ {start: 150, end: 300, expectations: [verifier.expectedUEs[2]]},
+ {start: 300, end: 400, expectations: [verifier.expectedUEs[3]]},
+ {start: 400, end: 500, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_oneAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 18, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 38, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 58, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 62, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 62, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_twoAnimations', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 18, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 38, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 58, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 218, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 220, end: 222});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 238, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 240, end: 242});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 258, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 260, end: 262});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 62, eventCount: 4},
+ {title: 'Idle', start: 62, end: 220, eventCount: 0},
+ {title: 'WebGL Animation', start: 220, end: 262, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 62, expectations: [verifier.expectedUEs[0]]},
+ {start: 62, end: 220, expectations: [verifier.expectedUEs[1]]},
+ {start: 220, end: 262, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_oneWithAnimationEventsOneWithout', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 18, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 38, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 58, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 220, end: 222});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 240, end: 242});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 260, end: 262});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 62, eventCount: 4},
+ {title: 'Idle', start: 62, end: 262, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 62, expectations: [verifier.expectedUEs[0]]},
+ {start: 62, end: 262, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_noAnimationEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 220, end: 222});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 240, end: 242});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 260, end: 262});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 262, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 262, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_animationEventsOnly', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'PageAnimator::serviceScriptedAnimations',
+ start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 40, end: 42});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 42, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 42, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_oneEvent', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 4, end: 6});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 2, eventCount: 1},
+ {title: 'Idle', start: 2, end: 6, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 2, expectations: [verifier.expectedUEs[0]]},
+ {start: 2, end: 6, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('cssAnimations', function() {
+ // CSS Animations happen on the renderer process, not the browser process.
+ // They are merged if they overlap.
+ // They are merged with other kinds of animations.
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 0, end: 130, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'Animation', start: 131, end: 200, isTopLevel: true});
+ ChromeTestUtils.addFrameEvent(model, {start: 150, end: 160});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 150, end: 180});
+ ChromeTestUtils.addFrameEvent(model, {start: 290, end: 300});
+ };
+ verifier.expectedUEs = [
+ {title: 'CSS Animation', start: 0, end: 200, eventCount: 4},
+ {title: 'Fling Animation', start: 150, end: 300, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 150, expectations: [verifier.expectedUEs[0]]},
+ {start: 150, end: 200, expectations: [
+ verifier.expectedUEs[0],
+ verifier.expectedUEs[1],
+ ]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('cssAnimationStatesRunningAtEnd', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ // If a top-level Animation async slice has state-change instant
+ // events and the last one is a "running" event, then it will run
+ // to the end of the top level event.
+ const animationA = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 0, end: 500, isTopLevel: true});
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 100, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 200, args: {data: {state: 'idle'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 300, args: {data: {state: 'running'}}}));
+ model.rendererMain.asyncSliceGroup.push(animationA);
+ ChromeTestUtils.addFrameEvent(model, {start: 50, end: 60});
+ ChromeTestUtils.addFrameEvent(model, {start: 150, end: 160});
+ ChromeTestUtils.addFrameEvent(model, {start: 250, end: 260});
+ ChromeTestUtils.addFrameEvent(model, {start: 350, end: 360});
+ ChromeTestUtils.addFrameEvent(model, {start: 450, end: 460});
+ // We include a frame event off the end of the top level animation slice
+ // so we can test that it correctly stops the AnimationExpectation
+ // at the end of the top-level event, not tne end of the whole trace,
+ ChromeTestUtils.addFrameEvent(model, {start: 1050, end: 1060});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 1},
+ {title: 'CSS Animation', start: 100, end: 200, eventCount: 5},
+ {title: 'Idle', start: 200, end: 300, eventCount: 1},
+ {title: 'CSS Animation', start: 300, end: 500, eventCount: 6},
+ {title: 'Idle', start: 500, end: 1060, eventCount: 1},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[2]]},
+ {start: 300, end: 500, expectations: [verifier.expectedUEs[3]]},
+ {start: 500, end: 1060, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('cssAnimationStates', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ // If a top-level Animation async slice does not have state-change instant
+ // subSlices, then assume that the animation was running throughout the
+ // async slice.
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'Animation', start: 181, end: 250, isTopLevel: true});
+ ChromeTestUtils.addFrameEvent(model, {start: 200, end: 240});
+
+ // Animation ranges should be merged if there is less than 32ms dead time
+ // between them.
+
+ // If a top-level Animation async slice has state-change instant events,
+ // then run a state machine to find the time ranges when the animation was
+ // actually running.
+
+ // This animation was running from 10-40 and 50-60.
+ const animationA = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 50, end: 500, isTopLevel: true});
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 71, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 104, args: {data: {state: 'pending'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 137, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 150, args: {data: {state: 'paused'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 281, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 350, args: {data: {state: 'idle'}}}));
+ model.rendererMain.asyncSliceGroup.push(animationA);
+ ChromeTestUtils.addFrameEvent(model, {start: 80, end: 90});
+ ChromeTestUtils.addFrameEvent(model, {start: 290, end: 300});
+
+ // An animation without a frame event isn't really an animation.
+ const animationC = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 350, end: 382, isTopLevel: true});
+ model.rendererMain.asyncSliceGroup.push(animationC);
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 350, args: {data: {state: 'idle'}}}));
+
+ // This animation was running from model.bounds.min-50 and
+ // 70-model.bounds.max.
+ const animationB = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 0, end: 500, isTopLevel: true});
+ animationB.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 40, args: {data: {state: 'finished'}}}));
+ animationB.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 382, args: {data: {state: 'running'}}}));
+ model.rendererMain.asyncSliceGroup.push(animationB);
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ ChromeTestUtils.addFrameEvent(model, {start: 390, end: 400});
+ };
+ verifier.expectedUEs = [
+ {title: 'CSS Animation', start: 0, end: 350, eventCount: 16},
+ {title: 'Idle', start: 350, end: 382, eventCount: 0},
+ {title: 'CSS Animation', start: 382, end: 500, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 350, expectations: [verifier.expectedUEs[0]]},
+ {start: 350, end: 382, expectations: [verifier.expectedUEs[1]]},
+ {start: 382, end: 500, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingThatIsntstopped', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 32, end: 100});
+ ChromeTestUtils.addFlingAnimationEvent(model, {start: 38, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 290, end: 300});
+ };
+ verifier.expectedUEs = [
+ {title: 'Fling Animation', start: 32, end: 200, eventCount: 3},
+ {title: 'Idle', start: 200, end: 300, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 32, end: 200, expectations: [verifier.expectedUEs[0]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingThatIsStopped', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 32, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 105, end: 150});
+ ChromeTestUtils.addFrameEvent(model, {start: 104, end: 105});
+ ChromeTestUtils.addFrameEvent(model, {start: 149, end: 150});
+ };
+ verifier.expectedUEs = [
+ {title: 'Fling Animation', start: 32, end: 105, eventCount: 3},
+ {title: 'Idle', start: 105, end: 150, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 32, end: 105, expectations: [verifier.expectedUEs[0]]},
+ {start: 105, end: 150, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingFling', function() {
+ // measurmt-traces/mobile/facebook_obama_scroll_dialog_box.html
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 0, end: 30});
+ ChromeTestUtils.addFrameEvent(model, {start: 40, end: 41});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 100, end: 130});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 100, end: 130});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 110, end: 140});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 170, end: 180});
+ ChromeTestUtils.addFrameEvent(model, {start: 150, end: 151});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 200, end: 210});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 200, end: 220});
+ ChromeTestUtils.addFrameEvent(model, {start: 230, end: 240});
+ };
+ verifier.expectedUEs = [
+ {title: 'Fling Animation', start: 0, end: 100, eventCount: 3},
+ {title: 'Touch Response', start: 100, end: 140, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Animation', start: 140, end: 210, eventCount: 3},
+ {title: 'Fling Animation', start: 200, end: 240, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 140, expectations: [verifier.expectedUEs[1]]},
+ {start: 140, end: 200, expectations: [verifier.expectedUEs[2]]},
+ {start: 200, end: 210, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 210, end: 240, expectations: [verifier.expectedUEs[3]]},
+ ];
+ verifier.verify();
+ });
+
+ test('load', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 25, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 0,
+ duration: 5.0,
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 50,
+ duration: 300,
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 7000, end: 7130});
+ };
+
+ verifier.expectedUEs = [
+ {title: 'Successful Load', start: 0, end: 350, eventCount: 4},
+ {title: 'Idle', start: 350, end: 7130, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 350, expectations: [verifier.expectedUEs[0]]},
+ {start: 350, end: 7130, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('loadStartup', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addRenderingEvent(model, {start: 2, end: 3});
+ ChromeTestUtils.addCreateThreadsEvent(model, {start: 5, end: 10});
+ // findStartupExpectations() should ignore subsequent CreateThreads
+ // events.
+ ChromeTestUtils.addCreateThreadsEvent(model, {start: 25, end: 30});
+ ChromeTestUtils.addFrameEvent(model, {start: 11, end: 20});
+ };
+ verifier.expectedUEs = [
+ {title: 'Startup', start: 2, end: 20, eventCount: 3},
+ {title: 'Idle', start: 20, end: 30, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 2, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 30, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('totalIdle', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('multipleIdles', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 25, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 10,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 10,
+ duration: 5.0,
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 50,
+ duration: 300,
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 7000, end: 7130});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 1},
+ {title: 'Successful Load', start: 10, end: 350, eventCount: 5},
+ {title: 'Idle', start: 350, end: 7130, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]},
+ {start: 10, end: 350, expectations: [verifier.expectedUEs[1]]},
+ {start: 350, end: 7130, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('touchStartTouchEndTap', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 200, end: 210});
+ };
+ verifier.expectedUEs = [
+ {title: 'Touch Response', start: 0, end: 210, eventCount: 2,
+ isAnimationBegin: true}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 210, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('touchMoveResponseAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 50, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 70, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 200, end: 300});
+ ChromeTestUtils.addFrameEvent(model, {start: 299, end: 300});
+ };
+ verifier.expectedUEs = [
+ {title: 'Touch Response', start: 0, end: 100, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Animation', start: 100, end: 300, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 300, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP,
+ {start: 0, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 300, end: 310});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP,
+ {start: 320, end: 350});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 50, eventCount: 1},
+ {title: 'Idle', start: 50, end: 300, eventCount: 0},
+ {title: 'Tap Response', start: 300, end: 350, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]},
+ {start: 50, end: 300, expectations: [verifier.expectedUEs[1]]},
+ {start: 300, end: 350, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapAndTapCancelResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 300, end: 350});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 100, eventCount: 1},
+ {title: 'Idle', start: 100, end: 300, eventCount: 0},
+ {title: 'Tap Response', start: 300, end: 350, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 300, expectations: [verifier.expectedUEs[1]]},
+ {start: 300, end: 350, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapCancelResponse', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 150, end: 200});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 200, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 200, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('pinchResponseAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_BEGIN,
+ {start: 100, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 130, end: 160});
+ ChromeTestUtils.addFrameEvent(model, {start: 159, end: 160});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 140, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 150, end: 205});
+ ChromeTestUtils.addFrameEvent(model, {start: 204, end: 205});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 210, end: 220});
+ ChromeTestUtils.addFrameEvent(model, {start: 219, end: 220});
+ // pause > 200ms
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 421, end: 470});
+ ChromeTestUtils.addFrameEvent(model, {start: 469, end: 470});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_END,
+ {start: 460, end: 500});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 1},
+ {title: 'Pinch Response', start: 100, end: 160, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Pinch Animation', start: 160, end: 220, eventCount: 6},
+ {title: 'Idle', start: 220, end: 421, eventCount: 0},
+ {title: 'Pinch Animation', start: 421, end: 500, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 160, expectations: [verifier.expectedUEs[1]]},
+ {start: 160, end: 220, expectations: [verifier.expectedUEs[2]]},
+ {start: 220, end: 421, expectations: [verifier.expectedUEs[3]]},
+ {start: 421, end: 500, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapThenScroll', function() {
+ // measurmt-traces/mobile/google_io_instrument_strumming.json
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 0, end: 20});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 40, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 50, end: 120});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 80, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 180, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ };
+ verifier.expectedUEs = [
+ {title: 'Touch Response', start: 0, end: 100, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Response', start: 50, end: 150, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Animation', start: 150, end: 200, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]},
+ {start: 50, end: 100, expectations: [
+ verifier.expectedUEs[0],
+ verifier.expectedUEs[1],
+ ]},
+ {start: 100, end: 150, expectations: [verifier.expectedUEs[1]]},
+ {start: 150, end: 200, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('pinchFlingTapTouchEventsOverlap', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 20, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 20, end: 30});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 20, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 60, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 60, end: 110});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_BEGIN,
+ {start: 60, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 65, end: 75});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 70, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 70, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 75, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 80, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 85, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 75, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 150, end: 200});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 150, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 180, end: 210});
+ ChromeTestUtils.addFrameEvent(model, {start: 209, end: 210});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 190, end: 210});
+ ChromeTestUtils.addFrameEvent(model, {start: 215, end: 220});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 20, eventCount: 1},
+ {title: 'Pinch Response', start: 20, end: 110,
+ eventCount: 9, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 110, end: 210,
+ eventCount: 7},
+ {title: 'Fling Animation', start: 180, end: 220, eventCount: 4}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 110, expectations: [verifier.expectedUEs[1]]},
+ {start: 110, end: 180, expectations: [verifier.expectedUEs[2]]},
+ {start: 180, end: 210, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 210, end: 220, expectations: [verifier.expectedUEs[3]]},
+ ];
+ verifier.verify();
+ });
+
+ test('scrollThenFling', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 0, end: 40});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 50, end: 100});
+ ChromeTestUtils.addFrameEvent(model, {start: 99, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 80, end: 100});
+ ChromeTestUtils.addFrameEvent(model, {start: 190, end: 200});
+ };
+ verifier.expectedUEs = [
+ {title: 'Scroll Animation', start: 0, end: 100, eventCount: 3},
+ {title: 'Fling Animation', start: 80, end: 200, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 80, expectations: [verifier.expectedUEs[0]]},
+ {start: 80, end: 100, expectations: [
+ verifier.expectedUEs[0],
+ verifier.expectedUEs[1],
+ ]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ /*
+ This test was generated from
+ /test_data/measurmt-traces/mobile/fling_HN_to_rest.json
+ */
+ test('flingHNToRest', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addEvent(model.browserMain,
+ {title: 'model start', start: 0, end: 1});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 1274, end: 1297});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 1274, end: 1305});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1343, end: 1350});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1359, end: 1366});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 1359, end: 1366});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 1359, end: 1367});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1359, end: 1387});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1375, end: 1385});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1375, end: 1416});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1389, end: 1404});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1389, end: 1429});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1405, end: 1418});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1405, end: 1449});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1419, end: 1432});
+ ChromeTestUtils.addFrameEvent(model, {start: 1431, end: 1432});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1419, end: 1474});
+ ChromeTestUtils.addFrameEvent(model, {start: 1473, end: 1474});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 1427, end: 1435});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 1427, end: 1474});
+ ChromeTestUtils.addFlingAnimationEvent(model, {start: 1440, end: 2300});
+ ChromeTestUtils.addEvent(model.browserMain,
+ {title: 'model end', start: 3184, end: 3185});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 1274, eventCount: 0},
+ {title: 'Scroll Response', start: 1274, end: 1387,
+ eventCount: 6, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 1387, end: 1474,
+ eventCount: 12},
+ {title: 'Fling Animation', start: 1427, end: 2300,
+ eventCount: 4},
+ {title: 'Idle', start: 2300, end: 3185, eventCount: 0}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 1274, expectations: [verifier.expectedUEs[0]]},
+ {start: 1274, end: 1387, expectations: [verifier.expectedUEs[1]]},
+ {start: 1387, end: 1427, expectations: [verifier.expectedUEs[2]]},
+ {start: 1427, end: 1474, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 1474, end: 2300, expectations: [verifier.expectedUEs[3]]},
+ {start: 2300, end: 3185, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('TapResponseOverlappingTouchAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 5, end: 15});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 10, end: 20});
+ ChromeTestUtils.addFrameEvent(model, {start: 19, end: 20});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP,
+ {start: 15, end: 100});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 100,
+ eventCount: 5}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('responseFramesNotInScrollAnimation', function() {
+ // fixResponseAnimationStarts in find_input_expectations moves the start of
+ // the Scroll Animation and needs to remove frame events that now lie
+ // out of the Scroll Animation's interval
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 0, end: 20});
+ ChromeTestUtils.addFrameEvent(model, {start: 5, end: 6});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 20, end: 40});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 20, end: 210});
+ // don't associate the follwing event to the Scroll Animation
+ ChromeTestUtils.addFrameEvent(model, {start: 25, end: 26});
+ ChromeTestUtils.addFrameEvent(model, {start: 190, end: 191});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_END,
+ {start: 200, end: 250});
+ };
+ verifier.expectedUEs = [
+ {title: 'Scroll Response', start: 0, end: 40,
+ eventCount: 2, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 40, end: 250, eventCount: 3},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 40, expectations: [verifier.expectedUEs[0]]},
+ {start: 40, end: 250, expectations: [verifier.expectedUEs[1]]}
+ ];
+ verifier.verify();
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/index.js b/chromium/third_party/catapult/tracing/tracing/index.js
new file mode 100644
index 00000000000..e9e5967c518
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/index.js
@@ -0,0 +1,24 @@
+// Copyright (c) 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.
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+const catapultPath = fs.realpathSync(path.join(__dirname, '..', '..'));
+const catapultBuildPath = path.join(catapultPath, 'catapult_build');
+
+const nodeBootstrap = require(path.join(
+ catapultBuildPath, 'node_bootstrap.js'));
+
+HTMLImportsLoader.addArrayToSourcePath(
+ nodeBootstrap.getSourcePathsForProject('tracing'));
+
+// Go!
+HTMLImportsLoader.loadHTML('/tracing/importer/import.html');
+HTMLImportsLoader.loadHTML('/tracing/model/model.html');
+HTMLImportsLoader.loadHTML('/tracing/extras/full_config.html');
+
+// Make the tracing namespace the main tracing export.
+module.exports = global.tr;
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/__init__.py b/chromium/third_party/catapult/tracing/tracing/metrics/__init__.py
new file mode 100644
index 00000000000..cffcee63c42
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/__init__.py
@@ -0,0 +1,15 @@
+# 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 os
+import sys
+
+
+_CATAPULT_DIR = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), '..', '..', '..'))
+
+_PI_PATH = os.path.join(_CATAPULT_DIR, 'perf_insights')
+
+if _PI_PATH not in sys.path:
+ sys.path.insert(1, _PI_PATH)
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html
new file mode 100644
index 00000000000..43c6b393dbe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics', function() {
+ function accessibilityMetric(histograms, model) {
+ const browserAccessibilityEventsHist = new tr.v.Histogram(
+ 'browser_accessibility_events',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);
+ browserAccessibilityEventsHist.description =
+ 'Browser accessibility events time';
+
+ const renderAccessibilityEventsHist = new tr.v.Histogram(
+ 'render_accessibility_events',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);
+ renderAccessibilityEventsHist.description =
+ 'Render accessibility events time';
+
+ const renderAccessibilityLocationsHist = new tr.v.Histogram(
+ 'render_accessibility_locations',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter);
+ renderAccessibilityLocationsHist.description =
+ 'Render accessibility locations time';
+
+ 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;
+
+ for (const slice of mainThread.getDescendantEvents()) {
+ if (!(slice instanceof tr.model.ThreadSlice)) continue;
+
+ if (slice.title ===
+ 'RenderAccessibilityImpl::SendPendingAccessibilityEvents') {
+ renderAccessibilityEventsHist.addSample(slice.duration,
+ {event: new tr.v.d.RelatedEventSet(slice)});
+ }
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html
new file mode 100644
index 00000000000..27dbf178637
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/accessibility_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function makeTestModel() {
+ return tr.c.TestUtils.newModel(function(model) {
+ const browserProcess = model.getOrCreateProcess(99);
+ browserProcess.name = 'Browser';
+ const browserThread = browserProcess.getOrCreateThread(2);
+ browserThread.name = 'CrBrowserMain';
+ browserThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'accessibility',
+ title: 'BrowserAccessibilityManager::OnAccessibilityEvents',
+ start: 1000,
+ dur: 71,
+ end: 1071,
+ }));
+ browserThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'accessibility',
+ title: 'BrowserAccessibilityManager::OnAccessibilityEvents',
+ start: 1500,
+ dur: 22,
+ end: 1522,
+ }));
+
+ const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ renderThread.name = 'CrRendererMain';
+ renderThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'accessibility',
+ title: 'RenderAccessibilityImpl::SendPendingAccessibilityEvents',
+ start: 800,
+ dur: 228,
+ end: 1028,
+ }));
+ renderThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'accessibility',
+ title: 'RenderAccessibilityImpl::SendLocationChanges',
+ start: 900,
+ dur: 12,
+ end: 912,
+ }));
+ renderThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'accessibility',
+ title: 'RenderAccessibilityImpl::SendPendingAccessibilityEvents',
+ start: 1400,
+ dur: 188,
+ end: 1588,
+ }));
+ });
+ }
+
+ test('accessibilityMetric', function() {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.accessibilityMetric(histograms, makeTestModel());
+ assert.closeTo(93, histograms.getHistogramNamed(
+ 'browser_accessibility_events').sum, 1e-2);
+ assert.closeTo(416, histograms.getHistogramNamed(
+ 'render_accessibility_events').sum, 1e-2);
+ assert.closeTo(12, histograms.getHistogramNamed(
+ 'render_accessibility_locations').sum, 1e-2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html b/chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html
new file mode 100644
index 00000000000..be843ee9aca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<!-- We import all files that register fixed color schemes which are used by
+ metrics so that metrics code can depend on a single place. -->
+<link rel="import" href="/tracing/extras/chrome/chrome_processes.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html b/chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html
new file mode 100644
index 00000000000..68c1b2f34ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/accessibility_metric.html">
+<link rel="import" href="/tracing/metrics/android_startup_metric.html">
+<link rel="import" href="/tracing/metrics/android_systrace_metric.html">
+<link rel="import" href="/tracing/metrics/blink/gc_metric.html">
+<link rel="import" href="/tracing/metrics/blink/leak_detection_metric.html">
+<link rel="import" href="/tracing/metrics/console_error_metric.html">
+<link rel="import" href="/tracing/metrics/cpu_process_metric.html">
+<link rel="import" href="/tracing/metrics/media_metric.html">
+<link rel="import" href="/tracing/metrics/rendering/rendering_metric.html">
+<link rel="import" href="/tracing/metrics/sample_exception_metric.html">
+<link rel="import" href="/tracing/metrics/sample_metric.html">
+<link rel="import" href="/tracing/metrics/spa_navigation_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/clock_sync_latency_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/cpu_time_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/expected_queueing_time_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/limited_cpu_time_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/long_tasks_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/memory_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/new_cpu_time_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/power_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/responsiveness_metric.html">
+<link rel="import" href="/tracing/metrics/system_health/webview_startup_metric.html">
+<link rel="import" href="/tracing/metrics/tabs_metric.html">
+<link rel="import" href="/tracing/metrics/tracing_metric.html">
+<link rel="import" href="/tracing/metrics/v8/execution_metric.html">
+<link rel="import" href="/tracing/metrics/v8/gc_metric.html">
+<link rel="import" href="/tracing/metrics/v8/runtime_stats_metric.html">
+<link rel="import" href="/tracing/metrics/v8/v8_metrics.html">
+<link rel="import" href="/tracing/metrics/vr/frame_cycle_duration_metric.html">
+<link rel="import" href="/tracing/metrics/vr/webvr_metric.html">
+<link rel="import" href="/tracing/metrics/webrtc/webrtc_rendering_metric.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html
new file mode 100644
index 00000000000..4b6bc49ee68
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+// The |androidStartupMetric| produces metrics that start counting at the
+// earliest moment the Chrome code on Android is executed.
+// A few histograms are produced with the names as described below:
+// 1. messageloop_start_time - time till the message loop of the browser main
+// starts processing posted tasts (after having loaded the Profile)
+// 2. first_contentful_paint_time - time to the first contentful paint of the
+// page loaded at startup
+// The metric also supports multiple browser restarts, in this case multiple
+// samples would be added to the histograms above.
+tr.exportTo('tr.metrics.sh', function() {
+ const MESSAGE_LOOP_EVENT_NAME =
+ 'Startup.BrowserMessageLoopStartTimeFromMainEntry3';
+ const FIRST_CONTENTFUL_PAINT_EVENT_NAME = 'firstContentfulPaint';
+ function androidStartupMetric(histograms, model) {
+ // Walk the browser slices, extract timestamps for the browser start,
+ // message loop start. TODO(crbug.com/883290): re-introduce
+ // request_start_time.
+ 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);
+ }
+ }
+ }
+
+ // Walk the renderer slices and extract the 'first contentful paint'
+ // histogram samples.
+ 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);
+ // There are usually several 'First Contentful Paint' events recorded
+ // for each page load. Take only the first one per renderer.
+ break;
+ }
+ }
+ }
+
+ // Fallback to scanning all processes if important events are not found.
+ let totalBrowserStarts = messageLoopStartEvents.length;
+ let totalFcpEvents = firstContentfulPaintEvents.length;
+ if (totalFcpEvents !== totalBrowserStarts || totalBrowserStarts === 0) {
+ messageLoopStartEvents = [];
+ firstContentfulPaintEvents = [];
+ // Sometimes either the browser process or the renderer process does not
+ // have the proper name attached. This often happens when both chrome
+ // trace and systrace are merged. Other multi-process trickery, like
+ // Perfetto, may also cause this.
+ for (const proc of Object.values(model.processes)) {
+ for (const ev of proc.getDescendantEvents()) {
+ if (ev.title === MESSAGE_LOOP_EVENT_NAME) {
+ messageLoopStartEvents.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;
+ }
+
+ // Sometimes a number of early trace events are not recorded because tracing
+ // takes time to start. This leads to having more FCP events than
+ // messageloop_start events. As a workaround ignore the FCP events for which
+ // there are no browser starts.
+ function orderEvents(event1, event2) {
+ return event1.start - event2.start;
+ }
+ messageLoopStartEvents.sort(orderEvents);
+ firstContentfulPaintEvents.sort(orderEvents);
+
+ if (totalFcpEvents < totalBrowserStarts) {
+ throw new Error('Found less FCP events (' + totalFcpEvents +
+ ') than browser starts (' + totalBrowserStarts + ')');
+ }
+
+ // Group the relevant events with the corresponding browser starts and emit
+ // the metrics.
+ 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, []);
+ // The earliest browser start is skipped because it is affected by the state
+ // of the system coming from the time before the benchmark started. Removing
+ // these influencing factors allows reducing measurement noise.
+ // Note: Two early starts are ignored below, the reasons for spurious
+ // slowdowns of the 2nd run are not known yet, see http://crbug.com/891797.
+ let fcpIndex = 0;
+ for (let loopStartIndex = 0; loopStartIndex < totalBrowserStarts;) {
+ const startEvent = messageLoopStartEvents[loopStartIndex];
+ if (fcpIndex === totalFcpEvents) {
+ break;
+ }
+
+ // Skip all FCP events that appear before the next browser start.
+ const fcpEvent = firstContentfulPaintEvents[fcpIndex];
+ if (fcpEvent.start < startEvent.start) {
+ fcpIndex++;
+ continue;
+ }
+
+ // The pair of matching events is found.
+ loopStartIndex++;
+
+ // Skip the two initial FCP events and (potentially missing) browser
+ // starts.
+ if (fcpIndex < 2) {
+ continue;
+ }
+
+ // Record the histograms.
+ messageLoopStartHistogram.addSample(startEvent.duration,
+ {events: new tr.v.d.RelatedEventSet([startEvent])});
+ firstContentfulPaintHistogram.addSample(
+ fcpEvent.end - startEvent.start,
+ {events: new tr.v.d.RelatedEventSet([startEvent, fcpEvent])});
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(androidStartupMetric);
+
+ return {
+ androidStartupMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html
new file mode 100644
index 00000000000..5ebf601fec6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/android_startup_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBrowserThread(model) {
+ const browserProcess = model.getOrCreateProcess(tr.b.GUID.allocateSimple());
+ const mainThread = browserProcess.getOrCreateThread(
+ tr.b.GUID.allocateSimple());
+ // Initializing the thread name helps passing validation checks made by the
+ // ChromeModelHelper.
+ mainThread.name = 'CrBrowserMain';
+ return mainThread;
+ }
+
+ function createRendererThread(model) {
+ const rendererProcess = model.getOrCreateProcess(
+ tr.b.GUID.allocateSimple());
+ const rendererMainThread =
+ rendererProcess.getOrCreateThread(tr.b.GUID.allocateSimple());
+ rendererMainThread.name = 'CrRendererMain';
+ return rendererMainThread;
+ }
+
+ // Adds a browser and renderer to the process, with a few key events necessary
+ // to calculate the |androidStartupMetric|. An |offset| can be added to all
+ // events and the length of a few events can be extended by
+ // |incrementForMetrics|.
+ function fillModelWithOneBrowserSession(model, offset, incrementForMetrics) {
+ // In order for the tests below to succeed with strictEqual, the floating
+ // point values should have exact representation as IEEE754 float.
+ createBrowserThread(model).asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'startup',
+ title: 'Startup.BrowserMessageLoopStartTimeFromMainEntry3',
+ start: (offset + 6800.125),
+ duration: (incrementForMetrics + 1700.0625)}));
+ const rendererMainThread = createRendererThread(model);
+ rendererMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: (offset + 8400.125 + incrementForMetrics),
+ duration: 0.0,
+ args: {frame: '0x0'}}));
+
+ // Add an extra FCP event in the same renderer process appearing after the
+ // initial FCP even to check that it is ignored by metric computations.
+ rendererMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: (offset + 8400.125 + incrementForMetrics + 0.125),
+ duration: 0.0,
+ args: {frame: '0x0'}}));
+ }
+
+ // Adds early messageloop and FCP events. The metric should ignore these very
+ // first messageloop start and FCP events in the trace. The specific lengths
+ // are not important.
+ function addEarlyEventsToBeIgnored(model, offset) {
+ createBrowserThread(model).asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'startup',
+ title: 'Startup.BrowserMessageLoopStartTimeFromMainEntry3',
+ start: (offset + 1.0),
+ duration: 10.0}));
+ createRendererThread(model).sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: (offset + 2.0),
+ duration: 1.0,
+ args: {frame: '0x0'}}));
+ }
+
+ function makeTestModel(offset, incrementForMetrics) {
+ return tr.c.TestUtils.newModel(function(model) {
+ fillModelWithOneBrowserSession(model, offset, incrementForMetrics);
+ addEarlyEventsToBeIgnored(model, offset);
+ addEarlyEventsToBeIgnored(model, offset + 20.0);
+ });
+ }
+
+ // Checks recording of the main histograms in the simplest case.
+ test('androidStartupMetric_simple', function() {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidStartupMetric(histograms, makeTestModel(0.0, 0.0));
+ const messageLoopStartHistogram = histograms.getHistogramNamed(
+ 'messageloop_start_time');
+ assert.strictEqual(1, messageLoopStartHistogram.numValues);
+ assert.strictEqual(1700.0625, messageLoopStartHistogram.average);
+ const firstContentfulPaintHistogram = histograms.getHistogramNamed(
+ 'first_contentful_paint_time');
+ assert.strictEqual(1, firstContentfulPaintHistogram.numValues);
+ assert.strictEqual(1600.0, firstContentfulPaintHistogram.average);
+ });
+
+ // Emulates loss of the initial message loop start event. Checks that this
+ // event is ignored and the |androidStartupMetric| does not crash.
+ test('androidStartupMetric_missingOneBrowserStart', function() {
+ function makeTestModelWithOneEventMissing() {
+ return tr.c.TestUtils.newModel(function(model) {
+ fillModelWithOneBrowserSession(model, 0.0, 0.0);
+ // Note: the initial Startup.BrowserMessageLoopStartTimeFromMainEntry3'
+ // is intentionally missing.
+ createRendererThread(model).sliceGroup.pushSlice(
+ tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 2.0,
+ duration: 1.0,
+ args: {frame: '0x0'}}));
+ createBrowserThread(model).asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'startup',
+ title: 'Startup.BrowserMessageLoopStartTimeFromMainEntry3',
+ start: (20.0 + 1.0),
+ duration: 10.0}));
+ createRendererThread(model).sliceGroup.pushSlice(
+ tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: (20.0 + 2.0),
+ duration: 1.0,
+ args: {frame: '0x0'}}));
+ });
+ }
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidStartupMetric(histograms,
+ makeTestModelWithOneEventMissing(0.0));
+ const messageLoopStartHistogram = histograms.getHistogramNamed(
+ 'messageloop_start_time');
+ assert.strictEqual(1, messageLoopStartHistogram.numValues);
+ assert.strictEqual(1700.0625, messageLoopStartHistogram.average);
+ const firstContentfulPaintHistogram = histograms.getHistogramNamed(
+ 'first_contentful_paint_time');
+ assert.strictEqual(1, firstContentfulPaintHistogram.numValues);
+ assert.strictEqual(1600.0, firstContentfulPaintHistogram.average);
+ });
+
+ // Checks the metrics after adding an offset to events in the model, and
+ // making a few durations longer by a constant.
+ test('androidStartupMetric_withOffsetAndLongerTask', function() {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidStartupMetric(histograms, makeTestModel(5.0, 7.0));
+ const messageLoopStartHistogram = histograms.getHistogramNamed(
+ 'messageloop_start_time');
+ assert.strictEqual(1, messageLoopStartHistogram.numValues);
+ assert.strictEqual(1707.0625, messageLoopStartHistogram.average);
+ const firstContentfulPaintHistogram = histograms.getHistogramNamed(
+ 'first_contentful_paint_time');
+ assert.strictEqual(1, firstContentfulPaintHistogram.numValues);
+ assert.strictEqual(1607.0, firstContentfulPaintHistogram.average);
+ });
+
+ test('androidStartupMetric_twoSessions', function() {
+ function makeTestModelWithTwoSessionsOneDelayed(
+ offset, incrementForMetrics) {
+ return tr.c.TestUtils.newModel(function(model) {
+ fillModelWithOneBrowserSession(model, 0.0, 0.0);
+ fillModelWithOneBrowserSession(model, offset, incrementForMetrics);
+ addEarlyEventsToBeIgnored(model, 0.0, 0.0);
+ addEarlyEventsToBeIgnored(model, 0.0, 1.0);
+ });
+ }
+ const delta = 0.125;
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidStartupMetric(histograms,
+ makeTestModelWithTwoSessionsOneDelayed(10000.0, delta));
+ const messageLoopStartHistogram = histograms.getHistogramNamed(
+ 'messageloop_start_time');
+ assert.strictEqual(2, messageLoopStartHistogram.numValues);
+ assert.strictEqual(1700.0625, messageLoopStartHistogram.min);
+ assert.strictEqual(1700.0625 + delta, messageLoopStartHistogram.max);
+ const firstContentfulPaintHistogram = histograms.getHistogramNamed(
+ 'first_contentful_paint_time');
+ assert.strictEqual(2, firstContentfulPaintHistogram.numValues);
+ assert.strictEqual(1600.0, firstContentfulPaintHistogram.min);
+ assert.strictEqual(1600.0 + delta, firstContentfulPaintHistogram.max);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html
new file mode 100644
index 00000000000..96c1ca0295c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html
@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ const MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS = 2000;
+ // Post-startup activity draw delay.
+ 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);
+ }
+ }
+ return threads;
+ }
+
+ function findUIThread(process) {
+ if (process === undefined) return undefined;
+ const threads = findThreads(process, 'UI Thread');
+ if (threads !== undefined && threads.length === 1) {
+ return threads[0];
+ }
+ return process.threads[process.pid];
+ }
+
+ // Returns slices with actual app's process startup, excluding other delays.
+ function findLaunchSlices(model) {
+ const launches = [];
+ const binders = findThreads(findProcess('system_server', model), 'Binder');
+ for (const binderId in binders) {
+ const binder = binders[binderId];
+ for (const sliceId in binder.asyncSliceGroup.slices) {
+ const slice = binder.asyncSliceGroup.slices[sliceId];
+ if (slice.title.startsWith('launching:')) {
+ launches.push(slice);
+ }
+ }
+ }
+ return launches;
+ }
+
+ // Try to find draw event when activity just shown.
+ function findDrawSlice(appName, startNotBefore, model) {
+ let drawSlice = undefined;
+ const thread = findUIThread(findProcess(appName, model));
+ if (thread === undefined) return undefined;
+
+ for (const sliceId in thread.sliceGroup.slices) {
+ const slice = thread.sliceGroup.slices[sliceId];
+ if (slice.start < startNotBefore + MIN_DRAW_DELAY_IN_MS ||
+ slice.start > startNotBefore + MAX_DRAW_DELAY_IN_MS) continue;
+ if (slice.title !== 'draw') continue;
+ // TODO(kraynov): Add reportFullyDrawn() support.
+ if (drawSlice === undefined || slice.start < drawSlice.start) {
+ drawSlice = slice;
+ }
+ }
+ return drawSlice;
+ }
+
+ // Try to find input event before a process starts.
+ function findInputEventSlice(endNotAfter, model) {
+ const endNotBefore = endNotAfter - MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS;
+ let inputSlice = undefined;
+ const systemUi = findUIThread(findProcess('com.android.systemui', model));
+ if (systemUi === undefined) return undefined;
+
+ for (const sliceId in systemUi.asyncSliceGroup.slices) {
+ const slice = systemUi.asyncSliceGroup.slices[sliceId];
+ if (slice.end > endNotAfter || slice.end < endNotBefore) continue;
+ if (slice.title !== 'deliverInputEvent') continue;
+ if (inputSlice === undefined || slice.end > inputSlice.end) {
+ inputSlice = slice;
+ }
+ }
+ return inputSlice;
+ }
+
+ function computeStartupTimeInMs(appName, launchSlice, model) {
+ let startupStart = launchSlice.start;
+ let startupEnd = launchSlice.end;
+ const drawSlice = findDrawSlice(appName, launchSlice.end, model);
+ if (drawSlice !== undefined) {
+ startupEnd = drawSlice.end;
+ }
+ const inputSlice = findInputEventSlice(launchSlice.start, model);
+ if (inputSlice !== undefined) {
+ startupStart = inputSlice.start;
+ }
+ return startupEnd - startupStart;
+ }
+
+ // App startup time metric.
+ function measureStartup(histograms, model) {
+ const launches = findLaunchSlices(model);
+ for (const sliceId in launches) {
+ const launchSlice = launches[sliceId];
+ const appName = launchSlice.title.split(': ')[1];
+ const startupMs = computeStartupTimeInMs(appName, launchSlice, model);
+ histograms.createHistogram(`android:systrace:startup:${appName}`,
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, startupMs);
+ }
+ }
+
+ // Metric which measures time spent by process threads in each thread state.
+ // The value of metric is a time percentage relative to the length of selected
+ // range of interest.
+ function measureThreadStates(histograms, model, rangeOfInterest) {
+ for (const pid in model.processes) {
+ const process = model.processes[pid];
+ if (process.name === undefined) continue;
+
+ let hasSlices = false;
+ let timeRunning = 0;
+ let timeRunnable = 0;
+ let timeSleeping = 0;
+ let timeUninterruptible = 0;
+ let timeBlockIO = 0;
+ let timeUnknown = 0;
+
+ for (const tid in process.threads) {
+ const thread = process.threads[tid];
+ if (thread.timeSlices === undefined) continue;
+
+ for (const sliceId in thread.timeSlices) {
+ const slice = thread.timeSlices[sliceId];
+ const sliceRange =
+ tr.b.math.Range.fromExplicitRange(slice.start, slice.end);
+ const intersection = rangeOfInterest.findIntersection(sliceRange);
+ const duration = intersection.duration;
+ if (duration === 0) continue;
+ hasSlices = true;
+
+ if (slice.title === 'Running') {
+ timeRunning += duration;
+ } else if (slice.title === 'Runnable') {
+ timeRunnable += duration;
+ } else if (slice.title === 'Sleeping') {
+ timeSleeping += duration;
+ } else if (slice.title.startsWith('Uninterruptible')) {
+ timeUninterruptible += duration;
+ if (slice.title.includes('Block I/O')) timeBlockIO += duration;
+ } else {
+ timeUnknown += duration;
+ }
+ }
+ }
+
+ if (hasSlices) {
+ // For sake of simplicity we don't count wall time for each
+ // thread/process and just calculate relative values against selected
+ // range of interest.
+ const wall = rangeOfInterest.max - rangeOfInterest.min;
+ histograms.createHistogram(
+ `android:systrace:threadtime:${process.name}:running`,
+ tr.b.Unit.byName.normalizedPercentage, timeRunning / wall);
+ histograms.createHistogram(
+ `android:systrace:threadtime:${process.name}:runnable`,
+ tr.b.Unit.byName.normalizedPercentage, timeRunnable / wall);
+ histograms.createHistogram(
+ `android:systrace:threadtime:${process.name}:sleeping`,
+ tr.b.Unit.byName.normalizedPercentage, timeSleeping / wall);
+ histograms.createHistogram(
+ `android:systrace:threadtime:${process.name}:blockio`,
+ tr.b.Unit.byName.normalizedPercentage, timeBlockIO / wall);
+ histograms.createHistogram(
+ `android:systrace:threadtime:${process.name}:uninterruptible`,
+ tr.b.Unit.byName.normalizedPercentage, timeUninterruptible / wall);
+
+ // In case of changing names in systrace and importer.
+ if (timeUnknown > 0) {
+ histograms.createHistogram(
+ `android:systrace:threadtime:${process.name}:unknown`,
+ tr.b.Unit.byName.normalizedPercentage, timeUnknown / wall);
+ }
+ }
+ }
+ }
+
+ function androidSystraceMetric(histograms, model, options) {
+ let rangeOfInterest = model.bounds;
+ if (options !== undefined && options.rangeOfInterest !== undefined) {
+ rangeOfInterest = options.rangeOfInterest;
+ }
+
+ measureStartup(histograms, model);
+ measureThreadStates(histograms, model, rangeOfInterest);
+ }
+
+ tr.metrics.MetricRegistry.register(androidSystraceMetric, {
+ supportsRangeOfInterest: true
+ });
+
+ return {
+ androidSystraceMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html
new file mode 100644
index 00000000000..76936416503
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/android_systrace_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SYSTRACE_CLOCK_SYNC =
+ '<100>-100 (-----) [000] ...1 0.000000: tracing_mark_write: ' +
+ 'trace_event_clock_sync: parent_ts=0\n' +
+ '<100>-100 (-----) [000] ...1 0.000000: tracing_mark_write: ' +
+ 'trace_event_clock_sync: realtime_ts=1487002000000';
+
+ // Some event is required to help importer detect a parent process.
+ const SYSTRACE_SYSTEM_SERVER_ANNOTATION =
+ 'system_server-101 ( 101) [000] ...1 0.550000: ' +
+ 'tracing_mark_write: S|101|dummyEvent|201\n' +
+ 'system_server-101 ( 101) [000] ...1 0.551000: ' +
+ 'tracing_mark_write: F|101|dummyEvent|201';
+
+ const SYSTRACE_TOUCH_SLICE =
+ 'com.android.systemui-102 ( 102) [000] ...1 2.500000: ' +
+ 'tracing_mark_write: S|102|deliverInputEvent|202\n' +
+ 'com.android.systemui-102 ( 102) [000] ...1 2.510000: ' +
+ 'tracing_mark_write: F|102|deliverInputEvent|202';
+
+ const SYSTRACE_LAUNCH_SLICE =
+ 'Binder:101_C-103 ( 101) [000] ...1 2.750000: ' +
+ 'tracing_mark_write: S|101|launching: com.android.apps.sms|203\n' +
+ 'android.display-104 ( 101) [000] ...1 4.250000: ' +
+ 'tracing_mark_write: F|101|launching: com.android.apps.sms|203';
+
+ const SYSTRACE_DRAW_SLICE =
+ 'com.android.apps.sms-105 ( 105) [000] ...1 4.450000: ' +
+ 'tracing_mark_write: B|105|draw\n' +
+ 'com.android.apps.sms-105 ( 105) [000] ...1 4.455000: ' +
+ 'tracing_mark_write: E';
+
+ function makeModel(systraceLines) {
+ const events = JSON.stringify({
+ traceEvents: [],
+ systemTraceEvents: SYSTRACE_CLOCK_SYNC + '\n' + systraceLines.join('\n')
+ });
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ // Fix missing process names.
+ for (const pid in model.processes) {
+ const process = model.processes[pid];
+ if (process.name !== undefined) continue;
+ if (process.threads[pid] !== undefined) {
+ process.name = process.threads[pid].name;
+ }
+ }
+ return model;
+ }
+
+ function testStartup(systrace, expectedTimeInMs) {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidSystraceMetric(histograms, makeModel(systrace));
+ assert.lengthOf(histograms, 1);
+ const startupHist = histograms.getHistogramNamed(
+ 'android:systrace:startup:com.android.apps.sms').running;
+ assert.strictEqual(startupHist.count, 1);
+ assert.closeTo(startupHist.mean, expectedTimeInMs, 1e-5);
+ }
+
+ test('androidSystraceMetric_startup_noData', function() {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidSystraceMetric(histograms, makeModel([]));
+ assert.lengthOf(histograms, 0);
+ });
+
+ test('androidSystraceMetric_startup_simple', function() {
+ const systrace = [
+ SYSTRACE_SYSTEM_SERVER_ANNOTATION,
+ SYSTRACE_TOUCH_SLICE,
+ SYSTRACE_LAUNCH_SLICE,
+ SYSTRACE_DRAW_SLICE
+ ];
+ testStartup(systrace, 1955);
+ });
+
+ test('androidSystraceMetric_startup_noTouch', function() {
+ const systrace = [
+ SYSTRACE_SYSTEM_SERVER_ANNOTATION,
+ SYSTRACE_LAUNCH_SLICE,
+ SYSTRACE_DRAW_SLICE
+ ];
+ testStartup(systrace, 1705);
+ });
+
+ test('androidSystraceMetric_startup_noDraw', function() {
+ const systrace = [
+ SYSTRACE_SYSTEM_SERVER_ANNOTATION,
+ SYSTRACE_TOUCH_SLICE,
+ SYSTRACE_LAUNCH_SLICE,
+ ];
+ testStartup(systrace, 1750);
+ });
+
+ test('androidSystraceMetric_startup_noTouchNoDraw', function() {
+ const systrace = [
+ SYSTRACE_SYSTEM_SERVER_ANNOTATION,
+ SYSTRACE_LAUNCH_SLICE,
+ ];
+ testStartup(systrace, 1500);
+ });
+
+ test('androidSystraceMetric_threadtime_simple', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ const process = model.getOrCreateProcess(42);
+ process.name = 'garbage_producer';
+ const thread = process.getOrCreateThread(42);
+ thread.timeSlices = [
+ tr.c.TestUtils.newThreadSlice(thread, 'Sleeping', 0, 100),
+ tr.c.TestUtils.newThreadSlice(thread, 'Running', 100, 400)
+ ];
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.androidSystraceMetric(histograms, model);
+ assert.lengthOf(histograms, 5);
+
+ const assertHistValue = function(name, expectedValue) {
+ const hist = histograms.getHistogramNamed(
+ `android:systrace:threadtime:garbage_producer:${name}`);
+ assert.strictEqual(hist.running.count, 1);
+ assert.closeTo(hist.running.mean, expectedValue, 1e-5);
+ };
+ assertHistValue('running', 0.8);
+ assertHistValue('runnable', 0);
+ assertHistValue('sleeping', 0.2);
+ assertHistValue('blockio', 0);
+ assertHistValue('uninterruptible', 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html
new file mode 100644
index 00000000000..641c8bd0c9f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html
@@ -0,0 +1,265 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/v8/utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.blink', function() {
+ // Maps the Blink GC events in timeline to telemetry friendly names.
+ 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);
+ }
+
+ 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;
+
+ // 0.1 steps from 0 to 20 since it is the most common range.
+ // Exponentially increasing steps from 20 to 200.
+ 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;
+ }
+
+ /**
+ * Example output:
+ * - blink-gc-atomic-phase
+ */
+ 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);
+ }
+ );
+ }
+
+ /**
+ * Example output:
+ * - blink-gc-total_sum
+ */
+ 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);
+ }
+ );
+ }
+
+ /**
+ * Example output:
+ * - blink-gc-atomic-phase_idle_deadline_overrun,
+ * - blink-gc-atomic-phase_outside_idle,
+ * - blink-gc-atomic-phase_percentage_idle.
+ */
+ function addIdleTimesOfTopEvents(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ isNonForcedBlinkGarbageCollectionEvent,
+ blinkGarbageCollectionEventName,
+ function(name, events) {
+ addIdleTimes(histograms, model, name, events);
+ }
+ );
+ }
+
+ /**
+ * Example output:
+ * - blink-gc-total_idle_deadline_overrun,
+ * - blink-gc-total_outside_idle,
+ * - blink-gc-total_percentage_idle.
+ */
+ 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;
+ // Don't count time over the deadline as being inside idle time.
+ // Since the deadline should be relative to wall clock we
+ // compare allotted_time_ms with wall duration instead of thread
+ // duration, and then assume the thread duration was inside idle
+ // for the same percentage of time.
+ 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 isV8OrBlinkTopLevelGarbageCollectionEvent(event) {
+ return tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent(event) ||
+ isNonNestedNonForcedBlinkGarbageCollectionEvent(event);
+ }
+
+ /**
+ * Example output:
+ * - unified-gc-total_sum
+ * - unified-gc-total_max
+ * - unified-gc-total_count
+ */
+ 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);
+ }
+ );
+ }
+
+ return {
+ blinkGcMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html
new file mode 100644
index 00000000000..924527f126b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html
@@ -0,0 +1,347 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/blink/gc_metric.html">
+<link rel="import" href="/tracing/metrics/v8/utils.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel(start, end, slices) {
+ const opts = {
+ customizeModelCallback(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(2);
+ const group = thread.sliceGroup;
+ slices.forEach(function(slice) {
+ group.pushSlice(tr.c.TestUtils.newSliceEx(slice));
+ });
+ group.createSubSlices();
+ }
+ };
+ const model = tr.c.TestUtils.newModelWithEvents([], opts);
+ return model;
+ }
+
+ function constructName(name, suffix) {
+ return name + '_' + suffix;
+ }
+
+ function run(slices) {
+ const histograms = new tr.v.HistogramSet();
+ const startTime = slices.reduce(
+ (acc, slice) => (Math.min(acc, slice.start)));
+ const endTime = slices.reduce((acc, slice) => (Math.max(acc, slice.end)));
+ const model = createModel(startTime - 1, endTime + 1, slices);
+ tr.metrics.blink.blinkGcMetric(histograms, model);
+ return histograms;
+ }
+
+ test('topEvents', function() {
+ const events = {
+ 'BlinkGC.AtomicPhase': 'blink-gc-atomic-phase',
+ 'BlinkGC.CompleteSweep': 'blink-gc-complete-sweep',
+ 'BlinkGC.LazySweepInIdle': 'blink-gc-lazy-sweep-idle'
+ };
+ for (const [timelineName, telemetryName] of Object.entries(events)) {
+ const slices = [
+ {
+ title: timelineName, args: {}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ }
+ ];
+ const actual = run(slices);
+
+ let value = actual.getHistogramNamed(telemetryName);
+ assert.strictEqual(value.running.sum, 100);
+ assert.strictEqual(value.numValues, 1);
+ assert.strictEqual(value.average, 100);
+ assert.strictEqual(value.running.max, 100);
+ assert.closeTo(value.getApproximatePercentile(0.90), 100, 1);
+
+ value = actual.getHistogramNamed(
+ `${telemetryName}_idle_deadline_overrun`);
+ assert.strictEqual(value.running.sum, 0);
+ assert.strictEqual(value.numValues, 1);
+ assert.strictEqual(value.average, 0);
+ assert.strictEqual(value.running.max, 0);
+
+ value = actual.getHistogramNamed(`${telemetryName}_outside_idle`);
+ assert.strictEqual(value.running.sum, 100);
+ assert.strictEqual(value.numValues, 1);
+ assert.strictEqual(value.average, 100);
+
+ value = actual.getHistogramNamed(`${telemetryName}_percentage_idle`);
+ assert.strictEqual(value.average, 0);
+ }
+ });
+
+ test('idleTimes', function() {
+ const histograms = new tr.v.HistogramSet();
+ const slices = [
+ {
+ title: 'SingleThreadIdleTaskRunner::RunTask',
+ args: {'allotted_time_ms': 100}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ },
+ {
+ title: 'BlinkGC.AtomicPhase', args: {}, start: 110, end: 190,
+ cpuStart: 110, cpuEnd: 190
+ }
+ ];
+ const actual = run(slices);
+
+ let value = actual.getHistogramNamed('blink-gc-atomic-phase');
+ assert.strictEqual(value.running.sum, 80);
+ assert.strictEqual(value.numValues, 1);
+ assert.strictEqual(value.average, 80);
+ assert.strictEqual(value.running.max, 80);
+
+ value = actual.getHistogramNamed(
+ 'blink-gc-atomic-phase_idle_deadline_overrun');
+ assert.strictEqual(value.running.sum, 0);
+ assert.strictEqual(value.average, 0);
+ assert.strictEqual(value.running.max, 0);
+
+ value = actual.getHistogramNamed('blink-gc-atomic-phase_outside_idle');
+ assert.strictEqual(value.running.sum, 0);
+ assert.strictEqual(value.average, 0);
+ assert.strictEqual(value.running.max, 0);
+
+ value = actual.getHistogramNamed('blink-gc-atomic-phase_percentage_idle');
+ assert.strictEqual(value.average, 1);
+ });
+
+ test('idleTimeOverrun', function() {
+ const histograms = new tr.v.HistogramSet();
+ const slices = [
+ {
+ title: 'SingleThreadIdleTaskRunner::RunTask',
+ args: {'allotted_time_ms': 10}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ },
+ {
+ title: 'BlinkGC.AtomicPhase', args: {}, start: 110, end: 190,
+ cpuStart: 110, cpuEnd: 190
+ }
+ ];
+ const actual = run(slices);
+
+ let value = actual.getHistogramNamed('blink-gc-atomic-phase');
+ assert.strictEqual(value.running.sum, 80);
+ assert.strictEqual(value.numValues, 1);
+ assert.strictEqual(value.average, 80);
+ assert.strictEqual(value.running.max, 80);
+
+ value = actual.getHistogramNamed(
+ 'blink-gc-atomic-phase_idle_deadline_overrun');
+ assert.strictEqual(value.running.sum, 70);
+ assert.strictEqual(value.average, 70);
+ assert.strictEqual(value.running.max, 70);
+
+ value = actual.getHistogramNamed('blink-gc-atomic-phase_outside_idle');
+ assert.strictEqual(value.running.sum, 70);
+ assert.strictEqual(value.average, 70);
+ assert.strictEqual(value.running.max, 70);
+
+ value = actual.getHistogramNamed('blink-gc-atomic-phase_percentage_idle');
+ assert.closeTo(value.average, 1 / 8, 1e-6);
+ });
+
+ test('totalTimeForBlinkGC', function() {
+ const histograms = new tr.v.HistogramSet();
+ const slices = [
+ {
+ title: 'BlinkGC.AtomicPhase', args: {}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ },
+ {
+ title: 'BlinkGC.LazySweepInIdle', args: {}, start: 210,
+ end: 290, cpuStart: 210, cpuEnd: 290
+ }
+ ];
+ const actual = run(slices);
+
+ let value = actual.getHistogramNamed('blink-gc-total');
+ assert.strictEqual(value.running.sum, 180);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 90);
+ assert.strictEqual(value.running.max, 100);
+
+ value = actual.getHistogramNamed('blink-gc-total_idle_deadline_overrun');
+ assert.strictEqual(value.running.sum, 0);
+ assert.strictEqual(value.average, 0);
+ assert.strictEqual(value.running.max, 0);
+
+ value = actual.getHistogramNamed('blink-gc-total_outside_idle');
+ assert.strictEqual(value.running.sum, 180);
+ assert.strictEqual(value.average, 90);
+ assert.strictEqual(value.running.max, 100);
+
+ value = actual.getHistogramNamed('blink-gc-total_percentage_idle');
+ assert.strictEqual(value.average, 0);
+ });
+
+ test('totalTimeForUnifiedGC', function() {
+ const histograms = new tr.v.HistogramSet();
+ const slices = [
+ {
+ title: 'V8.GCFinalizeMC', args: {},
+ start: 100, end: 300, cpuStart: 100, cpuEnd: 300
+ },
+ {
+ title: 'BlinkGC.AtomicPhase', args: {},
+ start: 310, end: 410, cpuStart: 310, cpuEnd: 410
+ }
+ ];
+ const actual = run(slices);
+
+ const value = actual.getHistogramNamed('unified-gc-total');
+ assert.strictEqual(value.running.sum, 300);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 150);
+ assert.strictEqual(value.running.max, 200);
+ });
+
+ test('totalTimeForUnifiedGCBlinkNestedInV8', function() {
+ // Nested Blink GC in V8 top-level GC can happen during unified garbage
+ // collection, or when callbacks that trigger e.g. sweeping are fired
+ // from V8's GC. These should only be accounted once.
+ const histograms = new tr.v.HistogramSet();
+ const slices = [
+ {
+ title: 'V8.GCFinalizeMC', args: {},
+ start: 100, end: 300, cpuStart: 100, cpuEnd: 300
+ },
+ // Nested events should be ignored.
+ {
+ title: 'BlinkGC.CompleteSweep', args: {},
+ start: 200, end: 270, cpuStart: 200, cpuEnd: 270
+ },
+ {
+ title: 'BlinkGC.IncrementalMarkingStartMarking', args: {},
+ start: 280, end: 290, cpuStart: 280, cpuEnd: 290
+ },
+ // Next event is outside of nesting and should be accounted for.
+ {
+ title: 'BlinkGC.IncrementalMarkingStartMarking', args: {},
+ start: 310, end: 320, cpuStart: 310, cpuEnd: 320
+ },
+ ];
+ const actual = run(slices);
+
+ const value = actual.getHistogramNamed('unified-gc-total');
+ assert.strictEqual(value.running.sum, 210);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 105);
+ assert.strictEqual(value.running.max, 200);
+ });
+
+ function getSlicesWithForcedV8GCs() {
+ return [
+ {
+ title: tr.metrics.v8.utils.forcedGCEventName(), args: {},
+ start: 100, end: 300, cpuStart: 100, cpuEnd: 300
+ },
+ // Following nested events should be ignored.
+ {
+ title: 'V8.GCFinalizeMC', args: {},
+ start: 100, end: 300, cpuStart: 100, cpuEnd: 300
+ },
+ {
+ title: 'BlinkGC.CompleteSweep', args: {},
+ start: 200, end: 270, cpuStart: 200, cpuEnd: 270
+ },
+ {
+ title: 'BlinkGC.IncrementalMarkingStartMarking', args: {},
+ start: 280, end: 290, cpuStart: 280, cpuEnd: 290
+ },
+ // Next event happens after the forced GC and should be accounted for.
+ {
+ title: 'BlinkGC.IncrementalMarkingStartMarking', args: {},
+ start: 310, end: 320, cpuStart: 310, cpuEnd: 320
+ },
+ {
+ title: 'BlinkGC.AtomicPhase', args: {'forced': false},
+ start: 320, end: 330, cpuStart: 320, cpuEnd: 330
+ },
+ ];
+ }
+
+ test('ignoreForcedV8GCEventsForUnifiedMetric', function() {
+ // Any events nested in a forced GC should be ignored.
+ const histograms = new tr.v.HistogramSet();
+ const actual = run(getSlicesWithForcedV8GCs());
+ const value = actual.getHistogramNamed('unified-gc-total');
+ assert.strictEqual(value.running.sum, 20);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 10);
+ assert.strictEqual(value.running.max, 10);
+ });
+
+ test('ignoreForcedV8GCEventsForBlinkMetric', function() {
+ // Any events nested in a forced GC should be ignored.
+ const histograms = new tr.v.HistogramSet();
+ const actual = run(getSlicesWithForcedV8GCs());
+ const value = actual.getHistogramNamed('blink-gc-total');
+ assert.strictEqual(value.running.sum, 20);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 10);
+ assert.strictEqual(value.running.max, 10);
+ });
+
+ function getSlicesWithForcedBlinkGCs() {
+ return [
+ // Following nested events should be ignored.
+ {
+ title: 'BlinkGC.CompleteSweep', args: {'forced': true},
+ start: 200, end: 270, cpuStart: 200, cpuEnd: 270
+ },
+ {
+ title: 'BlinkGC.AtomicPhase', args: {'forced': true},
+ start: 280, end: 290, cpuStart: 280, cpuEnd: 290
+ },
+ // Next events are not forced and should be accounted for.
+ {
+ title: 'BlinkGC.AtomicPhase', args: {},
+ start: 310, end: 320, cpuStart: 310, cpuEnd: 320
+ },
+ {
+ title: 'BlinkGC.AtomicPhase', args: {'forced': false},
+ start: 320, end: 330, cpuStart: 320, cpuEnd: 330
+ },
+ ];
+ }
+
+ test('ignoreForcedBlinkGCEventsForUnifiedMetric', function() {
+ // Any forced Blink GC events should be ignored.
+ const histograms = new tr.v.HistogramSet();
+ const actual = run(getSlicesWithForcedBlinkGCs());
+ const value = actual.getHistogramNamed('unified-gc-total');
+ assert.strictEqual(value.running.sum, 20);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 10);
+ assert.strictEqual(value.running.max, 10);
+ });
+
+ test('ignoreForcedBlinkGCEventsForBlinkMetric', function() {
+ // Any forced Blink GC events should be ignored.
+ const histograms = new tr.v.HistogramSet();
+ const actual = run(getSlicesWithForcedBlinkGCs());
+ const value = actual.getHistogramNamed('blink-gc-total');
+ assert.strictEqual(value.running.sum, 20);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 10);
+ assert.strictEqual(value.running.max, 10);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html
new file mode 100644
index 00000000000..432b41d5d73
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.blink', function() {
+ function leakDetectionMetric(histograms, model) {
+ // Extract renderer pids.
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (modelHelper === undefined) {
+ throw new Error('Chrome is not present.');
+ }
+ const rendererHelpers = modelHelper.rendererHelpers;
+ if (Object.keys(rendererHelpers).length === 0) {
+ throw new Error('Renderer process is not present.');
+ }
+ const pids = Object.keys(rendererHelpers);
+
+ // Get the dumps.
+ const chromeDumps = tr.metrics.sh
+ .splitGlobalDumpsByBrowserName(model, undefined).get('chrome');
+
+ const sumCounter = new Map();
+ // Add up counters for all the renderer processes.
+
+ for (const pid of pids) {
+ for (const [key, count] of countLeakedBlinkObjects(chromeDumps, pid)) {
+ sumCounter.set(key, (sumCounter.get(key) || 0) + count);
+ }
+ }
+
+ for (const [key, count] of sumCounter) {
+ histograms.createHistogram('Leaked ' + key,
+ tr.b.Unit.byName.count_smallerIsBetter, count);
+ }
+
+ for (const [key, count] of sumCounter) {
+ if (count > 0) {
+ throw new Error('Memory leak is found.');
+ }
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(leakDetectionMetric);
+
+ function countLeakedBlinkObjects(dumps, pid) {
+ if (dumps === undefined || dumps.length < 2) {
+ throw new Error('Expected at least two memory dumps.');
+ }
+ const firstCounter = countBlinkObjects(dumps[0], pid);
+ const lastCounter = countBlinkObjects(dumps[dumps.length - 1], pid);
+ const diffCounter = new Map();
+ for (const [key, lastCount] of lastCounter) {
+ diffCounter.set(key, lastCount - firstCounter.get(key));
+ }
+ return diffCounter;
+ }
+
+ function countBlinkObjects(dump, pid) {
+ const counter = new Map();
+ const processesMemoryDumps = dump.processMemoryDumps;
+ if (processesMemoryDumps[pid] === undefined) return counter;
+ const blinkObjectsDump = processesMemoryDumps[pid].memoryAllocatorDumps
+ .find(dump => dump.fullName === 'blink_objects');
+ for (const v of blinkObjectsDump.children) {
+ counter.set(v.name, v.numerics.object_count.value);
+ }
+ return counter;
+ }
+
+ return {
+ leakDetectionMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html
new file mode 100644
index 00000000000..f5b2e4add39
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/blink/leak_detection_metric.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const BLINK_OBJECT_LIST = ['AudioHandler', 'Document', 'Frame',
+ 'JSEventListener', 'LayoutObject', 'MediaKeys', 'MediaKeySession', 'Node',
+ 'Resource', 'ScriptPromise', 'PausableObject', 'V8PerContextData',
+ 'WorkerGlobalScope'];
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const allZeroArray = new Array(BLINK_OBJECT_LIST.length).fill(0);
+ const oneLeakArray = new Array(BLINK_OBJECT_LIST.length).fill(0);
+ oneLeakArray[1] = 1;
+ const multipleLeaksArray = new Array(BLINK_OBJECT_LIST.length).fill(1);
+
+ function createProcessWithName(model, name) {
+ const uniquePid =
+ Math.max.apply(null, Object.keys(model.processes).concat([0])) + 1;
+ const process = model.getOrCreateProcess(uniquePid);
+ process.name = name;
+ process.getOrCreateThread(1).name = 'Cr' + name + 'Main';
+ return process;
+ }
+
+ function createTimestamp(model, browser, rendererValuePairs, timestamp) {
+ const gmd1 = addGlobalMemoryDump(model, {ts: timestamp});
+ const pmdBrowser1 = addProcessMemoryDump(gmd1, browser, {ts: timestamp});
+ for (const pair of rendererValuePairs) {
+ addDumpsToRenderer(gmd1, pair.renderer, pair.values, timestamp);
+ }
+ }
+
+ function addDumpsToRenderer(gmd, renderer, values, timestamp) {
+ const pmdRenderer = addProcessMemoryDump(gmd, renderer, {ts: timestamp});
+ pmdRenderer.memoryAllocatorDumps = [
+ newAllocatorDump(pmdRenderer, 'blink_objects', { children:
+ createBlinkObjectCountList(pmdRenderer, values)})];
+ }
+
+ function getNumericLeakCount(histograms, index) {
+ return histograms.getHistogramNamed('Leaked ' +
+ BLINK_OBJECT_LIST[index]).statisticsScalars.get('sum').value;
+ }
+
+ function createBlinkObjectCountList(renderer, values) {
+ const blinkObjectCountList = [];
+ for (let i = 0; i < values.length; i++) {
+ blinkObjectCountList.push(newAllocatorDump(renderer,
+ 'blink_objects/' + BLINK_OBJECT_LIST[i], { numerics:
+ { object_count: values[i] }}));
+ }
+ return blinkObjectCountList;
+ }
+
+ test('testNoRenderer', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ createProcessWithName(model, 'Browser');
+ });
+ const histograms = new tr.v.HistogramSet();
+ assert.throws(
+ function() {tr.metrics.blink.leakDetectionMetric(histograms, model);},
+ 'Renderer process is not present.');
+ });
+
+ test('testZeroDump', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ createProcessWithName(model, 'Browser');
+ createProcessWithName(model, 'Renderer');
+ });
+ const histograms = new tr.v.HistogramSet();
+ assert.throws(
+ function() {tr.metrics.blink.leakDetectionMetric(histograms, model);},
+ 'Expected at least two memory dumps.');
+ });
+
+ test('testOnlyOneDump', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const browser = createProcessWithName(model, 'Browser');
+ const renderer = createProcessWithName(model, 'Renderer');
+ const pair = [{renderer, values: allZeroArray}];
+ createTimestamp(model, browser, pair, 40);
+ });
+ const histograms = new tr.v.HistogramSet();
+ assert.throws(
+ function() {tr.metrics.blink.leakDetectionMetric(histograms, model);},
+ 'Expected at least two memory dumps.');
+ });
+
+ test('testNoLeak', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const browser = createProcessWithName(model, 'Browser');
+ const renderer = createProcessWithName(model, 'Renderer');
+ const pair = [{renderer, 'values': allZeroArray}];
+ createTimestamp(model, browser, pair, 20);
+ createTimestamp(model, browser, pair, 40);
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.blink.leakDetectionMetric(histograms, model);
+ for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) {
+ assert.strictEqual(getNumericLeakCount(histograms, i), 0);
+ }
+ });
+
+ test('testOneLeakDetection', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const browser = createProcessWithName(model, 'Browser');
+ const renderer = createProcessWithName(model, 'Renderer');
+ const pair1 = [{renderer, 'values': allZeroArray}];
+ const pair2 = [{renderer, 'values': oneLeakArray}];
+ createTimestamp(model, browser, pair1, 20);
+ createTimestamp(model, browser, pair2, 40);
+ });
+ const histograms = new tr.v.HistogramSet();
+ assert.throws(
+ function() {tr.metrics.blink.leakDetectionMetric(histograms, model);},
+ 'Memory leak is found.');
+ for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) {
+ if (i === 1) {
+ assert.strictEqual(getNumericLeakCount(histograms, i), 1);
+ } else {
+ assert.strictEqual(getNumericLeakCount(histograms, i), 0);
+ }
+ }
+ });
+
+ test('testMultipleLeakDetections', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const browser = createProcessWithName(model, 'Browser');
+ const renderer = createProcessWithName(model, 'Renderer');
+ const pair1 = [{renderer, 'values': allZeroArray}];
+ const pair2 = [{renderer, 'values': multipleLeaksArray}];
+ createTimestamp(model, browser, pair1, 20);
+ createTimestamp(model, browser, pair2, 40);
+ });
+ const histograms = new tr.v.HistogramSet();
+ assert.throws(
+ function() {tr.metrics.blink.leakDetectionMetric(histograms, model);},
+ 'Memory leak is found.');
+ for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) {
+ assert.strictEqual(getNumericLeakCount(histograms, i), 1);
+ }
+ });
+
+ test('testMultipleRendererWithLeaks', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const browser = createProcessWithName(model, 'Browser');
+ const renderer1 = createProcessWithName(model, 'Renderer');
+ const renderer2 = createProcessWithName(model, 'Renderer');
+ const pair1 = [{'renderer': renderer1, 'values': allZeroArray},
+ {'renderer': renderer2, 'values': allZeroArray}];
+ const pair2 = [{'renderer': renderer1, 'values': multipleLeaksArray},
+ {'renderer': renderer2, 'values': multipleLeaksArray}];
+ createTimestamp(model, browser, pair1, 20);
+ createTimestamp(model, browser, pair2, 40);
+ });
+ const histograms = new tr.v.HistogramSet();
+ assert.throws(
+ function() {tr.metrics.blink.leakDetectionMetric(histograms, model);},
+ 'Memory leak is found.');
+ for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) {
+ assert.strictEqual(getNumericLeakCount(histograms, i), 2);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt b/chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt
new file mode 100644
index 00000000000..a8d19745dae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt
@@ -0,0 +1,187 @@
+IMPORTANT DEBUGGING NOTE: batches of tests are run inside their
+own process. For debugging a test inside a debugger, use the
+--gtest_filter=<your_test_name> flag along with
+--single-process-tests.
+Using sharding settings from environment. This is shard 0/1
+Using 1 parallel jobs.
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always
+[ RUN ] IndexDataManagerPerfTest.Run
+*RESULT IndexDataManger_run: score= 281471 score
+[ OK ] IndexDataManagerPerfTest.Run (5008 ms)
+[1/37] IndexDataManagerPerfTest.Run (5008 ms)
+[ RUN ] BufferSubDataBenchmark.Run/d3d11_float4_every1
+*RESULT BufferSubData_d3d11_float4_every1: score= 232 score
+[ OK ] BufferSubDataBenchmark.Run/d3d11_float4_every1 (5117 ms)
+[2/37] BufferSubDataBenchmark.Run/d3d11_float4_every1 (5117 ms)
+[ RUN ] BufferSubDataBenchmark.Run/d3d9_float4_every1
+*RESULT BufferSubData_d3d9_float4_every1: score= 237 score
+[ OK ] BufferSubDataBenchmark.Run/d3d9_float4_every1 (5386 ms)
+[3/37] BufferSubDataBenchmark.Run/d3d9_float4_every1 (5386 ms)
+[ RUN ] BufferSubDataBenchmark.Run/gl_float4_every1
+*RESULT BufferSubData_gl_float4_every1: score= 245 score
+[ OK ] BufferSubDataBenchmark.Run/gl_float4_every1 (5195 ms)
+[4/37] BufferSubDataBenchmark.Run/gl_float4_every1 (5195 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/d3d9
+*RESULT DrawCallPerf_d3d9: score= 2993 score
+[ OK ] DrawCallPerfBenchmark.Run/d3d9 (10062 ms)
+[5/37] DrawCallPerfBenchmark.Run/d3d9 (10062 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/d3d9_null
+*RESULT DrawCallPerf_d3d9_null: score= 25046 score
+[ OK ] DrawCallPerfBenchmark.Run/d3d9_null (10047 ms)
+[6/37] DrawCallPerfBenchmark.Run/d3d9_null (10047 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/d3d11
+*RESULT DrawCallPerf_d3d11: score= 2741 score
+[ OK ] DrawCallPerfBenchmark.Run/d3d11 (10015 ms)
+[7/37] DrawCallPerfBenchmark.Run/d3d11 (10015 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/d3d11_null
+*RESULT DrawCallPerf_d3d11_null: score= 28607 score
+[ OK ] DrawCallPerfBenchmark.Run/d3d11_null (9999 ms)
+[8/37] DrawCallPerfBenchmark.Run/d3d11_null (9999 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/d3d11_render_to_texture_null
+*RESULT DrawCallPerf_d3d11_render_to_texture_null: score= 25868 score
+[ OK ] DrawCallPerfBenchmark.Run/d3d11_render_to_texture_null (10015 ms)
+[9/37] DrawCallPerfBenchmark.Run/d3d11_render_to_texture_null (10015 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/gl
+*RESULT DrawCallPerf_gl: score= 4123 score
+[ OK ] DrawCallPerfBenchmark.Run/gl (10031 ms)
+[10/37] DrawCallPerfBenchmark.Run/gl (10031 ms)
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always
+[ RUN ] DrawCallPerfBenchmark.Run/gl_null
+*RESULT DrawCallPerf_gl_null: score= 189804 score
+[ OK ] DrawCallPerfBenchmark.Run/gl_null (10015 ms)
+[11/37] DrawCallPerfBenchmark.Run/gl_null (10015 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/gl_render_to_texture_null
+*RESULT DrawCallPerf_gl_render_to_texture_null: score= 189155 score
+[ OK ] DrawCallPerfBenchmark.Run/gl_render_to_texture_null (10031 ms)
+[12/37] DrawCallPerfBenchmark.Run/gl_render_to_texture_null (10031 ms)
+[ RUN ] DrawCallPerfBenchmark.Run/default_validation_only
+*RESULT DrawCallPerf_default_validation_only: score= 2690 score
+[ OK ] DrawCallPerfBenchmark.Run/default_validation_only (5023 ms)
+[13/37] DrawCallPerfBenchmark.Run/default_validation_only (5023 ms)
+[ RUN ] DynamicPromotionPerfTest.Run/d3d11
+*RESULT DynamicPromotion_d3d11: score= 39354 score
+[ OK ] DynamicPromotionPerfTest.Run/d3d11 (6552 ms)
+[14/37] DynamicPromotionPerfTest.Run/d3d11 (6552 ms)
+[ RUN ] DynamicPromotionPerfTest.Run/d3d9
+*RESULT DynamicPromotion_d3d9: score= 21060 score
+[ OK ] DynamicPromotionPerfTest.Run/d3d9 (5522 ms)
+[15/37] DynamicPromotionPerfTest.Run/d3d9 (5522 ms)
+[ RUN ] EGLInitializePerfTest.Run/ES2_D3D11
+*RESULT EGLInitialize_run: score= 155 score
+*RESULT EGLInitialize_run: LoadDLLs= 0.0000000000 ms
+*RESULT EGLInitialize_run: D3D11CreateDevice= 4.0051480051 ms
+*RESULT EGLInitialize_run: InitResources= 0.0000000000 ms
+[ OK ] EGLInitializePerfTest.Run/ES2_D3D11 (5008 ms)
+[16/37] EGLInitializePerfTest.Run/ES2_D3D11 (5008 ms)
+[ RUN ] IndexConversionPerfTest.Run/d3d11
+*RESULT IndexConversionPerfTest_d3d11: score= 5135 score
+[ OK ] IndexConversionPerfTest.Run/d3d11 (3166 ms)
+[17/37] IndexConversionPerfTest.Run/d3d11 (3166 ms)
+[ RUN ] IndexConversionPerfTest.Run/index_range_d3d11
+*RESULT IndexConversionPerfTest_index_range_d3d11: score= 68785 score
+[ OK ] IndexConversionPerfTest.Run/index_range_d3d11 (3011 ms)
+[18/37] IndexConversionPerfTest.Run/index_range_d3d11 (3011 ms)
+[ RUN ] InstancingPerfBenchmark.Run/d3d11
+*RESULT InstancingPerf_d3d11: score= 479 score
+[ OK ] InstancingPerfBenchmark.Run/d3d11 (10046 ms)
+[19/37] InstancingPerfBenchmark.Run/d3d11 (10046 ms)
+[ RUN ] InstancingPerfBenchmark.Run/d3d9
+*RESULT InstancingPerf_d3d9: score= 464 score
+[ OK ] InstancingPerfBenchmark.Run/d3d9 (10057 ms)
+[20/37] InstancingPerfBenchmark.Run/d3d9 (10057 ms)
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D44.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_12336\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D44.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_12336\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D44.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_12336\test_results.xml" --test-launcher-print-test-stdio=always
+[ RUN ] InstancingPerfBenchmark.Run/gl
+*RESULT InstancingPerf_gl: score= 427 score
+[ OK ] InstancingPerfBenchmark.Run/gl (10124 ms)
+[21/37] InstancingPerfBenchmark.Run/gl (10124 ms)
+[ RUN ] InterleavedAttributeDataBenchmark.Run/d3d11
+*RESULT InterleavedAttributeData_d3d11: score= 19 score
+[ OK ] InterleavedAttributeDataBenchmark.Run/d3d11 (5117 ms)
+[22/37] InterleavedAttributeDataBenchmark.Run/d3d11 (5117 ms)
+[ RUN ] InterleavedAttributeDataBenchmark.Run/d3d11_9_3
+*RESULT InterleavedAttributeData_d3d11: score= 25 score
+[ OK ] InterleavedAttributeDataBenchmark.Run/d3d11_9_3 (5225 ms)
+[23/37] InterleavedAttributeDataBenchmark.Run/d3d11_9_3 (5225 ms)
+[ RUN ] InterleavedAttributeDataBenchmark.Run/d3d9
+*RESULT InterleavedAttributeData_d3d9: score= 25 score
+[ OK ] InterleavedAttributeDataBenchmark.Run/d3d9 (5211 ms)
+[24/37] InterleavedAttributeDataBenchmark.Run/d3d9 (5211 ms)
+[ RUN ] InterleavedAttributeDataBenchmark.Run/gl
+*RESULT InterleavedAttributeData_gl: score= 25 score
+[ OK ] InterleavedAttributeDataBenchmark.Run/gl (5101 ms)
+[25/37] InterleavedAttributeDataBenchmark.Run/gl (5101 ms)
+[ RUN ] PointSpritesBenchmark.Run/d3d11_10_3px_3vars
+*RESULT PointSprites_d3d11_10_3px_3vars: score= 644 score
+[ OK ] PointSpritesBenchmark.Run/d3d11_10_3px_3vars (5023 ms)
+[26/37] PointSpritesBenchmark.Run/d3d11_10_3px_3vars (5023 ms)
+[ RUN ] PointSpritesBenchmark.Run/d3d9_10_3px_3vars
+*RESULT PointSprites_d3d9_10_3px_3vars: score= 730 score
+[ OK ] PointSpritesBenchmark.Run/d3d9_10_3px_3vars (5141 ms)
+[27/37] PointSpritesBenchmark.Run/d3d9_10_3px_3vars (5141 ms)
+[ RUN ] PointSpritesBenchmark.Run/gl_10_3px_3vars
+*RESULT PointSprites_gl_10_3px_3vars: score= 2159 score
+[ OK ] PointSpritesBenchmark.Run/gl_10_3px_3vars (5086 ms)
+[28/37] PointSpritesBenchmark.Run/gl_10_3px_3vars (5086 ms)
+[ RUN ] TexSubImageBenchmark.Run/d3d11
+*RESULT TexSubImage_d3d11: score= 294 score
+[ OK ] TexSubImageBenchmark.Run/d3d11 (5023 ms)
+[29/37] TexSubImageBenchmark.Run/d3d11 (5023 ms)
+[ RUN ] TexSubImageBenchmark.Run/d3d9
+*RESULT TexSubImage_d3d9: score= 298 score
+[ OK ] TexSubImageBenchmark.Run/d3d9 (5305 ms)
+[30/37] TexSubImageBenchmark.Run/d3d9 (5305 ms)
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D45.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_4706\test_results.xml" --test-launcher-print-test-stdio=always
+Still waiting for the following processes to finish:
+ "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D45.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_4706\test_results.xml" --test-launcher-print-test-stdio=always
+[ RUN ] TexSubImageBenchmark.Run/gl
+*RESULT TexSubImage_gl: score= 323 score
+[ OK ] TexSubImageBenchmark.Run/gl (5070 ms)
+[31/37] TexSubImageBenchmark.Run/gl (5070 ms)
+[ RUN ] TextureSamplingBenchmark.Run/d3d11_2samplers
+*RESULT TextureSampling_d3d11_2samplers: score= 128 score
+[ OK ] TextureSamplingBenchmark.Run/d3d11_2samplers (5070 ms)
+[32/37] TextureSamplingBenchmark.Run/d3d11_2samplers (5070 ms)
+[ RUN ] TextureSamplingBenchmark.Run/d3d9_2samplers
+*RESULT TextureSampling_d3d9_2samplers: score= 128 score
+[ OK ] TextureSamplingBenchmark.Run/d3d9_2samplers (5668 ms)
+[33/37] TextureSamplingBenchmark.Run/d3d9_2samplers (5668 ms)
+[ RUN ] TextureSamplingBenchmark.Run/gl_2samplers
+*RESULT TextureSampling_gl_2samplers: score= 136 score
+[ OK ] TextureSamplingBenchmark.Run/gl_2samplers (5054 ms)
+[34/37] TextureSamplingBenchmark.Run/gl_2samplers (5054 ms)
+[ RUN ] UniformsBenchmark.Run/d3d11_200_vertex_uniforms_200_fragment_uniforms
+*RESULT Uniforms_d3d11_200_vertex_uniforms_200_fragment_uniforms: score= 1797 score
+[ OK ] UniformsBenchmark.Run/d3d11_200_vertex_uniforms_200_fragment_uniforms (5195 ms)
+[35/37] UniformsBenchmark.Run/d3d11_200_vertex_uniforms_200_fragment_uniforms (5195 ms)
+[ RUN ] UniformsBenchmark.Run/d3d9_200_vertex_uniforms_200_fragment_uniforms
+*RESULT Uniforms_d3d9_200_vertex_uniforms_200_fragment_uniforms: score= 1912 score
+[ OK ] UniformsBenchmark.Run/d3d9_200_vertex_uniforms_200_fragment_uniforms (5223 ms)
+[36/37] UniformsBenchmark.Run/d3d9_200_vertex_uniforms_200_fragment_uniforms (5223 ms)
+[ RUN ] UniformsBenchmark.Run/gl_200_vertex_uniforms_200_fragment_uniforms
+*RESULT Uniforms_gl_200_vertex_uniforms_200_fragment_uniforms: score= 5509 score
+[ OK ] UniformsBenchmark.Run/gl_200_vertex_uniforms_200_fragment_uniforms (5070 ms)
+[37/37] UniformsBenchmark.Run/gl_200_vertex_uniforms_200_fragment_uniforms (5070 ms)
+SUCCESS: all tests passed.
+Tests took 243 seconds.
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py
new file mode 100644
index 00000000000..6fb10a34b6f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py
@@ -0,0 +1,54 @@
+# 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 os
+
+import tracing_project
+import vinn
+
+FORMAT_TO_METHOD = {
+ 'chartjson': 'compareCharts',
+ 'buildbot': 'compareBuildbotOutputs'
+}
+
+_COMPARE_SAMPLES_CMD_LINE = os.path.join(
+ os.path.dirname(__file__), 'compare_samples_cmdline.html')
+
+
+def CompareSamples(sample_a, sample_b, metric, data_format='chartjson'):
+ """Compare the values of a metric from two samples from benchmark output.
+
+ Args:
+ sample_a, sample_b (str): comma-separated lists of paths to the benchmark
+ output.
+ metric (str): Metric name in slash-separated format [2 or 3 part].
+ data_format (str): The format the samples are in. Supported values are:
+ 'chartjson', 'valueset', 'buildbot'.
+ Returns:
+ JSON encoded dict with the values parsed form the samples and the result of
+ the hypothesis testing comparison of the samples under the 'result' key.
+ Possible values for the result key are:
+ 'NEED_MORE_DATA', 'REJECT' and 'FAIL_TO_REJECT'.
+ Where the null hypothesis is that the samples belong to the same population.
+ i.e. a 'REJECT' result would make it reasonable to conclude that
+ there is a significant difference between the samples. (e.g. a perf
+ regression).
+ """
+
+ method = FORMAT_TO_METHOD[data_format]
+ project = tracing_project.TracingProject()
+ all_source_paths = list(project.source_paths)
+
+ def MakeAbsPaths(l):
+ return ','.join(map(os.path.abspath, l.split(',')))
+
+ return vinn.RunFile(
+ _COMPARE_SAMPLES_CMD_LINE,
+ source_paths=all_source_paths,
+ js_args=[
+ method,
+ MakeAbsPaths(sample_a),
+ MakeAbsPaths(sample_b),
+ metric
+ ])
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html
new file mode 100644
index 00000000000..f774f03f1bd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html
@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+const escapeChars = s => s.replace(/[\:|=\/#&,]/g, '_');
+
+function findUnescapedKey(escaped, d) {
+ if (!d) {
+ return undefined;
+ }
+
+ for (const k of Object.keys(d)) {
+ if (escapeChars(k) === escapeChars(escaped)) {
+ return k;
+ }
+ }
+}
+
+function geoMeanFromHistogram(h) {
+ if (!h.hasOwnProperty('buckets')) return 0.0;
+ let count = 0;
+ let sumOfLogs = 0;
+ for (const bucket of h.buckets) {
+ if (bucket.hasOwnProperty('high')) {
+ bucket.mean = (bucket.low + bucket.high) / 2.0;
+ } else {
+ bucket.mean = bucket.low;
+ }
+
+ if (bucket.mean > 0) {
+ sumOfLogs += Math.log(bucket.mean) * bucket.count;
+ count += bucket.count;
+ }
+ }
+ if (count === 0) return 0.0;
+ return Math.exp(sumOfLogs / count);
+}
+
+function guessFullTIRMetricName(metricName) {
+ const parts = metricName.split('/');
+ if (parts.length === 2) {
+ return metricName + '/summary';
+ }
+ return undefined;
+}
+
+function splitMetric(metricName) {
+ const parts = metricName.split('/');
+ let interactionName;
+ let traceName = 'summary';
+ let chartName = parts[0];
+ if (parts.length === 3) {
+ // parts[1] is the interactionName
+ if (parts[1]) chartName = parts[1] + '@@' + chartName;
+ traceName = parts[2];
+ } else if (parts.length === 2) {
+ if (chartName !== parts[1]) traceName = parts[1];
+ } else {
+ throw new Error('Could not parse metric name.');
+ }
+ return [chartName, traceName];
+}
+
+function valuesFromCharts(listOfCharts, metricName) {
+ const allValues = [];
+ const chartAndTrace = splitMetric(metricName);
+ for (const charts of listOfCharts) {
+ const chartName = findUnescapedKey(chartAndTrace[0], charts.charts);
+ if (chartName) {
+ const traceName = findUnescapedKey(
+ chartAndTrace[1], charts.charts[chartName]);
+ if (traceName) {
+ if (charts.charts[chartName][traceName].type ===
+ 'list_of_scalar_values') {
+ if (charts.charts[chartName][traceName].values === null) continue;
+ allValues.push(tr.b.math.Statistics.mean(
+ charts.charts[chartName][traceName].values));
+ }
+ if (charts.charts[chartName][traceName].type === 'histogram') {
+ allValues.push(
+ geoMeanFromHistogram(charts.charts[chartName][traceName]));
+ }
+ if (charts.charts[chartName][traceName].type === 'scalar') {
+ allValues.push(charts.charts[chartName][traceName].value);
+ }
+ }
+ }
+ }
+ return allValues;
+}
+
+function valuesFromChartsWithFallback(listOfCharts, metricName) {
+ const allValues = valuesFromCharts(listOfCharts, metricName);
+ if (allValues.length > 0) return allValues;
+
+ // If this had a tir_label, the "summary" part may have been stripped by
+ // the dashboard during upload. We can re-add it here.
+ const fullMetricName = guessFullTIRMetricName(metricName);
+ if (!fullMetricName) return [];
+
+ return valuesFromCharts(listOfCharts, fullMetricName);
+}
+
+function parseFiles(files) {
+ const results = [];
+ for (const path of files) {
+ const current = tr.b.getSync('file://' + path);
+ results.push(JSON.parse(current));
+ }
+ return results;
+}
+
+const escapeForRegExp = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+
+const strFromRE = re => re.toString().split('/')[1];
+
+function valuesFromBuildbotOutput(out, metric) {
+ if (!out) return [];
+
+ let stringVals = [];
+ const floatVals = [];
+ const chartAndTrace = splitMetric(metric);
+ const metricRE = escapeForRegExp(
+ 'RESULT ' + chartAndTrace[0] + ': ' + chartAndTrace[1] + '=');
+ const singleResultRE = new RegExp(metricRE +
+ strFromRE(/\s*([-]?[\d\.]+)/), 'g');
+ const multiResultsRE = new RegExp(metricRE +
+ strFromRE(/\s*\[\s*([\d\., -]+)\s*\]/), 'g');
+ const meanStdDevRE = new RegExp(metricRE +
+ strFromRE(/\s*\{\s*([-]?\d*(?:\.\d*)?),\s*([-]?\d*(?:\.\d*)?)\}/), 'g');
+ for (const line of out.split(/\r?\n/)) {
+ const singleResultMatch = singleResultRE.exec(line);
+ const multiResultsMatch = multiResultsRE.exec(line);
+ const meanStdDevMatch = meanStdDevRE.exec(line);
+ if (singleResultMatch && singleResultMatch.length > 1) {
+ stringVals.push(singleResultMatch[1]);
+ } else if (multiResultsMatch && multiResultsMatch.length > 1) {
+ const values = multiResultsMatch[1].split(',');
+ stringVals = stringVals.concat(values);
+ } else if (meanStdDevMatch && meanStdDevMatch.length > 1) {
+ stringVals.push(meanStdDevMatch[1]);
+ }
+ }
+ for (const val of stringVals) {
+ const f = parseFloat(val);
+ if (!isNaN(f)) floatVals.push(f);
+ }
+ return floatVals;
+}
+
+function parseMultipleBuildbotStreams(files, metric) {
+ let allValues = [];
+ for (const path of files) {
+ let contents;
+ try {
+ contents = tr.b.getSync('file://' + path);
+ } catch (ex) {
+ const err = new Error('Could not open' + path);
+ err.name = 'File loading error';
+ throw err;
+ }
+ allValues = allValues.concat(valuesFromBuildbotOutput(contents, metric));
+ }
+ return allValues;
+}
+
+const buildComparisonResultOutput = function(a, b) {
+ let comparisonResult;
+ if (!a.length || !b.length) {
+ comparisonResult = {
+ significance: tr.b.math.Statistics.Significance.NEED_MORE_DATA
+ };
+ } else {
+ comparisonResult = tr.b.math.Statistics.mwu(
+ a, b, tr.b.math.Statistics.DEFAULT_ALPHA,
+ tr.b.math.Statistics.MAX_SUGGESTED_SAMPLE_SIZE).asDict();
+ }
+ return {
+ sampleA: a,
+ sampleB: b,
+ result: comparisonResult
+ };
+};
+
+const SampleComparison = {
+
+ compareBuildbotOutputs(
+ buildbotOutputAPathList, buildbotOutputBPathList, metric) {
+ const aPaths = buildbotOutputAPathList.split(',');
+ const bPaths = buildbotOutputBPathList.split(',');
+ const sampleA = parseMultipleBuildbotStreams(aPaths, metric);
+ const sampleB = parseMultipleBuildbotStreams(bPaths, metric);
+ return buildComparisonResultOutput(sampleA, sampleB);
+ },
+
+ compareCharts(chartPathListA, chartPathListB, metric) {
+ const aPaths = chartPathListA.split(',');
+ const bPaths = chartPathListB.split(',');
+ const chartsA = parseFiles(aPaths);
+ const chartsB = parseFiles(bPaths);
+ const sampleA = valuesFromChartsWithFallback(chartsA, metric);
+ const sampleB = valuesFromChartsWithFallback(chartsB, metric);
+ return buildComparisonResultOutput(sampleA, sampleB);
+ }
+
+};
+
+if (tr.isHeadless) {
+ const [method, ...rest] = sys.argv.slice(1);
+ if (SampleComparison[method]) {
+ console.log(JSON.stringify(SampleComparison[method](...rest)));
+ }
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py
new file mode 100644
index 00000000000..df58e8ba8ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py
@@ -0,0 +1,336 @@
+# 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.
+
+from __future__ import print_function
+
+import json
+import math
+import os
+import random
+import tempfile
+import unittest
+
+from tracing.metrics import compare_samples
+
+
+REJECT = 'REJECT'
+FAIL_TO_REJECT = 'FAIL_TO_REJECT'
+NEED_MORE_DATA = 'NEED_MORE_DATA'
+
+
+def Mean(l):
+ if len(l):
+ return float(sum(l))/len(l)
+ return 0
+
+
+class CompareSamplesUnittest(unittest.TestCase):
+ def setUp(self):
+ self._tempfiles = []
+ self._tempdir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ for tf in self._tempfiles:
+ try:
+ os.remove(tf)
+ except OSError:
+ pass
+ try:
+ os.rmdir(self._tempdir)
+ except OSError:
+ pass
+
+ def NewJsonTempfile(self, jsonable_contents):
+ f_handle, new_json_file = tempfile.mkstemp(
+ suffix='.json',
+ dir=self._tempdir,
+ text=True)
+ os.close(f_handle)
+ self._tempfiles.append(new_json_file)
+ with open(new_json_file, 'w') as f:
+ json.dump(jsonable_contents, f)
+ return new_json_file
+
+ def MakeMultipleChartJSONHistograms(self, metric, seed, mu, sigma, n, m):
+ result = []
+ random.seed(seed)
+ for _ in range(m):
+ result.append(self.MakeChartJSONHistogram(metric, mu, sigma, n))
+ return result
+
+ def MakeChartJSONHistogram(self, metric, mu, sigma, n):
+ """Creates a histogram for a normally distributed pseudo-random sample.
+
+ This function creates a deterministic pseudo-random sample and stores it in
+ chartjson histogram format to facilitate the testing of the sample
+ comparison logic.
+
+ For simplicity we use sqrt(n) buckets with equal widths.
+
+ Args:
+ metric (str pair): name of chart, name of the trace.
+ seed (hashable obj): to make the sequences deterministic we seed the RNG.
+ mu (float): desired mean for the sample
+ sigma (float): desired standard deviation for the sample
+ n (int): number of values to generate.
+ """
+ chart_name, trace_name = metric
+ values = [random.gauss(mu, sigma) for _ in range(n)]
+ bucket_count = int(math.ceil(math.sqrt(len(values))))
+ width = (max(values) - min(values))/(bucket_count - 1)
+ prev_bucket = min(values)
+ buckets = []
+ for _ in range(bucket_count):
+ buckets.append({'low': prev_bucket,
+ 'high': prev_bucket + width,
+ 'count': 0})
+ prev_bucket += width
+ for value in values:
+ for bucket in buckets:
+ if value >= bucket['low'] and value < bucket['high']:
+ bucket['count'] += 1
+ break
+ charts = {
+ 'charts': {
+ chart_name: {
+ trace_name: {
+ 'type': 'histogram',
+ 'buckets': buckets
+ }
+ }
+ }
+ }
+ return self.NewJsonTempfile(charts)
+
+ def MakeChart(self, metric, seed, mu, sigma, n, keys=None):
+ """Creates a normally distributed pseudo-random sample. (continuous).
+
+ This function creates a deterministic pseudo-random sample and stores it in
+ chartjson format to facilitate the testing of the sample comparison logic.
+
+ Args:
+ metric (str pair): name of chart, name of the trace.
+ seed (hashable obj): to make the sequences deterministic we seed the RNG.
+ mu (float): desired mean for the sample
+ sigma (float): desired standard deviation for the sample
+ n (int): number of values to generate.
+ """
+ chart_name, trace_name = metric
+ random.seed(seed)
+ values = [random.gauss(mu, sigma) for _ in range(n)]
+ charts = {
+ 'charts': {
+ chart_name: {
+ trace_name: {
+ 'type': 'list_of_scalar_values',
+ 'values': values}
+ }
+ }
+ }
+ if keys:
+ grouping_keys = dict(enumerate(keys))
+ charts['charts'][chart_name][trace_name]['grouping_keys'] = grouping_keys
+ return self.NewJsonTempfile(charts)
+
+ def MakeNoneValuesChart(self, metric, keys=None):
+ """Creates a chart with merged None values.
+
+ Args:
+ metric (str pair): name of chart, name of the trace.
+ """
+ chart_name, trace_name = metric
+ charts = {
+ 'charts': {
+ chart_name: {
+ trace_name: {
+ 'type': 'list_of_scalar_values',
+ 'values': None
+ }
+ }
+ }
+ }
+ if keys:
+ grouping_keys = dict(enumerate(keys))
+ charts['charts'][chart_name][trace_name]['grouping_keys'] = grouping_keys
+ return self.NewJsonTempfile(charts)
+
+ def MakeCharts(self, metric, seed, mu, sigma, n, keys=None):
+ return [
+ self.MakeChartJSONScalar(metric, seed + '%d' % i, mu, sigma, keys)
+ for i in range(n)]
+
+ def MakeChartJSONScalar(self, metric, seed, mu, sigma, keys=None):
+ """Creates a normally distributed pseudo-random sample. (continuous).
+
+ This function creates a deterministic pseudo-random sample and stores it in
+ chartjson format to facilitate the testing of the sample comparison logic.
+
+ Args:
+ metric (str pair): name of chart, name of the trace.
+ seed (hashable obj): to make the sequences deterministic we seed the RNG.
+ mu (float): desired mean for the sample
+ sigma (float): desired standard deviation for the sample
+ """
+ chart_name, trace_name = metric
+ random.seed(seed)
+ charts = {
+ 'charts': {
+ chart_name: {
+ trace_name: {
+ 'type': 'scalar',
+ 'value': random.gauss(mu, sigma)}
+ }
+ }
+ }
+ if keys:
+ grouping_keys = dict(enumerate(keys))
+ charts['charts'][chart_name][trace_name]['grouping_keys'] = grouping_keys
+ return self.NewJsonTempfile(charts)
+
+ def testCompareClearRegressionListOfScalars(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join(self.MakeCharts(metric=metric, seed='lower',
+ mu=10, sigma=1, n=10))
+ higher_values = ','.join(self.MakeCharts(metric=metric, seed='higher',
+ mu=20, sigma=2, n=10))
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], REJECT)
+
+ def testCompareListOfScalarsWithNoneValue(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join(self.MakeCharts(metric=metric, seed='lower',
+ mu=10, sigma=1, n=10))
+ lower_values += ',' + self.MakeNoneValuesChart(metric=metric)
+ higher_values = ','.join(self.MakeCharts(metric=metric, seed='higher',
+ mu=20, sigma=2, n=10))
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], REJECT)
+
+ def testCompareClearRegressionScalars(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join(
+ [self.MakeChartJSONScalar(
+ metric=metric, seed='lower', mu=10, sigma=1) for _ in range(10)])
+ higher_values = ','.join(
+ [self.MakeChartJSONScalar(
+ metric=metric, seed='higher', mu=20, sigma=2) for _ in range(10)])
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], REJECT)
+
+ def testCompareUnlikelyRegressionWithMultipleRuns(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join(
+ self.MakeCharts(
+ metric=metric, seed='lower', mu=10, sigma=1, n=20))
+ higher_values = ','.join(
+ self.MakeCharts(
+ metric=metric, seed='higher', mu=10.01, sigma=0.95, n=20))
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], FAIL_TO_REJECT)
+
+ def testCompareTIRLabel(self):
+ tir_metric = ('some_chart', 'some_label', 'some_trace')
+ tir_metric_name = ('%s@@%s' % (tir_metric[1], tir_metric[0]), tir_metric[2])
+ lower_values = ','.join(self.MakeCharts(
+ metric=tir_metric_name, seed='lower', mu=10, sigma=1, n=10))
+ higher_values = ','.join(self.MakeCharts(
+ metric=tir_metric_name, seed='higher', mu=20, sigma=2, n=10))
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(tir_metric)).stdout)
+ self.assertEqual(result['result']['significance'], REJECT)
+
+ def testCompareTIRLabelMissingSummary(self):
+ tir_metric = ('some_chart', 'some_label')
+ tir_metric_name = ('%s@@%s' % (tir_metric[1], tir_metric[0]), 'summary')
+ lower_values = ','.join(self.MakeCharts(
+ metric=tir_metric_name, seed='lower', mu=10, sigma=1, n=10))
+ higher_values = ','.join(self.MakeCharts(
+ metric=tir_metric_name, seed='higher', mu=20, sigma=2, n=10))
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(tir_metric)).stdout)
+ self.assertEqual(result['result']['significance'], REJECT)
+
+ def testCompareInsufficientData(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower',
+ mu=10, sigma=1, n=5)])
+ higher_values = ','.join([self.MakeChart(metric=metric, seed='higher',
+ mu=10.40, sigma=0.95, n=5)])
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], NEED_MORE_DATA)
+
+ def testCompareMissingFile(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower',
+ mu=10, sigma=1, n=5)])
+ higher_values = '/path/does/not/exist.json'
+ with self.assertRaises(RuntimeError):
+ compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric))
+
+ def testCompareMissingMetric(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower',
+ mu=10, sigma=1, n=5)])
+ higher_values = ','.join([self.MakeChart(metric=metric, seed='higher',
+ mu=20, sigma=2, n=5)])
+ metric = ('some_chart', 'missing_trace')
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], NEED_MORE_DATA)
+
+ def testCompareBadChart(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower',
+ mu=10, sigma=1, n=5)])
+ higher_values = self.NewJsonTempfile(['obviously', 'not', 'a', 'chart]'])
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], NEED_MORE_DATA)
+
+ def testCompareBuildbotOutput(self):
+ bb = os.path.join(os.path.dirname(__file__),
+ 'buildbot_output_for_compare_samples_test.txt')
+ result = compare_samples.CompareSamples(
+ bb, bb, 'DrawCallPerf_gl/score',
+ data_format='buildbot')
+ result = json.loads(result.stdout)
+ self.assertEqual(result['result']['significance'], NEED_MORE_DATA)
+ self.assertEqual(Mean(result['sampleA']), 4123)
+ self.assertEqual(Mean(result['sampleB']), 4123)
+
+ def testCompareChartJsonHistogram(self):
+ metric = ('some_chart', 'some_trace')
+ lower_values = ','.join(self.MakeMultipleChartJSONHistograms(
+ metric=metric, seed='lower', mu=10, sigma=1, n=100, m=10))
+ higher_values = ','.join(self.MakeMultipleChartJSONHistograms(
+ metric=metric, seed='higher', mu=20, sigma=2, n=100, m=10))
+ result = json.loads(compare_samples.CompareSamples(
+ lower_values, higher_values, '/'.join(metric)).stdout)
+ self.assertEqual(result['result']['significance'], REJECT)
+
+ def testParseComplexMetricName(self):
+ full_metric_name = ('memory:chrome:all_processes:reported_by_os:'
+ 'system_memory:native_heap:'
+ 'proportional_resident_size_avg/blank_about/'
+ 'blank_about_blank')
+ chart_name = ('blank_about@@memory:chrome:all_processes:reported_by_os:'
+ 'system_memory:native_heap:proportional_resident_size_avg')
+ trace_name = 'blank:about:blank'
+ metric = chart_name, trace_name
+ keys = 'blank', 'about'
+ lower_values = ','.join(self.MakeCharts(metric=metric, seed='lower',
+ mu=10, sigma=1, n=10, keys=keys))
+ higher_values = ','.join(self.MakeCharts(metric=metric, seed='higher',
+ mu=20, sigma=2, n=10, keys=keys))
+ result = compare_samples.CompareSamples(
+ lower_values, higher_values, full_metric_name).stdout
+ print(result)
+ result = json.loads(result)
+ self.assertEqual(result['result']['significance'], REJECT)
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html
new file mode 100644
index 00000000000..5ed27d0f6be
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.console', function() {
+ const COUNT_BOUNDARIES = tr.v.HistogramBinBoundaries.createExponential(
+ 1, 1e4, 30);
+ // We store a single value, so we only need one of the statistics to keep
+ // track. We choose the average for that.
+ const SUMMARY_OPTIONS = tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS;
+
+ // Sources of console error message that we are interested in.
+ const SOURCES = ['all', 'js', 'network'];
+
+ /**
+ * This metric counts slices named 'ConsoleErrorMessage' and adds:
+ * - console:error:all - all console error messages.
+ * - console:error:js - console error messages coming from JS.
+ * - console:error:network - console error messages coming from network.
+ * If a console error message does not come from JS or network, it is still
+ * accounted in the console_error_all.
+ */
+ function consoleErrorMetric(histograms, model) {
+ const counts = {};
+ for (const source of SOURCES) {
+ counts[source] = 0;
+ }
+ for (const slice of model.getDescendantEvents()) {
+ if (slice.category === 'blink.console' &&
+ slice.title === 'ConsoleMessage::Error') {
+ const source = slice.args.source.toLowerCase();
+ counts.all++;
+ if (source in counts) {
+ counts[source]++;
+ }
+ }
+ if (slice.category === 'v8.console' && (
+ slice.title === 'V8ConsoleMessage::Exception' ||
+ slice.title === 'V8ConsoleMessage::Error' ||
+ slice.title === 'V8ConsoleMessage::Assert')) {
+ counts.all++;
+ counts.js++;
+ }
+ }
+ for (const source of SOURCES) {
+ histograms.createHistogram(
+ `console:error:${source}`,
+ tr.b.Unit.byName.count_smallerIsBetter,
+ counts[source], {
+ description: `Number of ${source} console error messages`,
+ summaryOptions: SUMMARY_OPTIONS
+ });
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(consoleErrorMetric);
+
+ return {
+ consoleErrorMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html
new file mode 100644
index 00000000000..eff2917fc47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/console_error_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function makeEvent(cat, name, source, timestamp) {
+ return {
+ cat,
+ name,
+ args: {source},
+ ts: timestamp,
+ pid: 52,
+ tid: 53,
+ ph: 'B'
+ };
+ }
+
+ test('consoleErrorMetric_noErrors', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ makeEvent('foo', '', 10),
+ makeEvent('bar', '', 20)
+ ];
+ tr.metrics.console.consoleErrorMetric(histograms,
+ tr.c.TestUtils.newModelWithEvents([events]));
+ const all = histograms.getHistogramNamed('console:error:all').running;
+ assert.strictEqual(all.count, 1);
+ assert.strictEqual(all.mean, 0);
+ const js = histograms.getHistogramNamed('console:error:js').running;
+ assert.strictEqual(js.count, 1);
+ assert.strictEqual(js.mean, 0);
+ const net = histograms.getHistogramNamed('console:error:network').running;
+ assert.strictEqual(net.count, 1);
+ assert.strictEqual(net.mean, 0);
+ });
+
+ test('consoleErrorMetric_Errors', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ makeEvent('blink.console', 'foo', '', 10),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'XML', 20),
+ makeEvent('blink.console', 'bar', '', 30),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 40),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'JS', 50),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 60),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 70),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'JS', 80),
+ makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 90),
+ makeEvent('bar', '', 300)
+ ];
+ tr.metrics.console.consoleErrorMetric(histograms,
+ tr.c.TestUtils.newModelWithEvents([events]));
+ const all = histograms.getHistogramNamed('console:error:all').running;
+ assert.strictEqual(all.count, 1);
+ assert.strictEqual(all.mean, 7);
+ const js = histograms.getHistogramNamed('console:error:js').running;
+ assert.strictEqual(js.count, 1);
+ assert.strictEqual(js.mean, 2);
+ const net = histograms.getHistogramNamed('console:error:network').running;
+ assert.strictEqual(net.count, 1);
+ assert.strictEqual(net.mean, 4);
+ });
+
+ test('consoleErrorMetric_V8', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ makeEvent('v8.console', 'foo', '', 10),
+ makeEvent('v8.console', 'V8ConsoleMessage::Error', '', 20),
+ makeEvent('v8.console', 'bar', '', 30),
+ makeEvent('v8.console', 'V8ConsoleMessage::Exception', '', 40),
+ makeEvent('v8.console', 'V8ConsoleMessage::Assert', '', 50),
+ makeEvent('v8.console', 'V8ConsoleMessage::Ignore', '', 60),
+ makeEvent('v8.console', 'V8ConsoleMessage::Ignore', '', 70),
+ makeEvent('v8.console', 'bar', '', 300)
+ ];
+ tr.metrics.console.consoleErrorMetric(histograms,
+ tr.c.TestUtils.newModelWithEvents([events]));
+ const all = histograms.getHistogramNamed('console:error:all').running;
+ assert.strictEqual(all.count, 1);
+ assert.strictEqual(all.mean, 3);
+ const js = histograms.getHistogramNamed('console:error:js').running;
+ assert.strictEqual(js.count, 1);
+ assert.strictEqual(js.mean, 3);
+ const net = histograms.getHistogramNamed('console:error:network').running;
+ assert.strictEqual(net.count, 1);
+ assert.strictEqual(net.mean, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html
new file mode 100644
index 00000000000..c858a8bcb2d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ function getCpuSnapshotsFromModel(model) {
+ const snapshots = [];
+ for (const pid in model.processes) {
+ const snapshotInstances =
+ model.processes[pid].objects.getAllInstancesNamed('CPUSnapshots');
+ if (!snapshotInstances) continue;
+
+ for (const object of snapshotInstances[0].snapshots) {
+ snapshots.push(object.args.processes);
+ }
+ }
+ return snapshots;
+ }
+
+ function getProcessSumsFromSnapshot(snapshot) {
+ const processSums = new Map();
+ for (const processData of snapshot) {
+ const processName = processData.name;
+ if (!(processSums.has(processName))) {
+ processSums.set(processName, {sum: 0.0, paths: new Set()});
+ }
+ processSums.get(processName).sum += parseFloat(processData.pCpu);
+ // The process path may be missing on Windows because of AccessDenied
+ // error thrown by psutil package used by CPU tracing agent.
+ if (processData.path) {
+ processSums.get(processName).paths.add(processData.path);
+ }
+ }
+ return processSums;
+ }
+
+ function buildNumericsFromSnapshots(snapshots) {
+ const processNumerics = new Map();
+ for (const snapshot of snapshots) {
+ const processSums = getProcessSumsFromSnapshot(snapshot);
+ for (const [processName, processData] of processSums.entries()) {
+ if (!(processNumerics.has(processName))) {
+ processNumerics.set(processName, {
+ numeric: new tr.v.Histogram('cpu:percent:' + processName,
+ tr.b.Unit.byName.normalizedPercentage_smallerIsBetter),
+ paths: new Set()
+ });
+ }
+ processNumerics.get(processName).numeric.addSample(
+ processData.sum / 100.0);
+ for (const path of processData.paths) {
+ processNumerics.get(processName).paths.add(path);
+ }
+ }
+ }
+ return processNumerics;
+ }
+
+ function cpuProcessMetric(histograms, model) {
+ const snapshots = getCpuSnapshotsFromModel(model);
+ const processNumerics = buildNumericsFromSnapshots(snapshots);
+ for (const [processName, processData] of processNumerics) {
+ const numeric = processData.numeric;
+ // Treat missing snapshots as zeros. A process is missing from a snapshots
+ // when its CPU usage was below minimum threshold when the snapshot was
+ // taken.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html
new file mode 100644
index 00000000000..55e404c5b4d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/cpu_process_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function makeModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events]);
+ }
+
+ test('cpuProcessMetric_noData', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}
+ ];
+ tr.metrics.sh.cpuProcessMetric(histograms, makeModel(events));
+ assert.lengthOf(histograms, 0);
+ });
+
+ test('cpuProcessMetric_singleSnapshots', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {
+ 'name': 'CPUSnapshots',
+ 'args': {
+ 'snapshot': {
+ 'processes': [
+ {'path': '/usr/sbin/crudd', 'pCpu': '99.0', 'pid': '13495',
+ 'pMem': '0.0', 'name': 'crudd'},
+ {'path': '/opt/chrome/chrome', 'pCpu': '0.8',
+ 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'},
+ {'path': '/opt/chrome/chrome', 'pCpu': '0.3',
+ 'pid': '29661', 'pMem': '0.9', 'name': 'chrome'}
+ ]
+ }
+ },
+ 'pid': 52, 'ts': '2226221225693.658', 'tid': 53, 'ph': 'O',
+ 'local': true, 'id': '0x1000'
+ }
+ ];
+ tr.metrics.sh.cpuProcessMetric(histograms, makeModel(events));
+
+ assert.isDefined(histograms.getHistogramNamed('cpu:percent:chrome'));
+ assert.isDefined(histograms.getHistogramNamed('cpu:percent:crudd'));
+ const chromeValue = histograms.getHistogramNamed('cpu:percent:chrome');
+ const chromeStatistics = chromeValue.running;
+ assert.strictEqual(chromeStatistics.count, 1);
+ assert.closeTo(chromeStatistics.mean, 0.011, 1e-5);
+ assert.closeTo(chromeStatistics.max, 0.011, 1e-5);
+ assert.instanceOf(chromeValue.diagnostics.get('paths'), tr.v.d.GenericSet);
+ const paths = tr.b.getOnlyElement(chromeValue.diagnostics.get('paths'));
+ assert.strictEqual(paths, '/opt/chrome/chrome');
+ });
+
+ test('cpuProcessMetric_multipleSnapshots', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {
+ 'name': 'CPUSnapshots',
+ 'args': {
+ 'snapshot': {
+ 'processes': [
+ {'path': '/usr/sbin/crudd', 'pCpu': '99.0', 'pid': '13495',
+ 'pMem': '0.0', 'name': 'crudd'},
+ {'path': '/opt/chrome/chrome', 'pCpu': '0.8',
+ 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'}
+ ]
+ }
+ },
+ 'pid': 52, 'ts': '2226221225693.658', 'tid': 53,
+ 'ph': 'O', 'local': true, 'id': '0x1000'
+ },
+ {
+ 'name': 'CPUSnapshots',
+ 'args': {
+ 'snapshot': {
+ 'processes': [
+ {'path': '/usr/sbin/crudd', 'pCpu': '1.3', 'pid': '13495',
+ 'pMem': '0.0', 'name': 'crudd'},
+ {'path': '/opt/chrome/chrome', 'pCpu': '0.6',
+ 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'},
+ {'path': '/opt/chromium/chrome', 'pCpu': '0.1',
+ 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'},
+ {'path': '/usr/sbin/mnp_logger', 'pCpu': '0.2', 'pid': '6543',
+ 'pMem': '0.1', 'name': 'mnp_logger'}
+ ]
+ }
+ },
+ 'pid': 52, 'ts': '2226222262064.4473', 'tid': 53,
+ 'ph': 'O', 'local': true, 'id': '0x1000'
+ }
+ ];
+ tr.metrics.sh.cpuProcessMetric(histograms, makeModel(events));
+ assert.isDefined(histograms.getHistogramNamed('cpu:percent:chrome'));
+ assert.isDefined(histograms.getHistogramNamed('cpu:percent:crudd'));
+ assert.isDefined(histograms.getHistogramNamed('cpu:percent:mnp_logger'));
+ const chromeValue = histograms.getHistogramNamed('cpu:percent:chrome');
+ const chromeStatistics = chromeValue.running;
+ assert.strictEqual(chromeStatistics.count, 2);
+ assert.closeTo(chromeStatistics.mean, 0.0075, 1e-5);
+ assert.strictEqual(chromeStatistics.max, 0.008);
+ assert.instanceOf(chromeValue.diagnostics.get('paths'), tr.v.d.GenericSet);
+ const paths = Array.from(chromeValue.diagnostics.get('paths'));
+ assert.lengthOf(paths, 2);
+ assert.strictEqual(paths[0], '/opt/chrome/chrome');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/discover.py b/chromium/third_party/catapult/tracing/tracing/metrics/discover.py
new file mode 100644
index 00000000000..ef532c6d0df
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/discover.py
@@ -0,0 +1,34 @@
+# 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 json
+import os
+
+import tracing_project
+import vinn
+
+
+_DISCOVER_CMD_LINE = os.path.join(
+ os.path.dirname(__file__), 'discover_cmdline.html')
+
+
+def DiscoverMetrics(modules_to_load):
+ """ Returns a list of registered metrics.
+
+ Args:
+ modules_to_load: a list of modules (string) to be loaded before discovering
+ the registered metrics.
+ """
+ assert isinstance(modules_to_load, list)
+ project = tracing_project.TracingProject()
+ all_source_paths = list(project.source_paths)
+
+ res = vinn.RunFile(
+ _DISCOVER_CMD_LINE, source_paths=all_source_paths,
+ js_args=modules_to_load)
+
+ if res.returncode != 0:
+ raise RuntimeError('Error running metrics_discover_cmdline: ' + res.stdout)
+ else:
+ return [str(m) for m in json.loads(res.stdout)]
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html b/chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html
new file mode 100644
index 00000000000..c8aab5cd98d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+function discoverMetrics(args) {
+ for (let i = 0; i < args.length; i++) {
+ const filename = args[i];
+ HTMLImportsLoader.loadHTML(filename);
+ }
+
+ const metrics = tr.metrics.MetricRegistry.getAllRegisteredTypeInfos();
+ const discoveredMetricNames = [];
+ for (let i = 0; i < metrics.length; i++) {
+ discoveredMetricNames.push(metrics[i].constructor.name);
+ }
+ console.log(JSON.stringify(discoveredMetricNames));
+ return 0;
+}
+
+if (tr.isHeadless) {
+ quit(discoverMetrics(sys.argv.slice(1)));
+}
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py b/chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py
new file mode 100644
index 00000000000..10b81b547c8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py
@@ -0,0 +1,20 @@
+# Copyright (c) 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 unittest
+
+from tracing.metrics import discover
+
+class MetricsDiscoverUnittest(unittest.TestCase):
+ def testMetricsDiscoverEmpty(self):
+ self.assertFalse(discover.DiscoverMetrics([]))
+
+ def testMetricsDiscoverNonEmpty(self):
+ self.assertEquals(['sampleMetric'], discover.DiscoverMetrics(
+ ['/tracing/metrics/sample_metric.html']))
+
+ def testMetricsDiscoverMultipleMetrics(self):
+ self.assertGreater(
+ len(discover.DiscoverMetrics(
+ ['/tracing/metrics/all_metrics.html'])), 1)
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html
new file mode 100644
index 00000000000..2e1ec342a5e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html
@@ -0,0 +1,395 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<!--
+media_metric uses Chrome trace events to calculate metrics about video
+and audio playback. It is meant to be used for pages with <video> and/or
+<audio> elements. It is used by videostack-eng@google.com team for
+regression testing.
+
+See class PerPlaybackData for details on each of the values that are measured.
+
+This metric supports media playbacks happening simultaneously over multiple
+pages, supports multiple media elements on a page, and supports multiple
+playbacks with each element. It does not support media playback using
+flash or any other technology not provided by Chrome videostack team.
+
+Please inform crouleau@chromium.org and johnchen@chromium.org about
+changes to this file.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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)) {
+ // Find the threads we're interested in, and if a needed thread
+ // is missing, no need to look further in this process.
+ 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);
+ }
+
+ if (audioThreads.length !== 0) {
+ processData.calculateTimeToAudioPlays(audioThreads);
+ }
+
+ processData.calculateSeekTimes(mainThread);
+ processData.calculateBufferingTimes(mainThread);
+
+ processData.addMetricToHistograms(histograms);
+ }
+ }
+
+ // PerProcessData manages all metric values associated with a renderer
+ // process. The process can have multiple media playbacks.
+ class PerProcessData {
+ constructor() {
+ // All the perf data we collect for a process are stored in a Map.
+ // Each key of the map is an ID of a media playback, and the value
+ // associated with each key is a PerPlaybackData object containing
+ // all the perf data for that playback.
+ 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);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ addMetricToHistograms(histograms) {
+ for (const [id, playbackData] of this.playbackIdToDataMap_) {
+ playbackData.addMetricToHistograms(histograms);
+ }
+ }
+
+ // @private
+ getPerPlaybackObject_(playbackId) {
+ let perPlaybackObject = this.playbackIdToDataMap_.get(playbackId);
+ if (perPlaybackObject === undefined) {
+ // The trace isn't complete, and didn't contain the DoLoad event for
+ // this playback. Create a new PerPlaybackData object for this playback.
+ perPlaybackObject = new PerPlaybackData(undefined);
+ this.playbackIdToDataMap_.set(playbackId, perPlaybackObject);
+ }
+ return perPlaybackObject;
+ }
+ }
+
+ // PerPlaybackData contains all metric values associated with a single
+ // media playback.
+ class PerPlaybackData {
+ constructor(playStartTime) {
+ this.playStart_ = playStartTime;
+ this.timeToVideoPlay_ = undefined;
+ this.timeToAudioPlay_ = undefined;
+ this.bufferingTime_ = undefined;
+ this.droppedFrameCount_ = 0;
+ this.seekError_ = false;
+ this.seekTimes_ = new Map();
+ this.currentSeek_ = undefined;
+ }
+
+ // API methods for retrieving metric values. Each method returns undefined
+ // if no value is available (e.g., timeToVideoPlay() returns undefined for
+ // an audio-only playback).
+
+ // Returns how long after a video is requested to start playing before
+ // the video actually starts. If time_to_video_play regresses, then users
+ // will click to play videos and then have to wait longer before the videos
+ // start actually playing.
+ get timeToVideoPlay() {
+ return this.timeToVideoPlay_;
+ }
+
+ // Similar to timeToVideoPlay, but measures the time delay before audio
+ // starts playing.
+ get timeToAudioPlay() {
+ return this.timeToAudioPlay_;
+ }
+
+ // Returns the difference between the actual play time of media vs its
+ // expected play time. Ideally the two should be the same. If actual play
+ // time is significantly longer than expected play time, it indicates that
+ // there were stalls during the play for buffering or some other reasons.
+ // Current limitation: Buffering time isn't calculated if seek occurred
+ // during playback, and it gives incorrect value if the playback isn't
+ // from beginning to end without pauses.
+ get bufferingTime() {
+ return this.bufferingTime_;
+ }
+
+ // Reports the number of video frames that were dropped. Ideally this
+ // should be 0. If a large number of frames are dropped, the video playback
+ // will not be smooth.
+ get droppedFrameCount() {
+ // We should report dropped frame count as long as video was played.
+ return (this.timeToVideoPlay_ !== undefined) ?
+ this.droppedFrameCount_ : undefined;
+ }
+
+ // Returns a Map containing seek times. The keys of the map are numerical
+ // values indicating the target location of the seek, in unit of seconds.
+ // The values of the map are objects with the following public properties:
+ // * pipelineSeekTime: amount of time taken by media pipeline to process
+ // this seek operation, from when the seek request is received, to when
+ // the pipeline starts processing at the new location, in milliseconds.
+ // * seekTime: how long after a user requests a seek operation before the
+ // seek completes and the media starts playing at the new location, as
+ // perceived by the user, in milliseconds.
+ get seekTimes() {
+ if (this.seekError_ || this.currentSeek_ !== undefined) return new Map();
+ return this.seekTimes_;
+ }
+
+ // API methods for processing data from trace events.
+
+ processVideoRenderTime(videoRenderTime) {
+ // Each video playback can generate multiple Render events, one for
+ // each frame. For calculating time to video play, we only use the
+ // first Render event.
+ if (this.playStart_ !== undefined &&
+ this.timeToVideoPlay_ === undefined) {
+ this.timeToVideoPlay_ = videoRenderTime - this.playStart_;
+ }
+ }
+
+ processAudioRenderTime(audioRenderTime) {
+ if (this.playStart_ !== undefined &&
+ this.timeToAudioPlay_ === undefined) {
+ this.timeToAudioPlay_ = audioRenderTime - this.playStart_;
+ }
+ }
+
+ processVideoFramesDropped(count) {
+ this.droppedFrameCount_ += count;
+ }
+
+ // We support multiple seeks per element, as long as they seek to different
+ // target time. Thus the seek times are stored in a Map instead of a scalar
+ // property. The key of the map is event.args.target, which is a numerical
+ // value indicating the target location of the seek, in unit of seconds.
+ // For example, with a seek to 5 seconds mark, event.args.target === 5.
+ // The value of the map is an object with 4 properties (the first two are
+ // added during object creation, the latter two are added as the data
+ // become available):
+ // * target: seek target time (same as the map key)
+ // * startTime: timestamp of the event marking start of seek
+ // * pipelineSeekTime: amount of time taken by media pipeline to process
+ // this seek (milliseconds)
+ // * seekTime: amount of seek time perceived by the user (milliseconds)
+ // If any unexpected conditions occur, we stop processing and set an error
+ // flag this.seekError_.
+ // TODO(https://github.com/catapult-project/catapult/issues/3976):
+ // Emit detailed warnings.
+ processDoSeek(target, startTime) {
+ // currentSeek_ refers to the object associated with the
+ // seek that is currently being processed for this media element.
+ // It is used to match seek end events against seek start events.
+ if (this.currentSeek_ !== undefined) {
+ // TODO(https://github.com/catapult-project/catapult/issues/3976):
+ // Warning 'Overlapping seek not supported'.
+ this.seekError_ = true;
+ return;
+ }
+ this.currentSeek_ = { target, startTime };
+ this.seekTimes_.set(target, this.currentSeek_);
+ }
+
+ processOnPipelineSeeked(target, time) {
+ if (this.seekError_) return;
+ const currentSeek = this.currentSeek_;
+ if (currentSeek === undefined) {
+ // OK to have this event when there is no active seek, as this event
+ // can be generated for other reasons, e.g., initial loading of media
+ // generates this event with target of 0 seconds.
+ return;
+ }
+ if (currentSeek.target !== target) {
+ // TODO(https://github.com/catapult-project/catapult/issues/3976):
+ // Warning 'WebMediaPlayerImpl::OnPipelineSeeked to unexpected target'.
+ this.seekError_ = true;
+ return;
+ }
+ if (currentSeek.pipelineSeekTime !== undefined) {
+ // TODO(https://github.com/catapult-project/catapult/issues/3976):
+ // Warning 'Multiple WebMediaPlayerImpl::OnPipelineSeeked events'.
+ this.seekError_ = true;
+ return;
+ }
+ currentSeek.pipelineSeekTime = time - currentSeek.startTime;
+ }
+
+ processBufferingHaveEnough(time) {
+ if (this.seekError_) return;
+ const currentSeek = this.currentSeek_;
+ if (currentSeek === undefined) {
+ // No current seek means this event is generated by non-seek related
+ // events, e.g., initial loading of media.
+ return;
+ }
+ if (currentSeek.pipelineSeekTime === undefined) {
+ // Since we haven't seen WebMediaPlayerImpl::OnPipelineSeeked event
+ // event yet, this event is triggered by something else, e.g., a
+ // have_nothing->have_enough cycle due to underflow from decoders.
+ return;
+ }
+ currentSeek.seekTime = time - currentSeek.startTime;
+ // Finished processing current seek.
+ this.currentSeek_ = undefined;
+ }
+
+ processOnEnded(playEndTime, duration) {
+ if (this.playStart_ === undefined) return;
+ // Can't calculate buffering time if there were any seeks.
+ if (this.seekTimes_.size !== 0 || this.seekError_) return;
+ // Play was resumed after it ended previously.
+ if (this.bufferingTime_ !== undefined) return;
+ // Convert duration from seconds to milliseconds.
+ duration = tr.b.convertUnit(duration, tr.b.UnitPrefixScale.METRIC.NONE,
+ tr.b.UnitPrefixScale.METRIC.MILLI);
+ const playTime = playEndTime - this.playStart_;
+ if (this.timeToVideoPlay_ !== undefined) {
+ this.bufferingTime_ = playTime - duration - this.timeToVideoPlay_;
+ } else if (this.timeToAudioPlay !== undefined) {
+ this.bufferingTime_ = playTime - duration - this.timeToAudioPlay_;
+ }
+ }
+
+ 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()) {
+ // key is a numerical value that can have '.' when converted to
+ // string. However, '.' causes problems in histogram names, so
+ // replace with '_'.
+ 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);
+ }
+
+ // @private
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html
new file mode 100644
index 00000000000..a404b8e1980
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html
@@ -0,0 +1,423 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/media_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // Arbitrarily selected process ID and thread IDs we'll use in test data
+ const procId = 52;
+ const tidMain = 1;
+ const tidCompositor = 53;
+ const tidAudio = 55;
+
+ function doLoadEvent(timestamp, opt_id) {
+ return {name: 'WebMediaPlayerImpl::DoLoad', args: {id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
+ }
+
+ function videoRenderEvent(timestamp, opt_id) {
+ return {name: 'VideoRendererImpl::Render', args: {id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidCompositor, ph: 'X'};
+ }
+
+ function audioRenderEvent(timestamp, opt_id) {
+ return {name: 'AudioRendererImpl::Render', args: {id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidAudio, ph: 'X'};
+ }
+
+ function videoFramesDroppedEvent(timestamp, frameCount, opt_id) {
+ return {name: 'VideoFramesDropped',
+ args: {count: frameCount, id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidCompositor, ph: 'X'};
+ }
+
+ function onEndedEvent(timestamp, mediaDuration, opt_id) {
+ return {name: 'WebMediaPlayerImpl::OnEnded',
+ args: {duration: mediaDuration, id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
+ }
+
+ function doSeekEvent(timestamp, targetTime, opt_id) {
+ return {name: 'WebMediaPlayerImpl::DoSeek',
+ args: {target: targetTime, id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
+ }
+
+ function seekedEvent(timestamp, targetTime, opt_id) {
+ return {name: 'WebMediaPlayerImpl::OnPipelineSeeked',
+ args: {target: targetTime, id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
+ }
+
+ function bufferEnoughEvent(timestamp, opt_id) {
+ return {name: 'WebMediaPlayerImpl::BufferingHaveEnough',
+ args: {id: opt_id || 0},
+ pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
+ }
+
+ function threadMarker(threadName, threadId) {
+ return {name: 'thread_name', args: {name: threadName},
+ pid: procId, ts: 0, cat: '__metadata', tid: threadId, ph: 'M'};
+ }
+
+ const mainThreadMarker = threadMarker('CrRendererMain', tidMain);
+ const compositorThreadMarker = threadMarker('Compositor', tidCompositor);
+ const audioThreadMarker = threadMarker('AudioOutputDevice', tidAudio);
+
+ function makeModel(events) {
+ return tr.c.TestUtils.newModelWithEvents([events]);
+ }
+
+ function checkCloseTo(histograms, histogramName, expectedValue) {
+ assert.isDefined(histograms.getHistogramNamed(histogramName));
+ const value = histograms.getHistogramNamed(histogramName);
+ const statistics = value.running;
+ assert.strictEqual(statistics.count, 1);
+ assert.closeTo(statistics.mean, expectedValue, 1e-5);
+ }
+
+ function checkCloseToMultiple(histograms, histogramName, expectedCount,
+ expectedMin, expectedMean, expectedMax) {
+ assert.isDefined(histograms.getHistogramNamed(histogramName));
+ const value = histograms.getHistogramNamed(histogramName);
+ const statistics = value.running;
+ assert.strictEqual(statistics.count, expectedCount);
+ assert.closeTo(statistics.min, expectedMin, 1e-5);
+ assert.closeTo(statistics.mean, expectedMean, 1e-5);
+ assert.closeTo(statistics.max, expectedMax, 1e-5);
+ }
+
+ function checkEqual(histograms, histogramName, expectedValue) {
+ assert.isDefined(histograms.getHistogramNamed(histogramName));
+ const value = histograms.getHistogramNamed(histogramName);
+ const statistics = value.running;
+ assert.strictEqual(statistics.count, 1);
+ assert.strictEqual(statistics.mean, expectedValue);
+ }
+
+ test('mediaMetric_noData', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.lengthOf(histograms, 0);
+ });
+
+ test('mediaMetric_videoTimeToPlay', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(100),
+ videoRenderEvent(300),
+ // Video renderer always generate multiple render events,
+ // one for each frame. For calculation of time-to-play,
+ // only the first render event is relevant. Here we put in
+ // a second render event to make sure it's ignored by the
+ // metric computation code.
+ videoRenderEvent(400),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'time_to_video_play', 0.2);
+ });
+
+ test('mediaMetric_audioTimeToPlay', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ mainThreadMarker,
+ audioThreadMarker,
+ doLoadEvent(1000),
+ audioRenderEvent(1100),
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'time_to_audio_play', 0.1);
+ });
+
+ test('mediaMetric_bufferingTimeVideo', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ videoRenderEvent(1600),
+ onEndedEvent(10051500, 10),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'buffering_time', 50);
+ });
+
+ test('mediaMetric_bufferingTimeAudio', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ mainThreadMarker,
+ audioThreadMarker,
+ doLoadEvent(1000),
+ audioRenderEvent(1500),
+ onEndedEvent(5002500, 5),
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'buffering_time', 1);
+ });
+
+ // With seek, no buffering time should be reported
+ test('mediaMetric_noBufferingTime', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ videoRenderEvent(1600),
+ onEndedEvent(10066666, 10),
+ doSeekEvent(525, 1.2),
+ seekedEvent(719, 1.2),
+ bufferEnoughEvent(825),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.isUndefined(histograms.getHistogramNamed('buffering_time'));
+ });
+
+ test('mediaMetric_droppedFrameCount', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ videoFramesDroppedEvent(123456, 3),
+ videoFramesDroppedEvent(234567, 6),
+ videoFramesDroppedEvent(345678, 1),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkEqual(histograms, 'dropped_frame_count', 10);
+ });
+
+ test('mediaMetric_droppedFrameCountZero', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkEqual(histograms, 'dropped_frame_count', 0);
+ });
+
+ test('mediaMetric_droppedFrameCountNoVideo', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ audioRenderEvent(1500),
+ mainThreadMarker,
+ audioThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.isUndefined(histograms.getHistogramNamed('dropped_frame_count'));
+ });
+
+ test('mediaMetric_seekTimeVideo', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ doSeekEvent(2000, 1.2),
+ seekedEvent(2500, 1.2),
+ bufferEnoughEvent(3000),
+ doSeekEvent(15000, 3.7),
+ seekedEvent(75000, 3.7),
+ bufferEnoughEvent(95000),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'pipeline_seek_time_1_2', 0.5);
+ checkCloseTo(histograms, 'seek_time_1_2', 1);
+ checkCloseTo(histograms, 'pipeline_seek_time_3_7', 60);
+ checkCloseTo(histograms, 'seek_time_3_7', 80);
+ });
+
+ test('mediaMetric_seekTimeAudio', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ audioRenderEvent(1500),
+ doSeekEvent(2000, 1.2),
+ seekedEvent(2500, 1.2),
+ bufferEnoughEvent(3000),
+ doSeekEvent(15000, 3.7),
+ seekedEvent(75000, 3.7),
+ bufferEnoughEvent(95000),
+ mainThreadMarker,
+ audioThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'pipeline_seek_time_1_2', 0.5);
+ checkCloseTo(histograms, 'seek_time_1_2', 1);
+ checkCloseTo(histograms, 'pipeline_seek_time_3_7', 60);
+ checkCloseTo(histograms, 'seek_time_3_7', 80);
+ });
+
+ test('mediaMetric_seekOverlap', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ doSeekEvent(2000, 1.2),
+ seekedEvent(2500, 1.2),
+ doSeekEvent(2800, 3.7), // Out of order
+ bufferEnoughEvent(3000),
+ seekedEvent(75000, 3.7),
+ bufferEnoughEvent(95000),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.isDefined(histograms.getHistogramNamed('time_to_video_play'));
+ assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2'));
+ });
+
+ test('mediaMetric_seekIncomplete', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ doSeekEvent(2000, 1.2),
+ seekedEvent(2500, 1.2),
+ // Missing bufferEnoughEvent
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.isDefined(histograms.getHistogramNamed('time_to_video_play'));
+ assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2'));
+ });
+
+ test('mediaMetric_seekWrongTarget', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(1500),
+ doSeekEvent(2000, 1.2),
+ seekedEvent(2500, 2.7), // Wrong target, should be 1.2
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.isDefined(histograms.getHistogramNamed('time_to_video_play'));
+ assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2'));
+ });
+
+ test('mediaMetric_multiplePlaybacks', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000, 0),
+ doLoadEvent(1100, 1),
+ videoRenderEvent(1400, 1),
+ videoRenderEvent(1500, 0),
+ doLoadEvent(2000, 2),
+ doSeekEvent(2000, 1.2, 0),
+ doSeekEvent(2100, 1.2, 1),
+ videoRenderEvent(2100, 2),
+ doSeekEvent(2200, 1.2, 2),
+ seekedEvent(2500, 1.2, 2),
+ seekedEvent(2600, 1.2, 1),
+ seekedEvent(2700, 1.2, 0),
+ bufferEnoughEvent(3000, 0),
+ bufferEnoughEvent(3100, 1),
+ bufferEnoughEvent(3800, 2),
+ mainThreadMarker,
+ compositorThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ // Values are 0.5, 0.3, 0.1, with min 0.1, mean 0.3, max 0.5.
+ checkCloseToMultiple(histograms, 'time_to_video_play', 3, 0.1, 0.3, 0.5);
+ // Values are 0.7, 0.5, 0.3, with min 0.3, mean 0.5, max 0.7.
+ checkCloseToMultiple(histograms,
+ 'pipeline_seek_time_1_2', 3, 0.3, 0.5, 0.7);
+ // Values are 1, 1, 1.6, with min 1, mean 1.2, max 1.6
+ checkCloseToMultiple(histograms, 'seek_time_1_2', 3, 1, 1.2, 1.6);
+ });
+
+ // Scenario: Play mixed audio/video from start to finish
+ test('mediaMetric_playVideoScenario', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(2000),
+ videoRenderEvent(3000),
+ audioRenderEvent(3200),
+ videoRenderEvent(3300),
+ videoFramesDroppedEvent(123456, 4),
+ videoFramesDroppedEvent(234567, 2),
+ onEndedEvent(10013000, 10),
+ mainThreadMarker,
+ compositorThreadMarker,
+ audioThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'time_to_video_play', 1);
+ checkCloseTo(histograms, 'time_to_audio_play', 1.2);
+ checkCloseTo(histograms, 'buffering_time', 10);
+ checkEqual(histograms, 'dropped_frame_count', 6);
+ });
+
+ // Scenario: Play audio from start to finish
+ test('mediaMetric_playAudioScenario', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ audioRenderEvent(1500),
+ onEndedEvent(10002500, 10),
+ mainThreadMarker,
+ audioThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ assert.isUndefined(histograms.getHistogramNamed('time_to_video_play'));
+ checkCloseTo(histograms, 'time_to_audio_play', 0.5);
+ checkCloseTo(histograms, 'buffering_time', 1);
+ assert.isUndefined(histograms.getHistogramNamed('dropped_frame_count'));
+ });
+
+ // Scenario: Play audio/video with two seeks
+ test('mediaMetric_seekScenario', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ doLoadEvent(1000),
+ videoRenderEvent(2000),
+ audioRenderEvent(2020),
+ videoRenderEvent(2040),
+ doSeekEvent(5000, 0.5),
+ seekedEvent(5200, 0.5),
+ bufferEnoughEvent(6000),
+ videoFramesDroppedEvent(123456, 4),
+ doSeekEvent(200000, 9),
+ seekedEvent(210000, 9),
+ bufferEnoughEvent(220000),
+ videoFramesDroppedEvent(234567, 2),
+ onEndedEvent(300000, 10),
+ mainThreadMarker,
+ compositorThreadMarker,
+ audioThreadMarker,
+ ];
+ tr.metrics.mediaMetric(histograms, makeModel(events));
+ checkCloseTo(histograms, 'time_to_video_play', 1);
+ checkCloseTo(histograms, 'time_to_audio_play', 1.02);
+ assert.isUndefined(histograms.getHistogramNamed('buffering_time'));
+ checkEqual(histograms, 'dropped_frame_count', 6);
+ checkCloseTo(histograms, 'pipeline_seek_time_0_5', 0.2);
+ checkCloseTo(histograms, 'seek_time_0_5', 1);
+ checkCloseTo(histograms, 'pipeline_seek_time_9', 10);
+ checkCloseTo(histograms, 'seek_time_9', 20);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html
new file mode 100644
index 00000000000..1b9ac12e1cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html
@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/all_metrics.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/mre/failure.html">
+<link rel="import" href="/tracing/mre/function_handle.html">
+<link rel="import" href="/tracing/value/diagnostics/reserved_names.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics', function() {
+ /**
+ * @param {!tr.model.Model} model
+ * @param {!Object} options
+ * @param {!Array.<string>} options.metrics
+ * @param {!Function} addFailureCb
+ * @return {!tr.v.HistogramSet}
+ */
+ function runMetrics(model, options, addFailureCb) {
+ if (options === undefined) {
+ throw new Error('Options are required.');
+ }
+
+ const metricNames = options.metrics;
+ if (!metricNames) {
+ throw new Error('Metric names should be specified.');
+ }
+
+ const allMetricsStart = new Date();
+ const durationBreakdown = new tr.v.d.Breakdown();
+
+ const categories = getTraceCategories(model);
+
+ const histograms = new tr.v.HistogramSet();
+
+ histograms.createHistogram('trace_import_duration',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ model.stats.traceImportDurationMs, {
+ binBoundaries: tr.v.HistogramBinBoundaries.createExponential(
+ 1e-3, 1e5, 30),
+ description:
+ 'Duration that trace viewer required to import the trace',
+ summaryOptions: tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS,
+ });
+
+ for (const metricName of metricNames) {
+ const metricStart = new Date();
+ const metric = tr.metrics.MetricRegistry.findTypeInfoWithName(metricName);
+ if (metric === undefined) {
+ throw new Error(`"${metricName}" is not a registered metric.`);
+ }
+ validateTraceCategories(metric.metadata.requiredCategories, categories);
+ try {
+ metric.constructor(histograms, model, options);
+ } catch (e) {
+ const err = tr.b.normalizeException(e);
+ addFailureCb(new tr.mre.Failure(
+ undefined, 'metricMapFunction', model.canonicalUrl, err.typeName,
+ err.message, err.stack));
+ }
+ const metricMs = new Date() - metricStart;
+ histograms.createHistogram(
+ metricName + '_duration',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [metricMs]);
+ durationBreakdown.set(metricName, metricMs);
+ }
+
+ validateDiagnosticNames(histograms);
+
+ const allMetricsMs = new Date() - allMetricsStart +
+ model.stats.traceImportDurationMs;
+ durationBreakdown.set('traceImport', model.stats.traceImportDurationMs);
+ durationBreakdown.set('other', allMetricsMs - tr.b.math.Statistics.sum(
+ durationBreakdown, ([metricName, metricMs]) => metricMs));
+ const breakdownNames = tr.v.d.RelatedNameMap.fromEntries(new Map(
+ metricNames.map(metricName => [metricName, metricName + '_duration'])));
+ breakdownNames.set('traceImport', 'trace_import_duration');
+ histograms.createHistogram(
+ 'metrics_duration',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [
+ {
+ value: allMetricsMs,
+ diagnostics: {breakdown: durationBreakdown},
+ },
+ ], {
+ diagnostics: {breakdown: breakdownNames},
+ });
+
+ return histograms;
+ }
+
+ function getTraceCategories(model) {
+ for (const metadata of model.metadata) {
+ let config;
+ if (metadata.name === 'TraceConfig' && metadata.value) {
+ config = metadata.value;
+ }
+ if (metadata.name === 'metadata' && metadata.value &&
+ metadata.value['trace-config'] &&
+ metadata.value['trace-config'] !== '__stripped__') {
+ config = JSON.parse(metadata.value['trace-config']);
+ }
+ if (config) {
+ return {
+ excluded: config.excluded_categories || [],
+ included: config.included_categories || [],
+ };
+ }
+ }
+ }
+
+ 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}"`);
+ }
+ }
+ }
+
+ /**
+ * Ensure that metrics don't use reserved diagnostic names.
+ *
+ * @param {!tr.v.HistogramSet} histograms
+ */
+ 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}"`);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.Model} model
+ */
+ 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}`);
+ }
+ histograms.addSharedDiagnosticToAllHistograms(name, new type(value));
+ }
+ }
+ }
+
+ /**
+ * @param {!tr.mre.MreResult} result
+ * @param {!tr.model.Model} model
+ * @param {!Object} options
+ * @param {!Array.<string>} options.metrics
+ */
+ 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,
+ });
+ }
+ }
+ result.addPair('scalars', scalarDicts);
+ }
+
+ tr.mre.FunctionRegistry.register(metricMapFunction);
+
+ return {
+ metricMapFunction,
+ runMetrics,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html
new file mode 100644
index 00000000000..5be41925424
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/metric_map_function.html">
+<link rel="import" href="/tracing/metrics/sample_metric.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ test('metricMapTest', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const m = TestUtils.newModelWithEvents(JSON.stringify(events), {
+ shiftWorldToZero: false,
+ pruneEmptyContainers: false,
+ trackDetailedModelStats: true,
+ customizeModelCallback(m) {
+ const p1 = m.getOrCreateProcess(1);
+ const t2 = p1.getOrCreateThread(2);
+ t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ type: ThreadSlice,
+ name: 'some_slice',
+ start: 0, end: 10
+ }));
+ t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ type: ThreadSlice,
+ name: 'some_slice',
+ start: 20, end: 30
+ }));
+ }
+ });
+
+ assert.throw(function() {
+ const result = new tr.mre.MreResult();
+ tr.metrics.metricMapFunction(result, m, {});
+ }, Error, 'Metric names should be specified.');
+
+ assert.throw(function() {
+ const result = new tr.mre.MreResult();
+ tr.metrics.metricMapFunction(result, m, {'metrics': ['wrongMetric']});
+ }, Error, '"wrongMetric" is not a registered metric.');
+
+ const result = new tr.mre.MreResult();
+ tr.metrics.metricMapFunction(
+ result, m, {'metrics': ['sampleMetric']});
+ assert.property(result.pairs, 'histograms');
+ assert.strictEqual(result.pairs.histograms.length, 4);
+ assert.property(result.pairs, 'scalars');
+ assert.strictEqual(result.pairs.scalars.length, 19);
+ assert.lengthOf(result.failures, 0);
+ });
+
+ test('exceptionMetric', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const m = TestUtils.newModelWithEvents(JSON.stringify(events), {
+ shiftWorldToZero: false,
+ pruneEmptyContainers: false,
+ trackDetailedModelStats: true,
+ customizeModelCallback(m) {
+ const p1 = m.getOrCreateProcess(1);
+ const t2 = p1.getOrCreateThread(2);
+ t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ type: ThreadSlice,
+ name: 'some_slice',
+ start: 0, end: 10
+ }));
+ t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ type: ThreadSlice,
+ name: 'some_slice',
+ start: 20, end: 30
+ }));
+ }
+ });
+
+ const result = new tr.mre.MreResult();
+ tr.metrics.metricMapFunction(
+ result, m, {'metrics': ['sampleExceptionMetric']});
+ assert.property(result.pairs, 'histograms');
+ assert.strictEqual(result.pairs.histograms.length, 4);
+ assert.property(result.pairs, 'scalars');
+ assert.strictEqual(result.pairs.scalars.length, 19);
+ assert.lengthOf(result.failures, 1);
+ });
+
+ function invalidDiagnosticNameMetric(histograms, model) {
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [], {diagnostics: {
+ [tr.v.d.RESERVED_NAMES.BOTS]: new tr.v.d.GenericSet([]),
+ }});
+ }
+
+ tr.metrics.MetricRegistry.register(invalidDiagnosticNameMetric);
+
+ test('validateDiagnosticNames', function() {
+ const result = new tr.mre.MreResult();
+ const m = TestUtils.newModel();
+
+ assert.throw(function() {
+ tr.metrics.metricMapFunction(result, m, {
+ 'metrics': ['invalidDiagnosticNameMetric'],
+ });
+ }, Error, 'Illegal diagnostic name ' +
+ `"${tr.v.d.RESERVED_NAMES.BOTS}" on Histogram "a"`);
+ });
+
+ test('validateCanonicalUrl', function() {
+ const result = new tr.mre.MreResult();
+ const m = TestUtils.newModel(model => {
+ model.canonicalUrl = 'canonical url';
+ model.metadata = [{
+ value: {
+ telemetry: {
+ traceUrls: ['trace url'],
+ },
+ },
+ }];
+ });
+ assert.throw(function() {
+ tr.metrics.metricMapFunction(result, m, {metrics: ['sampleMetric']});
+ }, Error, 'canonicalUrl "canonical url" != traceUrl "trace url"');
+ });
+
+ function requiresDefaultCategoryMetric(histograms, model) {
+ }
+
+ tr.metrics.MetricRegistry.register(requiresDefaultCategoryMetric, {
+ requiredCategories: ['foo'],
+ });
+
+ function requiresDisabledCategoryMetric(histograms, model) {
+ }
+
+ tr.metrics.MetricRegistry.register(requiresDisabledCategoryMetric, {
+ requiredCategories: ['disabled-by-default-foo'],
+ });
+
+ test('validateRequiredCategories', function() {
+ const result = new tr.mre.MreResult();
+ let m = TestUtils.newModel(model => {
+ model.metadata = [{
+ name: 'metadata',
+ value: {
+ 'trace-config': JSON.stringify({
+ excluded_categories: ['foo'],
+ }),
+ },
+ }];
+ });
+
+ assert.throw(function() {
+ tr.metrics.metricMapFunction(result, m, {metrics:
+ ['requiresDefaultCategoryMetric']});
+ }, Error, 'Trace is missing required category "foo"');
+
+ m = TestUtils.newModel(model => {
+ model.metadata = [{
+ name: 'TraceConfig',
+ value: {
+ },
+ }];
+ });
+
+ assert.throw(function() {
+ tr.metrics.metricMapFunction(result, m, {metrics:
+ ['requiresDisabledCategoryMetric']});
+ }, Error, 'Trace is missing required category "disabled-by-default-foo"');
+ });
+
+ test('processStrippedConfig', function() {
+ const result = new tr.mre.MreResult();
+ const m = TestUtils.newModel(model => {
+ model.metadata = [{
+ name: 'metadata',
+ value: {
+ 'trace-config': '__stripped__'
+ },
+ }];
+ });
+ tr.metrics.metricMapFunction(
+ result, m, {'metrics': ['sampleMetric']});
+ assert.lengthOf(result.failures, 0);
+ });
+
+ test('metricMetrics', function() {
+ const model = new tr.Model();
+ // We can't customize the model the normal way using
+ // test_utils.newModel(customizeModelCallback) because that callback is run
+ // before the end of the import phase, so our import duration will be
+ // overwritten.
+ model.stats.traceImportDurationMs = 10;
+
+ const histograms = tr.metrics.runMetrics(
+ model, {'metrics': ['sampleMetric']});
+
+ assert.strictEqual(
+ histograms.getHistogramNamed('trace_import_duration').average, 10);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html
new file mode 100644
index 00000000000..05ea36dd12f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics', function() {
+ function MetricRegistry() {}
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.defaultMetadata = {};
+ tr.b.decorateExtensionRegistry(MetricRegistry, options);
+
+ function camelCaseToHackerString(camelCase) {
+ let hackerString = '';
+ for (const c of camelCase) {
+ const lowered = c.toLocaleLowerCase();
+ if (lowered === c) {
+ hackerString += c;
+ } else {
+ hackerString += '_' + lowered;
+ }
+ }
+ return hackerString;
+ }
+
+ function getCallStack() {
+ try {
+ throw new Error();
+ } catch (error) {
+ return error.stack;
+ }
+ }
+
+ function getPathsFromStack(stack) {
+ return stack.split('\n').map(line => {
+ line = line.replace(/^ */, '').split(':');
+ if (line.length < 4) return '';
+ return line[line.length - 3].split('/');
+ }).filter(x => x);
+ }
+
+ MetricRegistry.checkFilename = function(metricName, opt_metricPathForTest) {
+ if (metricName === 'runtimeStatsTotalMetric' ||
+ metricName === 'v8AndMemoryMetrics') {
+ // TODO(crbug.com/688342) Remove the runtimeStatsTotalMetric exception.
+ // TODO(3275) Remove the v8AndMemoryMetrics exception.
+ // https://github.com/catapult-project/catapult/issues/3275
+ 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;
+
+ // This filename is in paths[0]. If this file is not vulcanized, then the
+ // metric's filename is in paths[METRIC_STACK_INDEX]. If this file is
+ // vulcanized, then they are the same, and paths[METRIC_STACK_INDEX] is
+ // not the metric's filename.
+ 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].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.');
+ }
+
+ MetricRegistry.checkFilename(metric.name);
+ });
+
+ return {
+ MetricRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html
new file mode 100644
index 00000000000..c4123024ae6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('FindMetricByName', function() {
+ function aMetric(histograms, model) {
+ const n1 = new tr.v.Histogram('foo', tr.b.Unit.byName.count);
+ n1.addSample(1);
+ histograms.addHistogram(n1);
+ }
+ tr.metrics.MetricRegistry.register(aMetric);
+
+ function bMetric(histograms, model) {
+ const n1 = new tr.v.Histogram('foo', tr.b.Unit.byName.count);
+ const n2 = new tr.v.Histogram('bar', tr.b.Unit.byName.count);
+ n1.addSample(1);
+ n2.addSample(2);
+ histograms.addHistogram(n1);
+ histograms.addHistogram(n2);
+ }
+ tr.metrics.MetricRegistry.register(bMetric);
+
+ function cMetric(histograms, model) {
+ const n1 = new tr.v.Histogram('foo', tr.b.Unit.byName.count);
+ const n2 = new tr.v.Histogram('bar', tr.b.Unit.byName.count);
+ const n3 = new tr.v.Histogram('baz', tr.b.Unit.byName.count);
+ n1.addSample(1);
+ n2.addSample(2);
+ n3.addSample(3);
+ histograms.addHistogram(n1);
+ histograms.addHistogram(n2);
+ histograms.addHistogram(n3);
+ }
+ tr.metrics.MetricRegistry.register(cMetric);
+
+ const typeInfo = tr.metrics.MetricRegistry.findTypeInfoWithName(
+ 'bMetric');
+ assert.strictEqual(typeInfo.constructor, bMetric);
+ });
+
+ test('registerNonFunctionThrows', function() {
+ // Metrics must be functions.
+ assert.throws(function() {
+ tr.metrics.MetricRegistry.register('not a function');
+ });
+
+ // Metric names must end with "Metric" or "Metrics".
+ assert.throws(function() {
+ tr.metrics.MetricRegistry.register(function foo(histograms, model) {});
+ });
+
+ // Metrics take a HistogramSet and a Model and optionally an options
+ // dictionary.
+ assert.throws(function() {
+ tr.metrics.MetricRegistry.register(function fooMetric() {});
+ });
+
+ // Metrics take a HistogramSet and a Model and optionally an options
+ // dictionary.
+ assert.throws(function() {
+ tr.metrics.MetricRegistry.register(function fooMetric(a) {});
+ });
+ });
+
+ test('checkFilename', function() {
+ tr.metrics.MetricRegistry.checkFilename(
+ 'aMetric', ['metrics', 'a_metric.html']);
+ tr.metrics.MetricRegistry.checkFilename(
+ 'fooBarMetric', ['foo', 'bar_metric.html']);
+
+ // Expected fooMetric to be in a file named foo_metric.html; actual:
+ // foo/bar_metric.html
+ assert.throws(() => tr.metrics.MetricRegistry.checkFilename(
+ 'fooMetric', ['foo', 'bar_metric.html']));
+
+ // Expected fooMetric to be in a file named foo_metric.html; actual:
+ // cat/bar_metric.html
+ assert.throws(() => tr.metrics.MetricRegistry.checkFilename(
+ 'fooMetric', ['cat', 'bar_metric.html']));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py b/chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py
new file mode 100644
index 00000000000..b8cc2af8dde
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py
@@ -0,0 +1,75 @@
+# 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 os
+import sys
+
+from tracing.mre import function_handle
+from tracing.mre import gtest_progress_reporter
+from tracing.mre import map_runner
+from tracing.mre import file_handle
+from tracing.mre import job as job_module
+
+
+try:
+ StringTypes = basestring
+except NameError:
+ StringTypes = str
+
+
+_METRIC_MAP_FUNCTION_FILENAME = 'metric_map_function.html'
+
+_METRIC_MAP_FUNCTION_NAME = 'metricMapFunction'
+
+def _GetMetricsDir():
+ return os.path.dirname(os.path.abspath(__file__))
+
+def _GetMetricRunnerHandle(metrics):
+ assert isinstance(metrics, list)
+ for metric in metrics:
+ assert isinstance(metric, StringTypes)
+ metrics_dir = _GetMetricsDir()
+ metric_mapper_path = os.path.join(metrics_dir, _METRIC_MAP_FUNCTION_FILENAME)
+
+ modules_to_load = [function_handle.ModuleToLoad(filename=metric_mapper_path)]
+ options = {'metrics': metrics}
+ map_function_handle = function_handle.FunctionHandle(
+ modules_to_load, _METRIC_MAP_FUNCTION_NAME, options)
+
+ return job_module.Job(map_function_handle, None)
+
+def RunMetric(filename, metrics, extra_import_options=None,
+ report_progress=True, canonical_url=None):
+ filename_url = 'file://' + filename
+ if canonical_url is None:
+ canonical_url = filename_url
+ trace_handle = file_handle.URLFileHandle(canonical_url, filename_url)
+ result = RunMetricOnTraceHandles(
+ [trace_handle], metrics, extra_import_options, report_progress)
+ return result[canonical_url]
+
+def RunMetricOnTraceHandles(trace_handles, metrics, extra_import_options=None,
+ report_progress=True):
+ job = _GetMetricRunnerHandle(metrics)
+ with open(os.devnull, 'w') as devnull_f:
+ o_stream = sys.stdout
+ if not report_progress:
+ o_stream = devnull_f
+
+ runner = map_runner.MapRunner(
+ trace_handles, job, extra_import_options=extra_import_options,
+ progress_reporter=gtest_progress_reporter.GTestProgressReporter(
+ output_stream=o_stream))
+ map_results = runner.RunMapper()
+
+ return map_results
+
+def RunMetricOnTraces(filenames, metrics,
+ extra_import_options=None, report_progress=True):
+ trace_handles = []
+ for filename in filenames:
+ filename_url = 'file://' + filename
+ trace_handles.append(file_handle.URLFileHandle(filename_url, filename_url))
+
+ return RunMetricOnTraceHandles(trace_handles, metrics, extra_import_options,
+ report_progress)
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html
new file mode 100644
index 00000000000..e1ea92fd04a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains implementations of the following metrics.
+ *
+ * The addCpuUtilizationHistograms method can generate the following sets of
+ * metrics, dependeing on the input values for segments, shouldNormalize, and
+ * segmentCostFunc.
+ *
+ * thread_{thread group}_cpu_time_per_second
+ * =========================================
+ * segments: any set of interesting segments
+ * shouldNormalize: True
+ * segmentCostFunc: thread.getCpuTimeForRange
+ *
+ * This set of metrics show how much a thread group was busy during specific
+ * segments marked by a test. More precisely, it shows the average amount of CPU
+ * cores per seconds the thread group consumes during the segments.
+ *
+ * thread_{thread group}_cpu_time_per_frame
+ * ========================================
+ * segments: display compositor's frames
+ * shouldNormalize: False
+ * segmentCostFunc: thread.getCpuTimeForRange
+ *
+ * This set of metrics show the distribution of CPU usage of a thread
+ * group in each display compositor's frame.
+ *
+ * tasks_per_second_{thread group}
+ * ===============================
+ * segments: any set of interesting segments
+ * shouldNormalize: True
+ * segmentCostFunc: thread.getNumToplevelSlicesForRange
+ *
+ * This set of metrics show the average number of tasks per second of a thread
+ * group during specific segments marked by a test.
+ *
+ * tasks_per_frame_{thread group}
+ * ==============================
+ * segments: display compositor's frames
+ * shouldNormalize: False
+ * segmentCostFunc: thread.getNumToplevelSlicesForRange
+ *
+ * This set of metrics show the distribution of the number of task in each
+ * display compositor's frame of a thread group.
+ *
+ * Note: the CPU usage in all above-mentioned metrics, is approximated from
+ * top-level trace events in each thread; it does not come from the OS. So, the
+ * metric may be noisy and not be very meaningful for threads that do not have a
+ * message loop.
+ */
+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(
+ 'total_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 !== '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, 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);
+ }
+
+ // Add histogram breakdowns.
+ 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();
+ // Compute and store CPU times per categories and thread name.
+ 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 (!breakdowns.has(category)) {
+ breakdowns.set(category, new tr.v.d.Breakdown());
+ }
+ // TODO(chiniforooshan): We break down the CPU usage of each category
+ // by the thread name here. It will be more useful if we could add
+ // task names too. On the other hand, breaking down at task level may
+ // be too granular and we may end up with a ton of tiny slices that
+ // will not be that useful. Maybe we can break down by just top x
+ // tasks, or top x (thread, task) pairs?
+ //
+ // Another possbility to investigate is to break down by initiator
+ // type of the animation expectation.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html
new file mode 100644
index 00000000000..3f58d2868f0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html
@@ -0,0 +1,280 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/cpu_utilization.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('cpuPerSecond', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // A slice during the animation with CPU duration 2.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 }));
+ // A slice after the animation.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 }));
+
+
+ const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0);
+ rendererMain.name = 'CrRendererMain';
+ // A slice half of which intersects with the animation.
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 }));
+
+ const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererIO.name = 'Chrome_ChildIOThread';
+ rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addCpuUtilizationHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)], true,
+ (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange),
+ category => `thread_${category}_cpu_time_per_second`, 'description');
+
+ // Verify the browser and renderer main threads and IO threads CPU usage.
+ let hist = histograms.getHistogramNamed(
+ 'thread_browser_cpu_time_per_second');
+ assert.closeTo(0.2, hist.min, 1e-6);
+ assert.closeTo(0.2, hist.max, 1e-6);
+ assert.closeTo(0.2, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed(
+ 'thread_renderer_main_cpu_time_per_second');
+ assert.closeTo(0.4, hist.min, 1e-6);
+ assert.closeTo(0.4, hist.max, 1e-6);
+ assert.closeTo(0.4, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed('thread_IO_cpu_time_per_second');
+ assert.closeTo(0.1, hist.min, 1e-6);
+ assert.closeTo(0.1, hist.max, 1e-6);
+ assert.closeTo(0.1, hist.average, 1e-6);
+
+ // Also, verify fast_path threads, that includs IO threads and the browser
+ // main thread, but not the renderer main thread.
+ hist = histograms.getHistogramNamed(
+ 'thread_total_fast_path_cpu_time_per_second');
+ assert.closeTo(0.3, hist.min, 1e-6);
+ assert.closeTo(0.3, hist.max, 1e-6);
+ assert.closeTo(0.3, hist.average, 1e-6);
+
+ // Verify sum of all threads.
+ hist = histograms.getHistogramNamed('thread_total_all_cpu_time_per_second');
+ assert.closeTo(0.7, hist.min, 1e-6);
+ assert.closeTo(0.7, hist.max, 1e-6);
+ assert.closeTo(0.7, hist.average, 1e-6);
+ });
+
+ test('cpuPerFrame', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // A slice during the animation with CPU duration 2.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 }));
+ // A slice after the animation.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 }));
+
+
+ const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0);
+ rendererMain.name = 'CrRendererMain';
+ // A slice half of which intersects with the animation.
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 }));
+
+ const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererIO.name = 'Chrome_ChildIOThread';
+ rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addCpuUtilizationHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)], false,
+ (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange),
+ category => `thread_${category}_cpu_time_per_frame`, 'description');
+
+ // Verify the browser and renderer main threads and IO threads CPU usage.
+ let hist = histograms.getHistogramNamed(
+ 'thread_browser_cpu_time_per_frame');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed(
+ 'thread_renderer_main_cpu_time_per_frame');
+ assert.closeTo(4, hist.min, 1e-6);
+ assert.closeTo(4, hist.max, 1e-6);
+ assert.closeTo(4, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed('thread_IO_cpu_time_per_frame');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(1, hist.max, 1e-6);
+ assert.closeTo(1, hist.average, 1e-6);
+
+ // Also, verify fast_path threads, that includs IO threads and the browser
+ // main thread, but not the renderer main thread.
+ hist = histograms.getHistogramNamed(
+ 'thread_total_fast_path_cpu_time_per_frame');
+ assert.closeTo(3, hist.min, 1e-6);
+ assert.closeTo(3, hist.max, 1e-6);
+ assert.closeTo(3, hist.average, 1e-6);
+
+ // Verify sum of all threads.
+ hist = histograms.getHistogramNamed('thread_total_all_cpu_time_per_frame');
+ assert.closeTo(7, hist.min, 1e-6);
+ assert.closeTo(7, hist.max, 1e-6);
+ assert.closeTo(7, hist.average, 1e-6);
+ });
+
+ test('multipleSegments', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 8, end: 12, cpuStart: 2, cpuEnd: 4 }));
+
+
+ const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0);
+ rendererMain.name = 'CrRendererMain';
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 }));
+
+ const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererIO.name = 'Chrome_ChildIOThread';
+ rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addCpuUtilizationHistograms(
+ histograms, model,
+ [new tr.model.um.Segment(0, 5), new tr.model.um.Segment(5, 5)], false,
+ (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange),
+ category => `thread_${category}_cpu_time_per_frame`, 'description');
+
+ // The first slice is in the first segment, using 2ms of CPU. The rest are
+ // in the second segment, using 1 + 4 + 1 = 6ms of CPU.
+ const hist = histograms.getHistogramNamed(
+ 'thread_total_all_cpu_time_per_frame');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(6, hist.max, 1e-6);
+ assert.closeTo(4, hist.average, 1e-6);
+ });
+
+ test('otherThreads', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // A slice during the animation with CPU duration 2.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 }));
+ // A slice after the animation.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 }));
+
+
+ const thread1 = model.getOrCreateProcess(1).getOrCreateThread(0);
+ thread1.name = 'Thread1';
+ // A slice half of which intersects with the animation.
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 }));
+
+ const thread2 = model.getOrCreateProcess(1).getOrCreateThread(1);
+ thread2.name = 'Thread2';
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addCpuUtilizationHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)], false,
+ (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange),
+ category => `thread_${category}_cpu_time_per_frame`, 'description');
+
+ // Verify the browser and renderer main threads and IO threads CPU usage.
+ let hist = histograms.getHistogramNamed(
+ 'thread_browser_cpu_time_per_frame');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed('thread_other_cpu_time_per_frame');
+ assert.closeTo(5, hist.min, 1e-6);
+ assert.closeTo(5, hist.max, 1e-6);
+ assert.closeTo(5, hist.average, 1e-6);
+ });
+
+ test('tasksPerFrame', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // A slice during the animation with CPU duration 2.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 }));
+ // A slice after the animation.
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 }));
+
+
+ const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0);
+ rendererMain.name = 'CrRendererMain';
+ // A slice half of which intersects with the animation.
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 }));
+
+ const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererIO.name = 'Chrome_ChildIOThread';
+ rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addCpuUtilizationHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)], false,
+ (thread, segment) =>
+ thread.getNumToplevelSlicesForRange(segment.boundsRange),
+ category => `tasks_per_frame_${category}`, 'description');
+
+ // Verify the browser and renderer main threads and IO threads number of
+ // tasks.
+ let hist = histograms.getHistogramNamed('tasks_per_frame_browser');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(1, hist.max, 1e-6);
+ assert.closeTo(1, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed('tasks_per_frame_renderer_main');
+ assert.closeTo(0.5, hist.min, 1e-6);
+ assert.closeTo(0.5, hist.max, 1e-6);
+ assert.closeTo(0.5, hist.average, 1e-6);
+
+ hist = histograms.getHistogramNamed('tasks_per_frame_IO');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(1, hist.max, 1e-6);
+ assert.closeTo(1, hist.average, 1e-6);
+
+ // Also, verify fast_path threads, that includs IO threads and the browser
+ // main thread, but not the renderer main thread.
+ hist = histograms.getHistogramNamed('tasks_per_frame_total_fast_path');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+
+ // Verify sum of all threads.
+ hist = histograms.getHistogramNamed('tasks_per_frame_total_all');
+ assert.closeTo(2.5, hist.min, 1e-6);
+ assert.closeTo(2.5, hist.max, 1e-6);
+ assert.closeTo(2.5, hist.average, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html
new file mode 100644
index 00000000000..1c465423654
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html
@@ -0,0 +1,272 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/metrics/rendering/cpu_utilization.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains implementations of the following metrics.
+ *
+ * frame_times
+ * ===========
+ * The distribution of durations between consecutive display compositor's swap
+ * buffer calls, or DRM page flips on ChromeOS devices, during animations.
+ *
+ * percentage_smooth
+ * =================
+ * The percentage of frame_times that are less than 17ms.
+ *
+ * TODO(chiniforooshan): This metric weighs all frames equally. So, e.g.
+ * percentage_smooth is lower (worse) if we have 10 100ms-frames instead of 5
+ * 1s-frames. I think it makes more sense to compute the percentage of
+ * non-smooth time during animation.
+ *
+ * frame_lengths (Android only)
+ * ============================
+ * frame_times in vsyncs instead of milli-seconds. In other words, frame_lengths
+ * is the distribution of durations between consecutive display compositor's
+ * swap buffer calls, in terms of vsyncs. Short frames (ones shorter than half
+ * of the refresh period) are filtered out, unlike in frame_times.
+ *
+ * avg_surface_fps (Android only)
+ * ==============================
+ * Average number of frames, ignoring short frames, per second during
+ * animations.
+ *
+ * jank_count (Android only)
+ * =========================
+ * The number of times that frame lengths are increased, during animations. For
+ * example, if frame lengths are 1, 1, 1, 2, 3, 1, 1 vsyncs, then jank_count
+ * will be 2 (1 -> 2 and 2 -> 3).
+ *
+ * ui_frame_times
+ * ==============
+ * The distribution of durations between consecutive UI compositor frame's
+ * presentation times, during UI animations. In ChromeOS, if Ash uses its own
+ * instance of ui::compositor, then frames submitted by that compositor will be
+ * used. Otherwise, frames submitted by the browser's ui::compositor will be
+ * used.
+ *
+ * TODO(crbug.com/896231): we may want to consider reporting frame_times for all
+ * ui::compositors separately, e.g. as ash_frame_times and browser_frame_times.
+ *
+ * ui_percentage_smooth
+ * ====================
+ * The percentage of ui_frame_times that are less than 17ms.
+ */
+tr.exportTo('tr.metrics.rendering', function() {
+ // Various tracing events.
+ const DISPLAY_EVENT = 'BenchmarkInstrumentation::DisplayRenderingStats';
+ const DRM_EVENT = 'DrmEventFlipComplete';
+ const SURFACE_FLINGER_EVENT = 'vsync_before';
+ const COMPOSITOR_FRAME_PRESENTED_EVENT = 'FramePresented';
+
+ // In computing frame_lengths, avg_surface_fps, and jank_count, frames that
+ // are shorter than half a vsync are ignored.
+ const MIN_FRAME_LENGTH = 0.5;
+
+ // In computing the number of janks, frame length differences that are at
+ // least PAUSE_THRESHOLD vsyncs are considered pauses, not janks.
+ const PAUSE_THRESHOLD = 20;
+
+ const ASH_ENVIRONMENT = 'ash';
+ const BROWSER_ENVIRONMENT = 'browser';
+
+ function getDisplayCompositorPresentationEvents_(modelHelper) {
+ if (!modelHelper || !modelHelper.browserProcess) return [];
+ // On ChromeOS, DRM events, if they exist, are the source of truth. On
+ // Android, Surface Flinger events are the source of truth. Otherwise, look
+ // for display rendering stats. With viz, display rendering stats are
+ // emitted from the GPU process; otherwise, they are emitted from the
+ // browser process.
+ 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)) {
+ // For chrome versions before crrev.com/c/1282039.
+ 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) {
+ // We use filterArray for the sake of a cleaner code. The time complexity
+ // will be O(m + n log m), where m is |timestamps| and n is |segments|.
+ // Alternatively, we could directly loop through the timestamps and segments
+ // here for a slightly better time complexity of O(m + n).
+ 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));
+ }
+ }
+ return frameSegments;
+ }
+
+ function addBasicFrameTimeHistograms_(histograms, frameSegments, prefix) {
+ // TODO(chiniforooshan): Figure out what kind of break down makes sense
+ // here. Perhaps break down by tasks in the Viz/Browser process?
+ 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;
+
+ // Presentation Timestamps should be sorted.
+ 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, 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);
+
+ // If Surface Flinger information is captured, add Surface Flinger
+ // histograms.
+ 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;
+
+ // Presentation Timestamps should be sorted.
+ const timestamps = events.map(event => [event.start, event.start]);
+ const frameSements = computeFrameSegments_(timestamps, segments);
+ addBasicFrameTimeHistograms_(histograms, frameSements, 'ui_');
+ }
+
+ return {
+ addFrameTimeHistograms,
+ addUIFrameTimeHistograms,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html
new file mode 100644
index 00000000000..0304a5cc0c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html
@@ -0,0 +1,301 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/frame_time.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('frameTimes', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // Add four swap buffer events, at times 1, 2, 19, 21
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 1, end: 1 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 2, end: 2 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 19, end: 19 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 21, end: 21 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 20)]);
+
+ // The fourth frame is outside the interaction perdiod and should be
+ // discarded. The durations between the remaining three frames are 1 and 17.
+ let hist = histograms.getHistogramNamed('frame_times');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(17, hist.max, 1e-6);
+ assert.closeTo(9, hist.average, 1e-6);
+
+ // One of the two frame times is not smooth.
+ hist = histograms.getHistogramNamed('percentage_smooth');
+ assert.closeTo(50, hist.min, 1e-6);
+ assert.closeTo(50, hist.max, 1e-6);
+ assert.closeTo(50, hist.average, 1e-6);
+ });
+
+ test('frameTimes_drmEvents', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+
+ const gpuMain = model.getOrCreateProcess(1).getOrCreateThread(0);
+ gpuMain.name = 'CrGpuMain';
+ gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'DrmEventFlipComplete', start: 1, end: 1,
+ args: { data: { 'vblank.tv_sec': 0, 'vblank.tv_usec': 1000 } } }));
+ gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'DrmEventFlipComplete', start: 3, end: 3,
+ args: { data: { 'vblank.tv_sec': 0, 'vblank.tv_usec': 2000 } } }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 6)]);
+
+ // When computing frame times from DRM events, VBlank times should be used.
+ const hist = histograms.getHistogramNamed('frame_times');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(1, hist.max, 1e-6);
+ assert.closeTo(1, hist.average, 1e-6);
+ });
+
+ test('frameTimes_surfaceFlingerEvents', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 1, end: 1 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 2, end: 2 }));
+
+ const surfaceFlingerProcess = model.getOrCreateProcess(2);
+ surfaceFlingerProcess.name = 'SurfaceFlinger';
+ const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2);
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 1, end: 1}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 3, end: 3}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 6)]);
+
+ // Data from the Surface Flinger process should be used if it exists.
+ const hist = histograms.getHistogramNamed('frame_times');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+ });
+
+ test('frameLengths', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ model.metadata = [{
+ name: 'metadata',
+ value: {
+ surface_flinger: {
+ refresh_period: 3,
+ },
+ },
+ }];
+
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+
+ const surfaceFlingerProcess = model.getOrCreateProcess(2);
+ surfaceFlingerProcess.name = 'SurfaceFlinger';
+ const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2);
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 1, end: 1}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 4, end: 4}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 10, end: 10}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 13, end: 13}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 14, end: 14}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 20)]);
+
+ // Frame lengths are 3/3, 6/3, 3/3, and 1/3. The last one is too small and
+ // should be filtered out. So, the final result is [1, 2, 1].
+ const hist = histograms.getHistogramNamed('frame_lengths');
+ assert.closeTo(3, hist.numValues, 1e-6);
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(4, hist.sum, 1e-6);
+ });
+
+ test('avgSurfaceFPS', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ model.metadata = [{
+ name: 'metadata',
+ value: {
+ surface_flinger: {
+ refresh_period: 3,
+ },
+ },
+ }];
+
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+
+ const surfaceFlingerProcess = model.getOrCreateProcess(2);
+ surfaceFlingerProcess.name = 'SurfaceFlinger';
+ const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2);
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 1, end: 1}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 4, end: 4}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 10, end: 10}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 13, end: 13}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 14, end: 14}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 20)]);
+
+ // We have 3 frames (ignoring the very short ones) in 13 milliseconds.
+ const hist = histograms.getHistogramNamed('avg_surface_fps');
+ assert.closeTo(1, hist.numValues, 1e-6);
+ assert.closeTo(3 / 0.013, hist.min, 1e-6);
+ });
+
+ test('jankCount', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ model.metadata = [{
+ name: 'metadata',
+ value: {
+ surface_flinger: {
+ refresh_period: 3,
+ },
+ },
+ }];
+
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+
+ const surfaceFlingerProcess = model.getOrCreateProcess(2);
+ surfaceFlingerProcess.name = 'SurfaceFlinger';
+ const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2);
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 1, end: 1}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 4, end: 4}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 7, end: 7}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 13, end: 13}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 16, end: 16}));
+ surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'vsync_before', start: 79, end: 79}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 100)]);
+
+ // There is 1 jank in [1, 1, 2, 1, 21]. The last long frame is a pause.
+ const hist = histograms.getHistogramNamed('jank_count');
+ assert.closeTo(1, hist.numValues, 1e-6);
+ assert.closeTo(1, hist.min, 1e-6);
+ });
+
+ test('uiFrameTimes', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // Add four swap buffer events, at times 1, 2, 19, 21
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'FramePresented', start: 1, end: 1 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'FramePresented', start: 2, end: 2 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'FramePresented', start: 19, end: 19 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'FramePresented', start: 21, end: 21 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addUIFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 20)]);
+
+ // The fourth frame is outside the interaction perdiod and should be
+ // discarded. The durations between the remaining three frames are 1 and 17.
+ let hist = histograms.getHistogramNamed('ui_frame_times');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(17, hist.max, 1e-6);
+ assert.closeTo(9, hist.average, 1e-6);
+
+ // One of the two frame times is not smooth.
+ hist = histograms.getHistogramNamed('ui_percentage_smooth');
+ assert.closeTo(50, hist.min, 1e-6);
+ assert.closeTo(50, hist.max, 1e-6);
+ assert.closeTo(50, hist.average, 1e-6);
+ });
+
+ test('uiFrameTimesWithEnvironmentArg', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ // Add four swap buffer events, at times 1, 2, 3, 19
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'FramePresented',
+ start: 1,
+ end: 1,
+ args: { environment: 'ash' }
+ }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'FramePresented',
+ start: 2,
+ end: 2,
+ args: { environment: 'ash' }
+ }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'FramePresented',
+ start: 3,
+ end: 3,
+ args: { environment: 'browser' }
+ }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'FramePresented',
+ start: 19,
+ end: 19,
+ args: { environment: 'ash' }
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addUIFrameTimeHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 20)]);
+
+ // The 3rd frame is from 'browser' and should be discarded. Remaining
+ // timestamps are 1, 2, and 19 which indicate frame times of 1 and 17.
+ const hist = histograms.getHistogramNamed('ui_frame_times');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(17, hist.max, 1e-6);
+ assert.closeTo(2, hist.numValues, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html
new file mode 100644
index 00000000000..cbd691dacf3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/model/async_slice_group.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains implementations of the following metrics.
+ *
+ * input_event_latency
+ * ===================
+ * The distribution of durations between the time when an input event is
+ * generated to the time when the GPU service swaps buffers due to the input
+ * event. The time when an input is generated is the first of the following
+ * three timestamps that we can get:
+ *
+ * 1. Original kernel timestamp of the input event.
+ * 2. Timestamp when the UI event is created.
+ * 3. Timestamp when the input event is sent from RenderWidgetHost to the
+ * renderer.
+ *
+ * main_thread_scroll_latency
+ * ==========================
+ * The distribution of durations between the time when the main thread scroll
+ * listener update is begun to the time when the GPU service swaps buffers due
+ * to the scroll event.
+ *
+ * first_gesture_scroll_update_latency
+ * ===================================
+ * This shows the latency, as defined in input_event_latency, of the first
+ * gesture scroll update input event. The first event can often get delayed by
+ * work related to page loading.
+ */
+tr.exportTo('tr.metrics.rendering', function() {
+ // Interesting latency info component names.
+ 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* 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((x, y) => x[0] - y[0]);
+ 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.' });
+ }
+ }
+
+ return {
+ addLatencyHistograms,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html
new file mode 100644
index 00000000000..4a68b8e4655
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/latency.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('inputEventLatency', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browser = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browser.name = 'CrBrowserMain';
+ browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ start: 9, end: 10,
+ args: {
+ data: {
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10000},
+ INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 9000}
+ }
+ }
+ }));
+ browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ start: 7, end: 8,
+ args: {
+ data: {
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 8000},
+ INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 7000},
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {time: 6000},
+ }
+ }
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addLatencyHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)]);
+
+ // The first input latency is 10 - 9 = 1. The second input latency is
+ // 8 - 6 = 2.
+ const hist = histograms.getHistogramNamed('input_event_latency');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(1.5, hist.average, 1e-6);
+ });
+
+ test('mainThreadScrollLatency', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ model.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain';
+ const renderer = model.getOrCreateProcess(1).getOrCreateThread(0);
+ renderer.name = 'CrRendererMain';
+ renderer.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ title: 'Latency::ScrollUpdate',
+ start: 9, end: 10,
+ args: {
+ data: {
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10000},
+ LATENCY_BEGIN_SCROLL_LISTENER_UPDATE_MAIN_COMPONENT: {time: 9000}
+ }
+ }
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addLatencyHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)]);
+
+ const hist = histograms.getHistogramNamed('main_thread_scroll_latency');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(1, hist.max, 1e-6);
+ assert.closeTo(1, hist.average, 1e-6);
+ });
+
+ test('firstGestureScrollUpdateLatency', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browser = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browser.name = 'CrBrowserMain';
+ browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ start: 10, end: 11,
+ args: {
+ data: {
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10000},
+ INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 9000}
+ }
+ }
+ }));
+ browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ start: 7, end: 8,
+ args: {
+ data: {
+ INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 8000},
+ INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 7000},
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {time: 6000},
+ }
+ }
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addLatencyHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 20)]);
+
+ // The chronologically first gesture scroll update latency is 8 - 6 = 2.
+ const hist = histograms.getHistogramNamed(
+ 'first_gesture_scroll_update_latency');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 1e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html
new file mode 100644
index 00000000000..1b68e5d6a31
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains implementations of the following metrics.
+ *
+ * TODO(crbug.com/872334): document pipeline:* metrics here.
+ */
+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)) {
+ // It is possible for a client to submit multiple compositor frames for
+ // one begin-message. So most steps can be present multiple times.
+ // However, a begin-frame is issued only once, and received only once. So
+ // these steps should not be repeated.
+ if (event.args.step === 'IssueBeginFrame' ||
+ event.args.step === 'ReceiveBeginFrame') {
+ throw new Error('Unexpected duplicate step: ' + event.args.step);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ function generateBreakdownForCompositorPipelineInClient_(flow) {
+ const breakdown = new tr.v.d.Breakdown();
+ breakdown.set('time before GenerateRenderPass',
+ flow.GenerateRenderPass.start - flow.ReceiveBeginFrame.start);
+ breakdown.set('GenerateRenderPass duration',
+ flow.GenerateRenderPass.duration);
+ breakdown.set('GenerateCompositorFrame duration',
+ flow.GenerateCompositorFrame.duration);
+ breakdown.set('SubmitCompositorFrame duration',
+ flow.SubmitCompositorFrame.duration);
+ return breakdown;
+ }
+
+ function generateBreakdownForCompositorPipelineInService_(flow) {
+ const breakdown = new tr.v.d.Breakdown();
+ breakdown.set('Processing CompositorFrame on reception',
+ flow.ReceiveCompositorFrame.duration);
+ breakdown.set('Delay before SurfaceAggregation',
+ flow.SurfaceAggregation.start - flow.ReceiveCompositorFrame.end);
+ breakdown.set('SurfaceAggregation duration',
+ flow.SurfaceAggregation.duration);
+ return breakdown;
+ }
+
+ function generateBreakdownForDraw_(drawEvent) {
+ const breakdown = new tr.v.d.Breakdown();
+ for (const slice of drawEvent.subSlices) {
+ breakdown.set(slice.title, slice.duration);
+ }
+ 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;
+ 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 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()) {
+ // Report only for the cases that go through the complete pipeline.
+ if (!flow.IssueBeginFrame || !flow.ReceiveBeginFrame ||
+ !flow.SubmitCompositorFrame || !flow.SurfaceAggregation) {
+ continue;
+ }
+
+ const sourceFrameNumber = flow.SubmitCompositorFrame.parentSlice
+ .args.source_frame_number_;
+
+ const rasterDuration =
+ getRasterTaskTimes(sourceFrameNumber, model);
+
+ 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 (rasterDuration.start && rasterDuration.end) {
+ const receiveToStart = rasterDuration.start -
+ flow.ReceiveBeginFrame.start;
+ const receiveToEnd = rasterDuration.end - flow.ReceiveBeginFrame.end;
+
+ // receiveToStart can be negative if the earliest raster task for
+ // a frame starts before receiveBeginFrame starts.
+ // The same is true for receiveToEnd
+ // Only positive samples are added.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html
new file mode 100644
index 00000000000..332767c0e79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html
@@ -0,0 +1,366 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/pipeline.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function addPipelineForOneFrame(compositor, renderer, rasterWorker,
+ id, frame, displayTrace) {
+ const EVENT_NAME = 'Graphics.Pipeline';
+ if (frame.IssueBeginFrame) {
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.IssueBeginFrame, duration: 1, bindId: id,
+ args: {step: 'IssueBeginFrame'}}));
+ }
+ if (frame.ReceiveBeginFrame) {
+ renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.ReceiveBeginFrame, duration: 1, bindId: id,
+ args: {step: 'ReceiveBeginFrame'}}));
+ }
+ renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.GenerateRenderPass, duration: 1, bindId: id,
+ args: {step: 'GenerateRenderPass'}}));
+ renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.GenerateCompositorFrame, duration: 1, bindId: id,
+ args: {step: 'GenerateCompositorFrame'}}));
+ renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.SubmitCompositorFrame, duration: 1, bindId: id,
+ args: {source_frame_number_: id}}));
+ renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.SubmitCompositorFrame, duration: 1, bindId: id,
+ args: {step: 'SubmitCompositorFrame'}}));
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.ReceiveCompositorFrame, duration: 1, bindId: id,
+ args: {step: 'ReceiveCompositorFrame'}}));
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: EVENT_NAME,
+ start: frame.SurfaceAggregation, duration: 1, bindId: id,
+ args: {step: 'SurfaceAggregation', display_trace: displayTrace}}));
+ renderer.sliceGroup.createSubSlices();
+ rasterWorker.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'TaskGraphRunner::RunTask',
+ start: frame.rasterTaskStart, duration: 6, bindId: id,
+ args: {source_frame_number_: id}}));
+ }
+
+ function addDrawSlice(compositor, displayTrace, start, steps, opt_args) {
+ const EVENT_NAME = 'Graphics.Pipeline.DrawAndSwap';
+ let totalDuration = 0;
+ for (const duration of Object.values(steps)) {
+ totalDuration += duration;
+ }
+ const slice = tr.c.TestUtils.newAsyncSliceNamed(
+ EVENT_NAME, start, totalDuration);
+ slice.id = ':ptr:' + displayTrace;
+ slice.args = opt_args;
+ compositor.sliceGroup.pushSlice(slice);
+ totalDuration = 0;
+ for (const step in steps) {
+ slice.subSlices.push(tr.c.TestUtils.newAsyncSliceNamed(
+ step, start + totalDuration, steps[step]));
+ totalDuration += steps[step];
+ }
+ }
+
+ test('graphicsPipeline', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ const rendererCompositor =
+ model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererCompositor.name = 'Compositor';
+
+ // Creates a renderer thread
+ const renderer = model.getOrCreateProcess(2).getOrCreateThread(2);
+ renderer.name = 'CrRendererMain';
+ renderer.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5);
+ rasterWorker.name = 'CompositorTileWorker';
+
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 1, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 6, SurfaceAggregation: 10,
+ rasterTaskStart: 2
+ });
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 2, {
+ IssueBeginFrame: 15, ReceiveBeginFrame: 16,
+ GenerateRenderPass: 17, GenerateCompositorFrame: 18,
+ SubmitCompositorFrame: 19, ReceiveCompositorFrame: 20,
+ SurfaceAggregation: 21, rasterTaskStart: 22
+ });
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 3, {
+ IssueBeginFrame: 32, ReceiveBeginFrame: 34,
+ GenerateRenderPass: 35, GenerateCompositorFrame: 36,
+ SubmitCompositorFrame: 37, SubmitCompositorFrame: 38,
+ ReceiveCompositorFrame: 41, SurfaceAggregation: 44,
+ rasterTaskStart: 25
+ });
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPipelineHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 50)]);
+
+
+ const beginFrameTransport = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_transport');
+ const frameSubmission = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_frame_submission');
+ const surfaceAggregation = histograms.getHistogramNamed(
+ 'pipeline:frame_submission_to_display');
+ const rasterStart = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_raster_start');
+ const rasterEnd = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_raster_end');
+ assert.closeTo(beginFrameTransport.average, 4 / 3, 1e-6);
+ assert.closeTo(frameSubmission.average, 13 / 3, 1e-6);
+ assert.closeTo(surfaceAggregation.average, 13 / 3, 1e-6);
+ assert.closeTo(rasterStart.average, 6 / 2, 1e-6);
+ assert.closeTo(rasterEnd.average, 16 / 2, 1e-6);
+ });
+
+ test('graphicsPipeline_duplicateSteps', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ const rendererCompositor =
+ model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererCompositor.name = 'Compositor';
+
+ // Creates a renderer thread
+ const renderer = model.getOrCreateProcess(2).getOrCreateThread(2);
+ renderer.name = 'CrRendererMain';
+ renderer.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5);
+ rasterWorker.name = 'CompositorTileWorker';
+
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 1, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 6, SurfaceAggregation: 9,
+ rasterTaskStart: 3
+ });
+
+ // Add duplicate steps for SubmitCompositorFrame and the subsequent
+ // steps for the same trace-id.
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 1, {
+ GenerateRenderPass: 10, GenerateCompositorFrame: 11,
+ SubmitCompositorFrame: 12, ReceiveCompositorFrame: 15,
+ SurfaceAggregation: 18, rasterTaskStart: 3
+ });
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPipelineHistograms(
+ histograms, model,
+ [new tr.model.um.Segment(0, 50), new tr.model.um.Segment(0, 20)]);
+
+ const beginFrameTransport = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_transport');
+ const frameSubmission = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_frame_submission');
+ const surfaceAggregation = histograms.getHistogramNamed(
+ 'pipeline:frame_submission_to_display');
+ const rasterStart = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_raster_start');
+ const rasterEnd = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_raster_end');
+ assert.strictEqual(beginFrameTransport.average, 1);
+ assert.strictEqual(frameSubmission.average, 4);
+ assert.strictEqual(surfaceAggregation.average, 4);
+ assert.strictEqual(rasterStart.average, 1);
+ assert.strictEqual(rasterEnd.average, 6);
+ });
+
+ test('graphicsPipeline_duplicateRenderers', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ const rendererCompositor =
+ model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererCompositor.name = 'Compositor';
+
+ // Creates a renderer thread
+ const rendererWithIR = model.getOrCreateProcess(2).getOrCreateThread(2);
+ rendererWithIR.name = 'CrRendererMain';
+ rendererWithIR.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ const rasterWorkerWithIR = model.getOrCreateProcess(2)
+ .getOrCreateThread(5);
+ rasterWorkerWithIR.name = 'CompositorTileWorker';
+
+ const rendererWithoutIR = model.getOrCreateProcess(3)
+ .getOrCreateThread(2);
+ rendererWithoutIR.name = 'CrRendererMain';
+
+ const rasterWorkerWithoutIR = model.getOrCreateProcess(3)
+ .getOrCreateThread(5);
+ rasterWorkerWithoutIR.name = 'CompositorTileWorker';
+
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorkerWithIR, 1, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 6, SurfaceAggregation: 10,
+ rasterTaskStart: 3
+ });
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorkerWithoutIR, 2, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 6, SurfaceAggregation: 10,
+ rasterTaskStart: 4
+ });
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPipelineHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 50)]);
+
+ const rasterStart = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_raster_start');
+ const rasterEnd = histograms.getHistogramNamed(
+ 'pipeline:begin_frame_to_raster_end');
+ assert.strictEqual(rasterStart.average, 1);
+ assert.strictEqual(rasterEnd.average, 6);
+ });
+
+ test('graphicsPipeline_drawSteps', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ const rendererCompositor =
+ model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererCompositor.name = 'Compositor';
+
+ // Creates a renderer thread
+ const renderer = model.getOrCreateProcess(2).getOrCreateThread(2);
+ renderer.name = 'CrRendererMain';
+ renderer.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5);
+ rasterWorker.name = 'CompositorTileWorker';
+
+ const displayTrace = '1';
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 1, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 6, SurfaceAggregation: 9,
+ rasterTaskStart: 3
+ }, displayTrace);
+
+ addDrawSlice(browserMain, displayTrace, 10,
+ {Draw: 2, Swap: 1, WaitForAck: 5}, {});
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPipelineHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 50)]);
+
+ const drawHistogram = histograms.getHistogramNamed('pipeline:draw');
+ assert.strictEqual(drawHistogram.average, 8);
+ });
+
+ test('graphicsPipeline_drawCanceled', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ const rendererCompositor =
+ model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererCompositor.name = 'Compositor';
+
+ // Creates a renderer thread
+ const renderer = model.getOrCreateProcess(2).getOrCreateThread(2);
+ renderer.name = 'CrRendererMain';
+ renderer.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5);
+ rasterWorker.name = 'CompositorTileWorker';
+
+ const displayTrace = '1';
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 1, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 6, SurfaceAggregation: 9,
+ rasterTaskStart: 3
+ }, displayTrace);
+
+ addDrawSlice(browserMain, displayTrace, 10,
+ {Draw: 2, Swap: 1, WaitForAck: 5}, {status: 'canceled'});
+ addDrawSlice(browserMain, displayTrace, 15,
+ {Draw: 2, Swap: 1, WaitForAck: 5});
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPipelineHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 50)]);
+
+ const drawHistogram = histograms.getHistogramNamed('pipeline:draw');
+ assert.strictEqual(drawHistogram.average, 8);
+ });
+
+ test('graphicsPipeline_receiveCFAfterAnimation', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ const rendererCompositor =
+ model.getOrCreateProcess(1).getOrCreateThread(1);
+ rendererCompositor.name = 'Compositor';
+
+ // Creates a renderer thread
+ const renderer = model.getOrCreateProcess(2).getOrCreateThread(2);
+ renderer.name = 'CrRendererMain';
+ renderer.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5);
+ rasterWorker.name = 'CompositorTileWorker';
+
+ addPipelineForOneFrame(browserMain, rendererCompositor,
+ rasterWorker, 1, {
+ IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3,
+ GenerateCompositorFrame: 4, SubmitCompositorFrame: 5,
+ ReceiveCompositorFrame: 11, SurfaceAggregation: 15,
+ rasterTaskStart: 2
+ });
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPipelineHistograms(
+ histograms, model,
+ [new tr.model.um.Segment(0, 10), new tr.model.um.Segment(14, 16)]);
+
+ const surfaceAggregation = histograms.getHistogramNamed(
+ 'pipeline:frame_submission_to_display');
+ assert.strictEqual(surfaceAggregation.average, 10);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html
new file mode 100644
index 00000000000..7f3060561d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains implementations of the following metrics.
+ *
+ * mean_pixels_approximated
+ * ========================
+ * The percentage of tiles which are missing or of low or non-ideal resolution.
+ *
+ * TODO(crbug.com/875010): The documentation claims that
+ * mean_pixels_approximated should be greater than or equal to
+ * mean_pixels_checkerboarded. This claim is not consistent with numbers from
+ * the perf dashboard. We should either correct the documentations or fix the
+ * metrics.
+ *
+ * mean_pixels_checkerboarded
+ * ==========================
+ * The percentage of tiles which are only missing. It does not take into
+ * consideration tiles which are of low or non-ideal resolution.
+ */
+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) {
+ // TODO(crbug.com/877056): This is reported as an error in the legacy
+ // code, which indicates there should be no rendering stats event with
+ // zero visible content area. But, the TBMv2 implementation encounters
+ // such events. Maybe they are coming from OOPIFs which are ignored in
+ // the legacy code? Investigate why they exist and what should be
+ // done.
+ //
+ // throw new Error(`${VISIBLE_CONTENT_DATA} is zero`);
+ 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);
+ }
+ }
+ }
+
+ // TODO(crbug.com/892501): The averages are multiplied by 100 to match an
+ // error in legacy code so that we have continuity in graphs in the perf
+ // dashboard. We should fix historic data and remove the multiplication.
+ 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,
+ });
+ }
+
+ return {
+ addPixelsHistograms,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html
new file mode 100644
index 00000000000..10a77215c47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/pixels.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('pixelsApproximated', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ // Metric computation assumes that there is always a browser process.
+ model.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain';
+
+ const compositor = model.getOrCreateProcess(1).getOrCreateThread(1);
+ compositor.name = 'Compositor';
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'BenchmarkInstrumentation::ImplThreadRenderingStats',
+ start: 1, end: 1,
+ args: {
+ data: {
+ visible_content_area: 50,
+ approximated_visible_content_area: 8,
+ checkerboarded_visible_content_area: 3
+ }
+ }
+ }));
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'BenchmarkInstrumentation::ImplThreadRenderingStats',
+ start: 2, end: 2,
+ args: {
+ data: {
+ visible_content_area: 25,
+ approximated_visible_content_area: 6,
+ checkerboarded_visible_content_area: 5
+ }
+ }
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addPixelsHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)]);
+
+ // The mean of 8/50 and 6/25 is 0.2.
+ let hist = histograms.getHistogramNamed('mean_pixels_approximated');
+ assert.closeTo(20, hist.min, 1e-6);
+ assert.closeTo(20, hist.max, 1e-6);
+ assert.closeTo(20, hist.average, 1e-6);
+
+ // The mean of 3/50 and 5/25 is 0.13.
+ hist = histograms.getHistogramNamed('mean_pixels_checkerboarded');
+ assert.closeTo(13, hist.min, 1e-6);
+ assert.closeTo(13, hist.max, 1e-6);
+ assert.closeTo(13, hist.average, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html
new file mode 100644
index 00000000000..386bd444658
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains implementations of the following metrics.
+ *
+ * queueing_durations
+ * ==================
+ * This quantifies how out of sync the compositor and renderer threads are. It
+ * is the amount of wall time that elapses between a
+ * ScheduledActionSendBeginMainFrame event in the compositor thread and the
+ * corresponding BeginMainFrame event in the main thread.
+ *
+ * TODO(chiniforooshan): Does it make sense to just ignore data from OOPIF
+ * processes, like what we are doing here?
+ */
+tr.exportTo('tr.metrics.rendering', function() {
+ // Various tracing events.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html
new file mode 100644
index 00000000000..2ca870810dd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/queueing_duration.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('queueingDurations', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ // Metric computation assumes that there is always a browser process.
+ model.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain';
+
+ const compositor = model.getOrCreateProcess(1).getOrCreateThread(1);
+ compositor.name = 'Compositor';
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'ThreadProxy::ScheduledActionSendBeginMainFrame',
+ start: 3, end: 3,
+ args: { begin_frame_id: 2 }}));
+ compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'ThreadProxy::ScheduledActionSendBeginMainFrame',
+ start: 4, end: 4,
+ args: { begin_frame_id: 1 }}));
+
+ const main = model.getOrCreateProcess(1).getOrCreateThread(0);
+ main.name = 'CrRendererMain';
+ main.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'ThreadProxy::BeginMainFrame',
+ start: 5, end: 5,
+ args: { begin_frame_id: 1 }}));
+ main.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'ThreadProxy::BeginMainFrame',
+ start: 6, end: 6,
+ args: { begin_frame_id: 2 }}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.addQueueingDurationHistograms(
+ histograms, model, [new tr.model.um.Segment(0, 10)]);
+
+ const hist = histograms.getHistogramNamed('queueing_durations');
+ assert.closeTo(1, hist.min, 1e-6);
+ assert.closeTo(3, hist.max, 1e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html
new file mode 100644
index 00000000000..1024c9baa87
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/rendering/frame_time.html">
+<link rel="import" href="/tracing/metrics/rendering/latency.html">
+<link rel="import" href="/tracing/metrics/rendering/pipeline.html">
+<link rel="import" href="/tracing/metrics/rendering/pixels.html">
+<link rel="import" href="/tracing/metrics/rendering/queueing_duration.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.rendering', function() {
+ // Various tracing events.
+ 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);
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html
new file mode 100644
index 00000000000..77e0a2d9dd1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/rendering/rendering_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('renderingMetric_gestureIR', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0);
+ rendererMain.name = 'CrRendererMain';
+ rendererMain.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed(
+ 'Interaction.Gesture_LoadAction', 10, 10));
+
+ const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0);
+ browserMain.name = 'CrBrowserMain';
+ browserMain.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed(
+ 'SyntheticGestureController::running', 0, 5));
+ browserMain.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed(
+ 'SyntheticGestureController::running', 10, 5));
+ // Add four swap buffer events, at times 1, 2, 11, 13, 16
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 1, end: 1 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 2, end: 2 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 11, end: 11 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 13, end: 13 }));
+ browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ { title: 'BenchmarkInstrumentation::DisplayRenderingStats',
+ start: 16, end: 16 }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.rendering.renderingMetric(histograms, model);
+
+ // The gesture interaction record should be adjusted to [10, 15]. So, the
+ // first two frames and the fifth frame are outside the interaction record
+ // and should be discarded. The remaining frames are 11 and 13 which result
+ // in a single frame time of 2 = 13 - 11.
+ const hist = histograms.getHistogramNamed('frame_times');
+ assert.closeTo(2, hist.min, 1e-6);
+ assert.closeTo(2, hist.max, 2e-6);
+ assert.closeTo(2, hist.average, 1e-6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html
new file mode 100644
index 00000000000..01dd58cdf9c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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)) {
+ }
+
+ histograms.addHistogram(hist);
+ throw new Error('There was an error');
+ }
+
+ tr.metrics.MetricRegistry.register(sampleExceptionMetric);
+
+ return {
+ sampleExceptionMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html
new file mode 100644
index 00000000000..4a8be86b0d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics', function() {
+ function sampleMetric(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)) {
+ }
+
+ histograms.addHistogram(hist);
+ }
+
+ tr.metrics.MetricRegistry.register(sampleMetric);
+
+ return {
+ sampleMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html
new file mode 100644
index 00000000000..fcc5c8df4ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html
@@ -0,0 +1,248 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains helper functions to identify
+ * FrameLoader::updateForSameDocumentNavigation events on all renderer
+ * processes and find their preceding navigation start events.
+ *_______________________________________________________________
+ * browser: InputLatency/NavigationControllerImpl::GoToIndex |
+ *----------------------------------------
+ * renderer: LatencyInfo.Flow
+ * WebViewImpl::handleInputEvent
+ * FrameLoader::updateForSameDocumentNavigation
+ *----------------------------------------------------
+ * FrameLoader::updateForSameDocumentNavigation is called when SPA
+ * in-app navigation occurs.
+ * For details about how SPA in-app navigation is defined and
+ * how it is found based on FrameLoader::updateForSameDocumentNavigation,
+ * read the doc: https://goo.gl/1I3tqd.
+ */
+tr.exportTo('tr.metrics', function() {
+ const HANDLE_INPUT_EVENT_TITLE = 'WebViewImpl::handleInputEvent';
+
+ /**
+ * @returns {Map.<tr.model.Slice, tr.model.Slice>} A map of the
+ * elements in eventsB which immediately precede events in eventsA.
+ * For instance:
+ * eventsA: A1 A2 A3 A4
+ * eventsB: B1 B2 B3 B4 B5
+ * output: {A1: B2, A2: B3, A3: B4, A4: B5}
+ * or
+ * eventsA: A1 A2 A3 A4
+ * eventsB: B1
+ * output: {A1: B1, A2: B1, A3: B1, A4: B1}
+ */
+ function findPrecedingEvents_(eventsA, eventsB) {
+ const events = new Map();
+ let eventsBIndex = 0;
+ for (const eventA of eventsA) {
+ for (; eventsBIndex < eventsB.length; eventsBIndex++) {
+ if (eventsB[eventsBIndex].start > eventA.start) break;
+ }
+ // If statement prevents the situation when eventsB is empty.
+ if (eventsBIndex > 0) {
+ events.set(eventA, eventsB[eventsBIndex - 1]);
+ }
+ }
+ return events;
+ }
+
+ /**
+ * @returns {Map.<tr.model.Slice, tr.model.Slice>} A map of
+ * the elements in eventsB which immediately follow events
+ * in eventsA.
+ * For instance:
+ * eventsA: A1 A2 A3 A4
+ * eventsB: B1 B2 B3 B4 B5
+ * output: {A1:B2, A2:B3, A3:B4, A4:B5}
+ * or
+ * eventsA: A1 A2 A3 A4
+ * eventsB: B1
+ * output: {A1:B1, A2:B1, A3:B1}
+ */
+ function findFollowingEvents_(eventsA, eventsB) {
+ const events = new Map();
+ let eventsBIndex = 0;
+ for (const eventA of eventsA) {
+ for (; eventsBIndex < eventsB.length; eventsBIndex++) {
+ if (eventsB[eventsBIndex].start >= eventA.start) break;
+ }
+ // If statement prevents the situation when eventsB is empty
+ // and when it reaches the end of loop.
+ if (eventsBIndex >= 0 && eventsBIndex < eventsB.length) {
+ events.set(eventA, eventsB[eventsBIndex]);
+ }
+ }
+ return events;
+ }
+
+ /**
+ * @return {Array.<tr.model.Slice>} An array of events that may
+ * be qualified as a SPA navigation start candidate such as
+ * WebViewImpl::handleInputEvent and NavigationControllerImpl::GoToIndex.
+ */
+ function getSpaNavigationStartCandidates_(rendererHelper, browserHelper) {
+ const isNavStartEvent = e => {
+ if (e.title === HANDLE_INPUT_EVENT_TITLE && e.args.type === 'MouseUp') {
+ return true;
+ }
+ return e.title === 'NavigationControllerImpl::GoToIndex';
+ };
+
+ return [
+ ...rendererHelper.mainThread.sliceGroup.getDescendantEvents(),
+ ...browserHelper.mainThread.sliceGroup.getDescendantEvents()
+ ].filter(isNavStartEvent);
+ }
+
+ /**
+ * @return {Array.<tr.model.Slice>} An array of SPA navigation events.
+ * A SPA navigation event indicates the happening of a SPA navigation.
+ */
+ function getSpaNavigationEvents_(rendererHelper) {
+ const isNavEvent = e => e.category === 'blink' &&
+ e.title === 'FrameLoader::updateForSameDocumentNavigation';
+
+ return [...rendererHelper.mainThread.sliceGroup.getDescendantEvents()]
+ .filter(isNavEvent);
+ }
+
+ /**
+ * @return {Array.<tr.model.AsyncSlice>} An array of InputLatency events from
+ * the browser main thread.
+ */
+ function getInputLatencyEvents_(browserHelper) {
+ const isInputLatencyEvent = e => e.title === 'InputLatency::MouseUp';
+
+ return browserHelper.getAllAsyncSlicesMatching(isInputLatencyEvent);
+ }
+
+ /**
+ * @return {Map.<number, tr.model.Slice>} A mapping of trace_id value
+ * in each InputLatency event to the respective InputLatency event itself.
+ */
+ function getInputLatencyEventByBindIdMap_(browserHelper) {
+ const inputLatencyEventByBindIdMap = new Map();
+ for (const event of getInputLatencyEvents_(browserHelper)) {
+ inputLatencyEventByBindIdMap.set(event.args.data.trace_id, event);
+ }
+ return inputLatencyEventByBindIdMap;
+ }
+
+ /**
+ * @returns {Map.<tr.model.Slice, tr.model.AsyncSlice>} A mapping
+ * from a FrameLoader update navigation slice to its respective
+ * navigation start event, which can be an InputLatency async
+ * slice or a NavigationControllerImpl::GoToIndex slice.
+ */
+ function getSpaNavigationEventToNavigationStartMap_(
+ rendererHelper, browserHelper) {
+ const mainThread = rendererHelper.mainThread;
+ const spaNavEvents = getSpaNavigationEvents_(rendererHelper);
+ const navStartCandidates = getSpaNavigationStartCandidates_(
+ rendererHelper, browserHelper).sort(tr.importer.compareEvents);
+ const spaNavEventToNavStartCandidateMap =
+ findPrecedingEvents_(spaNavEvents, navStartCandidates);
+
+ const inputLatencyEventByBindIdMap =
+ getInputLatencyEventByBindIdMap_(browserHelper);
+ const spaNavEventToNavStartEventMap = new Map();
+ for (const [spaNavEvent, navStartCandidate] of
+ spaNavEventToNavStartCandidateMap) {
+ if (navStartCandidate.title === HANDLE_INPUT_EVENT_TITLE) {
+ const inputLatencySlice = inputLatencyEventByBindIdMap.get(
+ Number(navStartCandidate.parentSlice.bindId));
+ if (inputLatencySlice) {
+ spaNavEventToNavStartEventMap.set(spaNavEvent, inputLatencySlice);
+ }
+ } else {
+ spaNavEventToNavStartEventMap.set(spaNavEvent, navStartCandidate);
+ }
+ }
+ return spaNavEventToNavStartEventMap;
+ }
+
+ /**
+ * @return {Array.<tr.model.Slice>} An array of first paint events.
+ */
+ function getFirstPaintEvents_(rendererHelper) {
+ const isFirstPaintEvent = e => e.category === 'blink' &&
+ e.title === 'PaintLayerCompositor::updateIfNeededRecursive';
+
+ return [...rendererHelper.mainThread.sliceGroup.getDescendantEvents()]
+ .filter(isFirstPaintEvent);
+ }
+
+ /**
+ * @returns {Map.<tr.model.Slice, tr.model.Slice>} A mapping
+ * from a FrameLoader update navigation slice to its respective
+ * first paint slice.
+ */
+ function getSpaNavigationEventToFirstPaintEventMap_(rendererHelper) {
+ const spaNavEvents = getSpaNavigationEvents_(
+ rendererHelper).sort(tr.importer.compareEvents);
+ const firstPaintEvents = getFirstPaintEvents_(
+ rendererHelper).sort(tr.importer.compareEvents);
+
+ return findFollowingEvents_(spaNavEvents, firstPaintEvents);
+ }
+
+ /**
+ * @typedef {NavStartCandidates}
+ * @property {tr.model.AsyncSlice} inputLatencyAsyncSlice
+ * @property {tr.model.Slice} goToIndexSlice
+ */
+
+ /**
+ * @typedef {SpaNavObject}
+ * @property {NavStartCandidates} navStartCandidates
+ * @property {tr.model.Slice} firstPaintEvent
+ * @property {string} url
+ */
+
+ /**
+ * @returns {Array.<SpaNavObject>}
+ */
+ function findSpaNavigationsOnRenderer(rendererHelper, browserHelper) {
+ const spaNavEventToNavStartMap =
+ getSpaNavigationEventToNavigationStartMap_(
+ rendererHelper, browserHelper);
+ const spaNavEventToFirstPaintEventMap =
+ getSpaNavigationEventToFirstPaintEventMap_(rendererHelper);
+ const spaNavigations = [];
+ for (const [spaNavEvent, navStartEvent] of
+ spaNavEventToNavStartMap) {
+ if (spaNavEventToFirstPaintEventMap.has(spaNavEvent)) {
+ const firstPaintEvent =
+ spaNavEventToFirstPaintEventMap.get(spaNavEvent);
+ const isNavStartAsyncSlice =
+ navStartEvent instanceof tr.model.AsyncSlice;
+ spaNavigations.push({
+ navStartCandidates: {
+ inputLatencyAsyncSlice:
+ isNavStartAsyncSlice ? navStartEvent : undefined,
+ goToIndexSlice: isNavStartAsyncSlice ? undefined : navStartEvent
+ },
+ firstPaintEvent,
+ url: spaNavEvent.args.url
+ });
+ }
+ }
+ return spaNavigations;
+ }
+
+ return {
+ findSpaNavigationsOnRenderer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html
new file mode 100644
index 00000000000..ae87c1834e7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/spa_navigation_helper.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const RENDERER_PROCESS_ID = 1234;
+ const RENDERER_PROCESS_MAIN_THREAD_ID = 1;
+ const BROWSER_PROCESS_ID = 1;
+ const BROWSER_PROCESS_MAIN_THREAD_ID = 12;
+ const PAINT_UPDATE_TITLE =
+ 'PaintLayerCompositor::updateIfNeededRecursive';
+ const SPA_NAVIGATION_EVENT_TITLE =
+ 'FrameLoader::updateForSameDocumentNavigation';
+
+ function createChromeProcessesOnModel(model) {
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+ mainThread.name = 'CrRendererMain';
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserMainThread = browserProcess.getOrCreateThread(
+ BROWSER_PROCESS_MAIN_THREAD_ID);
+ browserMainThread.name = 'CrBrowserMain';
+ }
+
+ function addThreadSlice(model, title, timestamp, args) {
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title,
+ start: timestamp,
+ duration: 0.1,
+ args
+ }));
+ }
+
+ function addLatencyInfoFlowEvent(model, timestamp, bindId) {
+ const latencyInfoFlowSlice = tr.c.TestUtils.newSliceEx({
+ cat: 'input,benchmark',
+ title: 'LatencyInfo.Flow',
+ start: timestamp,
+ duration: 0.1,
+ bindId,
+ args: {step: 'handleInputEventMain'}
+ });
+ const handleInputEventSlice = tr.c.TestUtils.newSliceEx({
+ cat: 'blink,rail',
+ title: 'WebViewImpl::handleInputEvent',
+ start: timestamp + 1, // Assume handleInputEvent always delays 1ms.
+ duration: 0.1,
+ args: {type: 'MouseUp'}
+ });
+
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+
+ handleInputEventSlice.parentSlice = latencyInfoFlowSlice;
+ mainThread.sliceGroup.pushSlice(latencyInfoFlowSlice);
+ mainThread.sliceGroup.pushSlice(handleInputEventSlice);
+ }
+
+ function addInputLatencySlice(model, start, traceId) {
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserMainThread = browserProcess.getOrCreateThread(
+ BROWSER_PROCESS_MAIN_THREAD_ID);
+
+ browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'benchmark,latencyInfo,rail',
+ title: 'InputLatency::MouseUp',
+ start,
+ duration: 0.1,
+ args: {
+ data: {
+ trace_id: traceId
+ }
+ }
+ }));
+ }
+
+ function addGoToIndexSlice(model, timestamp) {
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserMainThread = browserProcess.getOrCreateThread(
+ BROWSER_PROCESS_MAIN_THREAD_ID);
+
+ browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'browser,navigation,benchmark',
+ title: 'NavigationControllerImpl::GoToIndex',
+ start: timestamp,
+ duration: 0.1
+ }));
+ }
+
+ function getSpaNavigations(model) {
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelpers = modelHelper.rendererHelpers;
+ const browserHelper = modelHelper.browserHelper;
+ let spaNavigations = [];
+ for (const rendererHelper of Object.values(rendererHelpers)) {
+ spaNavigations = spaNavigations.concat(
+ tr.metrics.findSpaNavigationsOnRenderer(
+ rendererHelper, browserHelper));
+ }
+ return spaNavigations;
+ }
+
+ test('findSpaNavigations_noSpaNavEvent', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addLatencyInfoFlowEvent(model, 75, '0x600000057');
+ addInputLatencySlice(model, 55, 25769803863);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 101);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 0);
+ });
+
+ test('findSpaNavigations_noLatencyInfoFlowEvent', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100);
+ addInputLatencySlice(model, 55, 25769803863);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 101);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 0);
+ });
+
+ test('findSpaNavigations_noNavStartEvent', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100);
+ addLatencyInfoFlowEvent(model, 75, '0x600000057');
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 101);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 0);
+ });
+
+ test('findSpaNavigations_noFirstPaintEvent', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100);
+ addLatencyInfoFlowEvent(model, 75, '0x600000057');
+ addInputLatencySlice(model, 55, 25769803863);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 0);
+ });
+
+ test('findSpaNavigations_inputLatencyAsNavStart', function() {
+ const URL = 'https://11111';
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100,
+ {url: URL});
+ addLatencyInfoFlowEvent(model, 75, '0x600000057');
+ addInputLatencySlice(model, 55, 25769803863);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 101);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 1);
+ assert.strictEqual(
+ spaNavigations[0].navStartCandidates.inputLatencyAsyncSlice.start, 55);
+ assert.strictEqual(
+ spaNavigations[0].navStartCandidates.goToIndexSlice, undefined);
+ assert.strictEqual(spaNavigations[0].firstPaintEvent.start, 101);
+ assert.strictEqual(spaNavigations[0].url, URL);
+ });
+
+ test('findSpaNavigations_goToIndexAsNavStart', function() {
+ const URL = 'https://11111';
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100,
+ {url: URL});
+ addGoToIndexSlice(model, 55);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 101);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 1);
+ assert.strictEqual(
+ spaNavigations[0].navStartCandidates.goToIndexSlice.start, 55);
+ assert.strictEqual(
+ spaNavigations[0].navStartCandidates.inputLatencyAsyncSlice, undefined);
+ assert.strictEqual(spaNavigations[0].firstPaintEvent.start, 101);
+ assert.strictEqual(spaNavigations[0].url, URL);
+ });
+
+ test('findSpaNavigations_multipleSpaNavs', function() {
+ const URL1 = 'https://11111';
+ const URL2 = 'https://22222';
+ const URL3 = 'https://33333';
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100,
+ {url: URL1});
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 200,
+ {url: URL2});
+ addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 300,
+ {url: URL3});
+
+ addLatencyInfoFlowEvent(model, 75, '0x600000057');
+ addLatencyInfoFlowEvent(model, 175, '0x6000000c2');
+ addLatencyInfoFlowEvent(model, 275, '0x60000010d');
+
+ addInputLatencySlice(model, 55, 25769803863);
+ addGoToIndexSlice(model, 65);
+ addInputLatencySlice(model, 155, 25769803970);
+ addInputLatencySlice(model, 255, 25769804045);
+
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 101);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 102);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 201);
+ addThreadSlice(model, PAINT_UPDATE_TITLE, 301);
+ });
+ const spaNavigations = getSpaNavigations(model);
+ assert.lengthOf(spaNavigations, 3);
+ spaNavigations.sort((spa1, spa2) =>
+ spa1.navStartCandidates.inputLatencyAsyncSlice.start -
+ spa2.navStartCandidates.inputLatencyAsyncSlice.start);
+ assert.strictEqual(
+ spaNavigations[0].navStartCandidates.inputLatencyAsyncSlice.start, 55);
+ assert.strictEqual(
+ spaNavigations[1].navStartCandidates.inputLatencyAsyncSlice.start, 155);
+ assert.strictEqual(
+ spaNavigations[2].navStartCandidates.inputLatencyAsyncSlice.start, 255);
+
+ assert.strictEqual(
+ spaNavigations[0].navStartCandidates.goToIndexSlice, undefined);
+ assert.strictEqual(
+ spaNavigations[1].navStartCandidates.goToIndexSlice, undefined);
+ assert.strictEqual(
+ spaNavigations[2].navStartCandidates.goToIndexSlice, undefined);
+
+ assert.strictEqual(spaNavigations[0].firstPaintEvent.start, 101);
+ assert.strictEqual(spaNavigations[1].firstPaintEvent.start, 201);
+ assert.strictEqual(spaNavigations[2].firstPaintEvent.start, 301);
+
+ assert.strictEqual(spaNavigations[0].url, URL1);
+ assert.strictEqual(spaNavigations[1].url, URL2);
+ assert.strictEqual(spaNavigations[2].url, URL3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html
new file mode 100644
index 00000000000..f8d6bb537f6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/spa_navigation_helper.html">
+<link rel="import" href="/tracing/metrics/system_health/breakdown_tree_helpers.html">
+<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics', function() {
+ const SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY =
+ tr.v.HistogramBinBoundaries.createExponential(1, 1000, 50);
+
+ /**
+ * This metric measures the duration between the input event
+ * causing a SPA navigation and the first paint event after it.
+ */
+ 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) {
+ // Chrome isn't present.
+ return;
+ }
+ const rendererHelpers = modelHelper.rendererHelpers;
+ if (!rendererHelpers) {
+ // We couldn't find any renderer processes.
+ 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;
+ // TODO(sunjian): rename convertTimestampToModelTime to something like
+ // convertTraceEventTsToModelTs and get rid of the first parameter.
+ 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);
+ }
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html
new file mode 100644
index 00000000000..44c38038d3b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/spa_navigation_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const RENDERER_PROCESS_ID = 1234;
+ const RENDERER_PROCESS_MAIN_THREAD_ID = 1;
+ const BROWSER_PROCESS_ID = 1;
+ const BROWSER_PROCESS_MAIN_THREAD_ID = 12;
+ const SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION =
+ 'spaNavigationStartToFpDuration';
+
+ function createChromeProcessesOnModel(model) {
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+ mainThread.name = 'CrRendererMain';
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserMainThread = browserProcess.getOrCreateThread(
+ BROWSER_PROCESS_MAIN_THREAD_ID);
+ browserMainThread.name = 'CrBrowserMain';
+ }
+
+ function addSpaNavigationEvent(model, timestamp, args) {
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'FrameLoader::updateForSameDocumentNavigation',
+ start: timestamp,
+ duration: 0.1,
+ args
+ }));
+ }
+
+ function addGoToIndexSlice(model, timestamp) {
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserMainThread = browserProcess.getOrCreateThread(
+ BROWSER_PROCESS_MAIN_THREAD_ID);
+ browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'browser,navigation,benchmark',
+ title: 'NavigationControllerImpl::GoToIndex',
+ start: timestamp,
+ duration: 0.1
+ }));
+ }
+
+ function addFirstPaintSlice(model, timestamp) {
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'PaintLayerCompositor::updateIfNeededRecursive',
+ start: timestamp,
+ duration: 0.1
+ }));
+ }
+
+ function addInputLatencyRelatedSlices(model, timestamp) {
+ const latencyInfoFlowSlice = tr.c.TestUtils.newSliceEx({
+ cat: 'input,benchmark',
+ title: 'LatencyInfo.Flow',
+ start: timestamp - 2,
+ duration: 0.1,
+ bindId: '0x600000057',
+ args: {step: 'handleInputEventMain'}
+ });
+ const handleInputEventSlice = tr.c.TestUtils.newSliceEx({
+ cat: 'blink,rail',
+ title: 'WebViewImpl::handleInputEvent',
+ start: timestamp - 1, // Assume handleInputEvent always delays 1ms.
+ duration: 0.1,
+ args: {type: 'MouseUp'}
+ });
+
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(
+ RENDERER_PROCESS_MAIN_THREAD_ID);
+
+ handleInputEventSlice.parentSlice = latencyInfoFlowSlice;
+ mainThread.sliceGroup.pushSlice(latencyInfoFlowSlice);
+ mainThread.sliceGroup.pushSlice(handleInputEventSlice);
+
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserMainThread = browserProcess.getOrCreateThread(
+ BROWSER_PROCESS_MAIN_THREAD_ID);
+ browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'benchmark,latencyInfo,rail',
+ title: 'InputLatency::MouseUp',
+ start: timestamp - 3,
+ duration: 0.1,
+ args: {
+ data: {
+ trace_id: 25769803863,
+ INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {
+ time: timestamp - 3 + 0.1
+ }
+ }
+ }
+ }));
+ }
+
+ function getHistogramNamed(name, model) {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.spaNavigationMetric(histograms, model);
+ const spaNavigationStartToFpDurationHist = histograms.getHistogramNamed(
+ name);
+ return spaNavigationStartToFpDurationHist;
+ }
+
+ test('spaNavStartToFirstPaintDuration_noChromeProcess', function() {
+ const model = tr.c.TestUtils.newModel();
+ const histogram = getHistogramNamed(
+ SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model);
+ assert(!histogram);
+ });
+
+ test('spaNavStartToFirstPaintDuration_noSpaNavStart', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addSpaNavigationEvent(model, 100);
+ addFirstPaintSlice(model, 101);
+ });
+ const histogram = getHistogramNamed(
+ SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model);
+ assert.strictEqual(0, histogram.numValues);
+ });
+
+ test('spaNavStartToFirstPaintDuration_noFirstPaint', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addInputLatencyRelatedSlices(model, 99);
+ addSpaNavigationEvent(model, 100);
+ });
+ const histogram = getHistogramNamed(
+ SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model);
+ assert.strictEqual(0, histogram.numValues);
+ });
+
+ test('spaNavStartToFirstPaintDuration_inputLatencyAsNavStart', function() {
+ const URL = 'https://11111';
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addInputLatencyRelatedSlices(model, 99);
+ addSpaNavigationEvent(model, 100, {url: URL});
+ addFirstPaintSlice(model, 101);
+ });
+ const histogram = getHistogramNamed(
+ SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model);
+ assert.strictEqual(1, histogram.running.count);
+ const navStartTs = model.convertTimestampToModelTime(
+ 'traceEventClock', 99 - 3 + 0.1);
+ const expectedDuration = 101 - navStartTs;
+ assert.closeTo(expectedDuration, histogram.running.sum, 0.5);
+
+ const binsWithSampleDiagnosticMaps =
+ histogram.allBins.filter(bin => bin.diagnosticMaps.length > 0);
+ const diagnostic = tr.b.getOnlyElement(binsWithSampleDiagnosticMaps[0]
+ .diagnosticMaps[0].get('Navigation infos'));
+ assert.strictEqual(diagnostic.url, URL);
+ assert.strictEqual(diagnostic.pid, RENDERER_PROCESS_ID);
+ assert.strictEqual(diagnostic.navStart, navStartTs);
+ assert.strictEqual(diagnostic.firstPaint, 101);
+ });
+
+ test('spaNavStartToFirstPaintDuration_goToIndexAsNavStart', function() {
+ const URL = 'https://11111';
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addGoToIndexSlice(model, 99);
+ addSpaNavigationEvent(model, 100, {url: URL});
+ addFirstPaintSlice(model, 101);
+ });
+ const histogram = getHistogramNamed(
+ SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model);
+ assert.strictEqual(1, histogram.running.count);
+ const expectedDuration = 101 - 99;
+ assert.closeTo(expectedDuration, histogram.running.sum, 0.5);
+
+ const binsWithSampleDiagnosticMaps =
+ histogram.allBins.filter(bin => bin.diagnosticMaps.length > 0);
+ const diagnostic = tr.b.getOnlyElement(binsWithSampleDiagnosticMaps[0]
+ .diagnosticMaps[0].get('Navigation infos'));
+ assert.strictEqual(diagnostic.url, URL);
+ assert.strictEqual(diagnostic.pid, RENDERER_PROCESS_ID);
+ assert.strictEqual(diagnostic.navStart, 99);
+ assert.strictEqual(diagnostic.firstPaint, 101);
+ });
+
+ test('spaNavStartToFirstPaintDuration_multipleSpaNavs', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ createChromeProcessesOnModel(model);
+ addInputLatencyRelatedSlices(model, 99);
+ addSpaNavigationEvent(model, 100);
+ addFirstPaintSlice(model, 101);
+
+ addInputLatencyRelatedSlices(model, 198);
+ addSpaNavigationEvent(model, 200);
+ addFirstPaintSlice(model, 201);
+
+ addInputLatencyRelatedSlices(model, 297);
+ addSpaNavigationEvent(model, 300);
+ addFirstPaintSlice(model, 301);
+ });
+ const histogram = getHistogramNamed(
+ SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model);
+ assert.strictEqual(3, histogram.running.count);
+ const expectedDuration1 = 101 - model.convertTimestampToModelTime(
+ 'traceEventClock', 99 - 3 + 0.1);
+ const expectedDuration2 = 201 - model.convertTimestampToModelTime(
+ 'traceEventClock', 198 - 3 + 0.1);
+ const expectedDuration3 = 301 - model.convertTimestampToModelTime(
+ 'traceEventClock', 297 - 3 + 0.1);
+ assert.closeTo(expectedDuration1 + expectedDuration2 + expectedDuration3,
+ histogram.running.sum, 0.5);
+ assert.closeTo(expectedDuration3, histogram.running.max, 0.5);
+ assert.closeTo(expectedDuration1, histogram.running.min, 0.5);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html
new file mode 100644
index 00000000000..4bbe2a694f4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html
@@ -0,0 +1,275 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/category_util.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ /**
+ * Returns the total self time of |event| within |rangeOfInterest|. Total self
+ * time is computed by finding time ranges that do not contain a descendant
+ * slice. Example:
+ *
+ * [ A ]
+ * | [ B ] [ C ] |
+ * | | [ D ] | | | |
+ * | | | | | |
+ * v v v v v v
+ * Ts : 0 50 80 100 150 180 200
+ * RoI : [ ]
+ *
+ * Total self time for A within |rangeOfInterest| is 100 - 30 = 70.
+ *
+ * @param {!tr.b.Event} event
+ * @param {!tr.b.math.Range} rangeOfInterest
+ * @return {number}
+ */
+ function getWallClockSelfTime_(event, rangeOfInterest) {
+ if (event.duration === 0) return 0;
+
+ const selfTimeRanges = [rangeOfInterest.findIntersection(event.range)];
+ for (const subSlice of event.subSlices) {
+ if (selfTimeRanges.length === 0) return 0;
+
+ const lastRange = selfTimeRanges.pop();
+ selfTimeRanges.push(
+ ...tr.b.math.Range.findDifference(lastRange, subSlice.range));
+ }
+
+ return tr.b.math.Statistics.sum(selfTimeRanges, r => r.duration);
+ }
+
+ /**
+ * Returns the CPU self time of |event| within |rangeOfInterest|. CPU self
+ * time of a slice is assumed to be evenly distributed over the wall clock
+ * self time ranges of the slice.
+ *
+ * @param {!tr.b.Event} event
+ * @param {!tr.b.math.Range} rangeOfInterest
+ * @return {number}
+ */
+ function getCPUSelfTime_(event, rangeOfInterest) {
+ if (event.duration === 0 || event.selfTime === 0) return 0;
+ if (event.cpuSelfTime === undefined) return 0;
+ const cpuTimeDensity = event.cpuSelfTime / event.selfTime;
+ return getWallClockSelfTime_(event, rangeOfInterest) * cpuTimeDensity;
+ }
+
+ /**
+ * @callback getEventAttributeCallback
+ * @param {!tr.b.Event} event The event to read an attribute from.
+ * @return {number} The value of the attribute.
+ */
+
+ /**
+ * Generate a breakdown tree from all slices of |mainThread| in
+ * |rangeOfInterest|. The callback function |getEventSelfTime| specify how to
+ * get self time from a given event.
+ *
+ * @param {!tr.model.Thread} mainThread
+ * @param {!tr.b.math.Range} rangeOfInterest
+ * @callback {getEventAttributeCallback} getEventSelfTime
+ * @return {Object.<string, Object>} A time breakdown object whose keys are
+ * Chrome userfriendly title & values are an object that show the total spent
+ * in |rangeOfInterest|, and the list of event labels of the
+ * group and their total time in |rangeOfInterest|.
+ *
+ * Example:
+ * {
+ * layout: {
+ * total: 100,
+ * events: {'FrameView::performPreLayoutTasks': 20,..}},
+ * v8_runtime: {
+ * total: 500,
+ * events: {'String::NewExternalTwoByte': 0.5,..}},
+ * ...
+ * }
+ */
+ function generateTimeBreakdownTree(mainThread, rangeOfInterest,
+ getEventSelfTime) {
+ if (mainThread === null) return;
+ const breakdownTree = {};
+ for (const title of
+ tr.e.chrome.ChromeUserFriendlyCategoryDriver.ALL_TITLES) {
+ breakdownTree[title] = {total: 0, events: {}};
+ }
+ // We do not look at async slices here.
+ for (const event of mainThread.sliceGroup.childEvents()) {
+ if (!rangeOfInterest.intersectsRangeExclusive(event.range)) continue;
+ const eventSelfTime = getEventSelfTime(event, rangeOfInterest);
+ const title =
+ tr.e.chrome.ChromeUserFriendlyCategoryDriver.fromEvent(event);
+
+ breakdownTree[title].total += eventSelfTime;
+ if (breakdownTree[title].events[event.title] === undefined) {
+ breakdownTree[title].events[event.title] = 0;
+ }
+ breakdownTree[title].events[event.title] +=
+ eventSelfTime;
+
+ let timeIntersectionRatio = 0;
+ if (event.duration > 0) {
+ timeIntersectionRatio =
+ rangeOfInterest.findExplicitIntersectionDuration(
+ event.start, event.end) / event.duration;
+ }
+
+ // TODO(#3846): v8_runtime is being double counted.
+ // TODO(#3846): v8_runtime slice name is wrong. It's 'stats'.
+ // TODO(#4296): v8_runtime should not be added for cpu time.
+ const v8Runtime = event.args['runtime-call-stat'];
+ if (v8Runtime !== undefined) {
+ const v8RuntimeObject = JSON.parse(v8Runtime);
+ for (const runtimeCall in v8RuntimeObject) {
+ // When the V8 Runtime Object contains 2 values, the 2nd value
+ // always represents the V8 Runtime duration.
+ if (v8RuntimeObject[runtimeCall].length === 2) {
+ if (breakdownTree.v8_runtime.events[runtimeCall] === undefined) {
+ breakdownTree.v8_runtime.events[runtimeCall] = 0;
+ }
+ const runtimeTime = tr.b.Unit.timestampFromUs(
+ v8RuntimeObject[runtimeCall][1] * timeIntersectionRatio);
+ breakdownTree.v8_runtime.total += runtimeTime;
+ breakdownTree.v8_runtime.events[runtimeCall] += runtimeTime;
+ }
+ }
+ }
+ }
+ return breakdownTree;
+ }
+
+ /**
+ * Adds 'blocked_on_network' and 'idle' to the |breakdownTree| that has been
+ * generated by |generateTimeBreakdownTree|. Taking into account the
+ * |networkEvents|, this function is able to distinguish between these two
+ * types of cpu idle time during the range |rangeOfInterest| not used by
+ * events of the main thread |mainThreadEvents|.
+ *
+ * @param {!Object.<string, Object>} breakdownTree The breakdownTree that has
+ * been generated by |generateTimeBreakdownTree|.
+ * @param {!tr.b.Event} mainThreadEvents The top level events of the main
+ * thread.
+ * @param {!tr.b.Event} networkEvents The network events in the renderer.
+ * @param {!tr.b.math.Range} rangeOfInterest The range for which
+ * |breakdownTree| is calculated.
+ */
+ function addIdleAndBlockByNetworkBreakdown_(breakdownTree, mainThreadEvents,
+ networkEvents, rangeOfInterest) {
+ const mainThreadEventRanges = tr.b.math.convertEventsToRanges(
+ mainThreadEvents);
+ const networkEventRanges = tr.b.math.convertEventsToRanges(
+ networkEvents);
+ const eventRanges = mainThreadEventRanges.concat(networkEventRanges);
+ const idleRanges =
+ tr.b.math.findEmptyRangesBetweenRanges(eventRanges, rangeOfInterest);
+ const totalFreeDuration = tr.b.math.Statistics.sum(idleRanges,
+ range => range.duration);
+ breakdownTree.idle = {total: totalFreeDuration, events: {}};
+
+ let totalBlockedDuration = rangeOfInterest.duration;
+ for (const [title, component] of Object.entries(breakdownTree)) {
+ // v8_runtime is a subcategory of script_execute.
+ // See github.com/catapult-project/catapult/commit/f3881d commit message.
+ // TODO(#2572) Make user friendly category hierarchy friend so this is not
+ // needed.
+ if (title === 'v8_runtime') continue;
+ totalBlockedDuration -= component.total;
+ }
+
+ breakdownTree.blocked_on_network = {
+ // Clamp breakdown at 0 to prevent negative values.
+ // TODO(#4299): Since we do not explicitly prevent overlapping slices on
+ // the same thread, slices can end up with negative wall clock self time
+ // and thus breakdown values can be negative. If we can prevent
+ // overlapping slices in the model, we can do this clamping for very small
+ // (e.g. < -0.1) values, accounting only for floating point errors, and
+ // fail loudly otherwise.
+ total: Math.max(totalBlockedDuration, 0),
+ events: {}
+ };
+ }
+
+ /**
+ * Generate a breakdown that attributes where wall clock time goes in
+ * |rangeOfInterest| on the renderer thread.
+ *
+ * @param {!tr.model.Thread} mainThread
+ * @param {!tr.b.math.Range} rangeOfInterest
+ * @return {Object.<string, Object>} A time breakdown object whose keys are
+ * Chrome userfriendly titles & values are an object that shows the total
+ * wall clock time spent in |rangeOfInterest|, and the list of event
+ * labels of the group and their total wall clock time in |rangeOfInterest|.
+ *
+ * Example:
+ * {
+ * layout: {
+ * total: 100,
+ * events: {'FrameView::performPreLayoutTasks': 20,..}},
+ * v8_runtime: {
+ * total: 500,
+ * events: {'String::NewExternalTwoByte': 0.5,..}},
+ * ...
+ * }
+ */
+ 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;
+ }
+
+ /**
+ * Generate a breakdown that attributes where CPU time goes in
+ * |rangeOfInterest| on the renderer thread.
+ *
+ * Due to approximations, it is possible for breakdowns to not add up to total
+ * CPU time in |rangeOfInterest|. Note that |rangeOfInterest| is a range of
+ * wall times, not CPU time.
+ *
+ * @param {!tr.model.Thread} mainThread
+ * @param {!tr.b.math.Range} rangeOfInterest
+ * @return {Object.<string, Object>} A time breakdown object whose keys are
+ * Chrome userfriendly titles & values are an object that shows the total
+ * CPU time spent in |rangeOfInterestCpuTime|, and the list of event labels
+ * of the group and their total durations in |rangeOfInterestCpuTime|.
+ *
+ * Example:
+ * {
+ * layout: {
+ * total: 100,
+ * events: {'FrameView::performPreLayoutTasks': 20,..}},
+ * v8_runtime: {
+ * total: 500,
+ * events: {'String::NewExternalTwoByte': 0.5,..}},
+ * ...
+ * }
+ */
+ function generateCpuTimeBreakdownTree(mainThread, rangeOfInterest) {
+ return generateTimeBreakdownTree(mainThread, rangeOfInterest,
+ getCPUSelfTime_);
+ }
+
+ return {
+ generateTimeBreakdownTree,
+ generateWallClockTimeBreakdownTree,
+ generateCpuTimeBreakdownTree,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html
new file mode 100644
index 00000000000..883c915c07f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html
@@ -0,0 +1,337 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const rendererPid = 12345;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(rendererPid);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+
+ // Our main thread looks like:
+ //
+ // [ parseHTML ] [ layout ] [ V8.Exec ]
+ // | [ V8.Exec ] | | | | [ layout ] |
+ // | | | | | | | | | |
+ // | | | | | | | | | |
+ // v v v v v v v v v v
+ // Ts: 200 250 300 400 450 550 570 600 620 650
+ // Cpu:1160 1200 1240 1320 1360 1440 1456 1480 1496 1520
+
+ // Add layout categories
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser',
+ start: 200,
+ duration: 200,
+ cpuStart: 1160,
+ cpuDuration: 160,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'devtools.timeline',
+ title: 'Script',
+ start: 250,
+ duration: 50,
+ cpuStart: 1200,
+ cpuDuration: 40,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ start: 250,
+ duration: 50,
+ args: {'runtime-call-stat': '{"ICMiss": [3, 150], "GC": [10, 60]}'},
+ cpuStart: 1200,
+ cpuDuration: 40,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'LocalFrameView::layout',
+ start: 450,
+ duration: 100,
+ cpuStart: 1360,
+ cpuDuration: 80,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ start: 570,
+ duration: 80,
+ args: {'runtime-call-stat': '{"DeOptimize": [1, 42], "GC": [3, 50]}'},
+ cpuStart: 1456,
+ cpuDuration: 64,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'WebViewImpl::updateAllLifecyclePhases',
+ start: 600,
+ duration: 20,
+ cpuStart: 1480,
+ cpuDuration: 16,
+ }));
+ });
+
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+
+ const rendererHelper = chromeHelper.rendererHelpers[rendererPid];
+
+ test('testWallClockTimeBreakdownNoIntersectingBoundary', function() {
+ const rangeOfInterest = tr.b.math.Range.fromExplicitRange(0, 1000);
+ const networkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, rangeOfInterest);
+ const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree(
+ rendererHelper.mainThread, networkEvents, rangeOfInterest);
+ assert.deepEqual({
+ total: 150,
+ events: {
+ 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 150
+ }
+ }, breakdownTree.parseHTML);
+ assert.deepEqual({
+ total: 120,
+ events: {
+ 'LocalFrameView::layout': 100,
+ 'WebViewImpl::updateAllLifecyclePhases': 20,
+ }
+ }, breakdownTree.layout);
+ assert.deepEqual({
+ total: 110,
+ events: {
+ 'V8.Execute': 110,
+ }
+ }, breakdownTree.script_execute);
+ assert.deepEqual({
+ total: 0.302,
+ events: {
+ 'DeOptimize': 0.042,
+ 'GC': 0.11,
+ 'ICMiss': 0.15,
+ }
+ }, breakdownTree.v8_runtime);
+ });
+
+ test('testWallClockTimeBreakdownIntersectingBoundary', function() {
+ // Our main thread looks like:
+ //
+ // [ parseHTML ] [ layout ] [ V8.Exec ]
+ // | [ V8.Exec ] | | | | [ layout ] |
+ // | | | | | | | | | |
+ // | | | | | | | | | |
+ // v v v v v v v v v v
+ // Ts: 200 250 300 400 450 550 570 600 620 650
+ // | |
+ // 275 610
+ const rangeOfInterest = tr.b.math.Range.fromExplicitRange(275, 610);
+ const networkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, rangeOfInterest);
+ const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree(
+ rendererHelper.mainThread, networkEvents, rangeOfInterest);
+ assert.deepEqual({
+ total: 100,
+ events: {
+ 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 100
+ }
+ }, breakdownTree.parseHTML);
+ assert.deepEqual({
+ total: 110,
+ events: {
+ 'LocalFrameView::layout': 100,
+ 'WebViewImpl::updateAllLifecyclePhases': 10,
+ }
+ }, breakdownTree.layout);
+ assert.deepEqual({
+ total: 55,
+ events: {
+ 'V8.Execute': 55,
+ }
+ }, breakdownTree.script_execute);
+ assert.deepEqual({
+ total: 0.151,
+ events: {
+ 'DeOptimize': 0.021,
+ 'GC': 0.055,
+ 'ICMiss': 0.075,
+ }
+ }, breakdownTree.v8_runtime);
+ });
+
+ test('testCpuTimeBreakdownNoIntersectingBoundary', function() {
+ const rangeOfInterest = tr.b.math.Range.fromExplicitRange(100, 800);
+ const breakdownTree = tr.metrics.sh.generateCpuTimeBreakdownTree(
+ rendererHelper.mainThread,
+ rangeOfInterest);
+ assert.deepEqual({
+ total: 120,
+ events: {
+ 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 120
+ }
+ }, breakdownTree.parseHTML);
+ assert.deepEqual({
+ total: 96,
+ events: {
+ 'LocalFrameView::layout': 80,
+ 'WebViewImpl::updateAllLifecyclePhases': 16,
+ }
+ }, breakdownTree.layout);
+ assert.deepEqual({
+ total: 88,
+ events: {
+ 'V8.Execute': 88,
+ }
+ }, breakdownTree.script_execute);
+ assert.deepEqual({
+ total: 0.302,
+ events: {
+ 'DeOptimize': 0.042,
+ 'GC': 0.11,
+ 'ICMiss': 0.15,
+ }
+ }, breakdownTree.v8_runtime);
+ });
+
+ test('testCpuTimeBreakdownIntersectingBoundary', function() {
+ // Our main thread looks like:
+ //
+ // [ parseHTML ] [ layout ] [ V8.Exec ]
+ // | [ V8.Exec ] | | | | [ layout ] |
+ // | | | | | | | | | |
+ // | | | | | | | | | |
+ // v v v v v v v v v v
+ // Ts: 200 250 300 400 450 550 570 600 620 650
+ // Cpu:1160 1200 1240 1320 1360 1440 1456 1480 1496 1520
+ // [ ]
+ // Ts RoI 275 610
+ const rangeOfInterest = tr.b.math.Range.fromExplicitRange(275, 610);
+ const breakdownTree = tr.metrics.sh.generateCpuTimeBreakdownTree(
+ rendererHelper.mainThread,
+ rangeOfInterest);
+ assert.deepEqual({
+ total: 80,
+ events: {
+ 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 80
+ }
+ }, breakdownTree.parseHTML);
+ assert.deepEqual({
+ total: 88,
+ events: {
+ 'LocalFrameView::layout': 80,
+ 'WebViewImpl::updateAllLifecyclePhases': 8,
+ }
+ }, breakdownTree.layout);
+ assert.deepEqual({
+ total: 44,
+ events: {
+ 'V8.Execute': 44,
+ }
+ }, breakdownTree.script_execute);
+ assert.deepEqual({
+ total: 0.151,
+ events: {
+ 'DeOptimize': 0.021,
+ 'GC': 0.055,
+ 'ICMiss': 0.075
+ }
+ }, breakdownTree.v8_runtime);
+ });
+
+ function createNetworkEvent(start, end, cat) {
+ return tr.c.TestUtils.newAsyncSliceEx({
+ cat,
+ title: 'network events',
+ start,
+ duration: end - start,
+ });
+ }
+
+ test('testBlockedOnNetwork_onlyNetEvent', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ const mainThread = model.getOrCreateProcess(0)
+ .getOrCreateThread(0);
+ mainThread.name = 'CrRendererMain';
+ const networkEvent = createNetworkEvent(100, 200, 'net');
+ mainThread.asyncSliceGroup.push(networkEvent);
+ });
+ const rendererHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper)
+ .rendererHelpers[0];
+ const rangeOfInterest = tr.b.math.Range.fromExplicitRange(0, 150);
+ const networkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, rangeOfInterest);
+ const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree(
+ rendererHelper.mainThread, networkEvents, rangeOfInterest);
+ assert.strictEqual(breakdownTree.blocked_on_network.total, 50);
+ assert.strictEqual(breakdownTree.idle.total, 100);
+ });
+
+ test('testBlockedOnNetwork_netEventAndMainThreadEvent', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ const mainThread = model.getOrCreateProcess(0)
+ .getOrCreateThread(0);
+ mainThread.name = 'CrRendererMain';
+ const networkEvent = createNetworkEvent(100, 200, 'net');
+ mainThread.asyncSliceGroup.push(networkEvent);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'LocalFrameView::layout',
+ start: 160,
+ duration: 140,
+ }));
+ });
+ const rendererHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper)
+ .rendererHelpers[0];
+ const rangeOfInterest = tr.b.math.Range.fromExplicitRange(150, 320);
+ const networkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, rangeOfInterest);
+ const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree(
+ rendererHelper.mainThread, networkEvents, rangeOfInterest);
+ assert.strictEqual(breakdownTree.layout.total, 140);
+ assert.strictEqual(breakdownTree.blocked_on_network.total, 10);
+ assert.strictEqual(breakdownTree.idle.total, 20);
+ });
+
+ test('testBlockedOnNetwork_rangeEmpty', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ const mainThread = model.getOrCreateProcess(0)
+ .getOrCreateThread(0);
+ mainThread.name = 'CrRendererMain';
+ const networkEvent = createNetworkEvent(100, 200, 'net');
+ mainThread.asyncSliceGroup.push(networkEvent);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink',
+ title: 'FrameView::layout',
+ start: 160,
+ duration: 140,
+ }));
+ });
+ const rendererHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper)
+ .rendererHelpers[0];
+ const rangeOfInterest = new tr.b.math.Range();
+ const networkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, rangeOfInterest);
+ const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree(
+ rendererHelper.mainThread, networkEvents, rangeOfInterest);
+ assert.strictEqual(breakdownTree.layout.total, 0);
+ assert.strictEqual(breakdownTree.blocked_on_network.total, 0);
+ assert.strictEqual(breakdownTree.idle.total, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html
new file mode 100644
index 00000000000..8493e899025
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ // Bin boundaries for clock sync latency. 0-20 ms with 0.2 ms bins.
+ // 20 ms is a good upper limit because the highest latencies we've seen are
+ // around 10-15 ms, and we expect average latency to go down as we improve
+ // the clock sync mechanism.
+ 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_' +
+ domains[i].toLowerCase() + '_to_' + domains[j].toLowerCase(),
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, LATENCY_BOUNDS);
+ hist.customizeSummaryOptions({
+ avg: true,
+ count: false,
+ max: false,
+ min: false,
+ std: false,
+ sum: false,
+ });
+ hist.description = 'Clock sync latency for domain ' + domains[i] +
+ ' to domain ' + domains[j];
+ hist.addSample(latency);
+ values.addHistogram(hist);
+ }
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(clockSyncLatencyMetric);
+
+ return {
+ clockSyncLatencyMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html
new file mode 100644
index 00000000000..27b5371763e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/system_health/clock_sync_latency_metric.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('clockSyncLatencyMetric', function() {
+ const model = new tr.Model();
+ model.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.TELEMETRY, 'ID01', 1.0, 4.0);
+ model.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.TELEMETRY, 'ID02', 2.0, 8.0);
+ model.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.BATTOR, 'ID01', 2.5);
+ model.clockSyncManager.addClockSyncMarker(
+ tr.model.ClockDomainId.WIN_QPC, 'ID02', 5.0);
+
+ const battorToWinQpcName = 'clock_sync_latency_' +
+ tr.model.ClockDomainId.BATTOR.toLowerCase() + '_to_' +
+ tr.model.ClockDomainId.WIN_QPC.toLowerCase();
+ const winQpcToTelemetryName = 'clock_sync_latency_' +
+ tr.model.ClockDomainId.TELEMETRY.toLowerCase() + '_to_' +
+ tr.model.ClockDomainId.WIN_QPC.toLowerCase();
+ const battorToTelemetryName = 'clock_sync_latency_' +
+ tr.model.ClockDomainId.BATTOR.toLowerCase() + '_to_' +
+ tr.model.ClockDomainId.TELEMETRY.toLowerCase();
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.clockSyncLatencyMetric(histograms, model);
+
+ let battorToWinQpcValue = undefined;
+ let winQpcToTelemetryValue = undefined;
+ let battorToTelemetryValue = undefined;
+ for (const value of histograms) {
+ if (value.name === battorToWinQpcName) {
+ battorToWinQpcValue = value;
+ } else if (value.name === winQpcToTelemetryName) {
+ winQpcToTelemetryValue = value;
+ } else if (value.name === battorToTelemetryName) {
+ battorToTelemetryValue = value;
+ }
+ }
+
+ // Clock sync graph is:
+ // [WIN_QPC] --6ms-> [TELEMETRY] --3ms-> [BATTOR]
+
+ assert.isDefined(battorToWinQpcValue);
+ assert.isDefined(winQpcToTelemetryValue);
+ assert.isDefined(battorToTelemetryValue);
+ assert.closeTo(battorToWinQpcValue.average, 9.0, 1e-5);
+ assert.closeTo(winQpcToTelemetryValue.average, 6.0, 1e-5);
+ assert.closeTo(battorToTelemetryValue.average, 3.0, 1e-5);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html
new file mode 100644
index 00000000000..f8a63e84ba7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ // Use a lower bound of 0.01 for the metric boundaries (when no CPU time
+ // is consumed) and an upper bound of 50 (fifty cores are all active
+ // for the entire time). We can't use zero exactly for the lower bound with an
+ // exponential histogram.
+ const CPU_TIME_PERCENTAGE_BOUNDARIES =
+ tr.v.HistogramBinBoundaries.createExponential(0.01, 50, 200);
+
+ /**
+ * This metric measures total CPU time for Chrome processes, per second of
+ * clock time.
+ * This metric requires only the 'toplevel' tracing category.
+ *
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.Model} model
+ * @param {!Object=} opt_options
+ */
+ function cpuTimeMetric(histograms, model, opt_options) {
+ let rangeOfInterest = model.bounds;
+
+ if (opt_options && opt_options.rangeOfInterest) {
+ rangeOfInterest = opt_options.rangeOfInterest;
+ } else {
+ // If no range of interest is provided, limit the relevant range to
+ // Chrome processes. This prevents us from normalizing against non-Chrome
+ // related slices in the trace.
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (chromeHelper) {
+ const chromeBounds = chromeHelper.chromeBounds;
+ if (chromeBounds) {
+ rangeOfInterest = chromeBounds;
+ }
+ }
+ }
+
+ let allProcessCpuTime = 0;
+
+ for (const pid in model.processes) {
+ const process = model.processes[pid];
+ if (tr.model.helpers.ChromeRendererHelper.isTracingProcess(process)) {
+ continue;
+ }
+
+ let processCpuTime = 0;
+ for (const tid in process.threads) {
+ const thread = process.threads[tid];
+ processCpuTime += thread.getCpuTimeForRange(rangeOfInterest);
+ }
+ allProcessCpuTime += processCpuTime;
+ }
+
+ // Normalize cpu time by clock time.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html
new file mode 100644
index 00000000000..e68b452f4f7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/metrics/system_health/cpu_time_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function computeCpuTime(customizeModelCallback, opt_options) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ customizeModelCallback(model);
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.cpuTimeMetric(histograms, model, opt_options);
+ return tr.b.getOnlyElement(histograms).average;
+ }
+
+ // There are two slices, each of length 50. The total bounds is 3000.
+ // This yields total CPU time of 100ms, averaged over 3 seconds is 33ms.
+ test('cpuTimeMetric_oneProcess', function() {
+ const sliceDuration = 50;
+ const totalDuration = 3000;
+ const value = computeCpuTime(function(model) {
+ model.rendererProcess = model.getOrCreateProcess(2);
+ model.rendererMain = model.rendererProcess.getOrCreateThread(3);
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: sliceDuration,
+ cpuStart: 0,
+ cpuDuration: sliceDuration,
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: totalDuration - sliceDuration,
+ duration: sliceDuration,
+ cpuStart: totalDuration - sliceDuration,
+ cpuDuration: sliceDuration,
+ }));
+ });
+ assert.closeTo(value, 0.033, 0.001);
+ });
+
+ // Normalize against chrome processes, whose slices (both with and without
+ // CPU data) go from 2900 to 3000.
+ test('cpuTimeMetric_browserProcess', function() {
+ const sliceDuration = 50;
+ const totalDuration = 3000;
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: totalDuration - 2 * sliceDuration,
+ duration: 2 * sliceDuration,
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: totalDuration - sliceDuration,
+ duration: sliceDuration,
+ cpuStart: totalDuration - sliceDuration,
+ cpuDuration: sliceDuration,
+ }));
+
+ const nonChromeProcess = model.getOrCreateProcess(1234);
+ nonChromeProcess.name = 'Telemetry';
+ const nonChromeThread = nonChromeProcess.getOrCreateThread(1);
+ nonChromeThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: sliceDuration,
+ cpuStart: 0,
+ cpuDuration: sliceDuration,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.cpuTimeMetric(histograms, model);
+ const value = tr.b.getOnlyElement(histograms).average;
+ assert.closeTo(value, 0.5, 0.001);
+ });
+
+ // Makes sure that rangeOfInterest works correctly.
+ test('cpuTimeMetric_oneProcess_rangeOfInterest', function() {
+ const sliceDuration = 50;
+ const totalDuration = 3000;
+ const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(-10, 30);
+ const options = {};
+ options.rangeOfInterest = rangeOfInterest;
+ const value = computeCpuTime(function(model) {
+ model.rendererProcess = model.getOrCreateProcess(2);
+ model.rendererMain = model.rendererProcess.getOrCreateThread(3);
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: sliceDuration,
+ cpuStart: 0,
+ cpuDuration: sliceDuration,
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: totalDuration - sliceDuration,
+ duration: sliceDuration,
+ cpuStart: totalDuration - sliceDuration,
+ cpuDuration: sliceDuration,
+ }));
+ }, options);
+ assert.closeTo(value, 0.75, 0.001);
+ });
+
+ // Process 1: There are two slices, each of length 50. The total bounds is
+ // 3000. Process 2: There is one slice of length 50.
+ // This yields total CPU time of 150ms, averaged over 3 seconds is 50ms.
+ test('cpuTimeMetric_twoProcesses', function() {
+ const sliceDuration = 50;
+ const totalDuration = 3000;
+ const value = computeCpuTime(function(model) {
+ model.rendererProcess = model.getOrCreateProcess(2);
+ model.rendererMain = model.rendererProcess.getOrCreateThread(3);
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: sliceDuration,
+ cpuStart: 0,
+ cpuDuration: sliceDuration,
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: totalDuration - sliceDuration,
+ duration: sliceDuration,
+ cpuStart: totalDuration - sliceDuration,
+ cpuDuration: sliceDuration,
+ }));
+
+ const otherProcess = model.getOrCreateProcess(3);
+ const otherThread = otherProcess.getOrCreateThread(4);
+ otherThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: sliceDuration,
+ cpuStart: 0,
+ cpuDuration: sliceDuration,
+ }));
+ });
+ assert.closeTo(value, 0.05, 0.001);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html
new file mode 100644
index 00000000000..6d88c1b81ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ const CPU_PERCENTAGE_UNIT =
+ tr.b.Unit.byName.normalizedPercentage_smallerIsBetter;
+ const CPU_TIME_UNIT = tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;
+
+ /**
+ * Returns a deep clone of CPU time multidimensional view path object.
+ *
+ * @param {!Array.<!Array.<string>>} previousPath
+ * @returns {!Array.<!Array.<string>>}
+ */
+ function clonePath_(previousPath) {
+ return previousPath.map(subPath => subPath.map(x => x));
+ }
+
+
+ /**
+ * Returns an object containing the processType, threadType, railStage, and
+ * initiatorType encoded in the provided CPU time multidimensional view path
+ * object. Decoding the path into this object can make code easier to
+ * understand than indexing directly into the path array.
+ *
+ * @param {!Array.<!Array.<string>>} path - A path in a CPU time
+ * multidimensional tree view.
+ * @returns {Object.<string, string>}
+ */
+ function decodePath_(path) {
+ return {
+ processType: path[0][0],
+ threadType: path[1][0],
+ railStage: path[2][0],
+ initiatorType: path[2][1]
+ };
+ }
+
+ /**
+ * Returns a unique string representation of |path|.
+ *
+ * Paths are of the following form in CPU time multidimensional trees:
+ * [[processType], [threadType], [railStage, initiatorType]]
+ *
+ * The returned string is of the form
+ * "$processtype:$threadType:$railStage:$initiatorType".
+ *
+ * @param {Array.<!Array.<string>>} path
+ * @returns {string}
+ */
+ function stringifyPathName_(path) {
+ const decodedPath = decodePath_(path);
+ return [
+ decodedPath.processType,
+ decodedPath.threadType,
+ decodedPath.railStage,
+ decodedPath.initiatorType
+ ].join(':');
+ }
+
+ /**
+ * This class is used to traverse a multidimensional tree view and report CPU
+ * percentage and CPU time from the tree as histograms.
+ */
+ class CpuTimeTreeDataReporter {
+ constructor() {
+ this.visitedSet_ = new Set();
+ }
+
+ /**
+ * Extracts CPU percentage and CPU time values from |node| located at |path|
+ * and adds values as histograms to |this.histogramSet_|. Each value is
+ * added as a single sample histogram.
+ *
+ * @param {!tr.b.MultiDimensionalViewNode} node
+ * @param {!Array.<!Array.<string>>} path
+ */
+ reportValuesFromNode_(node, path) {
+ const decodedPath = decodePath_(path);
+ const processType = decodedPath.processType || 'all_processes';
+ const threadType = decodedPath.threadType || 'all_threads';
+
+ // We need some RAIL stage and some initiator type to process a node.
+ // All RAIL stages and all initiator types are handled by the special
+ // 'all_stages' and 'all_initiators' nodes respectively.
+ if (!decodedPath.railStage || !decodedPath.initiatorType) return;
+ const {railStage, initiatorType} = decodedPath;
+
+ const serializedPathName =
+ [processType, threadType, railStage, initiatorType].join(':');
+
+ // node.values is a two element array. The first element holds
+ // cpuPercentage data and the second holds cpuTime data. The final
+ // '.total' (as opposed to '.self') signifies we're including all the data
+ // from children nodes. This is an artifact of how the multidimensional
+ // view data structure works and is not very relevant - we exclusively use
+ // '.total' for CPU time.
+ const cpuPercentageValue = node.values[0].total;
+ const cpuTimeValue = node.values[1].total;
+
+ this.histogramSet_.createHistogram(`cpuPercentage:${serializedPathName}`,
+ CPU_PERCENTAGE_UNIT, cpuPercentageValue);
+ this.histogramSet_.createHistogram(`cpuTime:${serializedPathName}`,
+ CPU_TIME_UNIT, cpuTimeValue);
+ }
+
+
+ /**
+ * Traverses all the paths of a multidimensional view subtree and reports
+ * node data to |this.histogramSet_|.
+ *
+ * @param {!tr.b.MultiDimensionalViewNode} root - Root of the subtree.
+ * @param {!Array.<!Array.<string>>} rootPath - Path of the subtree root
+ * node with respect to |this.rootNode_|.
+ */
+ reportDataFromTree_(root, rootPath) {
+ const rootPathString = stringifyPathName_(rootPath);
+ if (this.visitedSet_.has(rootPathString)) return;
+ this.visitedSet_.add(rootPathString);
+
+ this.reportValuesFromNode_(root, rootPath);
+
+ for (let dimension = 0; dimension < root.children.length; dimension++) {
+ const children = root.children[dimension];
+ for (const [name, node] of children) {
+ const childPath = clonePath_(rootPath);
+ childPath[dimension].push(name);
+ this.reportDataFromTree_(node, childPath);
+ }
+ }
+ }
+
+ /**
+ * Adds values from the multidimensional tree view rooted at |rootNode| as
+ * single value histograms in |histogramSet|.
+ *
+ * @param {!tr.b.MultiDimensionalViewNode} rootNode
+ * @param {!tr.v.HistogramSet} histogramSet
+ */
+ addTreeValuesToHistogramSet(rootNode, histogramSet) {
+ const rootPath = [[], [], []];
+ this.rootNode_ = rootNode;
+ this.histogramSet_ = histogramSet;
+ this.reportDataFromTree_(this.rootNode_, rootPath);
+ }
+
+ /**
+ * Reports values from the multidimensional tree view rooted at |rootNode|
+ * as single value histograms in |histogramSet|.
+ *
+ * The histograms are dynamically generated from the tree. The histogram
+ * names are of the form
+ * "${cpuTime|cpuPercentage}:${processType}:${threadType}:" +
+ * "${railStage}:${railStageInitiator}"
+ *
+ * cpuTime histograms contain total consumed cpu time, while cpuPercentage
+ * histograms contain cpu time as a percentage of wall time. In multicore
+ * situations, this percentage can be larger than 100.
+ *
+ * @param {!tr.b.MultiDimensionalViewNode} rootNode
+ * @param {!tr.v.HistogramSet} histogramSet
+ */
+ static reportToHistogramSet(rootNode, histogramSet) {
+ const reporter = new CpuTimeTreeDataReporter();
+ reporter.addTreeValuesToHistogramSet(rootNode, histogramSet);
+ }
+ }
+
+ return {
+ CpuTimeTreeDataReporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html
new file mode 100644
index 00000000000..cd72b21868d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_processes.html">
+<link rel="import" href="/tracing/metrics/system_health/cpu_time_tree_data_reporter.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const reportToHistogramSet =
+ tr.metrics.sh.CpuTimeTreeDataReporter.reportToHistogramSet;
+
+ test('reportToHistogramSet_reportsLeafNodes', () => {
+ const mdvBuilder = new tr.b.MultiDimensionalViewBuilder(
+ 3 /* dimensions (process, thread and rail stage / initiator) */,
+ 2 /* valueCount (cpuPercentage and cpuTime) */);
+ mdvBuilder.addPath(
+ [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']],
+ [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL);
+ const rootNode = mdvBuilder.buildTopDownTreeView();
+
+ const histograms = new tr.v.HistogramSet();
+ reportToHistogramSet(rootNode, histograms);
+
+ const cpuPercentageHistogram = histograms.getHistogramNamed(
+ 'cpuPercentage:browser_process:CrBrowserMain:Animation:CSS');
+ const cpuTimeHistogram = histograms.getHistogramNamed(
+ 'cpuTime:browser_process:CrBrowserMain:Animation:CSS');
+
+ // Histograms exist.
+ assert.isDefined(cpuPercentageHistogram);
+ assert.isDefined(cpuTimeHistogram);
+
+ // Each histogram contains a single sample.
+ assert.strictEqual(cpuPercentageHistogram.running.count, 1);
+ assert.strictEqual(cpuTimeHistogram.running.count, 1);
+
+ // Histogram sample value is correct.
+ assert.closeTo(cpuPercentageHistogram.sum, 42, 1e-7);
+ assert.closeTo(cpuTimeHistogram.sum, 43, 1e-7);
+ });
+
+ test('reportToHistogramSet_reportsAllProcesses', () => {
+ const mdvBuilder = new tr.b.MultiDimensionalViewBuilder(
+ 3 /* dimensions (process, thread and rail stage / initiator) */,
+ 2 /* valueCount (cpuPercentage and cpuTime) */);
+ mdvBuilder.addPath(
+ [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']],
+ [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL);
+ const rootNode = mdvBuilder.buildTopDownTreeView();
+
+ const histograms = new tr.v.HistogramSet();
+ reportToHistogramSet(rootNode, histograms);
+
+ const cpuPercentageHistogram = histograms.getHistogramNamed(
+ 'cpuPercentage:all_processes:CrBrowserMain:Animation:CSS');
+ const cpuTimeHistogram = histograms.getHistogramNamed(
+ 'cpuTime:all_processes:CrBrowserMain:Animation:CSS');
+
+ // Histograms exist.
+ assert.isDefined(cpuPercentageHistogram);
+ assert.isDefined(cpuTimeHistogram);
+
+ // Each histogram contains a single sample.
+ assert.strictEqual(cpuPercentageHistogram.running.count, 1);
+ assert.strictEqual(cpuTimeHistogram.running.count, 1);
+
+ // Histogram sample value is correct.
+ assert.closeTo(cpuPercentageHistogram.sum, 42, 1e-7);
+ assert.closeTo(cpuTimeHistogram.sum, 43, 1e-7);
+ });
+
+ test('reportToHistogramSet_reportsAllThreads', () => {
+ const mdvBuilder = new tr.b.MultiDimensionalViewBuilder(
+ 3 /* dimensions (process, thread and rail stage / initiator) */,
+ 2 /* valueCount (cpuPercentage and cpuTime) */);
+ mdvBuilder.addPath(
+ [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']],
+ [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL);
+ const rootNode = mdvBuilder.buildTopDownTreeView();
+
+ const histograms = new tr.v.HistogramSet();
+ reportToHistogramSet(rootNode, histograms);
+
+ const cpuPercentageHistogram = histograms.getHistogramNamed(
+ 'cpuPercentage:browser_process:all_threads:Animation:CSS');
+ const cpuTimeHistogram = histograms.getHistogramNamed(
+ 'cpuTime:browser_process:all_threads:Animation:CSS');
+
+ // Histograms exist.
+ assert.isDefined(cpuPercentageHistogram);
+ assert.isDefined(cpuTimeHistogram);
+
+ // Each histogram contains a single sample.
+ assert.strictEqual(cpuPercentageHistogram.running.count, 1);
+ assert.strictEqual(cpuTimeHistogram.running.count, 1);
+
+ // Histogram sample value is correct.
+ assert.closeTo(cpuPercentageHistogram.sum, 42, 1e-7);
+ assert.closeTo(cpuTimeHistogram.sum, 43, 1e-7);
+ });
+
+ test('reportToHistogramSet_doesNotAggregateStagesWithoutAllStagesNode',
+ () => {
+ const mdvBuilder = new tr.b.MultiDimensionalViewBuilder(
+ 3 /* dimensions (process, thread and rail stage / initiator) */,
+ 2 /* valueCount (cpuPercentage and cpuTime) */);
+ mdvBuilder.addPath(
+ [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']],
+ [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL);
+ const rootNode = mdvBuilder.buildTopDownTreeView();
+
+ const histograms = new tr.v.HistogramSet();
+ reportToHistogramSet(rootNode, histograms);
+
+ // all_stages:CSS does not make sense (e.g. we will never
+ // aggregate Scroll Response and Scroll Animation, even if that's a
+ // thing), so we use all_stages:all_initiators here.
+ assert.isUndefined(histograms.getHistogramNamed(
+ 'cpuPercentage:' +
+ 'browser_process:CrBrowserMain:all_stages:all_initiators'));
+ assert.isUndefined(histograms.getHistogramNamed(
+ 'cpuTime:' +
+ 'browser_process:CrBrowserMain:all_stages:all_initiators'));
+ });
+
+ test('reportToHistogramSet_' +
+ 'doesNotAggregateInitiatorsWithoutAllInitiatorsNode',
+ () => {
+ const mdvBuilder = new tr.b.MultiDimensionalViewBuilder(
+ 3 /* dimensions (process, thread and rail stage / initiator) */,
+ 2 /* valueCount (cpuPercentage and cpuTime) */);
+ mdvBuilder.addPath(
+ [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']],
+ [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL);
+ const rootNode = mdvBuilder.buildTopDownTreeView();
+
+ const histograms = new tr.v.HistogramSet();
+ reportToHistogramSet(rootNode, histograms);
+
+ assert.isUndefined(histograms.getHistogramNamed(
+ 'cpuPercentage:' +
+ 'browser_process:CrBrowserMain:Animation:all_initiators'));
+ assert.isUndefined(histograms.getHistogramNamed(
+ 'cpuTime:browser_process:CrBrowserMain:Animation:all_initiators'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html
new file mode 100644
index 00000000000..b9ef3de7ebd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html
@@ -0,0 +1,462 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/extras/chrome/estimated_input_latency.html">
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html">
+<link rel="import" href="/tracing/metrics/v8/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview
+ * This file defines the input latency metric estimated as the maximum
+ * expected queueing time (EQT) in sliding window of size 500ms.
+ *
+ * The EQT is defined as the average queueing time of a hypothetical input
+ * event arriving at a random time in the given time window.
+ * For more information see:
+ * - https://goo.gl/OQ2bX6
+ * - https://goo.gl/jmWpMl
+ * - https://goo.gl/lga4iO
+ */
+tr.exportTo('tr.metrics.sh', function() {
+ // The size of the sliding window is chosen arbitrarily (see
+ // https://goo.gl/lga4iO).
+ const WINDOW_SIZE_MS = 500;
+ const EQT_BOUNDARIES = tr.v.HistogramBinBoundaries
+ .createExponential(0.01, WINDOW_SIZE_MS, 50);
+
+ /**
+ * Returns true if the slice contains a forced GC event. Some stories force
+ * garbage collection before sampling memory usage. Since a forced GC takes
+ * long time we need to ignore it to avoid biasing the input latency results.
+ */
+ function containsForcedGC_(slice) {
+ return slice.findTopmostSlicesRelativeToThisSlice(
+ tr.metrics.v8.utils.isForcedGarbageCollectionEvent).length > 0;
+ }
+
+ /**
+ * @param {!string} name Name of the histogram.
+ * @param {!string} description Description of the histogram.
+ * @returns {!tr.v.Histogram}
+ */
+ 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,
+ },
+ });
+ }
+
+ /**
+ * Computes the maximum expected queueing time in the sliding time window
+ * of size 500ms (WINDOW_SIZE_MS). The function produces four Histograms:
+ * - total:500ms_window:renderer_eqt,
+ * - total:500ms_window:renderer_eqt_cpu,
+ * - interactive:500ms_window:renderer_eqt.
+ * - interactive:500ms_window:renderer_eqt_cpu.
+ * The 'total' histograms are computed for the whole trace. The 'interactive'
+ * histograms are computed for the time while the page is interactive.
+ * The 'cpu' histograms use the CPU time of the events instead of the wall-
+ * clock times. Each renderer process adds one sample to the histograms.
+ */
+ 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);
+ }
+
+ /**
+ * @callback EventTimesCallback
+ * @param {!tr.b.Event} event
+ * @return {{start: !number, duration: !number}} event start time and duration.
+ */
+
+ /**
+ * The actual implementation of the EQT metric.
+ * @param {!string} eqtName the metric name part of the histogram name.
+ * @param {!EventTimesCallback} getEventTimes.
+ * @param {!Array.<tr.model.helpers.ChromeRendererHelper>} rendererHelpers.
+ * @param {!Map.<number, Array.<number>>} rendererToInteractiveTimestamps
+ * a map from renderer pid to an array of interactive timestamps.
+ */
+ function addExpectedQueueingTimeMetric_(eqtName, getEventTimes, isCpuTime,
+ rendererHelpers, rendererToInteractiveTimestamps, histograms, model) {
+ /**
+ * Extracts tasks for EQT computation from the given renderer.
+ * A task is a pair of {start, end} times.
+ */
+ 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 = getOrCreateHistogram_(
+ histograms,
+ `total:${WINDOW_SIZE_MS}ms_window:${eqtName}`,
+ `The maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window` +
+ ' for a given renderer');
+ const interactiveHistogram = getOrCreateHistogram_(
+ histograms,
+ `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;
+ // Renderers with lifetime smaller than WINDOW_SIZE_MS do not have
+ // meaningful EQT.
+ if (rendererHelper.mainThread.bounds.duration < WINDOW_SIZE_MS) continue;
+
+ const tasks = getTasks(rendererHelper);
+ const {totalBreakdown, interactiveBreakdown} = getV8Contribution_(
+ eqtName,
+ getEventTimes,
+ isCpuTime,
+ totalHistogram,
+ interactiveHistogram,
+ rendererToInteractiveTimestamps,
+ histograms,
+ rendererHelper,
+ model);
+ totalHistogram.addSample(
+ tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(
+ rendererHelper.mainThread.bounds.min,
+ rendererHelper.mainThread.bounds.max,
+ WINDOW_SIZE_MS, tasks), {v8: totalBreakdown});
+ const interactiveTimestamps =
+ rendererToInteractiveTimestamps.get(rendererHelper.pid);
+ if (interactiveTimestamps.length === 0) continue;
+ if (interactiveTimestamps.length > 1) {
+ // TODO(ulan): Support multiple interactive time windows when
+ // https://crbug.com/692112 is fixed.
+ 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), {v8: interactiveBreakdown});
+ }
+ }
+
+ /**
+ * Computes the contribution of the selected events to the expected queueing
+ * time. We define the contribution as the maximum expected queueing time in
+ * the sliding time window of size 500ms (WINDOW_SIZE_MS) for the trace that
+ * is modified as follows:
+ * - from each top-level task remove all subevents except the selected events.
+ * - removing subevents shrinks a task by shifting its end time closer to
+ * the start time. The start time does not change.
+ *
+ * Similar to the expectedQueueingTime this function computes two histograms:
+ * total and interactive. For example:
+ * - total:500ms_window:renderer_eqt:v8,
+ * - interactive:500ms_window:renderer_eqt:v8.
+ * Each renderer process adds one sample to the histograms.
+ * Both histograms are added to the given histogram set.
+ *
+ * @param {!string} eqtName the metric name part of the histogram name.
+ * @param {!EventTimesCallback} getEventTimes.
+ * @param {boolean} isCpuTime
+ * @param {!tr.v.Histogram} totalEqtHistogram
+ * @param {!tr.v.Histogram} interactiveEqtHistogram
+ * @param {!Map.<number, Array.<number>>} rendererToInteractiveTimestamps
+ * a map from renderer pid to an array of interactive timestamps.
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @param {!tr.model.Model} model
+ * @return {{totalBreakdown: !tr.v.d.Breakdown,
+ * interactiveBreakdown: !tr.v.d.Breakdown}}
+ */
+ function getV8Contribution_(
+ eqtName, getEventTimes, isCpuTime, totalEqtHistogram,
+ interactiveEqtHistogram, rendererToInteractiveTimestamps, histograms,
+ rendererHelper, model) {
+ if (!model.categories.includes('v8')) return {};
+
+ const totalBreakdown = new tr.v.d.Breakdown();
+ const interactiveBreakdown = new tr.v.d.Breakdown();
+ // Include task extractors that use tracing.
+ const eventNamesWithTaskExtractors =
+ getV8EventNamesWithTaskExtractors_(getEventTimes);
+ if (!isCpuTime) {
+ // Include task extractors that use RCS. RCS does not provide cpu time
+ // so include these only for wall clock time.
+ 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);
+ }
+ let interactiveNames = interactiveEqtHistogram.diagnostics.get('v8');
+ if (!interactiveNames) {
+ interactiveNames = new tr.v.d.RelatedNameMap();
+ interactiveEqtHistogram.diagnostics.set('v8', interactiveNames);
+ }
+
+ 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 interactiveHistogram = getOrCreateHistogram_(histograms,
+ `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 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);
+
+ const interactiveTimestamps =
+ rendererToInteractiveTimestamps.get(rendererHelper.pid);
+ if (interactiveTimestamps.length === 0) continue;
+ if (interactiveTimestamps.length > 1) {
+ // TODO(ulan): Support multiple interactive time windows when
+ // https://crbug.com/692112 is fixed.
+ continue;
+ }
+ const interactiveWindow =
+ tr.b.math.Range.fromExplicitRange(interactiveTimestamps[0], Infinity)
+ .findIntersection(rendererHelper.mainThread.bounds);
+ const interactiveSample =
+ tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow(
+ interactiveWindow.min, interactiveWindow.max,
+ WINDOW_SIZE_MS, tasks);
+ interactiveHistogram.addSample(interactiveSample);
+ interactiveBreakdown.set(eventName, interactiveSample);
+ interactiveNames.set(eventName, interactiveHistogram.name);
+ }
+ return {totalBreakdown, interactiveBreakdown};
+ }
+
+ /**
+ * @callback TaskExtractor
+ * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @return {Array.<{start: !number, end: !number}>}
+ */
+
+ /**
+ * @param {!EventTimesCallback} getEventTimes.
+ * @returns {!Map.<string, TaskExtractor>} a map from V8 event names to
+ * the corresponding task extractor functions.
+ */
+ function getV8EventNamesWithTaskExtractors_(getEventTimes, cpuMetrics) {
+ /**
+ * @param {!tr.b.Event} slice.
+ * @param {!function(tr.b.Event): boolean} predicate that selects V8 events.
+ * @param {function(tr.b.Event): boolean} excludePredicate that excludes
+ * V8 events.
+ * @returns {!number} the total duration of topmost subslices of the given
+ * slice that satisfy the given |predicate| after filtering out any
+ * events that satisfy the |excludePredicate| in the subslices.
+ */
+ 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;
+ }
+
+ /**
+ * @param {!function(tr.b.Event): boolean} predicate that selects V8 events.
+ * @param {function(tr.b.Event): boolean} excludePredicate that excludes
+ * V8 events.
+ * @returns {!TaskExtractor} a function that extracts tasks from the given
+ * renderer. Each task is a pair of {start, end} times and its duration
+ * represents the contribution of the events selected by the
+ * given |predicate| and |excludePredicate|.
+ */
+ 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)
+ ]
+ ]);
+ }
+
+ /**
+ * @param {!EventTimesCallback} getEventTimes.
+ * @param {!function(!string): boolean} predicate that selects RCS category.
+ * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @returns {Array.<{start: !number, end: !number}>} a list of tasks. Each
+ * task is a pair of {start, end} times and its duration represents the
+ * the contribution of the events selected by the given |predicate|.
+ */
+ 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;
+ }
+ // Find all V8ThreadSlices in the top level slice.
+ const v8ThreadSlices = [];
+ for (const slice of topSlice.descendentSlices) {
+ if (tr.metrics.v8.utils.isV8RCSEvent(slice)) {
+ v8ThreadSlices.push(slice);
+ }
+ }
+
+ // Find the event specified by predicate.
+ 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;
+ }
+
+ /**
+ * @param {!EventTimesCallback} getEventTimes.
+ * @returns {!Map.<string, TaskExtractor>} a map from V8 event names to
+ * the corresponding task extractor functions.
+ */
+ 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;
+ }
+
+ tr.metrics.MetricRegistry.register(expectedQueueingTimeMetric);
+
+ return {
+ expectedQueueingTimeMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html
new file mode 100644
index 00000000000..b300c523964
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html
@@ -0,0 +1,409 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/metrics/system_health/expected_queueing_time_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function addInteractiveTimestamp(rendererProcess, mainThread, timestamp) {
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: timestamp,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: timestamp,
+ duration: 5.0,
+ }));
+ rendererProcess.objects.addSnapshot(
+ 'ptr', 'loading', 'FrameLoader', timestamp, {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: timestamp + 1,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: timestamp + 1,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ }
+
+ test('expectedQueueingTime', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ addInteractiveTimestamp(rendererProcess, mainThread, 200);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 0,
+ duration: 100,
+ cpuStart: 0,
+ cpuDuration: 50,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 3000,
+ duration: 10,
+ cpuStart: 3000,
+ cpuDuration: 5,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9000,
+ duration: 10,
+ cpuStart: 9000,
+ cpuDuration: 5,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.expectedQueueingTimeMetric(histograms, model);
+ const interactiveEqt = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt');
+ assert.strictEqual(0.1, interactiveEqt.average);
+ const totalEqt = histograms.getHistogramNamed(
+ 'total:500ms_window:renderer_eqt');
+ assert.strictEqual(10, totalEqt.average);
+ const interactiveEqtCpu = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt_cpu');
+ assert.strictEqual(0.025, interactiveEqtCpu.average);
+ const totalEqtCpu = histograms.getHistogramNamed(
+ 'total:500ms_window:renderer_eqt_cpu');
+ assert.strictEqual(2.5, totalEqtCpu.average);
+ });
+
+ test('expectedQueueingTime_noInteractive', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 0,
+ duration: 100,
+ cpuStart: 0,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 3000,
+ duration: 10,
+ cpuStart: 3000,
+ cpuDuration: 10,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9000,
+ duration: 10,
+ cpuStart: 9000,
+ cpuDuration: 10,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.expectedQueueingTimeMetric(histograms, model);
+ const interactiveEQT = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt');
+ assert.strictEqual(0, interactiveEQT.numValues);
+ });
+
+ test('expectedQueueingTime_multipleInteractive', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ addInteractiveTimestamp(rendererProcess, mainThread, 200);
+ addInteractiveTimestamp(rendererProcess, mainThread, 6000);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 0,
+ duration: 100,
+ cpuStart: 0,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 3000,
+ duration: 10,
+ cpuStart: 3000,
+ cpuDuration: 10,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 12000,
+ duration: 10,
+ cpuStart: 9000,
+ cpuDuration: 10,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.expectedQueueingTimeMetric(histograms, model);
+ const interactiveEQT = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt');
+ // TODO(ulan): Support multiple interactive time windows when
+ // https://crbug.com/692112 is fixed.
+ assert.strictEqual(0, interactiveEQT.numValues);
+ });
+
+ test('expectedQueueingTime_multipleRenderersAggregates', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ addInteractiveTimestamp(rendererProcess, mainThread, 200);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 0,
+ duration: 100,
+ cpuStart: 0,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 3000,
+ duration: 10,
+ cpuStart: 3000,
+ cpuDuration: 10,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9000,
+ duration: 10,
+ cpuStart: 9000,
+ cpuDuration: 10,
+ }));
+ const rendererProcess2 = model.getOrCreateProcess(1985);
+ const mainThread2 = rendererProcess2.getOrCreateThread(2);
+ mainThread2.name = 'CrRendererMain';
+ addInteractiveTimestamp(rendererProcess2, mainThread2, 200);
+ mainThread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9000,
+ duration: 0,
+ cpuStart: 9000,
+ cpuDuration: 0,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.expectedQueueingTimeMetric(histograms, model);
+ const eqt = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt');
+ assert.strictEqual(0.05, eqt.average);
+ });
+
+ test('expectedQueueingTime_relatedV8Histograms', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ addInteractiveTimestamp(rendererProcess, mainThread, 200);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 0,
+ duration: 100,
+ cpuStart: 0,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.GCFinalizeMC',
+ start: 50,
+ duration: 50,
+ cpuStart: 50,
+ cpuDuration: 50,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 3000,
+ duration: 10,
+ cpuStart: 3000,
+ cpuDuration: 10,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-v8.compile',
+ title: 'V8.RecompileSynchronous',
+ start: 3000,
+ duration: 5,
+ cpuStart: 3000,
+ cpuDuration: 5,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9000,
+ duration: 10,
+ cpuStart: 9000,
+ cpuDuration: 10,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.GCFinalizeMC',
+ start: 9000,
+ duration: 5,
+ cpuStart: 9000,
+ cpuDuration: 5,
+ }));
+ mainThread.sliceGroup.createSubSlices();
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.expectedQueueingTimeMetric(histograms, model);
+
+ const eqt = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt');
+ assert.strictEqual('interactive:500ms_window:renderer_eqt:v8',
+ eqt.diagnostics.get('v8').get('v8'));
+ assert.strictEqual(0.025,
+ eqt.getBinForValue(0.1).diagnosticMaps[0].get('v8').get('v8'));
+
+ const eqtCpu = histograms.getHistogramNamed(
+ 'interactive:500ms_window:renderer_eqt_cpu');
+ assert.strictEqual('interactive:500ms_window:renderer_eqt_cpu:v8',
+ eqtCpu.diagnostics.get('v8').get('v8'));
+ assert.strictEqual(0.025,
+ eqtCpu.getBinForValue(0.1).diagnosticMaps[0].get('v8').get('v8'));
+ });
+
+ test('expectedQueueingTimeRCS', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ addInteractiveTimestamp(rendererProcess, mainThread, 200);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 0,
+ duration: 100,
+ cpuStart: 0,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.newInstance',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 12555,
+ duration: 990,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ ParseLazy: [5, 3],
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 3000,
+ duration: 100,
+ cpuStart: 3000,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 3000,
+ duration: 100,
+ cpuStart: 3000,
+ cpuDuration: 100,
+ args: {
+ 'runtime-call-stats': {
+ CompileIgnition: [3, 5000],
+ OptimizeCode: [6, 40000],
+ JS_Execution: [1, 11000],
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 4000,
+ duration: 100,
+ cpuStart: 4000,
+ cpuDuration: 100,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 4000,
+ duration: 100,
+ cpuStart: 4000,
+ cpuDuration: 100,
+ args: {
+ 'runtime-call-stats': {
+ CompileIgnition: [20, 20000],
+ ParseLazy: [5, 10000],
+ CompileBackgroundIgnition: [3, 30000]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9000,
+ duration: 10,
+ cpuStart: 9000,
+ cpuDuration: 10,
+ }));
+ mainThread.sliceGroup.createSubSlices();
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.expectedQueueingTimeMetric(histograms, model);
+
+ const eqt = histograms.getHistogramNamed(
+ 'total:500ms_window:renderer_eqt');
+ assert.strictEqual(
+ 'total:500ms_window:renderer_eqt:v8:compile:optimize_rcs',
+ eqt.diagnostics.get('v8').get('v8:compile:optimize_rcs'));
+ assert.strictEqual(1.6, eqt.getBinForValue(10).diagnosticMaps[0].get(
+ 'v8').get('v8:compile:optimize_rcs'));
+
+ assert.strictEqual('total:500ms_window:renderer_eqt:v8:compile:parse_rcs',
+ eqt.diagnostics.get('v8').get('v8:compile:parse_rcs'));
+ assert.strictEqual(0.1, eqt.getBinForValue(10).diagnosticMaps[0].get(
+ 'v8').get('v8:compile:parse_rcs'));
+
+ assert.strictEqual(
+ 'total:500ms_window:renderer_eqt:v8:compile:compile-unoptimize_rcs',
+ eqt.diagnostics.get('v8').get('v8:compile:compile-unoptimize_rcs'));
+ assert.strictEqual(0.4, eqt.getBinForValue(10).diagnosticMaps[0].get(
+ 'v8').get('v8:compile:compile-unoptimize_rcs'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html
new file mode 100644
index 00000000000..40d5722c1a4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/new_cpu_time_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ const includeHistogramNames = [
+ 'cpuTime:all_processes:all_threads:all_stages:all_initiators',
+ 'cpuPercentage:all_processes:all_threads:all_stages:all_initiators',
+ 'cpuTime:browser_process:all_threads:all_stages:all_initiators',
+ 'cpuPercentage:browser_process:all_threads:all_stages:all_initiators',
+ 'cpuTime:renderer_processes:all_threads:all_stages:all_initiators',
+ 'cpuPercentage:renderer_processes:all_threads:all_stages:all_initiators',
+ 'cpuTime:gpu_process:all_threads:all_stages:all_initiators',
+ 'cpuPercentage:gpu_process:all_threads:all_stages:all_initiators',
+ 'cpuTime:renderer_processes:CrRendererMain:all_stages:all_initiators',
+ 'cpuPercentage:renderer_processes:CrRendererMain:all_stages:all_initiators',
+ 'cpuTime:browser_process:CrBrowserMain:all_stages:all_initiators',
+ 'cpuPercentage:browser_process:CrBrowserMain:all_stages:all_initiators',
+ 'cpuTime:all_processes:all_threads:Load:Successful',
+ 'cpuPercentage:all_processes:all_threads:Load:Successful',
+ ];
+
+ /**
+ * This metric is a limited version of new CPU Time metric. The new cpu time
+ * metric produces 300-500 histograms for a trace, which is overwhelming for
+ * some systems. This file exposes a subset of those histograms.
+ *
+ * TODO(#4044): Remove this metric once histogram pipeline is ready.
+ *
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.Model} model
+ * @param {!Object=} opt_options
+ */
+ function limitedCpuTimeMetric(histograms, model, opt_options) {
+ const allCpuHistograms = new tr.v.HistogramSet();
+ tr.metrics.sh.newCpuTimeMetric(allCpuHistograms, model, opt_options);
+
+ for (const histogramName of includeHistogramNames) {
+ const histogram = allCpuHistograms.getHistogramNamed(histogramName);
+ if (histogram) histograms.addHistogram(histogram);
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(limitedCpuTimeMetric, {
+ supportsRangeOfInterest: true
+ });
+
+ return {
+ limitedCpuTimeMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html
new file mode 100644
index 00000000000..b439f2e7d15
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html
@@ -0,0 +1,623 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/category_util.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/extras/chrome/time_to_interactive.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/breakdown_tree_helpers.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_thread_helper.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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;
+
+ /**
+ * @param {!tr.model.Process} process
+ * @param {!tr.b.math.Range} range
+ * @return {Array.<tr.model.Event>} An array of network events of a process
+ * and that are intersecting a range.
+ */
+ 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;
+ }
+
+ /**
+ * @param {!Object.<string, Object>} breakdownTree
+ * @return {tr.v.d.Breakdown} A breakdown with categories and the total time
+ * (ms) spent under each category.
+ */
+ 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) // 50ms step to 1s
+ .addLinearBins(3e3, 20) // 100ms step to 3s
+ .addExponentialBins(20e3, 20);
+
+ const TIME_TO_INTERACTIVE_BOUNDARIES = tr.v.HistogramBinBoundaries
+ // 90-th percentiile of TTI is around 40 seconds, across warm and cold
+ // loads. Data obtained through Cluster Telemetry analysis.
+ .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;
+ }
+
+ 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);
+ // Want to keep the event with the most recent timestamp
+ let validEvent;
+ for (const targetEvent of targetEvents) {
+ if (rendererHelper.isTelemetryInternalEvent(targetEvent)) continue;
+ if (validEvent === undefined) {
+ validEvent = targetEvent;
+ } else {
+ // Want to keep the event with the most recent timestamp
+ if (validEvent.start < targetEvent.start) {
+ validEvent = targetEvent;
+ }
+ }
+ }
+ return validEvent;
+ }
+
+ function getAboveTheFoldLoadedToVisibleSamples(rendererHelper,
+ navIdToNavStartEvents) {
+ const samples = [];
+ // Want to measure the time from when navigation starts to when the load
+ // event fired for all non-ad resources. This done with the associated
+ // navigation start event to the pc mark in the amp code, correlated by
+ // navigation id.
+ 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 = 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 getFirstViewportReadySamples(rendererHelper) {
+ const samples = [];
+ // Want to measure the time from when the document is visible to the time
+ // when the load event fired for all non-ad resources. This is done with
+ // two marks in the amp code: pc and visible.
+ const pcEvent = getMostRecentValidEvent(
+ rendererHelper, 'blink.user_timing', 'pc');
+ const visibleEvent = getMostRecentValidEvent(
+ rendererHelper, 'blink.user_timing', 'visible');
+ if (pcEvent !== undefined && visibleEvent !== undefined) {
+ samples.push({
+ value: 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);
+ }
+
+ // Ignore layout w/o preceding navigationStart, as they are not
+ // attributed to any time-to-X metric.
+ 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)
+ }
+ });
+ }
+ 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 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,
+ }]),
+ }
+ });
+ }
+
+ /**
+ * Object containing one value and associated diagnostics info for that value
+ * for a metric.
+ * @typedef {{value: number, diagnostics: !tr.v.d.DiagnosticMap}} MetricSample
+ */
+
+ /**
+ * Returns a MetricSample for interactivity metrics - First CPU Idle and Time
+ * to Interactive.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper
+ * @param {?number} eventTimestamp - Timestamp of the event for which the
+ * sample is being generated.
+ * @param {tr.model.ThreadSlice} navigationStartEvent
+ * @param {number} firstMeaningfulPaintTime
+ * @param {number} domContentLoadedEndTime
+ * @param {string} url - URL of the current main frame document.
+ * @returns {MetricSample|undefined}
+ */
+ 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 can be derived from value and navigationStartEvent,
+ // but it's useful to directly see the value in the UI.
+ 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));
+ const aboveTheFoldLoadedToVisibleSamples =
+ getAboveTheFoldLoadedToVisibleSamples(
+ rendererHelper, navIdToNavStartEvents);
+ const firstViewportReadySamples = getFirstViewportReadySamples(
+ rendererHelper);
+
+ return {
+ frameToNavStartEvents,
+ firstPaintSamples,
+ firstContentfulPaintSamples,
+ firstContentfulPaintCpuTimeSamples,
+ onLoadSamples,
+ aboveTheFoldLoadedToVisibleSamples,
+ firstViewportReadySamples,
+ };
+ }
+
+ function collectMetricsFromLoadExpectations(model, chromeHelper) {
+ // Add FMP, firstCpuIdle and interactive samples from load UE
+ 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);
+ }
+ 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,
+ };
+ }
+
+ function addSamplesToHistogram(samples, histogram, histograms) {
+ for (const sample of samples) {
+ histogram.addSample(sample.value, sample.diagnostics);
+
+ // Only add breakdown histograms for FCP.
+ // http://crbug.com/771610
+ 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 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 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);
+ addSamplesToHistogram(
+ samplesSet.aboveTheFoldLoadedToVisibleSamples,
+ aboveTheFoldLoadedToVisibleHistogram,
+ histograms);
+ addSamplesToHistogram(
+ samplesSet.firstViewportReadySamples,
+ firstViewportReadyHistogram,
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html
new file mode 100644
index 00000000000..16b2dad797f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html
@@ -0,0 +1,1142 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/metrics/system_health/loading_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('timeToFirstPaint', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'rail,loading,devtools.timeline',
+ title: 'firstPaint',
+ start: 1001,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('timeToFirstPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(801, hist.running.mean);
+ });
+
+
+ test('timeToFirstContentfulPaint', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'ResourceDispatcher::OnRequestComplete',
+ start: 200,
+ duration: 100,
+ cpuStart: 210,
+ cpuDuration: 25,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('timeToFirstContentfulPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(800, hist.running.mean);
+ const fcpResourceLoading = histograms.getHistogramNamed(
+ 'timeToFirstContentfulPaint:resource_loading');
+ assert.strictEqual(
+ hist.diagnostics.get('breakdown').get(
+ 'resource_loading'),
+ 'timeToFirstContentfulPaint:resource_loading');
+ assert.strictEqual(fcpResourceLoading.sum, 100);
+ assert.strictEqual(tr.b.getOnlyElement(hist.getBinForValue(
+ 800).diagnosticMaps).get('breakdown').get('resource_loading'), 100);
+ });
+
+
+ test('timeToFirstViewportReady', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'pc',
+ start: 1200,
+ duration: 0.0,
+ args: {data: {navigationId: '0xsecondnav'}}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'pc',
+ start: 1300,
+ duration: 0.0,
+ args: {data: {navigationId: '0xsecondnav'}}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'visible',
+ start: 300,
+ duration: 0.0,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('timeToFirstViewportReady');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(1000, hist.running.mean);
+ });
+
+
+ test('aboveTheFoldLoadedToVisibleHistogram', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'pc',
+ start: 1300,
+ duration: 0.0,
+ args: {data: {navigationId: '0xsecondnav'}}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 300,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef', data: {navigationId: '0xfirstnav'}}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 400,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef', data: {navigationId: '0xsecondnav'}}
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed(
+ 'aboveTheFoldLoadedToVisible');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(900, hist.running.mean);
+ });
+
+
+ test('timeToFirstContentfulPaintIgnoringWarmCache', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ // warm cache navigation
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'blink.console',
+ title: 'telemetry.internal.warm_cache.warm.start',
+ start: 250,
+ duration: 0.0
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'blink.console',
+ title: 'telemetry.internal.warm_cache.warm.end',
+ start: 1250,
+ duration: 0.0
+ }));
+
+ // measurement navigation
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 2000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 2100,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 2400,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('timeToFirstContentfulPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(400, hist.running.mean);
+ });
+
+ test('timeToFirstContentfulPaintInCpuTime', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'ResourceDispatcher::OnRequestComplete',
+ start: 200,
+ duration: 100,
+ cpuStart: 210,
+ cpuDuration: 25,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('cpuTimeToFirstContentfulPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(25, hist.running.mean);
+ assert.strictEqual(tr.b.getOnlyElement(hist.getBinForValue(
+ 25).diagnosticMaps).get('breakdown').get('resource_loading'), 25);
+ });
+
+ test('timeToFirstContentfulPaintWithNavId', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef', data: {navigationId: '0xfirstnav'}}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 400,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef', data: {navigationId: '0xsecondnav'}}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef', data: {navigationId: '0xfirstnav'}}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'ResourceDispatcher::OnRequestComplete',
+ start: 200,
+ duration: 100,
+ cpuStart: 210,
+ cpuDuration: 25,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('timeToFirstContentfulPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(800, hist.running.mean);
+ const fcpResourceLoading = histograms.getHistogramNamed(
+ 'timeToFirstContentfulPaint:resource_loading');
+ assert.strictEqual(
+ hist.diagnostics.get('breakdown').get(
+ 'resource_loading'),
+ 'timeToFirstContentfulPaint:resource_loading');
+ assert.strictEqual(fcpResourceLoading.sum, 100);
+ assert.strictEqual(tr.b.getOnlyElement(hist.getBinForValue(
+ 800).diagnosticMaps).get('breakdown').get('resource_loading'), 100);
+ });
+
+ test('timeToFirstMeaningfulPaint', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 600,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('timeToFirstMeaningfulPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(800, hist.running.mean);
+ });
+
+ // [-------CPU: 300-----------]
+ // | [---CPU: 100---] |
+ // | | | |
+ // v v v v
+ // Ts: 100 200 Start 300 500 600
+ // | |
+ // Start FMP
+ test('cpuTimeToFirstMeaningfulPaint_withEmbeddedSlices', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ cpuStart: 1160,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 600,
+ duration: 0.0,
+ cpuStart: 1480,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 100,
+ duration: 400,
+ cpuStart: 1000,
+ cpuDuration: 300,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'ResourceDispatcher::OnRequestComplete',
+ start: 200,
+ duration: 100,
+ cpuStart: 1150,
+ cpuDuration: 100,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('cpuTimeToFirstMeaningfulPaint');
+ assert.deepEqual(hist.sampleValues, [225]);
+ const histBin = hist.getBinForValue(225);
+ assert.closeTo(histBin.diagnosticMaps[0]
+ .get('breakdown').get('other'), 400 / 3, 0.1);
+ assert.strictEqual(histBin.diagnosticMaps[0]
+ .get('breakdown').get('resource_loading'), 100);
+ });
+
+ // [-------------] [------------------]
+ // | | | |
+ // v v v v
+ // Ts: 100 Start 300 500 FMP 700
+ // | |
+ // 200 600
+ test('cpuTimeToFirstMeaningfulPaint_withIntersectingBoundary', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ cpuStart: 1160,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 600,
+ duration: 0.0,
+ cpuStart: 1280,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 100,
+ duration: 200,
+ cpuStart: 1060,
+ cpuDuration: 180,
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 500,
+ duration: 200,
+ cpuStart: 1250,
+ cpuDuration: 100,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('cpuTimeToFirstMeaningfulPaint');
+ assert.strictEqual(1, hist.running.count);
+ assert.strictEqual(140, hist.running.mean);
+ });
+
+ // Render 1:
+ //
+ // [--------]
+ // | |
+ // v v
+ // CPU Time: Start 1180 1230 FMP
+ // | |
+ // 1160 1280
+ //
+ // Render 2:
+ //
+ // [-------------]
+ // | |
+ // v v
+ // CPU Time: Start 1170 1270 FMP
+ // | |
+ // 1160 1280
+ test('cpuTimeToFirstMeaningfulPaint_multipleRenderers', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess1 = model.rendererProcess;
+ let mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ cpuStart: 1160,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess1.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 600,
+ duration: 0.0,
+ cpuStart: 1280,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loader',
+ title: 'ResourceDispatcher::OnRequestComplete',
+ start: 300,
+ duration: 200,
+ cpuStart: 1180,
+ cpuDuration: 50
+ }));
+
+ const rendererProcess2 = model.getOrCreateProcess(10);
+ mainThread = rendererProcess2.getOrCreateThread(20);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ cpuStart: 1160,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess2.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 600,
+ duration: 0.0,
+ cpuStart: 1280,
+ cpuDuration: 0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 300,
+ duration: 200,
+ cpuStart: 1170,
+ cpuDuration: 100,
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('cpuTimeToFirstMeaningfulPaint');
+ assert.deepEqual(hist.sampleValues, [50, 100]);
+ const histBin1 = hist.getBinForValue(50);
+ assert.strictEqual(histBin1.diagnosticMaps[0]
+ .get('breakdown').get('resource_loading'), 50);
+ const histBin2 = hist.getBinForValue(100);
+ assert.strictEqual(histBin2.diagnosticMaps[0]
+ .get('breakdown').get('other'), 100);
+ });
+
+ function addFrameLoaderObject_(rendererProcess, timestamp) {
+ rendererProcess.objects.addSnapshot(
+ 'ptr', 'loading', 'FrameLoader', timestamp, {
+ isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com',
+ });
+ }
+
+ function addNavigationStart_(rendererMain, timestamp, opt_data) {
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: timestamp,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef', data: opt_data}
+ }));
+ }
+
+ // Some utility functions to make tests easier to read.
+ function addFirstMeaningfulPaint_(rendererMain, timestamp) {
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: timestamp,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ }
+
+ function addDomContentLoadedEnd_(rendererMain, timestamp) {
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: timestamp,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ }
+
+ function addTopLevelTask_(rendererMain, start, duration) {
+ rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start,
+ duration,
+ }));
+ }
+
+ function addNetworkRequest_(rendererMain, start, duration) {
+ const networkEvents = [];
+ rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start,
+ duration,
+ }));
+ }
+
+ test('loadExpectationMetrics_urlDirectlyFromNavStartEvent', function() {
+ // If the navigation start event contains the URL, we should not require a
+ // FrameLoader snapshot before the event. See https://crbug.com/824761.
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200,
+ {isLoadingMainFrame: true, documentLoaderURL: 'http://example.com'});
+ addNetworkRequest_(mainThread, 200, 250);
+ // FrameLoader creation time after navigation start.
+ rendererProcess.objects.idWasCreated(
+ 'ptr', 'loading', 'FrameLoader', 250);
+ rendererProcess.objects.addSnapshot(
+ 'ptr', 'loading', 'FrameLoader', 300, {
+ isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com',
+ });
+
+ addFirstMeaningfulPaint_(mainThread, 500);
+ addDomContentLoadedEnd_(mainThread, 600);
+
+ // New navigation to close the search window.
+ addNavigationStart_(mainThread, 7000,
+ {isLoadingMainFrame: true, documentLoaderURL: 'http://example.com'});
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const fmpHist = histograms.getHistogramNamed('timeToFirstMeaningfulPaint');
+ assert.strictEqual(1, fmpHist.running.count);
+ assert.strictEqual(300, fmpHist.running.mean);
+ // Both TTI and First CPU Idle firing at DCL.
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ assert.strictEqual(400, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ assert.strictEqual(400, firstCpuIdleHist.running.mean);
+ });
+
+ test('interactivityMetrics_notAffectedByShortTasks', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 250);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addFirstMeaningfulPaint_(mainThread, 500);
+ addDomContentLoadedEnd_(mainThread, 600);
+
+ const mainThreadTopLevelTasks = [
+ {start: 800, duration: 100}, // Long task
+ {start: 1500, duration: 200}, // Last long task. TTI at 1700.
+ {start: 2000, duration: 49}, // Short task.
+ ];
+ for (const task of mainThreadTopLevelTasks) {
+ addTopLevelTask_(mainThread, task.start, task.duration);
+ }
+
+ // New navigation to close the search window.
+ addNavigationStart_(mainThread, 7000);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ // 1700 - 200 (navStart) = 1500.
+ assert.strictEqual(1500, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ // 1700 - 200 (navStart) = 1500.
+ assert.strictEqual(1500, firstCpuIdleHist.running.mean);
+ });
+
+ test('interactivityMetrics_longTaskBeforeFMP', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addDomContentLoadedEnd_(mainThread, 600);
+ addTopLevelTask_(mainThread, 600, 200);
+ addFirstMeaningfulPaint_(mainThread, 1000); // TTI at FMP.
+ // New navigation to close the search window.
+ addNavigationStart_(mainThread, 7000);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ // 1000 - 200 (navStart) = 800.
+ assert.strictEqual(800, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ // 1000 - 200 (navStart) = 800.
+ assert.strictEqual(800, firstCpuIdleHist.running.mean);
+ });
+
+ test('interactivityMetrics_interactiveAtDCL', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ addDomContentLoadedEnd_(mainThread, 3000);
+ // New navigation to close the search window.
+ addNavigationStart_(mainThread, 7000);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ // 3000 - 200 (navStart) = 2800.
+ assert.strictEqual(2800, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ // 3000 - 200 (navStart) = 2800.
+ assert.strictEqual(2800, firstCpuIdleHist.running.mean);
+ });
+
+ test('interactivityMetrics_networkBusyBlocksInteractivity', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ addDomContentLoadedEnd_(mainThread, 1100);
+ // Network busy requires at least three network requests.
+ addNetworkRequest_(mainThread, 1000, 7000);
+ addNetworkRequest_(mainThread, 1001, 7001);
+ addNetworkRequest_(mainThread, 1002, 7002);
+ // 400ms task makes a "heavy task cluster" for idle.
+ addTopLevelTask_(mainThread, 1200, 400);
+ // Next long task is more than five seconds away, but TTI is not reached
+ // yet since network is busy. TTI is at the at of this task.
+ addTopLevelTask_(mainThread, 6800, 200);
+ // New navigation to close the search window.
+ addNavigationStart_(mainThread, 13000);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ // 7000 - 200 (navStart) = 6800.
+ assert.strictEqual(6800, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ // 1600 - 200 (navStart) = 1400. CPU Idle is not affected by network.
+ assert.strictEqual(1400, firstCpuIdleHist.running.mean);
+ });
+
+ test('interactivityMetrics_notReportedIfTracingEndsEarly', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addDomContentLoadedEnd_(mainThread, 600);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ addTopLevelTask_(mainThread, 2000, 400);
+ // Last task in the model. 2501 will be considered end of tracing.
+ addTopLevelTask_(mainThread, 2500, 1);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(0, ttiHist.numValues);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(0, firstCpuIdleHist.numValues);
+ });
+
+ test('interactivityMetrics_notReportedIfNextNavigationIsEarly', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addDomContentLoadedEnd_(mainThread, 600);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ addTopLevelTask_(mainThread, 2000, 400);
+ // New navigation to close the search window. The window is not big enough
+ // to reach TTI.
+ addNavigationStart_(mainThread, 3000);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(0, ttiHist.numValues);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(0, firstCpuIdleHist.numValues);
+ });
+
+ test('interactivityMetrics_reportsValueForLastNavigation', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addDomContentLoadedEnd_(mainThread, 600);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ addTopLevelTask_(mainThread, 2000, 400);
+ // Last task in the model. 8001 will be considered end of tracing, so
+ // there is sufficiently large window to detect TTI.
+ addTopLevelTask_(mainThread, 8000, 1);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ // 2400 - 200 (navStart) = 2200.
+ assert.strictEqual(2200, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ // 2400 - 200 (navStart) = 2200.
+ assert.strictEqual(2200, firstCpuIdleHist.running.mean);
+ });
+
+ test('interactivityMetrics_multipleRenderers', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const renderers =
+ [model.getOrCreateProcess(1984), model.getOrCreateProcess(1985)];
+
+ for (const i of [0, 1]) {
+ const rendererProcess = renderers[i];
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addDomContentLoadedEnd_(mainThread, 600);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ // Durations are 400 and 800 for i value of 0 an 1.
+ addTopLevelTask_(mainThread, 2000, (i + 1) * 400);
+ // New navigation to close the search window.
+ addNavigationStart_(mainThread, 10000);
+ }
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(2, ttiHist.running.count);
+ // 2800 - 200 (navStart) = 2200, and 2400 - 200 = 2200.
+ assert.strictEqual(2600, ttiHist.running.max);
+ assert.strictEqual(2200, ttiHist.running.min);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(2, firstCpuIdleHist.running.count);
+ // 2800 - 200 (navStart) = 2200, and 2400 - 200 = 2200.
+ assert.strictEqual(2600, firstCpuIdleHist.running.max);
+ assert.strictEqual(2200, firstCpuIdleHist.running.min);
+ });
+
+ test('interactivityMetrics_eventsFromMultipleFrame', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addNetworkRequest_(mainThread, 200, 50);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ // No long task. TTI is reached at 3000.
+ addDomContentLoadedEnd_(mainThread, 3000);
+
+ // DomContentLoadedEnd and NavigationStart for a different frame.
+ rendererProcess.objects.addSnapshot(
+ 'ptr', 'loading', 'FrameLoader', 4000, {
+ isLoadingMainFrame: true, frame: {id_ref: '0xffffffff'},
+ documentLoaderURL: 'http://example.com'
+ });
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 4000,
+ duration: 0.0,
+ args: {frame: '0xffffffff'},
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 4500,
+ duration: 0.0,
+ args: {frame: '0xffffffff'}
+ }));
+
+ // Last task in the model. 8001 will be considered end of tracing, so
+ // there is sufficiently large window to detect TTI.
+ addTopLevelTask_(mainThread, 8000, 1);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(1, ttiHist.running.count);
+ // 3000 - 200 (navStart) = 2800.
+ assert.strictEqual(2800, ttiHist.running.mean);
+ const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle');
+ assert.strictEqual(1, firstCpuIdleHist.running.count);
+ // 3000 - 200 (navStart) = 2800.
+ assert.strictEqual(2800, firstCpuIdleHist.running.mean);
+ });
+
+ test('timeToInteractive_notReportedWithoutResourceLoadEvents', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+
+ addNavigationStart_(mainThread, 200);
+ addFrameLoaderObject_(rendererProcess, 300);
+ addDomContentLoadedEnd_(mainThread, 600);
+ addFirstMeaningfulPaint_(mainThread, 1000);
+ addTopLevelTask_(mainThread, 2000, 400);
+ // Last task in the model. 8001 will be considered end of tracing, so
+ // there is sufficiently large window to detect TTI.
+ addTopLevelTask_(mainThread, 8000, 1);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const ttiHist = histograms.getHistogramNamed('timeToInteractive');
+ assert.strictEqual(0, ttiHist.numValues);
+ });
+
+
+ test('webView', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const process = model.rendererProcess;
+ const rendererThread = model.rendererMain;
+ rendererThread.name = 'Chrome_InProcRendererThread';
+ rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ process.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {
+ isLoadingMainFrame: true,
+ frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'
+ });
+ rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading,rail,devtools.timeline',
+ title: 'firstContentfulPaint',
+ start: 600,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.loadingMetric(histograms, model);
+ const fcp = histograms.getHistogramNamed('timeToFirstContentfulPaint');
+ assert.strictEqual(1, fcp.running.count);
+ assert.strictEqual(400, fcp.running.mean);
+ const fmp = histograms.getHistogramNamed('timeToFirstMeaningfulPaint');
+ assert.strictEqual(1, fmp.running.count);
+ assert.strictEqual(800, fmp.running.mean);
+ });
+
+ test('testGetNetworkEvents', function() {
+ // Our renderer looks like:
+ // [ irrelevant syncEvent ]
+ // [ irrelevant asyncEvent ]
+ // | [ d..netlog]
+ // [ netlog ] [ d..network] [ net ]
+ // | | | | | |
+ // | | | | | |
+ // | | | | | |
+ // v v v v v v
+ // Ts: 100 200 400 450 510 520
+ const rendererPid = 245;
+ const netEvent1 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'netlog',
+ title: 'Generic Network event',
+ start: 100,
+ duration: 100,
+ });
+ const netEvent2 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 400,
+ duration: 50,
+ });
+ const netEvent3 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'net',
+ title: 'ResourceLoad',
+ start: 510,
+ duration: 10,
+ });
+ const netEvent4 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'disabled-by-default-netlog',
+ title: 'ResourceLoad',
+ start: 510,
+ duration: 10,
+ });
+ const irrelevantAsyncEvent = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'irrelevant',
+ title: 'ResourceLoad',
+ start: 0,
+ duration: 510,
+ });
+ const irrelevantSyncEvent = tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 510,
+ args: {frame: '0xdeadbeef'}
+ });
+
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(rendererPid);
+ const thread1 = rendererProcess.getOrCreateThread(1);
+ thread1.name = 'CrRendererMain';
+ thread1.asyncSliceGroup.push(netEvent1);
+ const thread2 = rendererProcess.getOrCreateThread(2);
+ thread2.name = 'thread2';
+ thread2.asyncSliceGroup.push(netEvent2);
+ const thread3 = rendererProcess.getOrCreateThread(3);
+ thread3.name = 'thread2';
+ thread3.asyncSliceGroup.push(netEvent3);
+ const thread4 = rendererProcess.getOrCreateThread(4);
+ thread4.name = 'thread2';
+ thread4.asyncSliceGroup.push(netEvent4);
+ const thread5 = rendererProcess.getOrCreateThread(5);
+ thread5.name = 'thread5';
+ thread5.asyncSliceGroup.push(irrelevantAsyncEvent);
+ const thread6 = rendererProcess.getOrCreateThread(6);
+ thread6.name = 'thread6';
+ thread6.sliceGroup.pushSlice(irrelevantSyncEvent);
+ });
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelper = chromeHelper.rendererHelpers[rendererPid];
+ const allNetworkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, tr.b.math.Range.fromExplicitRange(0, 550));
+ assert.sameDeepMembers(
+ [netEvent1, netEvent2, netEvent3, netEvent4],
+ allNetworkEvents);
+
+ const partialNetworkEvents = tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, tr.b.math.Range.fromExplicitRange(0, 460));
+ assert.strictEqual(2, partialNetworkEvents.length);
+ assert.sameDeepMembers(
+ [netEvent1, netEvent2],
+ partialNetworkEvents);
+
+ const networkEventsWithIntersecting =
+ tr.metrics.sh.getNetworkEventsInRange(
+ rendererHelper.process, tr.b.math.Range.fromExplicitRange(0, 410));
+ assert.sameDeepMembers(
+ [netEvent1, netEvent2],
+ partialNetworkEvents);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html
new file mode 100644
index 00000000000..e42683d3164
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ const LONG_TASK_MS = 50;
+
+ // Anything longer than this should be so rare that its length beyond this is
+ // uninteresting.
+ const LONGEST_TASK_MS = 1000;
+
+ /**
+ * This helper function calls |cb| for each of the top-level tasks on the
+ * given thread in the given range whose duration is longer than LONG_TASK_MS.
+ *
+ * @param {tr.model.Thread} thread
+ * @param {tr.b.math.Range=} opt_range
+ * @param {function()} cb
+ * @param {Object=} opt_this
+ */
+ function iterateLongTopLevelTasksOnThreadInRange(
+ thread, opt_range, cb, opt_this) {
+ thread.sliceGroup.topLevelSlices.forEach(function(slice) {
+ if (opt_range &&
+ !opt_range.intersectsExplicitRangeInclusive(slice.start, slice.end)) {
+ return;
+ }
+
+ if (slice.duration < LONG_TASK_MS) return;
+
+ cb.call(opt_this, slice);
+ });
+ }
+
+ /**
+ * This helper function calls |cb| for each of the main renderer threads in
+ * the model.
+ *
+ * @param {tr.model.Model} model The model.
+ * @param {function()} cb Callback.
+ * @param {Object=} opt_this Context object.
+ */
+ function iterateRendererMainThreads(model, cb, opt_this) {
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (modelHelper !== undefined) {
+ Object.values(modelHelper.rendererHelpers).forEach(
+ function(rendererHelper) {
+ if (!rendererHelper.mainThread) return;
+
+ cb.call(opt_this, rendererHelper.mainThread);
+ });
+ }
+ }
+
+ const BIN_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(
+ LONG_TASK_MS, LONGEST_TASK_MS, 40);
+
+ /**
+ * This metric directly measures long tasks on renderer main threads.
+ * This metric requires only the 'toplevel' tracing category.
+ *
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.Model} model
+ * @param {!Object=} opt_options
+ */
+ function longTasksMetric(histograms, model, opt_options) {
+ const rangeOfInterest = opt_options ? opt_options.rangeOfInterest :
+ undefined;
+ const longTaskHist = histograms.createHistogram(
+ 'longTasks', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: BIN_BOUNDARIES,
+ description: 'durations of long tasks',
+ });
+
+ const relatedNames = new tr.v.d.RelatedNameMap();
+ longTaskHist.diagnostics.set('categories', relatedNames);
+
+ iterateRendererMainThreads(model, function(thread) {
+ iterateLongTopLevelTasksOnThreadInRange(
+ thread, rangeOfInterest, function(task) {
+ const breakdown = new tr.v.d.Breakdown();
+ breakdown.colorScheme =
+ tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;
+ for (const slice of task.descendentSlices) {
+ const sample = slice.cpuSelfTime;
+ if (sample === undefined) continue;
+
+ const category = model.getUserFriendlyCategoryFromEvent(slice);
+ const histName = 'longTasks:' + category;
+ let hist = histograms.getHistogramNamed(histName);
+ if (hist === undefined) {
+ hist = histograms.createHistogram(histName,
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: BIN_BOUNDARIES,
+ });
+ relatedNames.set(category, hist.name);
+ }
+ hist.addSample(sample, {
+ events: new tr.v.d.RelatedEventSet([slice]),
+ });
+ breakdown.set(category, sample + breakdown.get(category));
+ }
+
+ longTaskHist.addSample(task.duration, {
+ events: new tr.v.d.RelatedEventSet([task]),
+ categories: breakdown,
+ });
+ });
+ });
+ }
+
+ tr.metrics.MetricRegistry.register(longTasksMetric, {
+ supportsRangeOfInterest: true,
+ requiredCategories: ['toplevel'],
+ });
+
+ return {
+ longTasksMetric,
+ iterateLongTopLevelTasksOnThreadInRange,
+ iterateRendererMainThreads,
+ LONG_TASK_MS,
+ LONGEST_TASK_MS,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html
new file mode 100644
index 00000000000..ecfeabd2a5a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/metrics/system_health/long_tasks_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('longTasksMetric', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const proc = model.getOrCreateProcess(1);
+ const thread = proc.getOrCreateThread(2);
+ thread.name = 'CrRendererMain';
+ const longTask = tr.c.TestUtils.newSliceEx({
+ title: 'foo',
+ start: 0,
+ duration: 50,
+ });
+ thread.sliceGroup.pushSlice(longTask);
+
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'UpdateLayerTree',
+ start: 0,
+ duration: 1,
+ cpuStart: 0,
+ cpuDuration: 1,
+ }));
+
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'minorGC',
+ start: 1,
+ duration: 1,
+ cpuStart: 1,
+ cpuDuration: 1,
+ }));
+
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'Decode Image',
+ start: 2,
+ duration: 1,
+ cpuStart: 2,
+ cpuDuration: 1,
+ }));
+
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: 'Layout',
+ start: 3,
+ duration: 1,
+ cpuStart: 3,
+ cpuDuration: 1,
+ }));
+
+ model.addUserFriendlyCategoryDriver(
+ tr.e.chrome.ChromeUserFriendlyCategoryDriver);
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.longTasksMetric(histograms, model);
+
+ const longTaskHist = histograms.getHistogramNamed('longTasks');
+ assert.strictEqual(1, longTaskHist.numValues);
+
+ const relatedNames = longTaskHist.diagnostics.get('categories');
+ assert.instanceOf(relatedNames, tr.v.d.RelatedNameMap);
+ assert.strictEqual(relatedNames.get('layout'), 'longTasks:layout');
+ assert.strictEqual(relatedNames.get('gc'), 'longTasks:gc');
+ assert.strictEqual(relatedNames.get('composite'), 'longTasks:composite');
+ assert.strictEqual(relatedNames.get('imageDecode'),
+ 'longTasks:imageDecode');
+
+ const bin = longTaskHist.getBinForValue(longTaskHist.average);
+ const diagnostics = tr.b.getOnlyElement(bin.diagnosticMaps);
+ const breakdown = diagnostics.get('categories');
+ assert.instanceOf(breakdown, tr.v.d.Breakdown);
+ assert.strictEqual(breakdown.size, 4);
+
+ const taskEventSet = diagnostics.get('events');
+ assert.instanceOf(taskEventSet, tr.v.d.RelatedEventSet);
+ assert.lengthOf(taskEventSet, 1);
+
+ const layoutHist = histograms.getHistogramNamed('longTasks:layout');
+ assert.strictEqual(1, layoutHist.numValues);
+ assert.lengthOf(tr.b.getOnlyElement(layoutHist.getBinForValue(
+ layoutHist.average).diagnosticMaps).get('events'), 1);
+
+ const gcHist = histograms.getHistogramNamed('longTasks:gc');
+ assert.strictEqual(1, gcHist.numValues);
+ assert.lengthOf(tr.b.getOnlyElement(gcHist.getBinForValue(
+ gcHist.average).diagnosticMaps).get('events'), 1);
+
+ const compositeHist = histograms.getHistogramNamed('longTasks:composite');
+ assert.strictEqual(1, compositeHist.numValues);
+ assert.lengthOf(tr.b.getOnlyElement(compositeHist.getBinForValue(
+ compositeHist.average).diagnosticMaps).get('events'), 1);
+
+ const imageDecodeHist = histograms.getHistogramNamed(
+ 'longTasks:imageDecode');
+ assert.strictEqual(1, imageDecodeHist.numValues);
+ assert.lengthOf(tr.b.getOnlyElement(imageDecodeHist.getBinForValue(
+ imageDecodeHist.average).diagnosticMaps).get('events'), 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html
new file mode 100644
index 00000000000..68a90814bd0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html
@@ -0,0 +1,1332 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_processes.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
+ const DISPLAYED_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
+
+ const LEVEL_OF_DETAIL_NAMES = new Map();
+ LEVEL_OF_DETAIL_NAMES.set(BACKGROUND, 'background');
+ LEVEL_OF_DETAIL_NAMES.set(LIGHT, 'light');
+ LEVEL_OF_DETAIL_NAMES.set(DETAILED, 'detailed');
+
+ // Some detailed dumps contain heap profiler information.
+ const HEAP_PROFILER_DETAIL_NAME = 'heap_profiler';
+
+ const BOUNDARIES_FOR_UNIT_MAP = new WeakMap();
+ // For unitless numerics (process counts), we use 20 linearly scaled bins
+ // from 0 to 20.
+ BOUNDARIES_FOR_UNIT_MAP.set(count_smallerIsBetter,
+ tr.v.HistogramBinBoundaries.createLinear(0, 20, 20));
+ // For size numerics (subsystem and vm stats), we use 1 bin from 0 B to
+ // 1 KiB and 4*24 exponentially scaled bins from 1 KiB to 16 GiB (=2^24 KiB).
+ BOUNDARIES_FOR_UNIT_MAP.set(sizeInBytes_smallerIsBetter,
+ new tr.v.HistogramBinBoundaries(0)
+ .addBinBoundary(1024 /* 1 KiB */)
+ .addExponentialBins(16 * 1024 * 1024 * 1024 /* 16 GiB */, 4 * 24));
+
+ const CHROME_PROCESS_NAMES =
+ tr.e.chrome.chrome_processes.CHROME_PROCESS_NAMES;
+
+ function memoryMetric(values, model, opt_options) {
+ const rangeOfInterest =
+ opt_options ? opt_options.rangeOfInterest : undefined;
+ const browserNameToGlobalDumps =
+ tr.metrics.sh.splitGlobalDumpsByBrowserName(model, rangeOfInterest);
+ addGeneralMemoryDumpValues(browserNameToGlobalDumps, values);
+ addDetailedMemoryDumpValues(browserNameToGlobalDumps, values);
+ addMemoryDumpCountValues(browserNameToGlobalDumps, values);
+ }
+
+ const USER_FRIENDLY_BROWSER_NAMES = {
+ 'chrome': 'Chrome',
+ 'webview': 'WebView',
+ 'unknown_browser': 'an unknown browser'
+ };
+
+ /**
+ * Convert a canonical browser name used in value names to a user-friendly
+ * name used in value descriptions.
+ *
+ * Examples:
+ *
+ * CANONICAL BROWSER NAME -> USER-FRIENDLY NAME
+ * chrome -> Chrome
+ * unknown_browser -> an unknown browser
+ * webview2 -> WebView(2)
+ * unexpected -> 'unexpected' browser
+ */
+ function convertBrowserNameToUserFriendlyName(browserName) {
+ for (const baseName in USER_FRIENDLY_BROWSER_NAMES) {
+ if (!browserName.startsWith(baseName)) continue;
+
+ const userFriendlyBaseName = USER_FRIENDLY_BROWSER_NAMES[baseName];
+ const suffix = browserName.substring(baseName.length);
+ if (suffix.length === 0) {
+ return userFriendlyBaseName;
+ } else if (/^\d+$/.test(suffix)) {
+ return userFriendlyBaseName + '(' + suffix + ')';
+ }
+ }
+ return '\'' + browserName + '\' browser';
+ }
+
+
+ /**
+ * Convert a canonical process name used in value names to a user-friendly
+ * name used in value descriptions.
+ */
+ function convertProcessNameToUserFriendlyName(processName,
+ opt_requirePlural) {
+ switch (processName) {
+ case CHROME_PROCESS_NAMES.BROWSER:
+ return opt_requirePlural ? 'browser processes' : 'the browser process';
+ case CHROME_PROCESS_NAMES.RENDERER:
+ return 'renderer processes';
+ case CHROME_PROCESS_NAMES.GPU:
+ return opt_requirePlural ? 'GPU processes' : 'the GPU process';
+ case CHROME_PROCESS_NAMES.PPAPI:
+ return opt_requirePlural ? 'PPAPI processes' : 'the PPAPI process';
+ case CHROME_PROCESS_NAMES.ALL:
+ return 'all processes';
+ case CHROME_PROCESS_NAMES.UNKNOWN:
+ return 'unknown processes';
+ default:
+ return '\'' + processName + '\' processes';
+ }
+ }
+
+ /**
+ * Add general memory dump values calculated from all global memory dumps to
+ * |values|. In particular, this function adds the following values:
+ *
+ * * PROCESS COUNTS
+ * memory:{chrome, webview}:
+ * {browser_process, renderer_processes, ..., all_processes}:
+ * process_count
+ * type: tr.v.Histogram (over all matching global memory dumps)
+ * unit: count_smallerIsBetter
+ *
+ * * MEMORY USAGE REPORTED BY CHROME
+ * memory:{chrome, webview}:
+ * {browser_process, renderer_processes, ..., all_processes}:
+ * reported_by_chrome[:{v8, malloc, ...}]:
+ * {effective_size, allocated_objects_size, locked_size}
+ * type: tr.v.Histogram (over all matching global memory dumps)
+ * unit: sizeInBytes_smallerIsBetter
+ */
+ function addGeneralMemoryDumpValues(browserNameToGlobalDumps, values) {
+ addMemoryDumpValues(browserNameToGlobalDumps,
+ gmd => true /* process all global memory dumps */,
+ function(processDump, addProcessScalar) {
+ // Increment memory:<browser-name>:<process-name>:process_count value.
+ addProcessScalar({
+ source: 'process_count',
+ property: PROCESS_COUNT,
+ value: 1
+ });
+
+ if (processDump.totals !== undefined) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ property: RESIDENT_SIZE,
+ component: ['system_memory'],
+ value: processDump.totals.residentBytes
+ });
+ addProcessScalar({
+ source: 'reported_by_os',
+ property: PEAK_RESIDENT_SIZE,
+ component: ['system_memory'],
+ value: processDump.totals.peakResidentBytes
+ });
+ addProcessScalar({
+ source: 'reported_by_os',
+ property: PRIVATE_FOOTPRINT_SIZE,
+ component: ['system_memory'],
+ value: processDump.totals.privateFootprintBytes,
+ });
+ }
+
+ // Add memory:<browser-name>:<process-name>:reported_by_chrome:...
+ // values.
+ if (processDump.memoryAllocatorDumps === undefined) return;
+
+ processDump.memoryAllocatorDumps.forEach(function(rootAllocatorDump) {
+ CHROME_VALUE_PROPERTIES.forEach(function(property) {
+ addProcessScalar({
+ source: 'reported_by_chrome',
+ component: [rootAllocatorDump.name],
+ property,
+ value: rootAllocatorDump.numerics[property.name]
+ });
+ });
+ // Some dump providers add allocated objects size as
+ // "allocated_objects" child dump.
+ if (rootAllocatorDump.numerics.allocated_objects_size ===
+ undefined) {
+ const allocatedObjectsDump =
+ rootAllocatorDump.getDescendantDumpByFullName(
+ 'allocated_objects');
+ if (allocatedObjectsDump !== undefined) {
+ addProcessScalar({
+ source: 'reported_by_chrome',
+ component: [rootAllocatorDump.name],
+ property: ALLOCATED_OBJECTS_SIZE,
+ value: allocatedObjectsDump.numerics.size
+ });
+ }
+ }
+ });
+
+ // Add memory:<browser-name>:<process-name>:reported_by_chrome:
+ // {malloc, blinkgc, partitionalloc}:<largestCategory>:...
+ addTopHeapDumpCategoryValue(processDump, addProcessScalar);
+
+ // Add memory:<browser-name>:<process-name>:reported_by_chrome:v8:
+ // {heap, allocated_by_malloc}:...
+ addV8MemoryDumpValues(processDump, addProcessScalar);
+ },
+ function(componentTree) {
+ // Subtract memory:<browser-name>:<process-name>:reported_by_chrome:
+ // tracing:<size-property> from memory:<browser-name>:<process-name>:
+ // reported_by_chrome:<size-property> if applicable.
+ const tracingNode = componentTree.children[1].get('tracing');
+ if (tracingNode === undefined) return;
+
+ for (let i = 0; i < componentTree.values.length; i++) {
+ componentTree.values[i].total -= tracingNode.values[i].total;
+ }
+ }, values);
+ }
+
+ /**
+ * Add memory dump values for the top category in each allocator heap dump in
+ * the process dump.
+ *
+ * @param {!tr.model.ProcessMemoryDump} processDump The process memory dump.
+ * @param {!function} addProcessScalar The callback for adding a scalar value.
+ */
+ function addTopHeapDumpCategoryValue(processDump, addProcessScalar) {
+ if (!processDump.heapDumps) {
+ return;
+ }
+ for (const allocatorName in processDump.heapDumps) {
+ const heapDump = processDump.heapDumps[allocatorName];
+ if (heapDump.entries === undefined || heapDump.entries.length === 0) {
+ return;
+ }
+ // Create a map of category to total size.
+ const typeToSize = {};
+ for (let i = 0; i < heapDump.entries.length; i += 1) {
+ const entry = heapDump.entries[i];
+ // Count only the entries with empty backtrace which contains totals for
+ // the object type.
+ if (!entry.objectTypeName || entry.leafStackFrame) {
+ continue;
+ }
+ if (!typeToSize[entry.objectTypeName]) {
+ typeToSize[entry.objectTypeName] = 0;
+ }
+ typeToSize[entry.objectTypeName] += entry.size;
+ }
+
+ // Find the largest type in the heap dump.
+ let largestValue = 0;
+ let largestType = '';
+ for (const key in typeToSize) {
+ if (largestValue < typeToSize[key]) {
+ largestValue = typeToSize[key];
+ largestType = key;
+ }
+ }
+ addProcessScalar({
+ source: 'reported_by_chrome',
+ component: [allocatorName, largestType],
+ property: HEAP_CATEGORY_SIZE,
+ value: largestValue
+ });
+ }
+ }
+
+ /**
+ * Add memory dump values calculated from V8 components excluding
+ * 'heap_spaces/other_spaces'.
+ *
+ * @param {!tr.model.ProcessMemoryDump} processDump The process memory dump.
+ * @param {!function} addProcessScalar The callback for adding a scalar value.
+ */
+ function addV8MemoryDumpValues(processDump, addProcessScalar) {
+ const v8Dump = processDump.getMemoryAllocatorDumpByFullName('v8');
+ if (v8Dump === undefined) return;
+
+ v8Dump.children.forEach(function(isolateDump) {
+ // v8:allocated_by_malloc:...
+ const mallocDump = isolateDump.getDescendantDumpByFullName('malloc');
+ if (mallocDump !== undefined) {
+ addV8ComponentValues(mallocDump, ['v8', 'allocated_by_malloc'],
+ addProcessScalar);
+ }
+ // v8:heap:...
+ let heapDump = isolateDump.getDescendantDumpByFullName('heap');
+ if (heapDump === undefined) {
+ // Old V8 memory dumps call this 'heap_spaces'.
+ heapDump = isolateDump.getDescendantDumpByFullName('heap_spaces');
+ }
+ if (heapDump !== undefined) {
+ addV8ComponentValues(heapDump, ['v8', 'heap'], addProcessScalar);
+ heapDump.children.forEach(function(spaceDump) {
+ if (spaceDump.name === 'other_spaces') return;
+
+ addV8ComponentValues(spaceDump, ['v8', 'heap', spaceDump.name],
+ addProcessScalar);
+ });
+ }
+ });
+
+ // V8 generates bytecode when interpreting and code objects when
+ // compiling the javascript. Total code size includes the size
+ // of code and bytecode objects.
+ addProcessScalar({
+ source: 'reported_by_chrome',
+ component: ['v8'],
+ property: CODE_AND_METADATA_SIZE,
+ value: v8Dump.numerics.code_and_metadata_size
+ });
+ addProcessScalar({
+ source: 'reported_by_chrome',
+ component: ['v8'],
+ property: CODE_AND_METADATA_SIZE,
+ value: v8Dump.numerics.bytecode_and_metadata_size
+ });
+ }
+
+ /**
+ * Add memory dump values calculated from the specified V8 component.
+ *
+ * @param {!tr.model.MemoryAllocatorDump} v8Dump The V8 memory dump.
+ * @param {!Array<string>} componentPath The component path for reporting.
+ * @param {!function} addProcessScalar The callback for adding a scalar value.
+ */
+ function addV8ComponentValues(componentDump, componentPath,
+ addProcessScalar) {
+ CHROME_VALUE_PROPERTIES.forEach(function(property) {
+ addProcessScalar({
+ source: 'reported_by_chrome',
+ component: componentPath,
+ property,
+ value: componentDump.numerics[property.name]
+ });
+ });
+ }
+
+ const PROCESS_COUNT = {
+ unit: count_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ if (componentPath.length > 0) {
+ throw new Error('Unexpected process count non-empty component path: ' +
+ componentPath.join(':'));
+ }
+ return 'total number of ' + convertProcessNameToUserFriendlyName(
+ processName, true /* opt_requirePlural */);
+ }
+ };
+
+ const EFFECTIVE_SIZE = {
+ name: 'effective_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyName: 'effective size',
+ componentPreposition: 'of'
+ });
+ }
+ };
+
+ const ALLOCATED_OBJECTS_SIZE = {
+ name: 'allocated_objects_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyName: 'size of all objects allocated',
+ totalUserFriendlyPropertyName: 'size of all allocated objects',
+ componentPreposition: 'by'
+ });
+ }
+ };
+
+ const SHIM_ALLOCATED_OBJECTS_SIZE = {
+ name: 'shim_allocated_objects_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyName: 'size of all objects allocated through shim',
+ totalUserFriendlyPropertyName:
+ 'size of all allocated objects through shim',
+ componentPreposition: 'by'
+ });
+ }
+ };
+
+ const LOCKED_SIZE = {
+ name: 'locked_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyName: 'locked (pinned) size',
+ componentPreposition: 'of'
+ });
+ }
+ };
+
+ const PEAK_SIZE = {
+ name: 'peak_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyName: 'peak size',
+ componentPreposition: 'of'
+ });
+ }
+ };
+
+ const HEAP_CATEGORY_SIZE = {
+ name: 'heap_category_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyName: 'heap profiler category size',
+ componentPreposition: 'for'
+ });
+ }
+ };
+
+ const CODE_AND_METADATA_SIZE = {
+ name: 'code_and_metadata_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildChromeValueDescriptionPrefix(componentPath, processName, {
+ userFriendlyPropertyNamePrefix: 'size of',
+ userFriendlyPropertyName: 'code and metadata'
+ });
+ }
+ };
+
+ const CHROME_VALUE_PROPERTIES = [
+ EFFECTIVE_SIZE,
+ ALLOCATED_OBJECTS_SIZE,
+ SHIM_ALLOCATED_OBJECTS_SIZE,
+ LOCKED_SIZE,
+ PEAK_SIZE
+ ];
+
+ /**
+ * Build a description prefix for a memory:<browser-name>:<process-name>:
+ * reported_by_chrome:... value.
+ *
+ * @param {!Array<string>} componentPath The underlying component path (e.g.
+ * ['malloc']).
+ * @param {string} processName The canonical name of the process.
+ * @param {{
+ * userFriendlyPropertyName: string,
+ * userFriendlyPropertyNamePrefix: (string|undefined),
+ * totalUserFriendlyPropertyName: (string|undefined),
+ * componentPreposition: (string|undefined) }}
+ * formatSpec Specification of how the property should be formatted.
+ * @return {string} Prefix for the value's description (e.g.
+ * 'effective size of malloc in the browser process').
+ */
+ function buildChromeValueDescriptionPrefix(
+ componentPath, processName, formatSpec) {
+ const nameParts = [];
+ if (componentPath.length === 0) {
+ nameParts.push('total');
+ if (formatSpec.totalUserFriendlyPropertyName) {
+ nameParts.push(formatSpec.totalUserFriendlyPropertyName);
+ } else {
+ if (formatSpec.userFriendlyPropertyNamePrefix) {
+ nameParts.push(formatSpec.userFriendlyPropertyNamePrefix);
+ }
+ nameParts.push(formatSpec.userFriendlyPropertyName);
+ }
+ nameParts.push('reported by Chrome for');
+ } else {
+ if (formatSpec.componentPreposition === undefined) {
+ // Use component name as an adjective
+ // (e.g. 'size of V8 code and metadata').
+ if (formatSpec.userFriendlyPropertyNamePrefix) {
+ nameParts.push(formatSpec.userFriendlyPropertyNamePrefix);
+ }
+ nameParts.push(componentPath.join(':'));
+ nameParts.push(formatSpec.userFriendlyPropertyName);
+ } else {
+ // Use component name as a noun with a preposition
+ // (e.g. 'size of all objects allocated BY MALLOC').
+ if (formatSpec.userFriendlyPropertyNamePrefix) {
+ nameParts.push(formatSpec.userFriendlyPropertyNamePrefix);
+ }
+ nameParts.push(formatSpec.userFriendlyPropertyName);
+ nameParts.push(formatSpec.componentPreposition);
+ if (componentPath[componentPath.length - 1] === 'allocated_by_malloc') {
+ nameParts.push('objects allocated by malloc for');
+ nameParts.push(
+ componentPath.slice(0, componentPath.length - 1).join(':'));
+ } else {
+ nameParts.push(componentPath.join(':'));
+ }
+ }
+ nameParts.push('in');
+ }
+ nameParts.push(convertProcessNameToUserFriendlyName(processName));
+ return nameParts.join(' ');
+ }
+
+ const RESIDENT_SIZE = {
+ name: 'resident_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'resident set size (RSS)');
+ }
+ };
+
+ const PEAK_RESIDENT_SIZE = {
+ name: 'peak_resident_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'peak resident set size');
+ }
+ };
+
+ const PROPORTIONAL_RESIDENT_SIZE = {
+ name: 'proportional_resident_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'proportional resident size (PSS)');
+ }
+ };
+
+ const PRIVATE_DIRTY_SIZE = {
+ name: 'private_dirty_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'private dirty size');
+ }
+ };
+
+ const PRIVATE_FOOTPRINT_SIZE = {
+ name: 'private_footprint_size',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'private footprint size');
+ }
+ };
+
+ const JAVA_BASE_CLEAN_RESIDENT = {
+ name: 'java_base_clean_resident',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'java base odex and vdex total clean resident size');
+ }
+ };
+
+ const JAVA_BASE_PSS = {
+ name: 'java_base_pss',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'java base odex and vdex proportional resident size');
+ }
+ };
+
+ const NATIVE_LIBRARY_PRIVATE_CLEAN_RESIDENT = {
+ name: 'native_library_private_clean_resident',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'native library private clean resident size');
+ }
+ };
+
+ const NATIVE_LIBRARY_SHARED_CLEAN_RESIDENT = {
+ name: 'native_library_shared_clean_resident',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'native library shared clean resident size');
+ }
+ };
+
+ const NATIVE_LIBRARY_PROPORTIONAL_RESIDENT = {
+ name: 'native_library_proportional_resident',
+ unit: sizeInBytes_smallerIsBetter,
+ buildDescriptionPrefix(componentPath, processName) {
+ return buildOsValueDescriptionPrefix(componentPath, processName,
+ 'native library proportional resident size');
+ }
+ };
+
+ /**
+ * Build a description prefix for a memory:<browser-name>:<process-name>:
+ * reported_by_os:... value.
+ *
+ * @param {!Array<string>} componentPath The underlying component path (e.g.
+ * ['system', 'java_heap']).
+ * @param {string} processName The canonical name of the process.
+ * @param {string} userFriendlyPropertyName User-friendly name of the
+ * underlying property (e.g. 'private dirty size').
+ * @return {string} Prefix for the value's description (e.g.
+ * 'total private dirty size of the Java heal in the GPU process').
+ */
+ function buildOsValueDescriptionPrefix(
+ componentPath, processName, userFriendlyPropertyName) {
+ if (componentPath.length > 2) {
+ throw new Error('OS value component path for \'' +
+ userFriendlyPropertyName + '\' too long: ' + componentPath.join(':'));
+ }
+
+ const nameParts = [];
+ if (componentPath.length < 2) {
+ nameParts.push('total');
+ }
+
+ nameParts.push(userFriendlyPropertyName);
+
+ if (componentPath.length > 0) {
+ switch (componentPath[0]) {
+ case 'system_memory':
+ if (componentPath.length > 1) {
+ const userFriendlyComponentName =
+ SYSTEM_VALUE_COMPONENTS[componentPath[1]].userFriendlyName;
+ if (userFriendlyComponentName === undefined) {
+ throw new Error('System value sub-component for \'' +
+ userFriendlyPropertyName + '\' unknown: ' +
+ componentPath.join(':'));
+ }
+ nameParts.push('of', userFriendlyComponentName, 'in');
+ } else {
+ nameParts.push('of system memory (RAM) used by');
+ }
+ break;
+
+ case 'gpu_memory':
+ if (componentPath.length > 1) {
+ nameParts.push('of the', componentPath[1]);
+ nameParts.push('Android memtrack component in');
+ } else {
+ nameParts.push('of GPU memory (Android memtrack) used by');
+ }
+ break;
+
+ default:
+ throw new Error('OS value component for \'' +
+ userFriendlyPropertyName + '\' unknown: ' +
+ componentPath.join(':'));
+ }
+ } else {
+ nameParts.push('reported by the OS for');
+ }
+
+ nameParts.push(convertProcessNameToUserFriendlyName(processName));
+ return nameParts.join(' ');
+ }
+
+ /**
+ * Add heavy memory dump values calculated from heavy global memory dumps to
+ * |values|. In particular, this function adds the following values:
+ *
+ * * MEMORY USAGE REPORTED BY THE OS
+ * memory:{chrome, webview}:
+ * {browser_process, renderer_processes, ..., all_processes}:
+ * reported_by_os:system_memory:[{ashmem, native_heap, java_heap}:]
+ * {proportional_resident_size, private_dirty_size}
+ * memory:{chrome, webview}:
+ * {browser_process, renderer_processes, ..., all_processes}:
+ * reported_by_os:gpu_memory:[{gl, graphics, ...}:]
+ * proportional_resident_size
+ * type: tr.v.Histogram (over matching heavy global memory dumps)
+ * unit: sizeInBytes_smallerIsBetter
+ *
+ * * MEMORY USAGE REPORTED BY CHROME
+ * memory:{chrome, webview}:
+ * {browser_process, renderer_processes, ..., all_processes}:
+ * reported_by_chrome:v8:code_and_metadata_size
+ * type: tr.v.Histogram (over matching heavy global memory dumps)
+ * unit: sizeInBytes_smallerIsBetter
+ */
+ function addDetailedMemoryDumpValues(browserNameToGlobalDumps, values) {
+ addMemoryDumpValues(browserNameToGlobalDumps,
+ g => g.levelOfDetail === DETAILED,
+ function(processDump, addProcessScalar) {
+ // Add memory:<browser-name>:<process-name>:reported_by_os:
+ // system_memory:... values.
+ for (const [componentName, componentSpec] of
+ Object.entries(SYSTEM_VALUE_COMPONENTS)) {
+ const node = getDescendantVmRegionClassificationNode(
+ processDump.vmRegions, componentSpec.classificationPath);
+ const componentPath = ['system_memory'];
+ if (componentName) componentPath.push(componentName);
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: PROPORTIONAL_RESIDENT_SIZE,
+ value: node === undefined ?
+ 0 : (node.byteStats.proportionalResident || 0)
+ });
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: PRIVATE_DIRTY_SIZE,
+ value: node === undefined ?
+ 0 : (node.byteStats.privateDirtyResident || 0)
+ });
+
+ // Only add java base stats when they are nonzero.
+ if (node) {
+ if (node.byteStats.javaBasePss) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: JAVA_BASE_PSS,
+ value: node.byteStats.javaBasePss
+ });
+ }
+ if (node.byteStats.javaBaseCleanResident) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: JAVA_BASE_CLEAN_RESIDENT,
+ value: node.byteStats.javaBaseCleanResident
+ });
+ }
+ }
+
+ // Only add native library stats when they are nonzero.
+ if (node) {
+ if (node.byteStats.nativeLibraryPrivateCleanResident) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: NATIVE_LIBRARY_PRIVATE_CLEAN_RESIDENT,
+ value: node.byteStats.nativeLibraryPrivateCleanResident
+ });
+ }
+ if (node.byteStats.nativeLibrarySharedCleanResident) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: NATIVE_LIBRARY_SHARED_CLEAN_RESIDENT,
+ value: node.byteStats.nativeLibrarySharedCleanResident
+ });
+ }
+ if (node.byteStats.nativeLibraryProportionalResident) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: componentPath,
+ property: NATIVE_LIBRARY_PROPORTIONAL_RESIDENT,
+ value: node.byteStats.nativeLibraryProportionalResident
+ });
+ }
+ }
+ }
+
+ // Add memory:<browser-name>:<process-name>:reported_by_os:
+ // gpu_memory:... values.
+ const memtrackDump = processDump.getMemoryAllocatorDumpByFullName(
+ 'gpu/android_memtrack');
+ if (memtrackDump !== undefined) {
+ memtrackDump.children.forEach(function(memtrackChildDump) {
+ addProcessScalar({
+ source: 'reported_by_os',
+ component: ['gpu_memory', memtrackChildDump.name],
+ property: PROPORTIONAL_RESIDENT_SIZE,
+ value: memtrackChildDump.numerics.memtrack_pss
+ });
+ });
+ }
+ }, function(componentTree) {}, values);
+ }
+
+ // Specifications of components reported by the system.
+ const SYSTEM_VALUE_COMPONENTS = {
+ '': {
+ classificationPath: [],
+ },
+ 'java_heap': {
+ classificationPath: ['Android', 'Java runtime', 'Spaces'],
+ userFriendlyName: 'the Java heap'
+ },
+ 'ashmem': {
+ classificationPath: ['Android', 'Ashmem'],
+ userFriendlyName: 'ashmem'
+ },
+ 'native_heap': {
+ classificationPath: ['Native heap'],
+ userFriendlyName: 'the native heap'
+ },
+ 'stack': {
+ classificationPath: ['Stack'],
+ userFriendlyName: 'the thread stacks'
+ }
+ };
+
+ /**
+ * Get the descendant of a VM region classification |node| specified by the
+ * given |path| of child node titles. If |node| is undefined or such a
+ * descendant does not exist, this function returns undefined.
+ */
+ function getDescendantVmRegionClassificationNode(node, path) {
+ for (let i = 0; i < path.length; i++) {
+ if (node === undefined) break;
+
+ node = node.children.find(c => c.title === path[i]);
+ }
+ return node;
+ }
+
+ /**
+ * Add global memory dump counts to |values|. In particular, this function
+ * adds the following values:
+ *
+ * * DUMP COUNTS
+ * memory:{chrome, webview}:all_processes:dump_count
+ * [:{light, detailed, heap_profiler}]
+ * type: tr.v.Histogram
+ * unit: count_smallerIsBetter
+ *
+ * Note that unlike all other values generated by the memory metric, the
+ * global memory dump counts are NOT instances of tr.v.Histogram
+ * because it doesn't make sense to aggregate them (they are already counts
+ * over all global dumps associated with the relevant browser).
+ */
+ function addMemoryDumpCountValues(browserNameToGlobalDumps, values) {
+ browserNameToGlobalDumps.forEach(function(globalDumps, browserName) {
+ let totalDumpCount = 0;
+ const levelOfDetailNameToDumpCount = {};
+ LEVEL_OF_DETAIL_NAMES.forEach(function(levelOfDetailName) {
+ levelOfDetailNameToDumpCount[levelOfDetailName] = 0;
+ });
+ levelOfDetailNameToDumpCount[HEAP_PROFILER_DETAIL_NAME] = 0;
+
+ globalDumps.forEach(function(globalDump) {
+ totalDumpCount++;
+
+ // Increment the level-of-detail-specific dump count (if possible).
+ const levelOfDetailName =
+ LEVEL_OF_DETAIL_NAMES.get(globalDump.levelOfDetail);
+ if (levelOfDetailName === undefined) {
+ return; // Unknown level of detail.
+ }
+ levelOfDetailNameToDumpCount[levelOfDetailName]++;
+ if (globalDump.levelOfDetail === DETAILED) {
+ if (detectHeapProfilerInMemoryDump(globalDump)) {
+ levelOfDetailNameToDumpCount[HEAP_PROFILER_DETAIL_NAME]++;
+ }
+ }
+ });
+
+ // Add memory:<browser-name>:all_processes:dump_count[:<level>] values.
+ reportMemoryDumpCountAsValue(browserName, undefined /* total */,
+ totalDumpCount, values);
+ for (const [levelOfDetailName, levelOfDetailDumpCount] of
+ Object.entries(levelOfDetailNameToDumpCount)) {
+ reportMemoryDumpCountAsValue(browserName, levelOfDetailName,
+ levelOfDetailDumpCount, values);
+ }
+ });
+ }
+
+ /**
+ * Check whether detailed global dump has heap profiler information or not.
+ */
+ function detectHeapProfilerInMemoryDump(globalDump) {
+ for (const processDump of Object.values(globalDump.processMemoryDumps)) {
+ if (processDump.heapDumps && processDump.heapDumps.malloc) {
+ const mallocDump = processDump.heapDumps.malloc;
+ if (mallocDump.entries && mallocDump.entries.length > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Add a tr.v.Histogram value to |values| reporting that the number of
+ * |levelOfDetailName| memory dumps added by |browserName| was
+ * |levelOfDetailCount|.
+ */
+ function reportMemoryDumpCountAsValue(
+ browserName, levelOfDetailName, levelOfDetailDumpCount, values) {
+ // Construct the name of the memory value.
+ const nameParts = ['memory', browserName, 'all_processes', 'dump_count'];
+ if (levelOfDetailName !== undefined) {
+ nameParts.push(levelOfDetailName);
+ }
+ const name = nameParts.join(':');
+
+ // Build the underlying histogram for the memory value.
+ const histogram = new tr.v.Histogram(name, count_smallerIsBetter,
+ BOUNDARIES_FOR_UNIT_MAP.get(count_smallerIsBetter));
+ histogram.addSample(levelOfDetailDumpCount);
+
+ // If |levelOfDetail| argument is undefined it means a total value.
+ const userFriendlyLevelOfDetail =
+ (levelOfDetailName || 'all').replace('_', ' ');
+
+ // Build the options for the memory value.
+ histogram.description = [
+ 'total number of',
+ userFriendlyLevelOfDetail,
+ 'memory dumps added by',
+ convertBrowserNameToUserFriendlyName(browserName),
+ 'to the trace'
+ ].join(' ');
+
+ // Report the memory value.
+ values.addHistogram(histogram);
+ }
+
+ /**
+ * Add generic values extracted from process memory dumps and aggregated by
+ * process name and component path into |values|.
+ *
+ * For each browser and set of global dumps in |browserNameToGlobalDumps|,
+ * |customProcessDumpValueExtractor| is applied to every process memory dump
+ * associated with the global memory dump. The second argument provided to the
+ * callback is a function for adding extracted values:
+ *
+ * function sampleProcessDumpCallback(processDump, addProcessValue) {
+ * ...
+ * addProcessScalar({
+ * source: 'reported_by_chrome',
+ * component: ['system', 'native_heap'],
+ * property: 'proportional_resident_size',
+ * value: pssExtractedFromProcessDump2,
+ * descriptionPrefixBuilder(componentPath) {
+ * return 'PSS of ' + componentPath.join('/') + ' in';
+ * }
+ * });
+ * ...
+ * }
+ *
+ * For each global memory dump, the extracted values are summed by process
+ * name (browser_process, renderer_processes, ..., all_processes) and
+ * component path (e.g. gpu is a sum of gpu:gl, gpu:graphics, ...). The sums
+ * are then aggregated over all global memory dumps associated with the given
+ * browser. For example, assuming that |customProcessDumpValueExtractor|
+ * extracts 'proportional_resident_size' values for component paths
+ * ['X', 'A'], ['X', 'B'] and ['Y'] under the same 'source' from each process
+ * memory dump, the following values will be reported (for Chrome):
+ *
+ * memory:chrome:browser_process:source:X:A:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:A in all 'browser' process dumps in global dump 1,
+ * ...
+ * sum of X:A in all 'browser' process dumps in global dump N
+ * ]
+ *
+ * memory:chrome:browser_process:source:X:B:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:B in all 'browser' process dumps in global dump 1,
+ * ...
+ * sum of X:B in all 'browser' process dumps in global dump N
+ * ]
+ *
+ * memory:chrome:browser_process:source:X:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:A+X:B in all 'browser' process dumps in global dump 1,
+ * ...
+ * sum of X:A+X:B in all 'browser' process dumps in global dump N
+ * ]
+ *
+ * memory:chrome:browser_process:source:Y:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of Y in all 'browser' process dumps in global dump 1,
+ * ...
+ * sum of Y in all 'browser' process dumps in global dump N
+ * ]
+ *
+ * memory:chrome:browser_process:source:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:A+X:B+Y in all 'browser' process dumps in global dump 1,
+ * ...
+ * sum of X:A+X:B+Y in all 'browser' process dumps in global dump N
+ * ]
+ *
+ * ...
+ *
+ * memory:chrome:all_processes:source:X:A:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:A in all process dumps in global dump 1,
+ * ...
+ * sum of X:A in all process dumps in global dump N,
+ * ]
+ *
+ * memory:chrome:all_processes:source:X:B:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:B in all process dumps in global dump 1,
+ * ...
+ * sum of X:B in all process dumps in global dump N,
+ * ]
+ *
+ * memory:chrome:all_processes:source:X:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:A+X:B in all process dumps in global dump 1,
+ * ...
+ * sum of X:A+X:B in all process dumps in global dump N,
+ * ]
+ *
+ * memory:chrome:all_processes:source:Y:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of Y in all process dumps in global dump 1,
+ * ...
+ * sum of Y in all process dumps in global dump N
+ * ]
+ *
+ * memory:chrome:all_processes:source:proportional_resident_size :
+ * Histogram aggregated over [
+ * sum of X:A+X:B+Y in all process dumps in global dump 1,
+ * ...
+ * sum of X:A+X:B+Y in all process dumps in global dump N
+ * ]
+ *
+ * where global dumps 1 to N are the global dumps associated with the given
+ * browser.
+ *
+ * @param {!Map<string, !Array<!tr.model.GlobalMemoryDump>}
+ * browserNameToGlobalDumps Map from browser names to arrays of global
+ * memory dumps. The generic values will be extracted from the associated
+ * process memory dumps.
+ * @param {!function(!tr.model.GlobalMemoryDump): boolean}
+ * customGlobalDumpFilter Predicate for filtering global memory dumps.
+ * @param {!function(
+ * !tr.model.ProcessMemoryDump,
+ * !function(!{
+ * source: string,
+ * componentPath: (!Array<string>|undefined),
+ * property: !{name: string, unit: !tr.b.Unit, buildDescriptionPrefix:
+ * !function(!Array<string>, string): string},
+ * value: (!tr.v.Histogram|number|undefined)
+ * }))}
+ * customProcessDumpValueExtractor Callback for extracting values from a
+ * process memory dump.
+ * @param {!function(!tr.b.MultiDimensionalViewNode)}
+ * customComponentTreeModifier Callback applied to every component tree
+ * wrt each process name.
+ * @param {!tr.v.HistogramSet} values List of values to which the
+ * resulting aggregated values are added.
+ */
+ function addMemoryDumpValues(browserNameToGlobalDumps, customGlobalDumpFilter,
+ customProcessDumpValueExtractor, customComponentTreeModifier,
+ values) {
+ browserNameToGlobalDumps.forEach(function(globalDumps, browserName) {
+ const filteredGlobalDumps = globalDumps.filter(customGlobalDumpFilter);
+ const sourceToPropertyToBuilder = extractDataFromGlobalDumps(
+ filteredGlobalDumps, customProcessDumpValueExtractor);
+ reportDataAsValues(sourceToPropertyToBuilder, browserName,
+ customComponentTreeModifier, values);
+ });
+ }
+
+ /**
+ * For each global memory dump in |globalDumps|, calculate per-process-name
+ * sums of values extracted by |customProcessDumpValueExtractor| from the
+ * associated process memory dumps.
+ *
+ * This function returns the following nested map structure:
+ *
+ * Source name (Map key, e.g. 'reported_by_os')
+ * -> Property (Map key, e.g. PROPORTIONAL_RESIDENT_SIZE)
+ * -> processAndComponentTreeBuilder
+ *
+ * where |processAndComponentTreeBuilder| is a
+ * tr.b.MultiDimensionalViewBuilder:
+ *
+ * Process name (0th dimension key, e.g. 'browser_process') x
+ * Component path (1st dimension keys, e.g. ['system', 'native_heap'])
+ * -> Sum of value over the processes (number).
+ *
+ * See addMemoryDumpValues for more details.
+ */
+ function extractDataFromGlobalDumps(
+ globalDumps, customProcessDumpValueExtractor) {
+ const sourceToPropertyToBuilder = new Map();
+ const dumpCount = globalDumps.length;
+ globalDumps.forEach(function(globalDump, dumpIndex) {
+ for (const processDump of Object.values(globalDump.processMemoryDumps)) {
+ extractDataFromProcessDump(
+ processDump, sourceToPropertyToBuilder, dumpIndex, dumpCount,
+ customProcessDumpValueExtractor);
+ }
+ });
+ return sourceToPropertyToBuilder;
+ }
+
+ function extractDataFromProcessDump(processDump, sourceToPropertyToBuilder,
+ dumpIndex, dumpCount, customProcessDumpValueExtractor) {
+ // Process name is typically 'browser', 'renderer', etc.
+ const rawProcessName = processDump.process.name;
+ const processNamePath =
+ [tr.e.chrome.chrome_processes.canonicalizeProcessName(rawProcessName)];
+
+ customProcessDumpValueExtractor(
+ processDump,
+ function addProcessScalar(spec) {
+ if (spec.value === undefined) return;
+
+ const component = spec.component || [];
+ function createDetailsForErrorMessage() {
+ return ['source=', spec.source, ', property=',
+ spec.property.name || '(undefined)', ', component=',
+ component.length === 0 ? '(empty)' : component.join(':'),
+ ' in ', processDump.process.userFriendlyName].join('');
+ }
+
+ let value;
+ if (spec.value instanceof tr.b.Scalar) {
+ value = spec.value.value;
+ if (spec.value.unit !== spec.property.unit) {
+ throw new Error('Scalar unit for ' +
+ createDetailsForErrorMessage() + ' (' +
+ spec.value.unit.unitName +
+ ') doesn\'t match the unit of the property (' +
+ spec.property.unit.unitName + ')');
+ }
+ } else {
+ value = spec.value;
+ }
+
+ let propertyToBuilder = sourceToPropertyToBuilder.get(spec.source);
+ if (propertyToBuilder === undefined) {
+ propertyToBuilder = new Map();
+ sourceToPropertyToBuilder.set(spec.source, propertyToBuilder);
+ }
+
+ let builder = propertyToBuilder.get(spec.property);
+ if (builder === undefined) {
+ builder = new tr.b.MultiDimensionalViewBuilder(
+ 2 /* dimensions (process name and component path) */,
+ dumpCount /* valueCount */),
+ propertyToBuilder.set(spec.property, builder);
+ }
+
+ const values = new Array(dumpCount);
+ values[dumpIndex] = value;
+
+ builder.addPath(
+ [processNamePath, component] /* path */, values,
+ tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL /* valueKind */);
+ });
+ }
+
+ function reportDataAsValues(sourceToPropertyToBuilder, browserName,
+ customComponentTreeModifier, values) {
+ // For each source name (e.g. 'reported_by_os')...
+ sourceToPropertyToBuilder.forEach(function(propertyToBuilder, sourceName) {
+ // For each property (e.g. EFFECTIVE_SIZE)...
+ propertyToBuilder.forEach(function(builders, property) {
+ const tree = builders.buildTopDownTreeView();
+ reportComponentDataAsValues(browserName, sourceName, property,
+ [] /* processPath */, [] /* componentPath */, tree, values,
+ customComponentTreeModifier);
+ });
+ });
+ }
+
+ /**
+ * For the given |browserName| (e.g. 'chrome'), |property|
+ * (e.g. EFFECTIVE_SIZE), |processPath| (e.g. ['browser_process']),
+ * |componentPath| (e.g. ['v8']), add
+ * a tr.v.Histogram with |unit| aggregating the total
+ * values of the associated |componentNode| across all timestamps
+ * (corresponding to global memory dumps associated with the given browser)
+ * |values| for each process (e.g. 'gpu_process', 'browser_process', etc).
+ * We also report a special 'all_processes' histogram which agregates all
+ * others, this has a RelatedNameMap diagnostic explaining
+ * how it is built from the other histograms.
+ *
+ * See addMemoryDumpValues for more details.
+ */
+ function reportComponentDataAsValues(browserName, sourceName, property,
+ processPath, componentPath, tree, values, customComponentTreeModifier,
+ opt_cachedHistograms) {
+ const cachedHistograms = opt_cachedHistograms || new Map();
+ function recurse(processPath, componentPath, node) {
+ return reportComponentDataAsValues(browserName, sourceName, property,
+ processPath, componentPath, node, values,
+ customComponentTreeModifier, cachedHistograms);
+ }
+
+ function buildHistogram(processPath, componentPath, node) {
+ return buildNamedMemoryNumericFromNode(
+ browserName,
+ sourceName,
+ property,
+ processPath.length === 0 ? 'all_processes' : processPath[0],
+ componentPath,
+ node);
+ }
+
+ customComponentTreeModifier(tree);
+ const histogram = buildHistogram(processPath, componentPath, tree);
+ if (cachedHistograms.has(histogram.name)) {
+ return cachedHistograms.get(histogram.name);
+ }
+ cachedHistograms.set(histogram.name, histogram);
+
+ const processNames = new tr.v.d.RelatedNameMap();
+ for (const [childProcessName, childProcessNode] of tree.children[0]) {
+ processPath.push(childProcessName);
+ const childProcessHistogram =
+ recurse(processPath, componentPath, childProcessNode);
+ processNames.set(childProcessName, childProcessHistogram.name);
+ processPath.pop();
+ }
+
+ const componentNames = new tr.v.d.RelatedNameMap();
+ for (const [childComponentName, childComponentNode] of tree.children[1]) {
+ componentPath.push(childComponentName);
+ const childComponentHistogram =
+ recurse(processPath, componentPath, childComponentNode);
+ componentNames.set(childComponentName, childComponentHistogram.name);
+ componentPath.pop();
+ }
+
+ values.addHistogram(histogram);
+ if (tree.children[0].size > 0) {
+ histogram.diagnostics.set('processes', processNames);
+ }
+ if (tree.children[1].size > 0) {
+ histogram.diagnostics.set('components', componentNames);
+ }
+
+ return histogram;
+ }
+
+ /**
+ * Gets the name for a histogram.
+ * The histograms have the following naming scheme:
+ * memory:chrome:browser_process:reported_by_chrome:v8:heap:effective_size_avg
+ * ^browser ^process ^source ^component ^property
+ */
+ function getNumericName(
+ browserName, sourceName, propertyName, processName, componentPath) {
+ // Construct the name of the memory value.
+ const nameParts = ['memory', browserName, processName, sourceName].concat(
+ componentPath);
+ if (propertyName !== undefined) nameParts.push(propertyName);
+ return nameParts.join(':');
+ }
+
+ /**
+ * Gets the description of a histogram.
+ */
+ function getNumericDescription(
+ property, browserName, processName, componentPath) {
+ return [
+ property.buildDescriptionPrefix(componentPath, processName),
+ 'in',
+ convertBrowserNameToUserFriendlyName(browserName)
+ ].join(' ');
+ }
+
+ /**
+ * Create a memory tr.v.Histogram with |unit| and add all total values in
+ * |node| to it. Names and describes the histogram according to the
+ * |browserName|, |sourceName|, |property|, |processName| and
+ * |componentPath|.
+ */
+ function buildNamedMemoryNumericFromNode(
+ browserName, sourceName, property, processName, componentPath, node) {
+ const name = getNumericName(
+ browserName, sourceName, property.name, processName, componentPath);
+ const description = getNumericDescription(
+ property, browserName, processName, componentPath);
+
+ // Build the underlying numeric for the memory value.
+ const numeric = buildMemoryNumericFromNode(name, node, property.unit);
+ numeric.description = description;
+ return numeric;
+ }
+
+ function buildSampleDiagnostics(value, node) {
+ if (node.children.length < 2) return undefined;
+ const diagnostics = new Map();
+ const i = node.values.indexOf(value);
+
+ const processBreakdown = new tr.v.d.Breakdown();
+ processBreakdown.colorScheme =
+ tr.e.chrome.chrome_processes.PROCESS_COLOR_SCHEME_NAME;
+ for (const [name, subNode] of node.children[0]) {
+ processBreakdown.set(name, subNode.values[i].total);
+ }
+ if (processBreakdown.size > 0) {
+ diagnostics.set('processes', processBreakdown);
+ }
+
+ const componentBreakdown = new tr.v.d.Breakdown();
+ for (const [name, subNode] of node.children[1]) {
+ componentBreakdown.set(name, subNode.values[i].total);
+ }
+ if (componentBreakdown.size > 0) {
+ diagnostics.set('components', componentBreakdown);
+ }
+
+ if (diagnostics.size === 0) return undefined;
+ return diagnostics;
+ }
+
+ /**
+ * Create a memory tr.v.Histogram with |unit| and add all total values in
+ * |node| to it.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html
new file mode 100644
index 00000000000..e3a6c40d327
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html
@@ -0,0 +1,4249 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/system_health/memory_metric.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+ const SIZE_DELTA = tr.model.MemoryDumpTestUtils.SIZE_DELTA;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+ const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const StackFrame = tr.model.StackFrame;
+ const HeapEntry = tr.model.HeapEntry;
+ const HeapDump = tr.model.HeapDump;
+
+ function memoryMetricTest(
+ name, modelCallback, opt_options, expectedNumerics) {
+ test(name, function() {
+ // Create a model and a fake value list.
+ const model = tr.c.TestUtils.newModel(modelCallback);
+ const valueNameToValues = {};
+ const fakeValueList = {
+ addHistogram(value) {
+ let values = valueNameToValues[value.name];
+ if (values === undefined) {
+ valueNameToValues[value.name] = values = [];
+ }
+ values.push(value);
+ }
+ };
+
+ // Run the memory metric on the model.
+ tr.metrics.sh.memoryMetric(fakeValueList, model, opt_options);
+
+ // Check that the names of the added values match expectations.
+ const actualValueNames = Object.keys(valueNameToValues).sort();
+ const expectedValueNames = Object.keys(expectedNumerics).sort();
+ assert.deepEqual(actualValueNames, expectedValueNames, {
+ // Build the long error message lazily.
+ toString() {
+ const errorMessageParts = [];
+ function addValueNamesToError(type, valueNames, otherValueNames) {
+ const otherValueNamesSet = new Set(otherValueNames);
+ errorMessageParts.push(type, ' value names:');
+ if (valueNames.length === 0) {
+ errorMessageParts.push('\n(empty)');
+ } else {
+ valueNames.forEach(function(valueName) {
+ errorMessageParts.push('\n');
+ if (!otherValueNamesSet.has(valueName)) {
+ errorMessageParts.push('+++');
+ }
+ errorMessageParts.push(valueName);
+ });
+ }
+ }
+ addValueNamesToError('Expected', expectedValueNames,
+ actualValueNames);
+ errorMessageParts.push('\n');
+ addValueNamesToError('Actual', actualValueNames, expectedValueNames);
+ return errorMessageParts.join('');
+ }
+ });
+
+ // Check that the numeric values of the added values match expectations.
+ for (const [valueName, actualValues] of
+ Object.entries(valueNameToValues)) {
+ assert.lengthOf(actualValues, 1,
+ 'Multiple \'' + valueName + '\' values');
+ const actualHistogram = actualValues[0];
+ assert.instanceOf(actualHistogram, tr.v.Histogram);
+
+ const expectedHistogram = expectedNumerics[valueName];
+ assert.strictEqual(actualHistogram.unit, expectedHistogram.unit,
+ 'Invalid \'' + valueName + '\' unit (expected: ' +
+ expectedHistogram.unit.unitName, + ', actual: ' +
+ actualHistogram.unit.unitName + ')');
+
+ if (!(expectedHistogram.value instanceof Array)) {
+ assert.fail('Test sanity check: expected value must be an array');
+ }
+
+ assert.instanceOf(actualHistogram, tr.v.Histogram,
+ 'Invalid \'' + valueName + '\' class');
+ assert.strictEqual(actualHistogram.numValues,
+ expectedHistogram.value.length,
+ 'Invalid \'' + valueName + '\' Histogram numValues');
+ assert.closeTo(actualHistogram.sum,
+ expectedHistogram.value.reduce((a, b) => a + b, 0), SIZE_DELTA,
+ 'Invalid \'' + valueName + '\' Histogram sum');
+
+ // Check that the bin counts match.
+ const binToCount = new Map();
+ expectedHistogram.value.forEach(function(value) {
+ const bin = actualHistogram.getBinForValue(value);
+ binToCount.set(bin, (binToCount.get(bin) || 0) + 1);
+ });
+ actualHistogram.allBins.forEach(function(bin) {
+ binToCount.set(bin, (binToCount.get(bin) || 0) - bin.count);
+ });
+ binToCount.forEach(function(count, bin) {
+ assert.strictEqual(count, 0, 'Invalid \'' + valueName +
+ '\' bin count for range ' + bin.min + '-' + bin.max);
+ });
+
+ // Check that the description matches expectations.
+ assert.strictEqual(
+ actualHistogram.description, expectedHistogram.description,
+ 'Invalid \'' + valueName + '\' description');
+ }
+ });
+ }
+
+ function createProcessWithName(model, name) {
+ const uniquePid =
+ Math.max.apply(null, Object.keys(model.processes).concat([0])) + 1;
+ const process = model.getOrCreateProcess(uniquePid);
+ process.name = name;
+ return process;
+ }
+
+ function createChromeBrowserProcess(model) {
+ const process = createProcessWithName(model, 'Browser');
+ process.getOrCreateThread(1).name = 'CrBrowserMain';
+ return process;
+ }
+
+ function createWebViewProcess(model) {
+ const process = createChromeBrowserProcess(model);
+ process.getOrCreateThread(2).name = 'Chrome_InProcRendererThread';
+ return process;
+ }
+
+ memoryMetricTest('noDumps_noBrowser', function(model) {
+ createProcessWithName(model, 'Non-browser');
+ }, undefined /* opt_options */, {
+ /* no values */
+ });
+
+ memoryMetricTest('noDumps_chrome', function(model) {
+ createChromeBrowserProcess(model);
+ }, undefined /* opt_options */, {
+ 'memory:chrome:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:light': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by Chrome ' +
+ 'to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome to the ' +
+ 'trace'
+ }
+ });
+
+ memoryMetricTest('noDumps_multipleBrowsers', function(model) {
+ createChromeBrowserProcess(model);
+ createWebViewProcess(model);
+ createProcessWithName(model, 'Non-browser');
+ createChromeBrowserProcess(model);
+ }, undefined /* opt_options */, {
+ 'memory:chrome2:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome(2) ' +
+ 'to the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count:light': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome(2) to ' +
+ 'the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by ' +
+ 'Chrome(2) to the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome(2) to the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome(2) to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:light': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by Chrome ' +
+ 'to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome to the ' +
+ 'trace'
+ },
+ 'memory:webview:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:light': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by WebView to ' +
+ 'the trace'
+ },
+ 'memory:webview:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'WebView to the trace'
+ },
+ 'memory:webview:all_processes:dump_count': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by WebView to the ' +
+ 'trace'
+ }
+ });
+
+ memoryMetricTest('dumpCountsOnly_unknownBrowser', function(model) {
+ addGlobalMemoryDump(model, {ts: 45, levelOfDetail: DETAILED});
+ addGlobalMemoryDump(model, {ts: 65, levelOfDetail: BACKGROUND});
+ addGlobalMemoryDump(model, {ts: 68, levelOfDetail: LIGHT});
+ addGlobalMemoryDump(model, {ts: 89, levelOfDetail: DETAILED});
+ }, undefined /* opt_options */, {
+ 'memory:unknown_browser:all_processes:dump_count:detailed': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:background': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count': {
+ value: [4],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by an unknown ' +
+ 'browser to the trace'
+ }
+ });
+
+ memoryMetricTest('dumpCountsOnly_webview', function(model) {
+ const p = createWebViewProcess(model);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 45, levelOfDetail: LIGHT}), p, {ts: 45});
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 68, levelOfDetail: LIGHT}), p, {ts: 68});
+ }, undefined /* opt_options */, {
+ 'memory:webview:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:light': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by WebView to ' +
+ 'the trace'
+ },
+ 'memory:webview:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'WebView to the trace'
+ },
+ 'memory:webview:all_processes:dump_count': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by WebView to the ' +
+ 'trace'
+ },
+ 'memory:webview:all_processes:process_count': {
+ value: [1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in WebView'
+ },
+ 'memory:webview:browser_process:process_count': {
+ value: [1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in WebView'
+ }
+ });
+
+ memoryMetricTest('generalValues_chrome', function(model) {
+ const pBrowser = createChromeBrowserProcess(model);
+ const pRendererA = createProcessWithName(model, 'Renderer');
+ const pRendererB = createProcessWithName(model, 'Renderer');
+ const pPpapi = createProcessWithName(model, 'PPAPI Process');
+ const pUnknown = createProcessWithName(model, undefined);
+
+ // Timestamp 1.
+ const gmd1 = addGlobalMemoryDump(model, {ts: 20});
+ const pmdBrowser1 = addProcessMemoryDump(gmd1, pBrowser, {ts: 19});
+ pmdBrowser1.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser1, 'malloc', {numerics: {
+ size: 8,
+ allocated_objects_size: 4,
+ shim_allocated_objects_size: 3,
+ }})
+ ];
+ pmdBrowser1.totals = {
+ residentBytes: 200,
+ peakResidentBytes: 230,
+ privateFootprintBytes: 240,
+ };
+ const browserHeapDump = new HeapDump(pmdBrowser1);
+ const rootFrame1 = new StackFrame(
+ undefined, tr.b.GUID.allocateSimple(), undefined);
+ const childFrame1 = new StackFrame(
+ rootFrame1, tr.b.GUID.allocateSimple(), 'draw');
+ rootFrame1.addChild(childFrame1);
+ browserHeapDump.addEntry(
+ undefined, 'HTMLImportLoader', 1024, undefined);
+ browserHeapDump.addEntry(
+ rootFrame1, 'HTMLImportLoader', 1048576, undefined);
+ browserHeapDump.addEntry(undefined, '[unknown]', 17332, 42);
+ browserHeapDump.addEntry(childFrame1, '[unknown]', 26309, 10);
+ pmdBrowser1.heapDumps = {};
+ pmdBrowser1.heapDumps.malloc = browserHeapDump;
+
+ const pmdRendererA1 = addProcessMemoryDump(gmd1, pRendererA, {ts: 20});
+ pmdRendererA1.memoryAllocatorDumps = (function() {
+ const mallocDump =
+ newAllocatorDump(pmdRendererA1, 'malloc', {numerics: {size: 16}});
+ const partitionAllocDump =
+ newAllocatorDump(pmdRendererA1, 'partition_alloc');
+ const v8Dump = newAllocatorDump(pmdRendererA1, 'v8',
+ {numerics: {code_and_metadata_size: 16}});
+ addOwnershipLink(
+ addChildDump(partitionAllocDump, 'allocated_objects',
+ {numerics: {size: 32}}),
+ addChildDump(partitionAllocDump, 'partitions',
+ {numerics: {size: 24}}));
+ return [mallocDump, partitionAllocDump, v8Dump];
+ })();
+ const pmdGpu1 = addProcessMemoryDump(gmd1, pPpapi, {ts: 21});
+ pmdGpu1.memoryAllocatorDumps = [
+ newAllocatorDump(pmdGpu1, 'gpu', {numerics: {
+ size: 30,
+ allocated_objects_size: 25
+ }})
+ ];
+
+ // Timestamp 2.
+ const gmd2 = addGlobalMemoryDump(model, {ts: 40});
+ const pmdBrowser2 = addProcessMemoryDump(gmd2, pBrowser, {ts: 41});
+ pmdBrowser2.memoryAllocatorDumps = (function() {
+ const mallocDump = newAllocatorDump(pmdBrowser2, 'malloc',
+ {numerics: {size: 120}});
+ const tracingDump =
+ newAllocatorDump(pmdBrowser2, 'tracing', {numerics: {size: 40}});
+ return [mallocDump, tracingDump];
+ })();
+ const pmdRendererA2 = addProcessMemoryDump(gmd2, pRendererA, {ts: 39});
+ pmdRendererA2.memoryAllocatorDumps = (function() {
+ const partitionAllocDump =
+ newAllocatorDump(pmdRendererA2, 'partition_alloc');
+ addOwnershipLink(
+ addChildDump(partitionAllocDump, 'allocated_objects',
+ {numerics: {size: 320}}),
+ addChildDump(partitionAllocDump, 'partitions',
+ {numerics: {size: 240}}));
+ const v8Dump = newAllocatorDump(pmdRendererA2, 'v8',
+ {numerics: {size: 650}});
+ const isolateDumpA = addChildDump(v8Dump, 'isolate_A');
+ addChildDump(isolateDumpA, 'malloc', {numerics: {
+ size: 1,
+ peak_size: 2
+ }});
+ const heapDumpA = addChildDump(isolateDumpA, 'heap_spaces', {numerics: {
+ size: 42,
+ allocated_objects_size: 36
+ }});
+ addChildDump(heapDumpA, 'code_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ addChildDump(heapDumpA, 'large_object_space', {numerics: {
+ allocated_objects_size: 3,
+ size: 4
+ }});
+ addChildDump(heapDumpA, 'map_space', {numerics: {
+ allocated_objects_size: 5,
+ size: 6,
+ }});
+ addChildDump(heapDumpA, 'new_space', {numerics: {
+ allocated_objects_size: 7,
+ size: 8
+ }});
+ addChildDump(heapDumpA, 'old_space', {numerics: {
+ allocated_objects_size: 9,
+ size: 10
+ }});
+ addChildDump(heapDumpA, 'other_spaces', {numerics: {
+ allocated_objects_size: 11,
+ size: 12
+ }});
+ const isolateDumpB = addChildDump(v8Dump, 'isolate_B');
+ addChildDump(isolateDumpB, 'malloc', {numerics: {
+ size: 10,
+ peak_size: 20
+ }});
+ const heapDumpB = addChildDump(isolateDumpB, 'heap_spaces', {numerics: {
+ size: 12,
+ allocated_objects_size: 6
+ }});
+ addChildDump(heapDumpB, 'code_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ addChildDump(heapDumpB, 'large_object_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ addChildDump(heapDumpB, 'map_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2,
+ }});
+ addChildDump(heapDumpB, 'new_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ addChildDump(heapDumpB, 'old_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ addChildDump(heapDumpB, 'other_spaces', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ const isolateDumpC = addChildDump(v8Dump, 'isolate_C');
+ addChildDump(isolateDumpC, 'malloc', {numerics: {
+ size: 100,
+ }});
+ addChildDump(isolateDumpC, 'heap_spaces', {numerics: {
+ size: 2,
+ allocated_objects_size: 1
+ }});
+ const isolateDumpD = addChildDump(v8Dump, 'isolate_D');
+ addChildDump(isolateDumpD, 'malloc', {numerics: {
+ peak_size: 200,
+ }});
+ return [partitionAllocDump, v8Dump];
+ })();
+ const pmdRendererB2 = addProcessMemoryDump(gmd2, pRendererB, {ts: 40});
+ pmdRendererB2.memoryAllocatorDumps = [
+ newAllocatorDump(pmdRendererB2, 'v8', {numerics: {
+ size: 970,
+ allocated_objects_size: 860,
+ bytecode_and_metadata_size: 678
+ }}),
+ newAllocatorDump(pmdRendererB2, 'malloc',
+ {numerics: {allocated_objects_size: 750}})
+ ];
+ const pmdUnknown = addProcessMemoryDump(gmd2, pUnknown, {ts: 42});
+ pmdUnknown.memoryAllocatorDumps = [
+ newAllocatorDump(pmdRendererB2, 'v8', {numerics: {size: 111}})
+ ];
+
+ // Timestamp 3.
+ const gmd3 = addGlobalMemoryDump(model, {ts: 60});
+ const pmdBrowser3 = addProcessMemoryDump(gmd3, pBrowser, {ts: 60});
+ pmdBrowser3.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser3, 'malloc', {numerics: {
+ size: 8000,
+ allocated_objects_size: 4000,
+ shim_allocated_objects_size: 3000
+ }})
+ ];
+ const pmdRendererB3 = addProcessMemoryDump(gmd3, pRendererB, {ts: 61});
+ // Intentionally pmdRendererB3.memoryAllocatorDumps undefined.
+ const pmdGpu3 = addProcessMemoryDump(gmd3, pPpapi, {ts: 59});
+ pmdGpu3.memoryAllocatorDumps = [
+ newAllocatorDump(pmdGpu3, 'gpu', {numerics: {size: 300}})
+ ];
+
+ // Timestamp 4.
+ const gmd4 = addGlobalMemoryDump(model, {ts: 80});
+ const pmdBrowser4 = addProcessMemoryDump(gmd4, pBrowser, {ts: 81});
+ pmdBrowser4.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser4, 'malloc', {numerics: {size: 80000}})
+ ];
+ const pmdRendererB4 = addProcessMemoryDump(gmd4, pRendererB, {ts: 79});
+ pmdRendererB4.memoryAllocatorDumps = (function() {
+ const v8Dump = newAllocatorDump(pmdRendererB4, 'v8', {numerics: {
+ code_and_metadata_size: 21,
+ bytecode_and_metadata_size: 35,
+ size: 9e5
+ }});
+ const partitionAllocDump = newAllocatorDump(pmdRendererB4,
+ 'partition_alloc', {numerics: {size: 5e5}});
+ addOwnershipLink(partitionAllocDump, v8Dump);
+ return [v8Dump, partitionAllocDump];
+ })();
+ const rendererBHeapDump4 = new HeapDump(pmdRendererB4);
+ rendererBHeapDump4.addEntry(
+ undefined, 'BlinkObject', 1687992, undefined);
+ rendererBHeapDump4.addEntry(undefined, undefined, 2243546, 42);
+ rendererBHeapDump4.addEntry(undefined, 'BlinkObject', 1252376, 10);
+
+ pmdRendererB4.heapDumps = {};
+ pmdRendererB4.heapDumps.blinkgc = rendererBHeapDump4;
+
+ const pmdGpu4 = addProcessMemoryDump(gmd4, pPpapi, {ts: 80});
+ pmdGpu4.memoryAllocatorDumps = [
+ newAllocatorDump(pmdGpu4, 'gpu',
+ {numerics: {memtrack_pss: 666 /* ignored */}})
+ ];
+ }, undefined /* opt_options */, {
+ 'memory:chrome:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:light': {
+ value: [4],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by Chrome ' +
+ 'to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count': {
+ value: [4],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome to the ' +
+ 'trace'
+ },
+ 'memory:chrome:all_processes:process_count': {
+ value: [3, 4, 3, 3],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:effective_size': {
+ value: [30 + (8 + 16) + 32, (120 - 40) + 320 + (650 + 970) + 111,
+ 300 + 8000, 80000 + 5e5 + 4e5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:allocated_objects_size': {
+ value: [25 + 4 + 32, (36 + 6 + 1) + 750 + 860 + 320 + 40, 4000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by Chrome ' +
+ 'for all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:blinkgc:BlinkObject:heap_category_size':
+ {
+ value: [0, 0, 1687992 + 1252376, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for blinkgc:BlinkObject in ' +
+ 'all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:blinkgc:heap_category_size':
+ {
+ value: [0, 0, 1687992 + 1252376, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for blinkgc in all processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:shim_allocated_objects_size':
+ {
+ value: [3, 0, 3000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects through shim ' +
+ 'reported by Chrome for all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:peak_size':
+ {
+ value: [0, 2 + 20 + 200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak size reported by Chrome for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:resident_size': {
+ value: [200, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total resident set size (RSS) reported by the OS for all' +
+ ' processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:peak_resident_size': {
+ value: [230, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak resident set size reported by the OS for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:private_footprint_size': {
+ value: [240, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private footprint size reported by the OS for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:resident_size': {
+ value: [200, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total resident set size (RSS) of system memory (RAM) ' +
+ 'used by all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:peak_resident_size':
+ {
+ value: [230, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak resident set size of system memory (RAM) ' +
+ 'used by all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:private_footprint_size': { // eslint-disable-line max-len
+ value: [240, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private footprint size of system memory (RAM) ' +
+ 'used by all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:gpu:effective_size': {
+ value: [30, 0, 300, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of gpu in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:heap_category_size': {
+ value: [17332, 0, 1687992 + 1252376, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total heap profiler category size reported by Chrome for ' +
+ 'all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:[unknown]:heap_category_size':
+ {
+ value: [17332, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for malloc:[unknown] in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:gpu:allocated_objects_size':
+ {
+ value: [25, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by gpu in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:effective_size': {
+ value: [8 + 16, 120 - 40, 8000, 80000],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:heap_category_size':
+ {
+ value: [17332, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for malloc in all processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:shim_allocated_objects_size':
+ {
+ value: [3, 0, 3000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated through shim by malloc in ' +
+ 'all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:allocated_objects_size':
+ {
+ value: [4, 40 + 750, 4000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by malloc in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:partition_alloc:allocated_objects_size':
+ {
+ value: [32, 320, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by partition_alloc in ' +
+ 'all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:partition_alloc:effective_size':
+ {
+ value: [32, 320, 0, 5e5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of partition_alloc in all processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:tracing:effective_size': {
+ value: [0, 40, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of tracing in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:allocated_by_malloc:effective_size':
+ {
+ value: [0, 1 + 10 + 100, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of objects allocated by malloc for v8 ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:allocated_by_malloc:peak_size':
+ {
+ value: [0, 2 + 20 + 200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of objects allocated by malloc for v8 ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:code_and_metadata_size':
+ {
+ value: [16, 678, 0, 21 + 35],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of code and metadata reported by Chrome ' +
+ 'for all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:code_and_metadata_size':
+ {
+ value: [16, 678, 0, 21 + 35],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of v8 code and metadata in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:allocated_objects_size':
+ {
+ value: [0, 36 + 6 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:effective_size': {
+ value: [0, 42 + 12 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size':
+ {
+ value: [0, 1 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:code_space ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:code_space:effective_size':
+ {
+ value: [0, 2 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:code_space in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:large_object_space:allocated_objects_size':
+ {
+ value: [0, 3 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by ' +
+ 'v8:heap:large_object_space in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:large_object_space:effective_size':
+ {
+ value: [0, 4 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:large_object_space in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:map_space:allocated_objects_size':
+ {
+ value: [0, 5 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:map_space ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:map_space:effective_size':
+ {
+ value: [0, 6 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:map_space in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:new_space:allocated_objects_size':
+ {
+ value: [0, 7 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:new_space ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:new_space:effective_size':
+ {
+ value: [0, 8 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:new_space in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:old_space:allocated_objects_size':
+ {
+ value: [0, 9 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:old_space ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:heap:old_space:effective_size':
+ {
+ value: [0, 10 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:old_space in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:effective_size': {
+ value: [0, 650 + 970 + 111, 0, 4e5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:allocated_objects_size':
+ {
+ value: [0, (36 + 6 + 1) + 860, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8 in all processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:v8:peak_size':
+ {
+ value: [0, 2 + 20 + 200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of v8 in all processes in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:peak_resident_size': {
+ value: [230, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak resident set size reported by the OS for the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:peak_resident_size':
+ {
+ value: [230, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak resident set size of system memory (RAM) ' +
+ 'used by the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:private_footprint_size': {
+ value: [240, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private footprint size reported by the OS for the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:private_footprint_size':
+ {
+ value: [240, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private footprint size of system memory (RAM) ' +
+ 'used by the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:resident_size': {
+ value: [200, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total resident set size (RSS) reported by the OS for the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:resident_size':
+ {
+ value: [200, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total resident set size (RSS) of system memory (RAM) ' +
+ 'used by the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:process_count': {
+ value: [1, 1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:effective_size': {
+ value: [8, (120 - 40), 8000, 80000],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:heap_category_size': {
+ value: [17332, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total heap profiler category size reported by Chrome for ' +
+ 'the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:[unknown]:heap_category_size':
+ {
+ value: [17332, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for malloc:[unknown] in the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:allocated_objects_size': {
+ value: [4 + 40, 0, 4000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by Chrome ' +
+ 'for the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:shim_allocated_objects_size':
+ {
+ value: [3, 0, 3000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects through shim ' +
+ 'reported by Chrome for the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:effective_size': {
+ value: [8, 120 - 40, 8000, 80000],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:heap_category_size':
+ {
+ value: [17332, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for malloc in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:allocated_objects_size':
+ {
+ value: [4 + 40, 0, 4000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by malloc in the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:shim_allocated_objects_size':
+ {
+ value: [3, 0, 3000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated through shim by malloc in ' +
+ 'the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:tracing:effective_size': {
+ value: [0, 40, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of tracing in the browser process in Chrome'
+ },
+ 'memory:chrome:ppapi_process:process_count': {
+ value: [1, 0, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of PPAPI processes in Chrome'
+ },
+ 'memory:chrome:ppapi_process:reported_by_chrome:effective_size': {
+ value: [30, 0, 300, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the PPAPI ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:ppapi_process:reported_by_chrome:allocated_objects_size': {
+ value: [25, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by Chrome ' +
+ 'for the PPAPI process in Chrome'
+ },
+ 'memory:chrome:ppapi_process:reported_by_chrome:gpu:effective_size': {
+ value: [30, 0, 300, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of gpu in the PPAPI process in Chrome'
+ },
+ 'memory:chrome:ppapi_process:reported_by_chrome:gpu:allocated_objects_size':
+ {
+ value: [25, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by gpu in the PPAPI ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:renderer_processes:process_count': {
+ value: [1, 2, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:effective_size': {
+ value: [16 + 32, 320 + 650 + 970, 0, 5e5 + 4e5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:allocated_objects_size':
+ {
+ value: [32, (36 + 6 + 1) + 750 + 860 + 320, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by ' +
+ 'Chrome for renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:blinkgc:BlinkObject:heap_category_size':
+ {
+ value: [0, 0, 1687992 + 1252376, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for blinkgc:BlinkObject in ' +
+ 'renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:blinkgc:heap_category_size':
+ {
+ value: [0, 0, 1687992 + 1252376, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'heap profiler category size for blinkgc in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:peak_size':
+ {
+ value: [0, 2 + 20 + 200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak size reported by Chrome ' +
+ 'for renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:malloc:effective_size':
+ {
+ value: [16, 0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in renderer processes in ' +
+ 'Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:heap_category_size': {
+ value: [0, 0, 1687992 + 1252376, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total heap profiler category size reported by Chrome for ' +
+ 'renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:malloc:allocated_objects_size':
+ {
+ value: [0, 750, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by malloc in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:partition_alloc:allocated_objects_size':
+ {
+ value: [32, 320, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by partition_alloc in ' +
+ 'renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:partition_alloc:effective_size':
+ {
+ value: [32, 320, 0, 5e5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of partition_alloc in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:allocated_by_malloc:effective_size':
+ {
+ value: [0, 1 + 10 + 100, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of objects allocated by malloc for v8 ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:allocated_by_malloc:peak_size':
+ {
+ value: [0, 2 + 20 + 200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of objects allocated by malloc for v8 ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:allocated_objects_size':
+ {
+ value: [0, 36 + 6 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:code_and_metadata_size':
+ {
+ value: [16, 678, 0, 21 + 35],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of code and metadata reported by Chrome ' +
+ 'for renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:code_and_metadata_size':
+ {
+ value: [16, 678, 0, 21 + 35],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of v8 code and metadata in renderer processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:effective_size':
+ {
+ value: [0, 42 + 12 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap in renderer processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size':
+ {
+ value: [0, 1 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:code_space ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:code_space:effective_size':
+ {
+ value: [0, 2 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:code_space in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:large_object_space:allocated_objects_size':
+ {
+ value: [0, 3 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by ' +
+ 'v8:heap:large_object_space in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:large_object_space:effective_size':
+ {
+ value: [0, 4 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:large_object_space in ' +
+ 'renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:map_space:allocated_objects_size':
+ {
+ value: [0, 5 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:map_space ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:map_space:effective_size':
+ {
+ value: [0, 6 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:map_space in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:new_space:allocated_objects_size':
+ {
+ value: [0, 7 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:new_space ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:new_space:effective_size':
+ {
+ value: [0, 8 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:new_space in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:old_space:allocated_objects_size':
+ {
+ value: [0, 9 + 1, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:old_space ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:old_space:effective_size':
+ {
+ value: [0, 10 + 2, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:old_space in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:effective_size': {
+ value: [0, 650 + 970, 0, 4e5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:allocated_objects_size':
+ {
+ value: [0, (36 + 6 + 1) + 860, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8 in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:v8:peak_size':
+ {
+ value: [0, 2 + 20 + 200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of v8 in renderer processes in Chrome'
+ },
+ 'memory:chrome:unknown_processes:process_count': {
+ value: [0, 1, 0, 0],
+ unit: count_smallerIsBetter,
+ description: 'total number of unknown processes in Chrome'
+ },
+ 'memory:chrome:unknown_processes:reported_by_chrome:effective_size': {
+ value: [0, 111, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for unknown ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:unknown_processes:reported_by_chrome:v8:effective_size': {
+ value: [0, 111, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in unknown processes in Chrome'
+ },
+ });
+
+
+ memoryMetricTest('newV8Values', function(model) {
+ const pRendererA = createProcessWithName(model, 'Renderer');
+ const gmd1 = addGlobalMemoryDump(model, {ts: 20});
+ const pmdRendererA = addProcessMemoryDump(gmd1, pRendererA, {ts: 20});
+ pmdRendererA.memoryAllocatorDumps = (function() {
+ const v8Dump = newAllocatorDump(pmdRendererA, 'v8',
+ {numerics: {size: 0}});
+ const isolateDumpA = addChildDump(v8Dump, 'main');
+ const heapDumpA = addChildDump(isolateDumpA, 'heap');
+ addChildDump(heapDumpA, 'code_space', {numerics: {
+ allocated_objects_size: 10,
+ size: 20
+ }});
+ const workersDump = addChildDump(v8Dump, 'workers');
+ const heapDumpB = addChildDump(workersDump, 'heap');
+ const codeSpaceDumpB = addChildDump(heapDumpB, 'code_space');
+ addChildDump(codeSpaceDumpB, 'isolate_0x1234', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+ addChildDump(codeSpaceDumpB, 'isolate_0x5678', {numerics: {
+ allocated_objects_size: 3,
+ size: 4
+ }});
+ return [v8Dump];
+ })();
+ }, undefined /* opt_options */, {
+ 'memory:unknown_browser:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'an unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:process_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:effective_size': {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by Chrome ' +
+ 'for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap in all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap in all processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:code_space ' +
+ 'in all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:code_space:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:code_space in all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:v8:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:v8:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8 in all processes ' +
+ 'in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:process_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for renderer ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by Chrome ' +
+ 'for renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap in renderer ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap in renderer processes in ' +
+ 'an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8:heap:code_space ' +
+ 'in renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:code_space:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8:heap:code_space in renderer ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:effective_size':
+ {
+ value: [26],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in renderer processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:allocated_objects_size':
+ {
+ value: [14],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8 in renderer ' +
+ 'processes in an unknown browser'
+ }
+ });
+
+
+ memoryMetricTest('detailedValues_unknownBrowser', function(model) {
+ const pBrowser = createProcessWithName(model, 'Browser');
+ const pRendererA = createProcessWithName(model, 'Renderer');
+ const pRendererB = createProcessWithName(model, 'Renderer');
+ const pRendererC = createProcessWithName(model, 'Renderer');
+ const pGpu = createProcessWithName(model, 'GPU Process');
+
+ // Timestamp 1.
+ const gmd1 = addGlobalMemoryDump(model, {ts: 10, levelOfDetail: DETAILED});
+ const pmdBrowser1 = addProcessMemoryDump(gmd1, pBrowser, {ts: 9});
+ pmdBrowser1.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 128, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 8}),
+ new VMRegion(0xFBCD, 64, 5, '/data/chrome-WXYZ/base.apk',
+ { privateCleanResident: 8, sharedCleanResident: 4,
+ proportionalResident: 10}),
+ new VMRegion(0xFCCD, 64, 5, '/data/chrome-WXYZ/out/arm/base.odex',
+ { privateCleanResident: 0, sharedCleanResident: 6,
+ proportionalResident: 5})
+ ]);
+ pmdBrowser1.heapDumps = (function() {
+ const mallocDump = new tr.model.HeapDump(pmdBrowser1, 'malloc');
+ mallocDump.addEntry(undefined, undefined, 100, 500);
+ return {'malloc': mallocDump};
+ })();
+
+ const pmdRendererA1 = addProcessMemoryDump(gmd1, pRendererA, {ts: 10});
+ pmdRendererA1.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xEF01, 256, 0, '[anon:libc_malloc]',
+ {privateDirtyResident: 17}),
+ new VMRegion(0xFBCD, 64, 5, '/data/chrome-WXYZ/base.apk',
+ { privateCleanResident: 3, sharedCleanResident: 4,
+ proportionalResident: 5})
+ ]);
+ const pmdRendererB1 = addProcessMemoryDump(gmd1, pRendererB, {ts: 11});
+ pmdRendererB1.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0x2345, 512, 0, '[heap]',
+ {proportionalResident: 67, privateDirtyResident: 34}),
+ new VMRegion(0x7f29, 128, 0, '[stack:25451]',
+ {proportionalResident: 20, privateDirtyResident: 16})
+ ]);
+ const pmdGpu1 = addProcessMemoryDump(gmd1, pGpu, {ts: 10});
+ pmdGpu1.memoryAllocatorDumps = (function() {
+ const gpuDump = newAllocatorDump(pmdGpu1, 'gpu');
+ const memtrackDump = addChildDump(gpuDump, 'android_memtrack');
+ addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 100}});
+ addChildDump(memtrackDump, 'graphics', {numerics: {memtrack_pss: 200}});
+ return [gpuDump];
+ })();
+
+ // Timestamp 2 (light global memory dump, so it should be skipped for
+ // mmaps_* values).
+ const gmd2 = addGlobalMemoryDump(model, {ts: 20, levelOfDetail: LIGHT});
+ const pmdBrowser2 = addProcessMemoryDump(gmd2, pBrowser, {ts: 18});
+ pmdBrowser2.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0x999, 999, 999, '/dev/ashmem/dalvik-main space',
+ {proportionalResident: 999})
+ ]);
+ const pmdRendererB2 = addProcessMemoryDump(gmd2, pRendererB, {ts: 21});
+ const pmdRendererC2 = addProcessMemoryDump(gmd2, pRendererC, {ts: 22});
+ const pmdGpu2 = addProcessMemoryDump(gmd2, pGpu, {ts: 20});
+ pmdGpu2.memoryAllocatorDumps = (function() {
+ const gpuDump = newAllocatorDump(pmdGpu2, 'gpu');
+ const memtrackDump = addChildDump(gpuDump, 'android_memtrack');
+ addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 12345}});
+ return [gpuDump];
+ })();
+
+ // Timestamp 3.
+ const gmd3 = addGlobalMemoryDump(model, {ts: 30, levelOfDetail: DETAILED});
+ const pmdBrowser3 = addProcessMemoryDump(gmd3, pBrowser, {ts: 30});
+ pmdBrowser3.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 1024, 0, '/dev/ashmem/dalvik-non moving space',
+ {proportionalResident: 3, privateDirtyResident: 80})
+ ]);
+ const pmdRendererA3 = addProcessMemoryDump(gmd3, pRendererA, {ts: 29});
+ // Intentionally pmdRendererA3.vmRegions undefined.
+ const pmdRendererC3 = addProcessMemoryDump(gmd3, pRendererC, {ts: 31});
+ pmdRendererC3.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0x2345, 2048, 0, '/no/matching/category',
+ {proportionalResident: 200}),
+ new VMRegion(0x2345, 2048, 0, '/dev/ashmem', {proportionalResident: 500}),
+ ]);
+ const pmdGpu3 = addProcessMemoryDump(gmd3, pGpu, {ts: 30});
+ pmdGpu3.memoryAllocatorDumps = (function() {
+ const gpuDump = newAllocatorDump(pmdGpu3, 'gpu',
+ {numerics: {memtrack_pss: 6000 /* ignored */}});
+ const memtrackDump = addChildDump(gpuDump, 'android_memtrack',
+ {numerics: {memtrack_pss: 5000 /* ignored */}});
+ addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 3000}});
+ addChildDump(memtrackDump, 'graphics', {numerics: {ignored: 2000}});
+ addChildDump(memtrackDump, 'gfx', {numerics: {memtrack_pss: 1000}});
+ return [gpuDump];
+ })();
+ pmdGpu3.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xCDCD, 4096, 0, '/dev/ashmem/dalvik-zygote space',
+ {proportionalResident: 150, privateDirtyResident: 90})
+ ]);
+
+ // Timestamp 4.
+ const gmd4 = addGlobalMemoryDump(model, {ts: 40, levelOfDetail: DETAILED});
+ const pmdBrowser4 = addProcessMemoryDump(gmd4, pBrowser, {ts: 40});
+ }, undefined /* opt_options */, {
+ 'memory:unknown_browser:all_processes:dump_count:detailed': {
+ value: [3],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:heap_profiler': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count': {
+ value: [4],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:process_count': {
+ value: [4, 4, 4, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:proportional_resident_size':
+ {
+ value: [100 + 200, 3000 + 1000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of GPU memory ' +
+ '(Android memtrack) used by all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:java_base_clean_resident':
+ {
+ value: [6, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex total clean resident size ' +
+ 'reported by the OS for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:java_base_pss':
+ {
+ value: [5, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex proportional resident size ' +
+ 'reported by the OS for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:native_library_private_clean_resident':
+ {
+ value: [8, 3, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library private clean resident size ' +
+ 'reported by the OS for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:native_library_proportional_resident':
+ {
+ value: [10, 5, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library proportional resident size ' +
+ 'reported by the OS for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:native_library_shared_clean_resident':
+ {
+ value: [4, 4, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library shared clean resident size ' +
+ 'reported by the OS for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:gfx:proportional_resident_size':
+ {
+ value: [0, 1000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the gfx Android ' +
+ 'memtrack component in all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:gl:proportional_resident_size':
+ {
+ value: [100, 3000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the gl Android ' +
+ 'memtrack component in all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:graphics:proportional_resident_size':
+ {
+ value: [200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the graphics ' +
+ 'Android memtrack component in all processes in an unknown ' +
+ 'browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:private_dirty_size': {
+ value: [17 + 34 + 16 + 8, 80 + 90, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:proportional_resident_size':
+ {
+ value: [67 + 20 + 100 + 200, 700 + 3 + 1000 + 150 + 3000, 15 + 5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [17 + 34 + 16 + 8, 80 + 90, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [67 + 20, 700 + 3 + 150, 15 + 5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in all processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 500, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_base_clean_resident':
+ {
+ value: [6, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex total clean resident size ' +
+ 'of system memory (RAM) used by all processes in an unknown ' +
+ 'browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_base_pss':
+ {
+ value: [5, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex proportional resident size ' +
+ 'of system memory (RAM) used by all processes in an unknown ' +
+ 'browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [8, 80 + 90, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in all processes ' +
+ 'in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 3 + 150, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [17 + 34, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [67, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_library_private_clean_resident':
+ {
+ value: [8, 3, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library private clean resident size of ' +
+ 'system memory (RAM) used by all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_library_proportional_resident':
+ {
+ value: [10, 5, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library proportional resident size of ' +
+ 'system memory (RAM) used by all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_library_shared_clean_resident':
+ {
+ value: [4, 4, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library shared clean resident size of ' +
+ 'system memory (RAM) used by all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [16, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [20, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:process_count': {
+ value: [1, 1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:java_base_clean_resident':
+ {
+ value: [6, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex total clean resident size ' +
+ 'reported by the OS for the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:java_base_pss':
+ {
+ value: [5, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex proportional resident size ' +
+ 'reported by the OS for the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:native_library_private_clean_resident':
+ {
+ value: [8, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library private clean resident size ' +
+ 'reported by the OS for the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:native_library_proportional_resident':
+ {
+ value: [10, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library proportional resident size ' +
+ 'reported by the OS for the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:native_library_shared_clean_resident':
+ {
+ value: [4, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library shared clean resident size ' +
+ 'reported by the OS for the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:private_dirty_size':
+ {
+ value: [8, 80, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the ' +
+ 'browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:proportional_resident_size':
+ {
+ value: [15, 3, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [8, 80, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [13, 5, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the browser process ' +
+ 'in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_base_clean_resident':
+ {
+ value: [6, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex total clean resident size ' +
+ 'of system memory (RAM) used by the browser process in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_base_pss':
+ {
+ value: [5, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total java base odex and vdex proportional resident size ' +
+ 'of system memory (RAM) used by the browser process in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [8, 80, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the browser ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 3, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the browser ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_library_private_clean_resident':
+ {
+ value: [8, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library private clean resident size of ' +
+ 'system memory (RAM) used by the browser process in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_library_proportional_resident':
+ {
+ value: [10, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library proportional resident size of ' +
+ 'system memory (RAM) used by the browser process in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_library_shared_clean_resident':
+ {
+ value: [4, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library shared clean resident size of ' +
+ 'system memory (RAM) used by the browser process in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the browser ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:process_count': {
+ value: [1, 1, 1, 0],
+ unit: count_smallerIsBetter,
+ description: 'total number of GPU processes in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:proportional_resident_size':
+ {
+ value: [100 + 200, 3000 + 1000 + 150, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:private_dirty_size': {
+ value: [0, 90, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the GPU ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:proportional_resident_size':
+ {
+ value: [100 + 200, 3000 + 1000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of GPU memory ' +
+ '(Android memtrack) used by the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:gfx:proportional_resident_size':
+ {
+ value: [0, 1000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the gfx Android ' +
+ 'memtrack component in the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:gl:proportional_resident_size':
+ {
+ value: [100, 3000, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the gl Android ' +
+ 'memtrack component in the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:graphics:proportional_resident_size':
+ {
+ value: [200, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the graphics ' +
+ 'Android memtrack component in the GPU process in an unknown ' +
+ 'browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [0, 90, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 150, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the GPU process in ' +
+ 'an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [0, 90, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the GPU ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 150, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the GPU ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the GPU ' +
+ 'process in an unknown browser'
+ },
+ 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the GPU process in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:process_count': {
+ value: [2, 2, 2, 0],
+ unit: count_smallerIsBetter,
+ description: 'total number of renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:native_library_private_clean_resident':
+ {
+ value: [3, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library private clean resident size ' +
+ 'reported by the OS for renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:native_library_proportional_resident':
+ {
+ value: [5, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library proportional resident size ' +
+ 'reported by the OS for renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:native_library_shared_clean_resident':
+ {
+ value: [4, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library shared clean resident size ' +
+ 'reported by the OS for renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:private_dirty_size':
+ {
+ value: [17 + 34 + 16, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for ' +
+ 'renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:proportional_resident_size':
+ {
+ value: [67 + 20, 700, 5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [17 + 34 + 16, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [67 + 20, 700, 5],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in renderer processes ' +
+ 'in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 500, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in ' +
+ 'renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in renderer ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [17 + 34, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in renderer ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [67, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in renderer processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_library_private_clean_resident':
+ {
+ value: [3, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library private clean resident size of ' +
+ 'system memory (RAM) used by renderer processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_library_proportional_resident':
+ {
+ value: [5, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library proportional resident size of ' +
+ 'system memory (RAM) used by renderer processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_library_shared_clean_resident':
+ {
+ value: [4, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total native library shared clean resident size of ' +
+ 'system memory (RAM) used by renderer processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [16, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in renderer ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [20, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in renderer processes in an unknown browser'
+ }
+ });
+
+ memoryMetricTest('combined_chrome', function(model) {
+ const pBrowser = createChromeBrowserProcess(model);
+
+ // Timestamp 1.
+ const gmd1 = addGlobalMemoryDump(model, {ts: 10, levelOfDetail: DETAILED});
+ const pmdBrowser1 = addProcessMemoryDump(gmd1, pBrowser, {ts: 10});
+ pmdBrowser1.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 128, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 100})
+ ]);
+
+ // Timestamp 2 (light global memory dump, so it should be skipped for
+ // mmaps_* values).
+ const gmd2 = addGlobalMemoryDump(model, {ts: 20, levelOfDetail: LIGHT});
+ const pmdBrowser2 = addProcessMemoryDump(gmd2, pBrowser, {ts: 20});
+ pmdBrowser2.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser2, 'malloc', {numerics: {size: 32}})
+ ];
+
+ // Timestamp 3.
+ const gmd3 = addGlobalMemoryDump(model, {ts: 30, levelOfDetail: DETAILED});
+ const pmdBrowser3 = addProcessMemoryDump(gmd3, pBrowser, {ts: 30});
+ pmdBrowser3.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser3, 'malloc', {numerics: {size: 48}})
+ ];
+ pmdBrowser3.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 1024, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 150})
+ ]);
+ }, {} /* opt_options */, {
+ 'memory:chrome:all_processes:dump_count:detailed': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by Chrome ' +
+ 'to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count': {
+ value: [3],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome to the ' +
+ 'trace'
+ },
+ 'memory:chrome:all_processes:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:effective_size': {
+ value: [0, 32, 48],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:effective_size': {
+ value: [0, 32, 48],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:private_dirty_size': {
+ value: [100, 150],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:proportional_resident_size': {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [100, 150],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in all processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [100, 150],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by all processes in Chrome'
+ },
+ 'memory:chrome:browser_process:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:effective_size': {
+ value: [0, 32, 48],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:effective_size': {
+ value: [0, 32, 48],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:private_dirty_size': {
+ value: [100, 150],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:proportional_resident_size': {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the browser process ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [100, 150],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [100, 150],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the browser process in Chrome'
+ }
+ });
+
+ memoryMetricTest('combined_multipleBrowsers', function(model) {
+ const pWebView = createWebViewProcess(model);
+ const pChrome1 = createChromeBrowserProcess(model);
+ const pRenderer1 = createProcessWithName(model, 'Renderer');
+ const pGpu1 = createProcessWithName(model, 'GPU Process');
+ const pUnknownBrowser = createProcessWithName(model, 'Browser');
+ const pChrome2 = createChromeBrowserProcess(model);
+ const pRenderer2 = createProcessWithName(model, 'Renderer');
+
+ // Timestamp 1 (WebView).
+ const gmd1 = addGlobalMemoryDump(model, {ts: 0, levelOfDetail: LIGHT});
+ const pmdBrowser1 = addProcessMemoryDump(gmd1, pWebView, {ts: 0});
+ pmdBrowser1.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser1, 'malloc', {numerics: {size: 2}}),
+ newAllocatorDump(pmdBrowser1, 'v8', {numerics: {size: 4}})
+ ];
+
+ // Timestamp 2 (Chrome 1 + Renderer + GPU Process).
+ const gmd2 = addGlobalMemoryDump(model, {ts: 10, levelOfDetail: DETAILED});
+ const pmdBrowser2 = addProcessMemoryDump(gmd2, pChrome1, {ts: 12});
+ pmdBrowser2.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 9999, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 8})
+ ]);
+ const pmdGpu2 = addProcessMemoryDump(gmd2, pGpu1, {ts: 8});
+ pmdGpu2.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 9999, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 16})
+ ]);
+ const pmdRenderer2 = addProcessMemoryDump(gmd2, pRenderer1, {ts: 8});
+ pmdRenderer2.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser2, 'malloc', {numerics: {size: 32}})
+ ];
+
+ // Timestamp 3 (Chrome 2).
+ const gmd3 = addGlobalMemoryDump(model, {ts: 20, levelOfDetail: DETAILED});
+ const pmdBrowser3 = addProcessMemoryDump(gmd3, pChrome2, {ts: 20});
+ pmdBrowser3.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser3, 'malloc', {numerics: {size: 64}}),
+ newAllocatorDump(pmdBrowser3, 'sqlite', {numerics: {size: 128}}),
+ newAllocatorDump(pmdBrowser3, 'discardable', {numerics: {
+ size: 8388608,
+ locked_size: 4194304
+ }})
+ ];
+ pmdBrowser3.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 99, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 256})
+ ]);
+
+ // Timestamp 4 (Chrome 2 + Renderer).
+ const gmd4 = addGlobalMemoryDump(model, {ts: 30, levelOfDetail: LIGHT});
+ const pmdBrowser4 = addProcessMemoryDump(gmd4, pChrome2, {ts: 28});
+ pmdBrowser4.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser4, 'malloc', {numerics: {size: 512}}),
+ newAllocatorDump(pmdBrowser3, 'discardable', {numerics: {size: 16777216}})
+ ];
+ const pmdRenderer4 = addProcessMemoryDump(gmd4, pRenderer2, {ts: 32});
+ pmdRenderer4.memoryAllocatorDumps = [
+ newAllocatorDump(pmdRenderer4, 'malloc', {numerics: {size: 1024}}),
+ newAllocatorDump(pmdRenderer4, 'v8', {numerics: {size: 2048}})
+ ];
+
+ // Timestamp 5 (Unknown browser).
+ const gmd5 = addGlobalMemoryDump(model, {ts: 40, levelOfDetail: LIGHT});
+ const pmdBrowser5 = addProcessMemoryDump(gmd5, pUnknownBrowser, {ts: 40});
+ pmdBrowser5.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser5, 'malloc', {numerics: {size: 4096}}),
+ newAllocatorDump(pmdBrowser5, 'sqlite', {numerics: {size: 8192}}),
+ ];
+
+ // Timestamp 6 (WebView).
+ const gmd6 = addGlobalMemoryDump(model, {ts: 50, levelOfDetail: DETAILED});
+ const pmdBrowser6 = addProcessMemoryDump(gmd6, pWebView, {ts: 50});
+ pmdBrowser6.memoryAllocatorDumps = (function() {
+ const mallocDump = newAllocatorDump(pmdBrowser6, 'malloc',
+ {numerics: {size: 16384}});
+ const v8Dump = newAllocatorDump(pmdBrowser6, 'v8', {numerics: {
+ allocated_objects_size: 32768,
+ code_and_metadata_size: 33554432,
+ size: 67108864
+ }});
+ const isolateDump = addChildDump(v8Dump, 'isolateA');
+ addChildDump(isolateDump, 'malloc', {numerics: {
+ size: 1,
+ peak_size: 2
+ }});
+ return [mallocDump, v8Dump];
+ })();
+ pmdBrowser6.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 99999, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 65536})
+ ]);
+
+ // Timestamp 7 (Chrome 1 + GPU Process).
+ const gmd7 = addGlobalMemoryDump(model, {ts: 60, levelOfDetail: DETAILED});
+ const pmdBrowser7 = addProcessMemoryDump(gmd7, pChrome1, {ts: 63});
+ pmdBrowser7.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser4, 'malloc', {numerics: {size: 131072}}),
+ newAllocatorDump(pmdBrowser4, 'sqlite', {numerics: {size: 262144}})
+ ];
+ pmdBrowser7.vmRegions = VMRegionClassificationNode.fromRegions([
+ new VMRegion(0xABCD, 999999, 0, '/dev/ashmem/dalvik-non moving space',
+ {privateDirtyResident: 524288})
+ ]);
+ const pmdGpu7 = addProcessMemoryDump(gmd7, pGpu1, {ts: 57});
+ pmdGpu7.memoryAllocatorDumps = (function() {
+ const gpuDump = newAllocatorDump(pmdGpu7, 'gpu',
+ {numerics: {size: 1048576}});
+ const memtrackDump = addChildDump(gpuDump, 'android_memtrack');
+ addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 2097152}});
+ return [gpuDump];
+ })();
+
+ // Timestamp 8 (Chrome 1).
+ const gmd8 = addGlobalMemoryDump(
+ model, {ts: 76, levelOfDetail: BACKGROUND});
+ const pmdBrowser8 = addProcessMemoryDump(gmd8, pChrome1, {ts: 80});
+ pmdBrowser8.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser5, 'malloc', {numerics: {size: 1024}}),
+ newAllocatorDump(pmdBrowser5, 'sqlite', {numerics: {size: 2048}}),
+ ];
+
+ // Timestamp 9 (WebView).
+ const gmd9 = addGlobalMemoryDump(
+ model, {ts: 90, levelOfDetail: BACKGROUND});
+ const pmdBrowser9 = addProcessMemoryDump(gmd9, pWebView, {ts: 90});
+ pmdBrowser9.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser5, 'v8', {numerics: {size: 4096}}),
+ newAllocatorDump(pmdBrowser5, 'sqlite', {numerics: {size: 2048}}),
+ ];
+ }, undefined /* opt_options */, {
+ // WebView (GMD1, GMD6).
+ 'memory:webview:all_processes:dump_count:detailed': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by WebView to ' +
+ 'the trace'
+ },
+ 'memory:webview:all_processes:dump_count:background': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'WebView to the trace'
+ },
+ 'memory:webview:all_processes:dump_count': {
+ value: [3],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by WebView to the ' +
+ 'trace'
+ },
+ 'memory:webview:all_processes:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:effective_size': {
+ value: [4 + 2, 16384 + 67108864, 4096 + 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:code_and_metadata_size': {
+ value: [0, 33554432, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of code and metadata reported by Chrome for ' +
+ 'all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:allocated_objects_size': {
+ value: [0, 32768, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by Chrome ' +
+ 'for all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:peak_size':
+ {
+ value: [0, 2, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak size reported by Chrome for all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:malloc:effective_size': {
+ value: [2, 16384, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:sqlite:effective_size': {
+ value: [0, 2048, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:v8:allocated_by_malloc:effective_size':
+ {
+ value: [0, 1, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of objects allocated by malloc for v8 ' +
+ 'in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:v8:allocated_by_malloc:peak_size':
+ {
+ value: [0, 2, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of objects allocated by malloc for v8 ' +
+ 'in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:v8:effective_size': {
+ value: [4, 67108864, 4096],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:v8:code_and_metadata_size':
+ {
+ value: [0, 33554432, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of v8 code and metadata in all processes in ' +
+ 'WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:v8:allocated_objects_size':
+ {
+ value: [0, 32768, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8 in all processes ' +
+ 'in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_chrome:v8:peak_size':
+ {
+ value: [0, 2, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of v8 in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:private_dirty_size': {
+ value: [65536],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:proportional_resident_size': {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in all processes in ' +
+ 'WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [65536],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in all processes ' +
+ 'in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [65536],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by all processes in WebView'
+ },
+ 'memory:webview:browser_process:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:effective_size': {
+ value: [4 + 2, 16384 + 67108864, 4096 + 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:allocated_objects_size':
+ {
+ value: [0, 32768, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of all allocated objects reported by ' +
+ 'Chrome for the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:code_and_metadata_size':
+ {
+ value: [0, 33554432, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total size of code and metadata reported by Chrome ' +
+ 'for the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:peak_size':
+ {
+ value: [0, 2, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total peak size reported by Chrome ' +
+ 'for the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:malloc:effective_size': {
+ value: [2, 16384, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:v8:allocated_by_malloc:effective_size':
+ {
+ value: [0, 1, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of objects allocated by malloc for v8 ' +
+ 'in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:v8:allocated_by_malloc:peak_size':
+ {
+ value: [0, 2, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of objects allocated by malloc for v8 ' +
+ 'in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:sqlite:effective_size': {
+ value: [0, 0, 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:v8:effective_size': {
+ value: [4, 67108864, 4096],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:v8:allocated_objects_size':
+ {
+ value: [0, 32768, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of all objects allocated by v8 in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:v8:code_and_metadata_size':
+ {
+ value: [0, 33554432, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'size of v8 code and metadata in the browser process ' +
+ 'in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_chrome:v8:peak_size':
+ {
+ value: [0, 2, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'peak size of v8 in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:private_dirty_size': {
+ value: [65536],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the ' +
+ 'browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the browser process ' +
+ 'in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [65536],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [65536],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the browser process in WebView'
+ },
+
+ // Chrome 1 (GMD3, GMD4).
+ 'memory:chrome:all_processes:reported_by_os:gpu_memory:gl:proportional_resident_size':
+ {
+ value: [0, 2097152],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the gl Android ' +
+ 'memtrack component in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:dump_count:detailed': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:light': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:background': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by Chrome ' +
+ 'to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count': {
+ value: [3],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome to the ' +
+ 'trace'
+ },
+ 'memory:chrome:all_processes:process_count': {
+ value: [3, 2, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:effective_size': {
+ value: [32, 1048576 + 131072 + 262144, 1024 + 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:gpu:effective_size': {
+ value: [0, 1048576, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of gpu in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:malloc:effective_size': {
+ value: [32, 131072, 1024],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_chrome:sqlite:effective_size': {
+ value: [0, 262144, 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:private_dirty_size': {
+ value: [8 + 16, 524288],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:proportional_resident_size': {
+ value: [0, 2097152],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:gpu_memory:proportional_resident_size':
+ {
+ value: [0, 2097152],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of GPU memory ' +
+ '(Android memtrack) used by all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [8 + 16, 524288],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in all processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in all ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [8 + 16, 524288],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by all processes in Chrome'
+ },
+ 'memory:chrome:all_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by all processes in Chrome'
+ },
+ 'memory:chrome:browser_process:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:effective_size': {
+ value: [0, 131072 + 262144, 1024 + 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:malloc:effective_size': {
+ value: [0, 131072, 1024],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_chrome:sqlite:effective_size': {
+ value: [0, 262144, 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:private_dirty_size': {
+ value: [8, 524288],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:proportional_resident_size': {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the browser process ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [8, 524288],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the browser ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [8, 524288],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the browser process in Chrome'
+ },
+ 'memory:chrome:browser_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the browser process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_chrome:effective_size': {
+ value: [0, 1048576, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the GPU ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:gpu_memory:proportional_resident_size':
+ {
+ value: [0, 2097152],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of GPU memory ' +
+ '(Android memtrack) used by the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:gpu_memory:gl:proportional_resident_size':
+ {
+ value: [0, 2097152],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the gl Android ' +
+ 'memtrack component in the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:process_count': {
+ value: [1, 1, 0],
+ unit: count_smallerIsBetter,
+ description: 'total number of GPU processes in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_chrome:gpu:effective_size': {
+ value: [0, 1048576, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of gpu in the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:private_dirty_size': {
+ value: [16, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the GPU ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:proportional_resident_size': {
+ value: [0, 2097152],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the GPU process in ' +
+ 'Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [16, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the GPU ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the GPU ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the GPU ' +
+ 'process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [16, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the GPU process in Chrome'
+ },
+ 'memory:chrome:gpu_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the GPU process in Chrome'
+ },
+ 'memory:chrome:renderer_processes:process_count': {
+ value: [1, 0, 0],
+ unit: count_smallerIsBetter,
+ description: 'total number of renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:effective_size': {
+ value: [32, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_chrome:malloc:effective_size':
+ {
+ value: [32, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in renderer processes in ' +
+ 'Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:private_dirty_size': {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in renderer processes ' +
+ 'in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in ' +
+ 'renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in renderer ' +
+ 'processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by renderer processes in Chrome'
+ },
+ 'memory:chrome:renderer_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by renderer processes in Chrome'
+ },
+
+ // Chrome 2 (GMD2, GMD7).
+ 'memory:chrome2:all_processes:dump_count:detailed': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome(2) ' +
+ 'to the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome(2) to ' +
+ 'the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by ' +
+ 'Chrome(2) to the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome(2) to the trace'
+ },
+ 'memory:chrome2:all_processes:dump_count': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome(2) to ' +
+ 'the trace'
+ },
+ 'memory:chrome2:all_processes:process_count': {
+ value: [1, 2],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:discardable:effective_size':
+ {
+ value: [8388608, 16777216],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of discardable in all processes in ' +
+ 'Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:locked_size': {
+ value: [4194304, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total locked (pinned) size reported by Chrome for all ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:discardable:locked_size': {
+ value: [4194304, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'locked (pinned) size of discardable in all processes in ' +
+ 'Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:effective_size': {
+ value: [64 + 8388608 + 128, 512 + 1024 + 2048 + 16777216],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:malloc:effective_size': {
+ value: [64, 512 + 1024],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:sqlite:effective_size': {
+ value: [128, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_chrome:v8:effective_size': {
+ value: [0, 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:private_dirty_size': {
+ value: [256],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for all ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:proportional_resident_size': {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in all processes in ' +
+ 'Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in all ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [256],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in all processes ' +
+ 'in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in all ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in all ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [256],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by all processes in Chrome(2)'
+ },
+ 'memory:chrome2:all_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by all processes in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:process_count': {
+ value: [1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_chrome:effective_size': {
+ value: [64 + 8388608 + 128, 512 + 16777216],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the browser ' +
+ 'process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_chrome:locked_size': {
+ value: [4194304, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total locked (pinned) size reported by Chrome for the ' +
+ 'browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_chrome:malloc:effective_size': {
+ value: [64, 512],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in the browser process in ' +
+ 'Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_chrome:discardable:effective_size':
+ {
+ value: [8388608, 16777216],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of discardable in the browser process ' +
+ 'in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_chrome:discardable:locked_size':
+ {
+ value: [4194304, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'locked (pinned) size of discardable in the browser ' +
+ 'process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_chrome:sqlite:effective_size': {
+ value: [128, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in the browser process in ' +
+ 'Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:private_dirty_size': {
+ value: [256],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the ' +
+ 'browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for the browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the browser process ' +
+ 'in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [256],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the browser ' +
+ 'process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the browser ' +
+ 'process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the browser ' +
+ 'process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [256],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the browser process in Chrome(2)'
+ },
+ 'memory:chrome2:browser_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the browser process in Chrome(2)'
+ },
+ 'memory:chrome2:renderer_processes:process_count': {
+ value: [0, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of renderer processes in Chrome(2)'
+ },
+ 'memory:chrome2:renderer_processes:reported_by_chrome:effective_size': {
+ value: [0, 1024 + 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for renderer ' +
+ 'processes in Chrome(2)'
+ },
+ 'memory:chrome2:renderer_processes:reported_by_chrome:malloc:effective_size':
+ {
+ value: [0, 1024],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in renderer processes in ' +
+ 'Chrome(2)'
+ },
+ 'memory:chrome2:renderer_processes:reported_by_chrome:v8:effective_size': {
+ value: [0, 2048],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of v8 in renderer processes in Chrome(2)'
+ },
+
+ // Unknown browser (GMD5).
+ 'memory:unknown_browser:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:process_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:effective_size': {
+ value: [4096 + 8192],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for all ' +
+ 'processes in an unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:malloc:effective_size':
+ {
+ value: [4096],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in all processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:all_processes:reported_by_chrome:sqlite:effective_size':
+ {
+ value: [8192],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in all processes in an ' +
+ 'unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:process_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_chrome:effective_size':
+ {
+ value: [4096 + 8192],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total effective size reported by Chrome for the ' +
+ 'browser process in an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_chrome:malloc:effective_size':
+ {
+ value: [4096],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of malloc in the browser process in ' +
+ 'an unknown browser'
+ },
+ 'memory:unknown_browser:browser_process:reported_by_chrome:sqlite:effective_size':
+ {
+ value: [8192],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'effective size of sqlite in the browser process in ' +
+ 'an unknown browser'
+ }
+ });
+
+ memoryMetricTest('rangeOfInterest', function(model) {
+ const pChrome = createChromeBrowserProcess(model);
+ const pWebView = createWebViewProcess(model);
+
+ // Chrome: only the LIGHT dumps should be kept.
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 5, duration: 4, levelOfDetail: DETAILED}), pChrome);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 10, duration: 2, levelOfDetail: LIGHT}), pChrome);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 13, duration: 3, levelOfDetail: LIGHT}), pChrome);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 20, duration: 1, levelOfDetail: BACKGROUND}), pChrome);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 22, duration: 5, levelOfDetail: DETAILED}), pChrome);
+
+ // WebView: only the DETAILED dumps should be kept.
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 4, duration: 1, levelOfDetail: LIGHT}), pWebView);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 5, duration: 5, levelOfDetail: DETAILED}), pWebView);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 10, duration: 0, levelOfDetail: DETAILED}), pWebView);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 11, duration: 7, levelOfDetail: BACKGROUND}), pWebView);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 19, duration: 2, levelOfDetail: DETAILED}), pWebView);
+ addProcessMemoryDump(addGlobalMemoryDump(
+ model, {ts: 21, duration: 5, levelOfDetail: LIGHT}), pWebView);
+
+ // Unknown browser: only the LIGHT dump should be kept.
+ addGlobalMemoryDump(model, {ts: 5, duration: 3, levelOfDetail: DETAILED});
+ addGlobalMemoryDump(model, {ts: 9, duration: 12, levelOfDetail: LIGHT});
+ addGlobalMemoryDump(model, {ts: 22, duration: 3, levelOfDetail: DETAILED});
+ }, { /* opt_options */
+ rangeOfInterest: tr.b.math.Range.fromExplicitRange(10, 20)
+ }, {
+ 'memory:chrome:all_processes:dump_count': {
+ value: [3],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by Chrome to the ' +
+ 'trace'
+ },
+ 'memory:chrome:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:light': {
+ value: [2],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by Chrome to ' +
+ 'the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:background': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by Chrome ' +
+ 'to the trace'
+ },
+ 'memory:chrome:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'Chrome to the trace'
+ },
+ 'memory:chrome:all_processes:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in Chrome'
+ },
+ 'memory:chrome:browser_process:process_count': {
+ value: [1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in Chrome'
+ },
+
+ 'memory:webview:all_processes:dump_count': {
+ value: [4],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by WebView to the ' +
+ 'trace'
+ },
+ 'memory:webview:all_processes:dump_count:detailed': {
+ value: [3],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:light': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by WebView to ' +
+ 'the trace'
+ },
+ 'memory:webview:all_processes:dump_count:background': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by WebView ' +
+ 'to the trace'
+ },
+ 'memory:webview:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by ' +
+ 'WebView to the trace'
+ },
+ 'memory:webview:all_processes:process_count': {
+ value: [1, 1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:private_dirty_size': {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:proportional_resident_size': {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by the ' +
+ 'OS for all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in all processes in ' +
+ 'WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in all processes ' +
+ 'in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in all ' +
+ 'processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by all processes in WebView'
+ },
+ 'memory:webview:all_processes:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by all processes in WebView'
+ },
+ 'memory:webview:browser_process:process_count': {
+ value: [1, 1, 1, 1],
+ unit: count_smallerIsBetter,
+ description: 'total number of browser processes in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:private_dirty_size': {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size reported by the OS for the ' +
+ 'browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) reported by ' +
+ 'the OS for the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of ashmem in the browser process ' +
+ 'in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of ashmem in the ' +
+ 'browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the Java heap in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the Java heap in ' +
+ 'the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the native heap in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the native heap ' +
+ 'in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:stack:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'private dirty size of the thread stacks in the browser ' +
+ 'process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:stack:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'proportional resident size (PSS) of the thread stacks ' +
+ 'in the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:private_dirty_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total private dirty size of system memory (RAM) used ' +
+ 'by the browser process in WebView'
+ },
+ 'memory:webview:browser_process:reported_by_os:system_memory:proportional_resident_size':
+ {
+ value: [0, 0, 0],
+ unit: sizeInBytes_smallerIsBetter,
+ description: 'total proportional resident size (PSS) of system ' +
+ 'memory (RAM) used by the browser process in WebView'
+ },
+ 'memory:unknown_browser:all_processes:dump_count': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of all memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:detailed': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of detailed memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:light': {
+ value: [1],
+ unit: count_smallerIsBetter,
+ description: 'total number of light memory dumps added by an unknown ' +
+ 'browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:background': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of background memory dumps added by an ' +
+ 'unknown browser to the trace'
+ },
+ 'memory:unknown_browser:all_processes:dump_count:heap_profiler': {
+ value: [0],
+ unit: count_smallerIsBetter,
+ description: 'total number of heap profiler memory dumps added by an ' +
+ 'unknown browser to the trace'
+ }
+ });
+
+ test('allProcessesHasBreakdown', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const pBrowser = createChromeBrowserProcess(model);
+ const pRenderer = createProcessWithName(model, 'Renderer');
+ const pGpu = createProcessWithName(model, 'GPU Process');
+
+ const gmd = addGlobalMemoryDump(model, {ts: 20});
+ const pmdBrowser = addProcessMemoryDump(gmd, pBrowser, {ts: 19});
+ pmdBrowser.memoryAllocatorDumps = [
+ newAllocatorDump(pmdBrowser, 'malloc', {numerics: {
+ size: 8,
+ allocated_objects_size: 4
+ }})
+ ];
+
+ const pmdRenderer = addProcessMemoryDump(gmd, pRenderer, {ts: 20});
+ pmdRenderer.memoryAllocatorDumps = [
+ newAllocatorDump(pmdRenderer, 'malloc', {numerics: {size: 16}})
+ ];
+
+ const pmdGpu = addProcessMemoryDump(gmd, pGpu, {ts: 21});
+ pmdGpu.memoryAllocatorDumps = [
+ newAllocatorDump(pmdGpu, 'gpu', {numerics: {
+ size: 30,
+ allocated_objects_size: 25
+ }})
+ ];
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.memoryMetric(histograms, model);
+ const prefix = 'memory:chrome:';
+ const suffix = ':reported_by_chrome:malloc:effective_size';
+ const allHistogram =
+ histograms.getHistogramNamed(`${prefix}all_processes${suffix}`);
+ const rendererHistogram =
+ histograms.getHistogramNamed(`${prefix}renderer_processes${suffix}`);
+ const browserHistogram =
+ histograms.getHistogramNamed(`${prefix}browser_process${suffix}`);
+
+ const relatedNameMap = allHistogram.diagnostics.get('processes');
+ assert.strictEqual(relatedNameMap.get('browser_process'),
+ browserHistogram.name);
+ assert.strictEqual(relatedNameMap.get('renderer_processes'),
+ rendererHistogram.name);
+ assert.strictEqual(relatedNameMap.get('gpu_process'), undefined);
+
+ const bin = allHistogram.getBinForValue(allHistogram.average);
+ const breakdown = tr.b.getOnlyElement(bin.diagnosticMaps).get('processes');
+ assert.strictEqual(breakdown.get('browser_process'),
+ browserHistogram.average);
+ assert.strictEqual(breakdown.get('renderer_processes'),
+ rendererHistogram.average);
+ });
+
+ test('componentsHaveBreakdowns', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const pBrowser = createChromeBrowserProcess(model);
+
+ const gmd = addGlobalMemoryDump(model, {ts: 20});
+ const pmdBrowser = addProcessMemoryDump(gmd, pBrowser, {ts: 19});
+ const v8Dump = newAllocatorDump(pmdBrowser, 'v8', {numerics: {
+ size: 10,
+ }});
+
+ const isolateDumpA = addChildDump(v8Dump, 'isolate_A');
+ addChildDump(isolateDumpA, 'malloc', {numerics: {
+ size: 1,
+ peak_size: 2
+ }});
+
+ const heapDumpA = addChildDump(isolateDumpA, 'heap_spaces', {numerics: {
+ size: 42,
+ allocated_objects_size: 36
+ }});
+
+ addChildDump(heapDumpA, 'code_space', {numerics: {
+ allocated_objects_size: 1,
+ size: 2
+ }});
+
+ pmdBrowser.memoryAllocatorDumps = [v8Dump];
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.memoryMetric(histograms, model);
+ const prefix = 'memory:chrome:browser_process:reported_by_chrome';
+ const suffix = 'effective_size';
+ const browserHistogram = histograms.getHistogramNamed(
+ `${prefix}:v8:${suffix}`);
+ const heapHistogram = histograms.getHistogramNamed(
+ `${prefix}:v8:heap:${suffix}`);
+ const codeSpaceHistogram = histograms.getHistogramNamed(
+ `${prefix}:v8:heap:code_space:${suffix}`);
+
+ assert.strictEqual(browserHistogram.diagnostics.get('components').get(
+ 'heap'), heapHistogram.name);
+ assert.strictEqual(heapHistogram.diagnostics.get('components').get(
+ 'code_space'), codeSpaceHistogram.name);
+
+ const browserBin = browserHistogram.getBinForValue(
+ browserHistogram.average);
+ const browserBreakdown = tr.b.getOnlyElement(
+ browserBin.diagnosticMaps).get('components');
+ assert.strictEqual(browserBreakdown.get('heap'), heapHistogram.average);
+
+ const heapBin = heapHistogram.getBinForValue(heapHistogram.average);
+ const heapBreakdown = tr.b.getOnlyElement(
+ heapBin.diagnosticMaps).get('components');
+ assert.strictEqual(heapBreakdown.get('code_space'),
+ codeSpaceHistogram.average);
+ });
+
+ test('dumpIdBrowserClashThrows', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const pWebView = createWebViewProcess(model);
+ const pChrome = createChromeBrowserProcess(model);
+
+ const gmd = addGlobalMemoryDump(model, {ts: 10});
+ addProcessMemoryDump(gmd, pWebView, {ts: 9});
+ addProcessMemoryDump(gmd, pChrome, {ts: 11});
+ });
+ const histograms = new tr.v.HistogramSet();
+
+ assert.throws(function() {
+ tr.metrics.sh.memoryMetric(histograms, model);
+ }, 'Memory dump ID clash across multiple browsers with PIDs: 1 and 2');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html
new file mode 100644
index 00000000000..81b4f65ca79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cpu_time.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/cpu_time_tree_data_reporter.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Implements the new CPU time metric. This will eventually
+ * replace the current cpu_time_metric.html, but we're running this alongside
+ * the existing metric while we monitor its quality.
+ *
+ */
+tr.exportTo('tr.metrics.sh', function() {
+ /**
+ * This metric measures total CPU time and CPU time per unit of wall clock
+ * time for all combinations of process type, thread type, RAIL
+ * stage, and RAIL stage initiator present in the model.
+ *
+ * The metric generates histograms of the form
+ * ${cpuTime|cpuPercentage}:${process_type}:${thread_type}:
+ * ${rail_stage}:${rail_stage_initiator}
+ *
+ * 'cpuTime' histograms represent total consumed CPU time, while
+ * 'cpuPercentage' histograms represent CPU time as a percentage of wall clock
+ * time.
+ *
+ * Example histograms generated by this metric:
+ * - cpuTime:browser_process:CrBrowserMain:Animation:CSS
+ * - cpuPercentage:gpu_process:CrGpuMain:Response:Scroll
+
+ * For a given model, a single sample is generated for each histogram. For
+ * example, if the model contains three renderer processes, and 10 different
+ * Scroll Response ranges, the histogram
+ * cpuPercentage:renderer_process:CrRendererMain:Response:Scroll will still
+ * contain a single sample: the total CPU time consumed by all three renderer
+ * main threads over all 10 Scroll Response phases, divided by the total
+ * duration of those ranges. Since the three different main threads can
+ * potentially be running on three different CPU cores, the sample value of a
+ * cpuPercentage histogram can be more than 100%.
+ *
+ * The histograms are created as needed from the model - if a certain
+ * combination of process, thread, RAIL stage and initiator does not occur in
+ * the model, the histogram for that combination is not added.
+ *
+ * This metric requires only the 'toplevel' tracing category.
+ *
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.Model} model
+ * @param {!Object=} opt_options
+ */
+ function newCpuTimeMetric(histograms, model, opt_options) {
+ const rangeOfInterest = opt_options && opt_options.rangeOfInterest ?
+ opt_options.rangeOfInterest : model.bounds;
+
+ const rootNode = tr.e.chrome.CpuTime.constructMultiDimensionalView(
+ model, rangeOfInterest);
+
+ tr.metrics.sh.CpuTimeTreeDataReporter.reportToHistogramSet(
+ rootNode, histograms);
+ }
+
+ tr.metrics.MetricRegistry.register(newCpuTimeMetric, {
+ supportsRangeOfInterest: true
+ });
+
+ return {
+ newCpuTimeMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html
new file mode 100644
index 00000000000..a21b6b83ca6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/system_health/new_cpu_time_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const cpuTimeMetric = tr.metrics.sh.newCpuTimeMetric;
+ const CHROME_PROCESS_NAMES =
+ tr.e.chrome.chrome_processes.CHROME_PROCESS_NAMES;
+
+ test('cpuTimeMetric_customRangeOfInterest', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const process = model.getOrCreateProcess(1);
+ process.name = 'Browser';
+ const thread = process.getOrCreateThread(1);
+ thread.name = 'CrBrowserMain';
+
+ // Slice from 0 to 50.
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: 50,
+ cpuStart: 0,
+ cpuDuration: 25
+ }));
+
+ // Slice from 300 to 350.
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 300,
+ duration: 50,
+ cpuStart: 100,
+ cpuDuration: 25
+ }));
+ });
+
+ const metricNameSuffix = CHROME_PROCESS_NAMES.BROWSER +
+ ':CrBrowserMain:all_stages:all_initiators';
+ const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(0, 200);
+ const histograms = new tr.v.HistogramSet();
+ cpuTimeMetric(histograms, model, {rangeOfInterest});
+
+ const cpuPercentageHistogram = histograms.getHistogramNamed(
+ 'cpuPercentage:' + metricNameSuffix);
+ const cpuTimeHistogram = histograms.getHistogramNamed(
+ 'cpuTime:' + metricNameSuffix);
+
+ // Histograms exist.
+ assert.isDefined(cpuPercentageHistogram);
+ assert.isDefined(cpuTimeHistogram);
+
+ // Each histogram contains a single sample.
+ assert.strictEqual(cpuPercentageHistogram.running.count, 1);
+ assert.strictEqual(cpuTimeHistogram.running.count, 1);
+
+ // Assert histogram sample value is correct:
+ // rangeOfInterest only contains the first slice (with a CPU time of 25),
+ // and total duration of the range is 200.
+ assert.closeTo(cpuPercentageHistogram.sum, 25 / 200, 1e-7);
+ assert.closeTo(cpuTimeHistogram.sum, 25, 1e-7);
+ });
+
+ test('cpuTimeMetric_rangeOfInterestBeyondModelBounds', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const process = model.getOrCreateProcess(1);
+ process.name = 'Browser';
+ const thread = process.getOrCreateThread(1);
+ thread.name = 'CrBrowserMain';
+
+ // Slice from 0 to 50.
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: 50,
+ cpuStart: 0,
+ cpuDuration: 25
+ }));
+
+ // Slice from 300 to 350.
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 300,
+ duration: 50,
+ cpuStart: 100,
+ cpuDuration: 25
+ }));
+ });
+
+ const metricNameSuffix = CHROME_PROCESS_NAMES.BROWSER +
+ ':CrBrowserMain:all_stages:all_initiators';
+ const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(-100, 200);
+ const histograms = new tr.v.HistogramSet();
+ cpuTimeMetric(histograms, model, {rangeOfInterest});
+
+ const cpuPercentageHistogram = histograms.getHistogramNamed(
+ 'cpuPercentage:' + metricNameSuffix);
+ const cpuTimeHistogram = histograms.getHistogramNamed(
+ 'cpuTime:' + metricNameSuffix);
+
+ // Histograms exist.
+ assert.isDefined(cpuPercentageHistogram);
+ assert.isDefined(cpuTimeHistogram);
+
+ // Each histogram contains a single sample.
+ assert.strictEqual(cpuPercentageHistogram.running.count, 1);
+ assert.strictEqual(cpuTimeHistogram.running.count, 1);
+
+ // Assert histogram sample value is correct:
+ // model.bounds is [0, 350] here. If rangeOfInterest is beyond these bounds,
+ // the effective range for calculating total duration for cpuPercentage is
+ // the intersection of rangeOfInterest and model.bounds, which is [0, 200]
+ // here. rangeOfInterest only contains the first slice, with a CPU time of
+ // 25, and the total effective duration is 200, not 300.
+ assert.closeTo(cpuPercentageHistogram.sum, 25 / 200, 1e-7);
+ assert.closeTo(cpuTimeHistogram.sum, 25, 1e-7);
+ });
+
+ test('cpuTimeMetric_defaultRangeOfInterest', () => {
+ const model = tr.c.TestUtils.newModel(model => {
+ const process = model.getOrCreateProcess(1);
+ process.name = 'Browser';
+ const thread = process.getOrCreateThread(1);
+ thread.name = 'CrBrowserMain';
+
+ // Slice from 0 to 50.
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: 50,
+ cpuStart: 0,
+ cpuDuration: 25
+ }));
+
+ // Slice from 300 to 350.
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 300,
+ duration: 50,
+ cpuStart: 100,
+ cpuDuration: 25
+ }));
+ });
+
+ const metricNameSuffix = CHROME_PROCESS_NAMES.BROWSER +
+ ':CrBrowserMain:all_stages:all_initiators';
+ const histograms = new tr.v.HistogramSet();
+ cpuTimeMetric(histograms, model);
+
+ const cpuPercentageHistogram = histograms.getHistogramNamed(
+ 'cpuPercentage:' + metricNameSuffix);
+ const cpuTimeHistogram = histograms.getHistogramNamed(
+ 'cpuTime:' + metricNameSuffix);
+
+ // Histograms exist.
+ assert.isDefined(cpuPercentageHistogram);
+ assert.isDefined(cpuTimeHistogram);
+
+ // Each histogram contains a single sample.
+ assert.strictEqual(cpuPercentageHistogram.running.count, 1);
+ assert.strictEqual(cpuTimeHistogram.running.count, 1);
+
+ // Assert histogram sample value is correct:
+ // When no custom rangeOfInterest is set, the effective range for
+ // calculating CPU usage should be model.bounds, which is [0, 350] here.
+ // Total CPU time is 25 + 25 from the two slices.
+ assert.closeTo(cpuPercentageHistogram.sum, (25 + 25) / 350, 1e-7);
+ assert.closeTo(cpuTimeHistogram.sum, 25 + 25, 1e-7);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html
new file mode 100644
index 00000000000..8b343e07f49
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html
@@ -0,0 +1,348 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/importer/find_input_expectations.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ // If the power series doesn't cover the entire Chrome trace, then
+ // the results from Chrome tracing metrics will likely be inaccurate,
+ // so we don't report them. However, we allow the power series bounds
+ // to be up to 1 ms inside the Chrome trace and still count as
+ // covering the Chrome trace. This is to allow for small deviations
+ // due to clock sync latency and the finite sampling rate of the
+ // BattOr.
+ const CHROME_POWER_GRACE_PERIOD_MS = 1;
+
+ /**
+ * Creates an empty histogram to hold data for a given time interval.
+ *
+ * @returns {Object} An object of the form:
+ *
+ * {
+ * perSecond {boolean}: Whether the data for this time interval is given
+ * as per second, If not, it's given as an integral over the
+ * whole interval.
+ * energy {tr.v.Histogram}: Histogram giving energy use (i.e. energy in J
+ * if perSecond = False, power in W if perSecond = True)
+ * }
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Returns the intervals of time between navigation event and time to
+ * interactive.
+ */
+ function getNavigationTTIIntervals_(model) {
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const intervals = [];
+ 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;
+ }
+ if (expectation.timeToInteractive !== undefined) {
+ intervals.push(tr.b.math.Range.fromExplicitRange(
+ expectation.navigationStart.start, expectation.timeToInteractive));
+ }
+ }
+ return intervals.sort((x, y) => x.min - y.min);
+ }
+
+ /**
+ * Generates the set of time intervals that metrics should be run over.
+ * Time intervals include each UE (for UE-based metrics), the whole
+ * story (for the full-story metric), etc. Each time interval is given
+ * in the following form:
+ *
+ * {
+ * bounds {tr.b.math.Range}: Boundaries of the time interval.
+ * name {string}: Name of this interval. Used to generate the
+ * metric names.
+ * description {string}: Human readable description of the interval.
+ * Used to generate the metric descriptions.
+ * perSecond {boolean}: Whether metrics over this interval should be
+ * reported as per-second values (e.g. power). If not, integrated values
+ * over the whole interval (e.g. energy) are reported.
+ * }
+ *
+ */
+ function* computeTimeIntervals_(model) {
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const powerSeries = model.device.powerSeries;
+ if (powerSeries === undefined ||
+ powerSeries.samples.length === 0) {
+ return;
+ }
+ // Output the full story power metrics, which exists regardless of
+ // the presence of a Chrome trace.
+ yield {
+ bounds: model.bounds,
+ name: 'story',
+ description: 'user story',
+ perSecond: true
+ };
+
+ const chromeBounds = computeChromeBounds_(model);
+ if (chromeBounds.isEmpty) return;
+
+ const powerSeriesBoundsWithGracePeriod = tr.b.math.Range.fromExplicitRange(
+ powerSeries.bounds.min - CHROME_POWER_GRACE_PERIOD_MS,
+ powerSeries.bounds.max + CHROME_POWER_GRACE_PERIOD_MS);
+ if (!powerSeriesBoundsWithGracePeriod.containsRangeExclusive(
+ chromeBounds)) {
+ return;
+ }
+
+ // If Chrome bounds are good and the power trace covers the Chrome bounds,
+ // then output the Chrome specific metrics (loading and RAIL stages). Note
+ // that only the part of the time interval that overlaps the Chrome bounds
+ // should be included.
+ for (const interval of getRailStageIntervals_(model)) {
+ yield {
+ bounds: interval.bounds.findIntersection(chromeBounds),
+ name: interval.name,
+ description: interval.description,
+ perSecond: interval.perSecond
+ };
+ }
+
+ for (const interval of getLoadingIntervals_(model, chromeBounds)) {
+ yield {
+ bounds: interval.bounds.findIntersection(chromeBounds),
+ name: interval.name,
+ description: interval.description,
+ perSecond: interval.perSecond
+ };
+ }
+ }
+
+ /**
+ * Gets a list of time intervals for the RAIL stages. Each RAIL stage
+ * generates a time interval with the name equal to the name of the RAIL
+ * stage except made into lower case and with spaces replaced bu underscores
+ * e.g. "Scroll Animation" --> "scroll_animation". Each time interval is given
+ * in the following form:
+ *
+ * {
+ * bounds {tr.b.math.Range}: Boundaries of the time interval.
+ * name {string}: Name of this interval. Used to generate the
+ * metric names.
+ * description {string}: Human readable description of the interval.
+ * Used to generate the metric descriptions.
+ * perSecond {boolean}: Whether metrics over this interval should be
+ * reported as per-second values (e.g. power). If not, integrated values
+ * over the whole interval (e.g. energy) are reported.
+ * }
+ *
+ */
+ function* getRailStageIntervals_(model) {
+ for (const exp of model.userModel.expectations) {
+ const histogramName = exp.title.toLowerCase().replace(' ', '_');
+ const energyHist = undefined;
+ if (histogramName.includes('response')) {
+ yield {
+ bounds: tr.b.math.Range.fromExplicitRange(exp.start, exp.end),
+ name: histogramName,
+ description: 'RAIL stage ' + histogramName,
+ perSecond: false
+ };
+ } else if (histogramName.includes('animation') ||
+ histogramName.includes('idle')) {
+ yield {
+ bounds: tr.b.math.Range.fromExplicitRange(exp.start, exp.end),
+ name: histogramName,
+ description: 'RAIL stage ' + histogramName,
+ perSecond: true
+ };
+ }
+ }
+ }
+
+ /**
+ * Gets a list of time intervals for the RAIL stages. Each RAIL stage
+ * generates a time interval with the name equal to the name of the RAIL
+ * stage except made into lower case and with spaces replaced bu underscores
+ * e.g. "Scroll Animation" --> "scroll_animation". Each time interval is given
+ * in the following form:
+ *
+ * {
+ * bounds {tr.b.math.Range}: Boundaries of the time interval.
+ * name {string}: Name of this interval. Used to generate the
+ * metric names.
+ * description {string}: Human readable description of the interval.
+ * Used to generate the metric descriptions.
+ * perSecond {boolean}: Whether metrics over this interval should be
+ * reported as per-second values (e.g. power). If not, integrated values
+ * over the whole interval (e.g. energy) are reported.
+ * }
+ *
+ */
+ function* getLoadingIntervals_(model, chromeBounds) {
+ const ttiIntervals = getNavigationTTIIntervals_(model);
+ for (const ttiInterval of ttiIntervals) {
+ yield {
+ bounds: ttiInterval,
+ name: 'load',
+ description: 'page loads',
+ perSecond: false
+ };
+ }
+ }
+
+ /**
+ * @returns {tr.b.math.Range} The boundaries of the Chrome portion of the
+ * trace.
+ */
+ function computeChromeBounds_(model) {
+ const chromeBounds = new tr.b.math.Range();
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (chromeHelper === undefined) return chromeBounds;
+ for (const helper of chromeHelper.browserHelpers) {
+ if (helper.mainThread) {
+ chromeBounds.addRange(helper.mainThread.bounds);
+ }
+ }
+ for (const pid in chromeHelper.rendererHelpers) {
+ if (chromeHelper.rendererHelpers[pid].mainThread) {
+ chromeBounds.addRange(
+ chromeHelper.rendererHelpers[pid].mainThread.bounds);
+ }
+ }
+ return chromeBounds;
+ }
+
+ /**
+ * Adds the power histograms to the histogram set.
+ *
+ * Each power histogram is based on a specific time interval, and is named as
+ * follows:
+ *
+ * - [time_interval_name]:power, which contains a sample for each power
+ * sample data point during any time interval with the given type. Each
+ * sample represents the power draw during the period covered by that
+ * power sample.
+ *
+ * - [time_interval_name]:energy, which contains a sample for each time
+ * interval with the given type present in the trace. Each sample
+ * represents the total energy used over that time interval.
+ *
+ * The time intervals are as follows:
+ *
+ * - "story": The time interval covering the entire user story. There is
+ * always exactly one "story" interval.
+ *
+ * - "load" : The time interval covered by a page load, from navigationStart
+ * to timeToInteractive. There is one "load" interval for each page load
+ * in the trace.
+ *
+ * - "[user_expectation_type]" : Each Response, Animation, or Idle
+ * UE in the trace generates a time interval whose name is that of the UE,
+ * converted to lower case and with underscores in place of spaces.
+ * For instance, if there are three "Scroll Response" UEs in the trace,
+ * then there will be three "scroll_response" time intervals, so the
+ * histogram scroll_response:energy will contain three samples.
+ *
+ * Note that each time interval type only generates ONE of the "power" or
+ * "energy" histograms. Power histograms are generated for time intervals
+ * that represent events that occur over a period of time. This includes
+ * the following intervals
+ *
+ * - "story"
+ * - Any Animation or Idle UE
+ *
+ * For instance, "the energy it takes to play a video"
+ * does not have meaning because it depends on how long the video
+ * is; thus a more meaningful metric is power. In contrast, energy histograms
+ * are generated for time intervals that represent particular tasks
+ * which must be completed. This includes the following intervals:
+ *
+ * - "load"
+ * - Any Response UE
+ *
+ * For instance, if a change causes a response to take longer to process, then
+ * we want to count that as taking the energy over a longer period of time.
+ */
+ 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
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html
new file mode 100644
index 00000000000..9e8d47ea258
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html
@@ -0,0 +1,742 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/importer/find_input_expectations.html">
+<link rel="import" href="/tracing/metrics/system_health/power_metric.html">
+<link rel="import" href="/tracing/model/user_model/idle_expectation.html">
+<link rel="import" href="/tracing/model/user_model/load_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+function containsData(histograms, name) {
+ for (const value of histograms) {
+ if (value.name === name) {
+ return (value.running !== undefined);
+ }
+ }
+
+ return undefined;
+}
+
+function getMetricValueCount(histograms, name) {
+ for (const value of histograms) {
+ if (value.name === name && value.running !== undefined) {
+ return value.running.count;
+ }
+ }
+
+ return undefined;
+}
+
+function getMetricValueSum(histograms, name) {
+ for (const value of histograms) {
+ if (value.name === name && value.running !== undefined) {
+ return value.running.sum;
+ }
+ }
+
+ return undefined;
+}
+
+function getMetricValueMin(histograms, name) {
+ for (const value of histograms) {
+ if (value.name === name && value.running !== undefined) {
+ return value.running.min;
+ }
+ }
+
+ return undefined;
+}
+
+function getMetricValueAvg(histograms, name) {
+ for (const value of histograms) {
+ if (value.name === name && value.running !== undefined) {
+ return value.running.mean;
+ }
+ }
+
+ return undefined;
+}
+
+function getMetricValueMax(histograms, name) {
+ for (const value of histograms) {
+ if (value.name === name && value.running !== undefined) {
+ return value.running.max;
+ }
+ }
+
+ return undefined;
+}
+
+tr.b.unittest.testSuite(function() {
+ test('powerMetric_computesChromeBoundsCorrectly', function() {
+ // Tests if Chrome bounds are computed correctly when there
+ // are both browser and renderer threads in the trace.
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const rendererThread = model.rendererMain;
+ const browserProcess = model.browserProcess;
+ const browserThread = model.browserMain;
+ rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200.0,
+ duration: 500.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ browserThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000.0,
+ duration: 500.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 2000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 0, 2000));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueMin(histograms, 'idle:power'), 200.0, .01);
+ assert.closeTo(getMetricValueAvg(histograms, 'idle:power'), 849.5, .01);
+ assert.closeTo(getMetricValueMax(histograms, 'idle:power'), 1499.0, .01);
+ });
+
+ test('powerMetric_noPowerSeries', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.lengthOf(histograms, 0);
+ });
+
+ test('powerMetric_emptyPowerSeries', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.lengthOf(histograms, 0);
+ });
+
+ test('powerMetric_noChromeTrace', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ // Only the whole trace power metric should contain data.
+ assert(containsData(histograms, 'story:power'));
+ assert(!containsData(histograms, 'load:energy'));
+ assert(!containsData(histograms, 'after_load:power'));
+ });
+
+ test('powerMetric_emptyChromeTrace', function() {
+ const histograms = new tr.v.HistogramSet();
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.LoadExpectation(
+ model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 500));
+ });
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert(containsData(histograms, 'story:power'));
+ assert(!containsData(histograms, 'load:energy'));
+ assert(!containsData(histograms, 'after_load:power'));
+ });
+
+ test('powerMetric_powerSeriesStartsLate', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 300; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.LoadExpectation(
+ model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 500));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert(containsData(histograms, 'story:power'));
+ assert(!containsData(histograms, 'load:energy'));
+ assert(!containsData(histograms, 'after_load:power'));
+ });
+
+ test('powerMetric_powerSeriesEndsEarly', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 700; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.LoadExpectation(
+ model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 500));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert(containsData(histograms, 'story:power'));
+ assert(!containsData(histograms, 'load:energy'));
+ assert(!containsData(histograms, 'after_load:power'));
+ });
+
+ test('powerMetric_generic_oneStageEachType_irBeyondChrome', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(1234);
+ const mainThread = rendererProcess.getOrCreateThread(1);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 1500));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueSum(histograms,
+ 'scroll_response:energy'), 125, 0.5);
+ assert.closeTo(getMetricValueAvg(histograms,
+ 'idle:power'), 750, 0.5);
+ assert.closeTo(getMetricValueAvg(histograms,
+ 'story:power'), 500, 0.5);
+ });
+
+ test('powerMetric_story_minAvgMax', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 1500));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueMin(histograms, 'story:power'), 0, .01);
+ assert.closeTo(getMetricValueAvg(histograms, 'story:power'), 500, .01);
+ assert.closeTo(getMetricValueMax(histograms, 'story:power'), 1000, .01);
+ });
+
+ test('powerMetric_generic_oneUEBeforeChrome', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(1234);
+ const mainThread = rendererProcess.getOrCreateThread(1);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 500,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 0, 300));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 300, 1000));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueAvg(histograms,
+ 'idle:power'), 750, 0.5);
+ });
+
+ test('powerMetric_generic_multipleStagesEachType', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(1234);
+ const mainThread = rendererProcess.getOrCreateThread(1);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 200));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 200, 300));
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 500, 400));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 900, 100));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueSum(histograms,
+ 'scroll_response:energy'), 300, 0.6);
+ assert.closeTo(getMetricValueAvg(histograms,
+ 'idle:power'), 500, 0.6);
+ assert.strictEqual(getMetricValueCount(histograms,
+ 'scroll_response:energy'), 2);
+ assert.strictEqual(getMetricValueCount(histograms,
+ 'idle:power'), 400);
+ });
+
+ test('powerMetric_loading_oneInterval_samplesBeyondChrome', function() {
+ // Interval of load is [200, 15400].
+ // Trace goes until 22150.
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 200,
+ duration: 5.0
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 9180,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 9200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 9200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9350,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 11150,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 12550,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 14900,
+ duration: 500,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 22150,
+ duration: 10,
+ }));
+
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 15400; i++) {
+ model.device.powerSeries.addPowerSample(i, 20);
+ }
+ for (let i = 15401; i <= 22160; i++) {
+ model.device.powerSeries.addPowerSample(i, 10);
+ }
+ for (let i = 22160; i <= 30000; i++) {
+ model.device.powerSeries.addPowerSample(i, 10);
+ }
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+ // Energy for first load is 20 W * 15.2 s
+ // (interval from 0.2 s to 15.4 s)
+ assert.closeTo(
+ getMetricValueAvg(histograms, 'load:energy'), 304, 0.1);
+ });
+
+ test('powerMetric_loading_noMeaningfulPaint', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9350,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 11150,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 12550,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 14950,
+ duration: 500,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 22150,
+ duration: 10,
+ }));
+
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 15400; i++) {
+ model.device.powerSeries.addPowerSample(i, 20);
+ }
+ for (let i = 15401; i <= 22160; i++) {
+ model.device.powerSeries.addPowerSample(i, 10);
+ }
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+ // Energy for first load is 20 W * 15.2 s
+ // (interval from 0.2 s to 15.4 s)
+ assert.isUndefined(getMetricValueCount(histograms, 'after_load:power'));
+ });
+
+ test('powerMetric_scroll_oneStageEachType', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 500));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueAvg(histograms,
+ 'scroll_animation:power'), 250, 0.5);
+ });
+
+ test('powerMetric_scroll_multipleStagesEachType', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 200));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 200, 300));
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 500, 200));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 700, 300));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueAvg(histograms,
+ 'scroll_animation:power'), 350, 0.6);
+ });
+
+ test('powerMetric_video_oneStageEachType', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.VIDEO, 0, 500));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 500, 500));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueAvg(histograms, 'video_animation:power'),
+ 250, 0.5);
+ });
+
+ test('powerMetric_video_multipleStagesEachType', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 1000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.device.powerSeries = new tr.model.PowerSeries(model.device);
+ for (let i = 0; i <= 1000; i++) {
+ model.device.powerSeries.addPowerSample(i, i);
+ }
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.VIDEO, 0, 200));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 200, 300));
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.VIDEO, 500, 200));
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 700, 300));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.powerMetric(histograms, model);
+
+ assert.closeTo(getMetricValueAvg(histograms, 'video_animation:power'),
+ 350, 0.6);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html
new file mode 100644
index 00000000000..911c2d66d48
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/model/user_model/animation_expectation.html">
+<link rel="import" href="/tracing/model/user_model/load_expectation.html">
+<link rel="import" href="/tracing/model/user_model/response_expectation.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ // In the case of Response, Load, and DiscreteAnimation UEs, Responsiveness is
+ // derived from the time between when the user thinks they begin an interation
+ // (expectedStart) and the time when the screen first changes to reflect the
+ // interaction (actualEnd). There may be a delay between expectedStart and
+ // when chrome first starts processing the interaction (actualStart) if the
+ // main thread is busy. The user doesn't know when actualStart is, they only
+ // know when expectedStart is. User responsiveness, by definition, considers
+ // only what the user experiences, so "duration" is defined as actualEnd -
+ // expectedStart.
+
+ 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 ' +
+ animationExpectation.stableId);
+ }
+
+ let frameTimestamps = animationExpectation.frameEvents;
+ frameTimestamps = frameTimestamps.toArray().map(function(event) {
+ return event.start;
+ });
+
+ const absolute = true;
+ return tr.b.math.Statistics.timestampsDiscrepancy(
+ frameTimestamps, absolute);
+ }
+
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.model.Model} model
+ * @param {!Object=} opt_options
+ */
+ function responsivenessMetric(histograms, model, opt_options) {
+ const responseNumeric = new tr.v.Histogram('response latency',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ tr.v.HistogramBinBoundaries.createLinear(100, 1e3, 50));
+ const throughputNumeric = new tr.v.Histogram('animation throughput',
+ tr.b.Unit.byName.unitlessNumber_biggerIsBetter,
+ tr.v.HistogramBinBoundaries.createLinear(10, 60, 10));
+ const frameTimeDiscrepancyNumeric = new tr.v.Histogram(
+ 'animation frameTimeDiscrepancy',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 50).
+ addExponentialBins(1e4, 10));
+ const latencyNumeric = new tr.v.Histogram('animation latency',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ tr.v.HistogramBinBoundaries.createLinear(0, 300, 60));
+
+ model.userModel.expectations.forEach(function(ue) {
+ if (opt_options && opt_options.rangeOfInterest &&
+ !opt_options.rangeOfInterest.intersectsExplicitRangeInclusive(
+ ue.start, ue.end)) {
+ return;
+ }
+
+ const sampleDiagnosticMap = tr.v.d.DiagnosticMap.fromObject(
+ {relatedEvents: new tr.v.d.RelatedEventSet([ue])});
+
+ // Responsiveness is not defined for Idle or Startup expectations.
+ if (ue instanceof tr.model.um.IdleExpectation) {
+ return;
+ } else if (ue instanceof tr.model.um.StartupExpectation) {
+ return;
+ } else if (ue instanceof tr.model.um.LoadExpectation) {
+ // This is already covered by loadingMetric.
+ } else if (ue instanceof tr.model.um.ResponseExpectation) {
+ responseNumeric.addSample(ue.duration, sampleDiagnosticMap);
+ } else if (ue instanceof tr.model.um.AnimationExpectation) {
+ if (ue.frameEvents === undefined || ue.frameEvents.length === 0) {
+ // Ignore animation stages that do not have associated frames:
+ // https://github.com/catapult-project/catapult/issues/2446
+ return;
+ }
+ const throughput = computeAnimationThroughput(ue);
+ if (throughput === undefined) {
+ throw new Error('Missing throughput for ' +
+ ue.stableId);
+ }
+
+ throughputNumeric.addSample(throughput, sampleDiagnosticMap);
+
+ const frameTimeDiscrepancy = computeAnimationframeTimeDiscrepancy(ue);
+ if (frameTimeDiscrepancy === undefined) {
+ throw new Error('Missing frameTimeDiscrepancy for ' +
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html
new file mode 100644
index 00000000000..dbe10addd27
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/system_health/responsiveness_metric.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel(collectionOfFrameTimestamps) {
+ const opts = {
+ customizeModelCallback(model) {
+ const thread = tr.c.TestUtils.newFakeThread();
+ collectionOfFrameTimestamps.forEach(function(frameTimestamps) {
+ frameTimestamps.sort((a, b) => a - b);
+ const ue = new tr.model.um.AnimationExpectation(
+ model, 'test', frameTimestamps[0],
+ frameTimestamps[frameTimestamps.length - 1]);
+ const group = new tr.model.SliceGroup(thread);
+ frameTimestamps.forEach(function(time) {
+ group.pushSlice(tr.c.TestUtils.newSliceEx({
+ title: tr.model.helpers.IMPL_RENDERING_STATS,
+ start: time,
+ end: time,
+ cpuStart: time,
+ cpuEnd: time
+ }));
+ });
+ group.createSubSlices();
+ group.slices.forEach(function(slice) {
+ ue.associatedEvents.push(slice);
+ });
+ model.userModel.expectations.push(ue);
+ model.userModel.expectations.push(
+ new tr.model.um.StartupExpectation(model, 0, 1));
+ model.userModel.expectations.push(
+ new tr.model.um.ResponseExpectation(model, 'test', 1, 1));
+ });
+ }
+ };
+ return tr.c.TestUtils.newModelWithEvents([], opts);
+ }
+
+ test('animationThroughputNoFrames', function() {
+ const model = createModel([[]]);
+ const valueList = new tr.v.HistogramSet();
+ tr.metrics.sh.responsivenessMetric(valueList, model);
+ const value = valueList.getHistogramNamed('animation throughput');
+ assert.strictEqual(value.numValues, 0);
+ });
+
+ test('animationThroughputFramesAndNoFrames', function() {
+ const model = createModel([[0, 100], []]);
+ const valueList = new tr.v.HistogramSet();
+ tr.metrics.sh.responsivenessMetric(valueList, model);
+ const value = valueList.getHistogramNamed('animation throughput');
+ assert.strictEqual(value.average, 20);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html
new file mode 100644
index 00000000000..92bb7c71fb1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html
@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/extras/chrome/chrome_processes.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.sh', function() {
+ // Returns a weight for this score.
+ // score should be a number between 0 and 1 inclusive.
+ // This function is expected to be passed to tr.b.math.Statistics.weightedMean
+ // as its weightCallback.
+ function perceptualBlend(ir, index, score) {
+ // Lower scores are exponentially more important than higher scores
+ // due to the Peak-end rule.
+ // Other than that general rule, there is no specific reasoning behind this
+ // specific formula -- it is fairly arbitrary.
+ return Math.exp(1 - score);
+ }
+
+ function filterExpectationsByRange(irs, opt_range) {
+ const filteredExpectations = [];
+ irs.forEach(function(ir) {
+ if (!(ir instanceof tr.model.um.UserExpectation)) return;
+
+ if (!opt_range ||
+ opt_range.intersectsExplicitRangeInclusive(ir.start, ir.end)) {
+ filteredExpectations.push(ir);
+ }
+ });
+ return filteredExpectations;
+ }
+
+ /**
+ * Splits the global memory dumps in |model| by browser name.
+ *
+ * @param {!tr.Model} model The trace model from which the global dumps
+ * should be extracted.
+ * @param {!tr.b.math.Range=} opt_rangeOfInterest If provided, global memory
+ * dumps that do not inclusively intersect the range will be skipped.
+ * @return {!Map<string, !Array<!tr.model.GlobalMemoryDump>} A map from
+ * browser names to the associated global memory dumps.
+ */
+ function splitGlobalDumpsByBrowserName(model, opt_rangeOfInterest) {
+ const chromeModelHelper =
+ model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ const browserNameToGlobalDumps = new Map();
+ const globalDumpToBrowserHelper = new WeakMap();
+
+ // 1. For each browser process in the model, add its global memory dumps to
+ // |browserNameToGlobalDumps|. |chromeModelHelper| can be undefined if
+ // it fails to find any browser, renderer or GPU process (see
+ // tr.model.helpers.ChromeModelHelper.supportsModel).
+
+ if (chromeModelHelper) {
+ chromeModelHelper.browserHelpers.forEach(function(helper) {
+ // Retrieve the associated global memory dumps and check that they
+ // haven't been classified as belonging to another browser process.
+ const globalDumps = skipDumpsThatDoNotIntersectRange(
+ helper.process.memoryDumps.map(d => d.globalMemoryDump),
+ opt_rangeOfInterest);
+ globalDumps.forEach(function(globalDump) {
+ const existingHelper = globalDumpToBrowserHelper.get(globalDump);
+ if (existingHelper !== undefined) {
+ throw new Error('Memory dump ID clash across multiple browsers ' +
+ 'with PIDs: ' + existingHelper.pid + ' and ' + helper.pid);
+ }
+ globalDumpToBrowserHelper.set(globalDump, helper);
+ });
+
+ makeKeyUniqueAndSet(browserNameToGlobalDumps,
+ tr.e.chrome.chrome_processes.canonicalizeName(helper.browserName),
+ globalDumps);
+ });
+ }
+
+ // 2. If any global memory dump does not have any associated browser
+ // process for some reason, associate it with an 'unknown_browser' browser
+ // so that we don't lose the data.
+
+ const unclassifiedGlobalDumps = skipDumpsThatDoNotIntersectRange(
+ model.globalMemoryDumps.filter(g => !globalDumpToBrowserHelper.has(g)),
+ opt_rangeOfInterest);
+ if (unclassifiedGlobalDumps.length > 0) {
+ makeKeyUniqueAndSet(
+ browserNameToGlobalDumps, 'unknown_browser', unclassifiedGlobalDumps);
+ }
+
+ return browserNameToGlobalDumps;
+ }
+
+ /**
+ * Function for adding entries with duplicate keys to a map without
+ * overriding existing entries.
+ *
+ * This is achieved by appending numeric indices (2, 3, 4, ...) to duplicate
+ * keys. Example:
+ *
+ * const map = new Map();
+ * // map = Map {}.
+ *
+ * makeKeyUniqueAndSet(map, 'key', 'a');
+ * // map = Map {"key" => "a"}.
+ *
+ * makeKeyUniqueAndSet(map, 'key', 'b');
+ * // map = Map {"key" => "a", "key2" => "b"}.
+ * ^^^^
+ * makeKeyUniqueAndSet(map, 'key', 'c');
+ * // map = Map {"key" => "a", "key2" => "b", "key3" => "c"}.
+ * ^^^^ ^^^^
+ */
+ function makeKeyUniqueAndSet(map, key, value) {
+ let uniqueKey = key;
+ let nextIndex = 2;
+ while (map.has(uniqueKey)) {
+ uniqueKey = key + nextIndex;
+ nextIndex++;
+ }
+ 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));
+ }
+
+ /**
+ * Returns true if |category| is one of the categories of |event|, and |event|
+ * has title |title|.
+ *
+ * TODO(dproy): Make this a method on a suitable parent class of the
+ * event/slice classes that are used with this function.
+ */
+ function hasCategoryAndName(event, category, title) {
+ return event.title === title && event.category &&
+ tr.b.getCategoryParts(event.category).includes(category);
+ }
+
+ return {
+ hasCategoryAndName,
+ filterExpectationsByRange,
+ perceptualBlend,
+ splitGlobalDumpsByBrowserName
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html
new file mode 100644
index 00000000000..f7bea627333
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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';
+
+ // TODO(alexandermont): Only iterate over the processes and threads that
+ // could contain these events.
+ for (const slice of model.getDescendantEvents()) {
+ if (!(slice instanceof tr.model.ThreadSlice)) continue;
+
+ // WebViewStartupInterval is the title of the section of code that is
+ // entered (via android.os.Trace.beginSection) when WebView is started
+ // up. This value is defined in TelemetryActivity.java.
+ if (slice.title === 'WebViewStartupInterval') {
+ startupWallHist.addSample(slice.duration);
+ startupCPUHist.addSample(slice.cpuDuration);
+ }
+
+ // WebViewBlankUrlLoadInterval is the title of the section of code
+ // that is entered (via android.os.Trace.beginSection) when WebView
+ // is started up. This value is defined in TelemetryActivity.java.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html
new file mode 100644
index 00000000000..9f3c15224d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/system_health/webview_startup_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ function makeTestModel() {
+ return tr.c.TestUtils.newModel(function(model) {
+ const mainThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'webview',
+ title: 'WebViewStartupInterval',
+ start: 200,
+ duration: 42.0,
+ cpuStart: 150,
+ cpuDuration: 32.0
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'webview',
+ title: 'WebViewBlankUrlLoadInterval',
+ start: 250,
+ duration: 27.0,
+ cpuStart: 190,
+ cpuDuration: 17.0
+ }));
+ });
+ }
+
+ test('webviewStartupMetric', function() {
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.sh.webviewStartupMetric(histograms, makeTestModel());
+ assert.closeTo(42, histograms.getHistogramNamed(
+ 'webview_startup_wall_time').average, 1e-2);
+ assert.closeTo(32, histograms.getHistogramNamed(
+ 'webview_startup_cpu_time').average, 1e-2);
+ assert.closeTo(27, histograms.getHistogramNamed(
+ 'webview_url_load_wall_time').average, 1e-2);
+ assert.closeTo(17, histograms.getHistogramNamed(
+ 'webview_url_load_cpu_time').average, 1e-2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html
new file mode 100644
index 00000000000..a4b456353c3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.tabs', function() {
+ function tabsMetric(histograms, model, opt_options) {
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ if (!chromeHelper) {
+ // Chrome isn't present.
+ return;
+ }
+
+ const tabSwitchRequestDelays = [];
+ const TAB_SWITCHING_REQUEST_TITLE = 'TabSwitchVisibilityRequest';
+ // initialization, *startTabSwitchVisibilityRequest stores the
+ // first legal TabSwitchVisibilityRequest's start time.
+ 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);
+ // find the first legal TabSwitchVisibilityRequest's start time
+ 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) {
+ // push legal tabSwitchLatency which only starts after the
+ // first legal TabSwitchVisibilityRequest's start time
+ 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}});
+ }
+
+ tr.metrics.MetricRegistry.register(tabsMetric, {
+ supportsRangeOfInterest: false,
+ });
+
+ return {
+ tabsMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html
new file mode 100644
index 00000000000..a6d3f6b9606
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/tabs_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('tabsMetric_tabSwitching', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserThread = setUpBrowserThread(model);
+ addTabSwitchingData(browserThread);
+
+ const rendererMainThread = setUpRendererMainThread(model);
+ addTabSwitchingData(rendererMainThread);
+
+ const group = browserThread.asyncSliceGroup;
+ group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency',
+ title: 'TabSwitchVisibilityRequest', start: 3, duration: 10}));
+ group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency',
+ title: 'TabSwitchVisibilityRequest', start: 15, duration: 6}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.tabs.tabsMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('tab_switching_latency');
+ assert.strictEqual(hist.max, 43.0);
+ assert.strictEqual(hist.min, 24.0);
+ assert.strictEqual(hist.average, 33.0);
+ assert.strictEqual(hist.numValues, 6);
+ });
+
+ test('tabsMetric_tabSwitching_legacy', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserThread = setUpBrowserThread(model);
+ addTabSwitchingData(browserThread, true);
+
+ const group = browserThread.asyncSliceGroup;
+ group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency',
+ title: 'TabSwitchVisibilityRequest', start: 3, duration: 10}));
+ group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency',
+ title: 'TabSwitchVisibilityRequest', start: 15, duration: 6}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.tabs.tabsMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('tab_switching_latency');
+ assert.strictEqual(hist.max, 37.0);
+ assert.strictEqual(hist.min, 14.0);
+ assert.strictEqual(hist.average, 25.0);
+ assert.strictEqual(hist.numValues, 3);
+ });
+
+ test('tabsMetric_tabSwitchRequestDelay', function() {
+ const model = tr.c.TestUtils.newModel((model) => {
+ const browserThread = setUpBrowserThread(model);
+ const group = browserThread.asyncSliceGroup;
+ group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency',
+ title: 'TabSwitchVisibilityRequest', start: 0, duration: 10}));
+ group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency',
+ title: 'TabSwitchVisibilityRequest', start: 15, duration: 6}));
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.tabs.tabsMetric(histograms, model);
+ const hist = histograms.getHistogramNamed('tab_switching_request_delay');
+ assert.strictEqual(hist.average, 8);
+ });
+
+ function setUpBrowserThread(model) {
+ const BROWSER_PROCESS_ID = 1234;
+ const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID);
+ const browserThread = browserProcess.getOrCreateThread(2);
+ browserThread.name = 'CrBrowserMain';
+ return browserThread;
+ }
+
+ function setUpRendererMainThread(model) {
+ const RENDERER_PROCESS_ID = 2345;
+ const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID);
+ const mainThread = rendererProcess.getOrCreateThread(23);
+ mainThread.name = 'CrRendererMain';
+ return mainThread;
+ }
+
+ function addTabSwitchingData(thread, legacy) {
+ const group = thread.asyncSliceGroup;
+ group.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'latency',
+ title: 'TabSwitching::Latency',
+ start: 2,
+ duration: 24,
+ args: legacy ? {} : {latency: 24 },
+ }));
+ group.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'latency',
+ title: 'TabSwitching::Latency',
+ start: 4,
+ duration: 24,
+ args: legacy ? {} : {latency: 24 },
+ }));
+ group.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'latency',
+ title: 'TabSwitching::Latency',
+ start: 5,
+ duration: 14,
+ args: legacy ? {} : {latency: 32 },
+ }));
+ group.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'latency',
+ title: 'TabSwitching::Latency',
+ start: 6,
+ duration: 37,
+ args: legacy ? {} : {latency: 43 },
+ }));
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html
new file mode 100644
index 00000000000..332a2b1cd04
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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);
+
+ // By default, we store a single value, so we only need one of the
+ // statistics to keep track. We choose the average for that.
+ const SUMMARY_OPTIONS = tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS;
+
+ // Adds histograms specific to memory-infra dumps.
+ 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++;
+ }
+ }
+ }
+ }
+
+ histograms.createHistogram('memory_dump_cpu_overhead',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ totalOverhead / memoryDumpCount, {
+ binBoundaries: TIME_BOUNDARIES,
+ description:
+ 'Average CPU overhead on all threads per memory-infra dump',
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+
+ histograms.createHistogram('nonmemory_thread_memory_dump_cpu_overhead',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ nonMemoryInfraThreadOverhead / memoryDumpCount, {
+ binBoundaries: TIME_BOUNDARIES,
+ description: 'Average CPU overhead on non-memory-infra threads ' +
+ 'per memory-infra dump',
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+
+ for (const [providerName, overhead] of Object.entries(overheadByProvider)) {
+ histograms.createHistogram(`${providerName}_memory_dump_cpu_overhead`,
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ overhead.duration / overhead.count, {
+ binBoundaries: TIME_BOUNDARIES,
+ description:
+ `Average CPU overhead of ${providerName} per OnMemoryDump call`,
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ }
+
+ const memoryInfraEventsSize =
+ categoryNamesToTotalEventSizes.get(MEMORY_INFRA_TRACING_CATEGORY);
+ const memoryInfraTraceBytesValue = new tr.v.Histogram(
+ 'total_memory_dump_size',
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, BYTE_BOUNDARIES);
+ memoryInfraTraceBytesValue.description =
+ 'Total trace size of memory-infra dumps in bytes';
+ memoryInfraTraceBytesValue.customizeSummaryOptions(SUMMARY_OPTIONS);
+ memoryInfraTraceBytesValue.addSample(memoryInfraEventsSize);
+ histograms.addHistogram(memoryInfraTraceBytesValue);
+
+ const traceBytesPerDumpValue = new tr.v.Histogram(
+ 'memory_dump_size',
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, BYTE_BOUNDARIES);
+ traceBytesPerDumpValue.description =
+ 'Average trace size of memory-infra dumps in bytes';
+ traceBytesPerDumpValue.customizeSummaryOptions(SUMMARY_OPTIONS);
+ traceBytesPerDumpValue.addSample(memoryInfraEventsSize / memoryDumpCount);
+ histograms.addHistogram(traceBytesPerDumpValue);
+ }
+
+ // TODO(charliea): The metrics in this file should be renamed to have names
+ // more consistent with those in the rest of the codebase
+ // (e.g. 'trace_size_growth_per_second', not 'Max event size in bytes per
+ // second').
+ // https://github.com/catapult-project/catapult/issues/3233
+ function tracingMetric(histograms, model) {
+ if (!model.stats.hasEventSizesinBytes) return;
+
+ const eventStats = model.stats.allTraceEventStatsInTimeIntervals;
+ eventStats.sort((a, b) => a.timeInterval - b.timeInterval);
+
+ const totalTraceBytes = eventStats.reduce(
+ (a, b) => a + b.totalEventSizeinBytes, 0);
+
+ // We maintain a sliding window of records [start ... end-1] where end
+ // increments each time through the loop, and we move start just far enough
+ // to keep the window less than 1 second wide. Note that we need to compute
+ // the number of time intervals (i.e. units that timeInterval is given in)
+ // in one second to know how wide the sliding window should be.
+ let maxEventCountPerSec = 0;
+ let maxEventBytesPerSec = 0;
+ const INTERVALS_PER_SEC = Math.floor(
+ 1000 / model.stats.TIME_INTERVAL_SIZE_IN_MS);
+
+ let runningEventNumPerSec = 0;
+ let runningEventBytesPerSec = 0;
+ let start = 0;
+ let end = 0;
+
+ while (end < eventStats.length) {
+ // Slide the end marker forward. Moving the end marker from N
+ // to N+1 adds eventStats[N] to the sliding window.
+ runningEventNumPerSec += eventStats[end].numEvents;
+ runningEventBytesPerSec += eventStats[end].totalEventSizeinBytes;
+ end++;
+
+ // Slide the start marker forward so that the time interval covered
+ // by the window is less than 1 second wide.
+ while ((eventStats[end - 1].timeInterval -
+ eventStats[start].timeInterval) >= INTERVALS_PER_SEC) {
+ runningEventNumPerSec -= eventStats[start].numEvents;
+ runningEventBytesPerSec -= eventStats[start].totalEventSizeinBytes;
+ start++;
+ }
+
+ // Update maximum values.
+ 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()));
+
+ // Determine the category with the highest total event size.
+ 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,
+ // For testing only:
+ MEMORY_INFRA_TRACING_CATEGORY,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html
new file mode 100644
index 00000000000..5fbffe98f94
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/tracing_metric.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function makeModel(events, opt_track) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ trackDetailedModelStats: opt_track
+ });
+ }
+
+ function getEventStringSize(events, indices) {
+ return indices.reduce(function(sum, index) {
+ return sum + JSON.stringify(events[index]).length;
+ }, 0);
+ }
+
+ function checkDurationHistogram(histograms, metricName, expected) {
+ const histogram = histograms.getHistogramNamed(metricName);
+ assert.closeTo(1000 * histogram.average, expected, 0.1);
+ }
+
+ test('tracingMetric_hasEventSizesInBytes', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}
+ ];
+
+ const model = makeModel(JSON.stringify(events), true);
+ assert.isTrue(model.importOptions.trackDetailedModelStats);
+ tr.metrics.tracingMetric(histograms, model);
+ });
+
+ test('tracingMetric_totalTraceSize', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}
+ ];
+ const model = makeModel(JSON.stringify(events), true);
+ tr.metrics.tracingMetric(histograms, model);
+
+ const eventStringSize = getEventStringSize(events, [0, 1]);
+ const histogram = histograms.getHistogramNamed('trace_size');
+ assert.strictEqual(histogram.average, eventStringSize);
+ });
+
+ test('tracingMetric_maxValuePerSec', function() {
+ const ONE_SEC_IN_US = 1000000;
+ const events = [
+ {name: 'a', pid: 52, ts: 1, cat: 'foo', ph: 'B'},
+ {name: 'a', pid: 52, ts: ONE_SEC_IN_US + 1, cat: 'foo', ph: 'B'},
+ {name: 'a', pid: 52, ts: 2 * ONE_SEC_IN_US + 1, cat: 'foo', ph: 'B'},
+ {name: 'a', pid: 52, ts: 2 * ONE_SEC_IN_US + 3, cat: 'foo', ph: 'B'},
+ {name: 'a', pid: 52, ts: ONE_SEC_IN_US + 2, cat: 'foo', ph: 'B'},
+ {name: 'a', pid: 52, ts: 2 * ONE_SEC_IN_US + 2, cat: 'foo', ph: 'B'}
+ ];
+ const model = makeModel(JSON.stringify(events), true);
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.tracingMetric(histograms, model);
+
+ const maxEventCountPerSec = 3;
+ let histogram = histograms.getHistogramNamed('peak_event_rate');
+ assert.strictEqual(histogram.average, maxEventCountPerSec);
+
+ const maxEventBytesPerSec = getEventStringSize(events, [2, 3, 5]);
+ histogram = histograms.getHistogramNamed('peak_event_size_rate');
+ assert.strictEqual(histogram.average, maxEventBytesPerSec);
+ });
+
+ test('tracingMetric_diagnostics', function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 535, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'bb', args: {}, pid: 52, ts: 546, cat: 'bar', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'bb', args: {}, pid: 52, ts: 578, cat: 'bar', tid: 53, ph: 'E'}
+ ];
+ const model = makeModel(JSON.stringify(events), true);
+ tr.metrics.tracingMetric(histograms, model);
+
+ const DIAGNOSTIC_HISTOGRAMS = [
+ 'Max number of events per second',
+ 'Max event size in bytes per second',
+ 'Total trace size in bytes'
+ ];
+ for (const histogram of histograms) {
+ if (!DIAGNOSTIC_HISTOGRAMS.includes(histogram.name)) continue;
+
+ const d = histogram.diagnostics.get('category_with_max_event_size').value;
+ assert.strictEqual(d.name, 'foo');
+ assert.strictEqual(d.size_in_bytes, getEventStringSize(
+ events, [0, 1, 3]));
+ }
+ });
+
+ test('tracingMetric_memoryInfraTracingMetrics', function() {
+ const MEMORY_INFRA_TRACING_CATEGORY =
+ tr.metrics.MEMORY_INFRA_TRACING_CATEGORY;
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {name: 'OnMemoryDump', pid: 1, ts: 510, tid: 1, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp1'}, dur: 4}, // @suppress longLineCheck
+ {name: 'OnMemoryDump', pid: 1, ts: 520, tid: 7, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp2'}, dur: 15}, // @suppress longLineCheck
+ {name: 'OnMemoryDump', pid: 1, ts: 530, tid: 7, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp3'}, dur: 5}, // @suppress longLineCheck
+ {name: 'OnMemoryDump', pid: 2, ts: 510, tid: 2, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp1'}, dur: 9}, // @suppress longLineCheck
+ {name: 'OnMemoryDump', pid: 2, ts: 520, tid: 8, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp2'}, dur: 17}, // @suppress longLineCheck
+ {name: 'OnMemoryDump', pid: 2, ts: 530, tid: 8, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp3'}, dur: 7}, // @suppress longLineCheck
+ {name: 'OnMemoryDump', pid: 2, ts: 540, tid: 3, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp4'}, dur: 8}, // @suppress longLineCheck
+ {name: 'ProcessDumps', pid: 1, ts: 550, tid: 1, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {guid: 4}, dur: 8}, // @suppress longLineCheck
+ {name: 'ProcessDumps', pid: 2, ts: 540, tid: 2, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {guid: 4}, dur: 18}, // @suppress longLineCheck
+ {name: 'thread_name', pid: 1, ts: 0, tid: 1, ph: 'M', cat: '__metadata', args: {name: 'CrBrowsermain'}}, // @suppress longLineCheck
+ {name: 'thread_name', pid: 1, ts: 0, tid: 7, ph: 'M', cat: '__metadata', args: {name: 'MemoryInfra'}}, // @suppress longLineCheck
+ {name: 'thread_name', pid: 2, ts: 0, tid: 2, ph: 'M', cat: '__metadata', args: {name: 'CrRendererMain'}}, // @suppress longLineCheck
+ {name: 'thread_name', pid: 2, ts: 0, tid: 8, ph: 'M', cat: '__metadata', args: {name: 'MemoryInfra'}}, // @suppress longLineCheck
+ {name: 'thread_name', pid: 2, ts: 0, tid: 3, ph: 'M', cat: '__metadata', args: {name: 'Compositor'}} // @suppress longLineCheck
+ ];
+
+ const model = makeModel(JSON.stringify(events), true);
+ tr.model.MemoryDumpTestUtils.addGlobalMemoryDump(model, {ts: 550});
+ tr.metrics.tracingMetric(histograms, model);
+
+ const memoryCategorySize = events.filter(
+ slice => slice.cat === MEMORY_INFRA_TRACING_CATEGORY).reduce(
+ (acc, slice) => acc + JSON.stringify(slice).length, 0);
+ const totalSizeHistogram = histograms.getHistogramNamed(
+ 'total_memory_dump_size');
+ assert.strictEqual(totalSizeHistogram.average, memoryCategorySize);
+ const sizePerDumpHistogram = histograms.getHistogramNamed(
+ 'memory_dump_size');
+ assert.strictEqual(sizePerDumpHistogram.average, memoryCategorySize);
+
+ checkDurationHistogram(histograms, 'mdp1_memory_dump_cpu_overhead', 6.5);
+ checkDurationHistogram(histograms, 'mdp2_memory_dump_cpu_overhead', 16);
+ checkDurationHistogram(histograms, 'mdp3_memory_dump_cpu_overhead', 6);
+ checkDurationHistogram(histograms, 'mdp4_memory_dump_cpu_overhead', 8);
+ checkDurationHistogram(
+ histograms, 'nonmemory_thread_memory_dump_cpu_overhead', 47);
+ checkDurationHistogram(histograms, 'memory_dump_cpu_overhead', 91);
+ });
+
+ test('tracingMetric_traceImportDurationMetricWithoutTrackDetailedModelStats',
+ function() {
+ const histograms = new tr.v.HistogramSet();
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}
+ ];
+
+ const model = makeModel(JSON.stringify(events), false);
+ assert.isFalse(model.importOptions.trackDetailedModelStats);
+ tr.metrics.tracingMetric(histograms, model);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html
new file mode 100644
index 00000000000..87992515679
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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);
+ }
+
+ histograms.addHistogram(cpuSelfParseLazy);
+ histograms.addHistogram(wallSelfParseLazy);
+ }
+
+ function computeCompileFullCodeMetrics(histograms, model) {
+ const cpuSelfCompileFullCode = new tr.v.Histogram(
+ 'v8_compile_full_code_cpu_self',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuSelfCompileFullCode.description =
+ 'cpu self time spent performing compiling full code';
+ const wallSelfCompileFullCode = new tr.v.Histogram(
+ 'v8_compile_full_code_wall_self',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallSelfCompileFullCode.description =
+ 'wall self time spent performing compiling full code';
+
+ for (const e of model.findTopmostSlicesNamed('V8.CompileFullCode')) {
+ cpuSelfCompileFullCode.addSample(e.cpuSelfTime);
+ wallSelfCompileFullCode.addSample(e.selfTime);
+ }
+
+ histograms.addHistogram(cpuSelfCompileFullCode);
+ histograms.addHistogram(wallSelfCompileFullCode);
+ }
+
+ function computeCompileIgnitionMetrics(histograms, model) {
+ const cpuSelfCompileIgnition = new tr.v.Histogram(
+ 'v8_compile_ignition_cpu_self',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuSelfCompileIgnition.description =
+ 'cpu self time spent in compile ignition';
+ const wallSelfCompileIgnition = new tr.v.Histogram(
+ 'v8_compile_ignition_wall_self',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallSelfCompileIgnition.description =
+ 'wall self time spent in compile ignition';
+
+ for (const e of model.findTopmostSlicesNamed('V8.CompileIgnition')) {
+ cpuSelfCompileIgnition.addSample(e.cpuSelfTime);
+ wallSelfCompileIgnition.addSample(e.selfTime);
+ }
+
+ histograms.addHistogram(cpuSelfCompileIgnition);
+ histograms.addHistogram(wallSelfCompileIgnition);
+ }
+
+ function computeRecompileMetrics(histograms, model) {
+ const cpuTotalRecompileSynchronous = new tr.v.Histogram(
+ 'v8_recompile_synchronous_cpu_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuTotalRecompileSynchronous.description =
+ 'cpu total time spent in synchronous recompilation';
+ const wallTotalRecompileSynchronous = new tr.v.Histogram(
+ 'v8_recompile_synchronous_wall_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallTotalRecompileSynchronous.description =
+ 'wall total time spent in synchronous recompilation';
+ const cpuTotalRecompileConcurrent = new tr.v.Histogram(
+ 'v8_recompile_concurrent_cpu_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuTotalRecompileConcurrent.description =
+ 'cpu total time spent in concurrent recompilation';
+ const wallTotalRecompileConcurrent = new tr.v.Histogram(
+ 'v8_recompile_concurrent_wall_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallTotalRecompileConcurrent.description =
+ 'wall total time spent in concurrent recompilation';
+ // TODO(eakuefner): Stop computing overall histograms once dash v2 is ready.
+ // https://github.com/catapult-project/catapult/issues/2180
+ const cpuTotalRecompileOverall = new tr.v.Histogram(
+ 'v8_recompile_overall_cpu_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuTotalRecompileOverall.description =
+ 'cpu total time spent in synchronous or concurrent recompilation';
+ const wallTotalRecompileOverall = new tr.v.Histogram(
+ 'v8_recompile_overall_wall_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallTotalRecompileOverall.description =
+ 'wall total time spent in synchronous or concurrent recompilation';
+
+ for (const e of model.findTopmostSlicesNamed('V8.RecompileSynchronous')) {
+ cpuTotalRecompileSynchronous.addSample(e.cpuDuration);
+ wallTotalRecompileSynchronous.addSample(e.duration);
+ cpuTotalRecompileOverall.addSample(e.cpuDuration);
+ wallTotalRecompileOverall.addSample(e.duration);
+ }
+
+ histograms.addHistogram(cpuTotalRecompileSynchronous);
+ histograms.addHistogram(wallTotalRecompileSynchronous);
+
+ for (const e of model.findTopmostSlicesNamed('V8.RecompileConcurrent')) {
+ cpuTotalRecompileConcurrent.addSample(e.cpuDuration);
+ wallTotalRecompileConcurrent.addSample(e.duration);
+ cpuTotalRecompileOverall.addSample(e.cpuDuration);
+ wallTotalRecompileOverall.addSample(e.duration);
+ }
+
+ histograms.addHistogram(cpuTotalRecompileConcurrent);
+ histograms.addHistogram(wallTotalRecompileConcurrent);
+ histograms.addHistogram(cpuTotalRecompileOverall);
+ histograms.addHistogram(wallTotalRecompileOverall);
+ }
+
+ function computeOptimizeCodeMetrics(histograms, model) {
+ const cpuTotalOptimizeCode = new tr.v.Histogram(
+ 'v8_optimize_code_cpu_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuTotalOptimizeCode.description =
+ 'cpu total time spent in code optimization';
+ const wallTotalOptimizeCode = new tr.v.Histogram(
+ 'v8_optimize_code_wall_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallTotalOptimizeCode.description =
+ 'wall total time spent in code optimization';
+
+ for (const e of model.findTopmostSlicesNamed('V8.OptimizeCode')) {
+ cpuTotalOptimizeCode.addSample(e.cpuDuration);
+ wallTotalOptimizeCode.addSample(e.duration);
+ }
+
+ histograms.addHistogram(cpuTotalOptimizeCode);
+ histograms.addHistogram(wallTotalOptimizeCode);
+ }
+
+ function computeDeoptimizeCodeMetrics(histograms, model) {
+ const cpuTotalDeoptimizeCode = new tr.v.Histogram(
+ 'v8_deoptimize_code_cpu_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ cpuTotalDeoptimizeCode.description =
+ 'cpu total time spent in code deoptimization';
+ const wallTotalDeoptimizeCode = new tr.v.Histogram(
+ 'v8_deoptimize_code_wall_total',
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES);
+ wallTotalDeoptimizeCode.description =
+ 'wall total time spent in code deoptimization';
+
+ for (const e of model.findTopmostSlicesNamed('V8.DeoptimizeCode')) {
+ cpuTotalDeoptimizeCode.addSample(e.cpuDuration);
+ wallTotalDeoptimizeCode.addSample(e.duration);
+ }
+
+ histograms.addHistogram(cpuTotalDeoptimizeCode);
+ histograms.addHistogram(wallTotalDeoptimizeCode);
+ }
+
+ function executionMetric(histograms, model) {
+ computeExecuteMetrics(histograms, model);
+ computeParseLazyMetrics(histograms, model);
+ computeCompileIgnitionMetrics(histograms, model);
+ computeCompileFullCodeMetrics(histograms, model);
+ computeRecompileMetrics(histograms, model);
+ computeOptimizeCodeMetrics(histograms, model);
+ computeDeoptimizeCodeMetrics(histograms, model);
+ }
+
+ tr.metrics.MetricRegistry.register(executionMetric);
+
+ return {
+ executionMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html
new file mode 100644
index 00000000000..d42c5476706
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/v8/execution_metric.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('executionMetricBasic', function() {
+ const model = tr.c.TestUtils.newModel();
+ const histograms = new tr.v.HistogramSet();
+
+ tr.metrics.v8.executionMetric(histograms, model);
+
+ [
+ 'v8_execution_cpu_total',
+ 'v8_execution_wall_total',
+ 'v8_execution_cpu_self',
+ 'v8_execution_wall_self',
+ 'v8_parse_lazy_cpu_self',
+ 'v8_parse_lazy_wall_self',
+ 'v8_compile_full_code_cpu_self',
+ 'v8_compile_full_code_wall_self',
+ 'v8_compile_ignition_cpu_self',
+ 'v8_compile_ignition_wall_self',
+ 'v8_recompile_synchronous_cpu_total',
+ 'v8_recompile_synchronous_wall_total',
+ 'v8_recompile_concurrent_cpu_total',
+ 'v8_recompile_concurrent_wall_total',
+ 'v8_recompile_overall_cpu_total',
+ 'v8_recompile_overall_wall_total',
+ 'v8_optimize_code_cpu_total',
+ 'v8_optimize_code_wall_total',
+ 'v8_deoptimize_code_cpu_total',
+ 'v8_deoptimize_code_wall_total',
+ ].forEach(function(name) {
+ assert.isDefined(histograms.getHistogramNamed(name));
+ });
+ });
+
+ test('noDoubleCounting', function() {
+ const events = [
+ {name: 'V8.Execute', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 53,
+ ph: 'B'},
+ {name: 'V8.Execute', args: {}, pid: 52, ts: 100, cat: 'foo', tid: 53,
+ ph: 'E'},
+ {name: 'V8.Execute', args: {}, pid: 52, ts: 20, cat: 'foo', tid: 53,
+ ph: 'B'},
+ {name: 'V8.Execute', args: {}, pid: 52, ts: 40, cat: 'foo', tid: 53,
+ ph: 'E'}
+ ];
+
+ const model = tr.c.TestUtils.newModelWithEvents(JSON.stringify(events), {
+ shiftWorldToZero: false
+ });
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.v8.executionMetric(histograms, model);
+
+ const value = histograms.getHistogramNamed('v8_execution_wall_total');
+ assert.closeTo(value.running.sum, 0.1, 1e-5);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html
new file mode 100644
index 00000000000..c3d90cc2786
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html
@@ -0,0 +1,297 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/v8/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.v8', function() {
+ // The time window size for mutator utilization computation.
+ // It is equal to the duration of one frame corresponding to 60 FPS rendering.
+ const TARGET_FPS = 60;
+ const MS_PER_SECOND = 1000;
+ const WINDOW_SIZE_MS = MS_PER_SECOND / TARGET_FPS;
+
+ function gcMetric(histograms, model) {
+ addDurationOfTopEvents(histograms, model);
+ addTotalDurationOfTopEvents(histograms, model);
+ addDurationOfSubEvents(histograms, model);
+ addPercentageInV8ExecuteOfTopEvents(histograms, model);
+ addTotalPercentageInV8Execute(histograms, model);
+ addMarkCompactorMutatorUtilization(histograms, model);
+ addTotalMarkCompactorTime(histograms, model);
+ addTotalMarkCompactorMarkingTime(histograms, model);
+ }
+
+ tr.metrics.MetricRegistry.register(gcMetric);
+
+ const timeDurationInMs_smallerIsBetter =
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;
+ const percentage_biggerIsBetter =
+ tr.b.Unit.byName.normalizedPercentage_biggerIsBetter;
+ const percentage_smallerIsBetter =
+ tr.b.Unit.byName.normalizedPercentage_smallerIsBetter;
+
+ // 0.1 steps from 0 to 20 since it is the most common range.
+ // Exponentially increasing steps from 20 to 200.
+ 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 createNumericForSubEventTime(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: false,
+ 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, unit) {
+ const hist = new tr.v.Histogram(name, unit);
+ if (denominator === 0) {
+ hist.addSample(0);
+ } else {
+ hist.addSample(numerator / denominator);
+ }
+ hist.customizeSummaryOptions({
+ avg: true,
+ count: false,
+ max: false,
+ min: false,
+ std: false,
+ sum: false,
+ percentile: []
+ });
+ return hist;
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-full-mark-compactor.
+ */
+ function addDurationOfTopEvents(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent,
+ tr.metrics.v8.utils.topGarbageCollectionEventName,
+ function(name, events) {
+ const cpuDuration = createNumericForTopEventTime(name);
+ events.forEach(function(event) {
+ cpuDuration.addSample(event.cpuDuration);
+ });
+ histograms.addHistogram(cpuDuration);
+ }
+ );
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-total
+ */
+ function addTotalDurationOfTopEvents(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent,
+ event => 'v8-gc-total',
+ function(name, events) {
+ const cpuDuration = createNumericForTopEventTime(name);
+ events.forEach(function(event) {
+ cpuDuration.addSample(event.cpuDuration);
+ });
+ histograms.addHistogram(cpuDuration);
+ }
+ );
+ }
+
+ function isV8MarkCompactorSummary(event) {
+ return !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event) &&
+ tr.metrics.v8.utils.isMarkCompactorSummaryEvent(event);
+ }
+
+ function isV8MarkCompactorMarkingSummary(event) {
+ return !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event) &&
+ tr.metrics.v8.utils.isMarkCompactorMarkingSummaryEvent(event);
+ }
+
+ function createHistogramFromSummary(histograms, name, events) {
+ const foregroundDuration =
+ createNumericForTopEventTime(name + '-foreground');
+ const backgroundDuration =
+ createNumericForTopEventTime(name + '-background');
+ const totalDuration =
+ createNumericForTopEventTime(name + '-total');
+ const relatedNames = new tr.v.d.RelatedNameMap();
+ relatedNames.set('foreground', foregroundDuration.name);
+ relatedNames.set('background', backgroundDuration.name);
+ for (const event of events) {
+ foregroundDuration.addSample(event.args.duration);
+ backgroundDuration.addSample(event.args.background_duration);
+ const breakdownForTotal = new tr.v.d.Breakdown();
+ breakdownForTotal.set('foreground', event.args.duration);
+ breakdownForTotal.set('background', event.args.background_duration);
+ totalDuration.addSample(
+ event.args.duration + event.args.background_duration,
+ {breakdown: breakdownForTotal});
+ }
+ histograms.addHistogram(foregroundDuration);
+ histograms.addHistogram(backgroundDuration);
+ histograms.addHistogram(totalDuration, {breakdown: relatedNames});
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-mark-compactor-foreground
+ * - v8-gc-mark-compactor-background
+ * - v8-gc-mark-compactor-total
+ */
+ function addTotalMarkCompactorTime(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ isV8MarkCompactorSummary,
+ event => 'v8-gc-mark-compactor',
+ (name, events) => createHistogramFromSummary(histograms, name, events)
+ );
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-mark-compactor-marking-foreground
+ * - v8-gc-mark-compactor-marking-background
+ * - v8-gc-mark-compactor-marking-total
+ */
+ function addTotalMarkCompactorMarkingTime(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ isV8MarkCompactorMarkingSummary,
+ event => 'v8-gc-mark-compactor-marking',
+ (name, events) => createHistogramFromSummary(histograms, name, events)
+ );
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-full-mark-compactor-evacuate.
+ */
+ function addDurationOfSubEvents(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ tr.metrics.v8.utils.isNotForcedSubGarbageCollectionEvent,
+ tr.metrics.v8.utils.subGarbageCollectionEventName,
+ function(name, events) {
+ const cpuDuration = createNumericForSubEventTime(name);
+ events.forEach(function(event) {
+ cpuDuration.addSample(event.cpuDuration);
+ });
+ histograms.addHistogram(cpuDuration);
+ }
+ );
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-full-mark-compactor_percentage_in_v8_execute.
+ */
+ function addPercentageInV8ExecuteOfTopEvents(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent,
+ tr.metrics.v8.utils.topGarbageCollectionEventName,
+ function(name, events) {
+ addPercentageInV8Execute(histograms, model, name, events);
+ }
+ );
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-total_percentage_in_v8_execute.
+ */
+ function addTotalPercentageInV8Execute(histograms, model) {
+ tr.metrics.v8.utils.groupAndProcessEvents(model,
+ tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent,
+ event => 'v8-gc-total',
+ function(name, events) {
+ addPercentageInV8Execute(histograms, model, name, events);
+ }
+ );
+ }
+
+ function addPercentageInV8Execute(histograms, model, name, events) {
+ let cpuDurationInV8Execute = 0;
+ let cpuDurationTotal = 0;
+ events.forEach(function(event) {
+ const v8Execute = tr.metrics.v8.utils.findParent(
+ event, tr.metrics.v8.utils.isV8ExecuteEvent);
+ if (v8Execute) {
+ cpuDurationInV8Execute += event.cpuDuration;
+ }
+ cpuDurationTotal += event.cpuDuration;
+ });
+ const percentage = createPercentage(
+ name + '_percentage_in_v8_execute', cpuDurationInV8Execute,
+ cpuDurationTotal, percentage_smallerIsBetter);
+ histograms.addHistogram(percentage);
+ }
+
+ /**
+ * Example output:
+ * - v8-gc-mark-compactor-mmu-100ms_window.
+ */
+ function addMarkCompactorMutatorUtilization(histograms, model) {
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ const rendererHelpers = Object.values(chromeHelper.rendererHelpers);
+ tr.metrics.v8.utils.addMutatorUtilization(
+ 'v8-gc-mark-compactor-mmu',
+ tr.metrics.v8.utils.isNotForcedMarkCompactorEvent,
+ [100],
+ rendererHelpers,
+ histograms);
+ }
+
+ return {
+ gcMetric,
+ WINDOW_SIZE_MS, // For testing purposes only.
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html
new file mode 100644
index 00000000000..8c38b563e62
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html
@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/v8/gc_metric.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel(start, end, slices) {
+ return tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ const group = mainThread.sliceGroup;
+ for (const slice of slices) {
+ group.pushSlice(tr.c.TestUtils.newSliceEx(slice));
+ }
+ group.createSubSlices();
+ mainThread.updateBounds();
+ });
+ }
+
+ function constructName(name, suffix) {
+ return name + '_' + suffix;
+ }
+
+ function run(slices) {
+ const histograms = new tr.v.HistogramSet();
+ const startTime = slices.reduce(
+ (acc, slice) => (Math.min(acc, slice.start)));
+ const endTime = slices.reduce((acc, slice) => (Math.max(acc, slice.end)));
+ const model = createModel(startTime - 1, endTime + 1, slices);
+ tr.metrics.v8.gcMetric(histograms, model);
+ return histograms;
+ }
+
+ test('topEvents', function() {
+ const events = {
+ 'V8.GCCompactor': 'v8-gc-full-mark-compactor',
+ 'V8.GCFinalizeMC': 'v8-gc-latency-mark-compactor',
+ 'V8.GCFinalizeMCReduceMemory': 'v8-gc-memory-mark-compactor',
+ 'V8.GCIncrementalMarking': 'v8-gc-incremental-step',
+ 'V8.GCIncrementalMarkingFinalize': 'v8-gc-incremental-finalize',
+ 'V8.GCIncrementalMarkingStart': 'v8-gc-incremental-start',
+ 'V8.GCPhantomHandleProcessingCallback': 'v8-gc-phantom-handle-callback',
+ 'V8.GCScavenger': 'v8-gc-scavenger'
+ };
+ for (const [timelineName, telemetryName] of Object.entries(events)) {
+ const slices = [
+ {
+ title: timelineName, args: {}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ }
+ ];
+ const actual = run(slices);
+
+ const value = actual.getHistogramNamed(telemetryName);
+ assert.strictEqual(value.running.sum, 100);
+ assert.strictEqual(value.numValues, 1);
+ assert.strictEqual(value.average, 100);
+ assert.strictEqual(value.running.max, 100);
+ assert.closeTo(value.getApproximatePercentile(0.90), 100, 1);
+ }
+ });
+
+ test('subEvents', function() {
+ const slices = [
+ {
+ title: 'V8.GCFinalizeMC', args: {}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ },
+ {
+ title: 'V8.GC_MC_MARK', args: {}, start: 110, end: 190,
+ cpuStart: 110, cpuEnd: 190
+ },
+ ];
+ const actual = run(slices);
+ const telemetryName = 'v8-gc-latency-mark-compactor-mark';
+ const value = actual.getHistogramNamed(telemetryName);
+ assert.strictEqual(value.average, 80);
+ assert.strictEqual(value.running.max, 80);
+ assert.closeTo(value.getApproximatePercentile(0.90), 80, 1);
+ });
+
+ test('total', function() {
+ const slices = [
+ {
+ title: 'V8.GCFinalizeMC', args: {}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ },
+ {
+ title: 'V8.GCIncrementalMarking', args: {}, start: 210, end: 290,
+ cpuStart: 210, cpuEnd: 290
+ }
+ ];
+ const actual = run(slices);
+
+ const value = actual.getHistogramNamed('v8-gc-total');
+ assert.strictEqual(value.running.sum, 180);
+ assert.strictEqual(value.numValues, 2);
+ assert.strictEqual(value.average, 90);
+ assert.strictEqual(value.running.max, 100);
+ });
+
+ test('percentageInV8Execute', function() {
+ const slices = [
+ {
+ title: 'V8.Execute',
+ args: {}, start: 100, end: 200,
+ cpuStart: 100, cpuEnd: 200
+ },
+ {
+ title: 'V8.GCFinalizeMC', args: {}, start: 110, end: 190,
+ cpuStart: 110, cpuEnd: 190
+ },
+ {
+ title: 'V8.GCFinalizeMC', args: {}, start: 210, end: 220,
+ cpuStart: 210, cpuEnd: 220
+ }
+ ];
+ const actual = run(slices);
+ const value = actual.getHistogramNamed(
+ 'v8-gc-latency-mark-compactor_percentage_in_v8_execute');
+ assert.strictEqual(value.average,
+ (190 - 110) / ((190 - 110) + (220 - 210)));
+ });
+
+ test('markCompactorMutatorUtilization', function() {
+ const slices = [
+ {
+ title: 'V8.GCIncrementalMarkingStart',
+ args: {}, start: 100, end: 110,
+ cpuStart: 100, cpuEnd: 110
+ },
+ {
+ title: 'V8.GCIncrementalMarking',
+ args: {}, start: 150, end: 160,
+ cpuStart: 150, cpuEnd: 160
+ },
+ {
+ title: 'V8.GCIncrementalMarkingFinalize',
+ args: {}, start: 200, end: 220,
+ cpuStart: 200, cpuEnd: 220
+ },
+ {
+ title: 'V8.GCFinalizeMC',
+ args: {}, start: 250, end: 300,
+ cpuStart: 250, cpuEnd: 300
+ }
+ ];
+ const histograms = run(slices);
+ const mmu = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-mmu-100ms_window');
+ assert.closeTo(0.3, mmu.min, 1e-3);
+ assert.strictEqual(mmu.summaryOptions.get('min'), true);
+ assert.strictEqual(mmu.summaryOptions.get('max'), false);
+ });
+
+ test('markCompactorSummary', function() {
+ const slices = [
+ {
+ title: 'V8.GCMarkCompactorSummary',
+ args: {'duration': 3.1, 'background_duration': 1.3},
+ start: 100, end: 100,
+ cpuStart: 100, cpuEnd: 100
+ },
+ ];
+ const histograms = run(slices);
+ const markCompactorForeground = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-foreground');
+ assert.closeTo(3.1, markCompactorForeground.sum, 1e-3);
+ const markCompactorBackground = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-background');
+ assert.closeTo(1.3, markCompactorBackground.sum, 1e-3);
+ const markCompactorTotal = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-total');
+ assert.closeTo(4.4, markCompactorTotal.sum, 1e-3);
+ });
+
+ test('markCompactorMarkingSummary', function() {
+ const slices = [
+ {
+ title: 'V8.GCMarkCompactorMarkingSummary',
+ args: {'duration': 4.1, 'background_duration': 1.4},
+ start: 100, end: 100,
+ cpuStart: 100, cpuEnd: 100
+ },
+ ];
+ const histograms = run(slices);
+ const markCompactorForeground = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-marking-foreground');
+ assert.closeTo(4.1, markCompactorForeground.sum, 1e-3);
+ const markCompactorBackground = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-marking-background');
+ assert.closeTo(1.4, markCompactorBackground.sum, 1e-3);
+ const markCompactorTotal = histograms.getHistogramNamed(
+ 'v8-gc-mark-compactor-marking-total');
+ assert.closeTo(5.5, markCompactorTotal.sum, 1e-3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html
new file mode 100644
index 00000000000..286cbaa32f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html
@@ -0,0 +1,374 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/diagnostics/related_name_map.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.v8', function() {
+ const COUNT_CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries
+ .createExponential(1, 1000000, 50);
+ const DURATION_CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries
+ .createExponential(0.1, 10000, 50);
+ const SUMMARY_OPTIONS = {
+ std: false,
+ count: false,
+ sum: false,
+ min: false,
+ max: false,
+ };
+
+ function computeDomContentLoadedTime_(model) {
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ let domContentLoadedTime = 0;
+
+ for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) {
+ for (const ev of rendererHelper.mainThread.sliceGroup.childEvents()) {
+ if (ev.title === 'domContentLoadedEventEnd' &&
+ ev.start > domContentLoadedTime) {
+ domContentLoadedTime = ev.start;
+ }
+ }
+ }
+ return domContentLoadedTime;
+ }
+
+ function computeInteractiveTime_(model) {
+ const chromeHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ let interactiveTime = 0;
+ for (const expectation of model.userModel.expectations) {
+ if (tr.e.chrome.CHROME_INTERNAL_URLS.includes(
+ expectation.url)) {
+ continue;
+ }
+ if (!(expectation instanceof tr.model.um.LoadExpectation)) continue;
+ if (expectation.timeToInteractive === undefined) continue;
+ // TODO(fmeawad): Support multiple navigations.
+ if (interactiveTime !== 0) throw new Error('Too many navigations');
+ interactiveTime = expectation.timeToInteractive;
+ }
+ return interactiveTime;
+ }
+
+ function convertMicroToMilli_(time) {
+ return tr.b.convertUnit(time,
+ tr.b.UnitPrefixScale.METRIC.MICRO, tr.b.UnitPrefixScale.METRIC.MILLI);
+ }
+
+ // TODO(crbug.com/688342): Remove this function when runtimeStatsMetric is
+ // removed.
+ function computeRuntimeStats(histograms, slices) {
+ const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection();
+ runtimeGroupCollection.addSlices(slices);
+
+ function addHistogramsForRuntimeGroup(runtimeGroup, optRelatedNameMaps) {
+ histograms.createHistogram(
+ `${runtimeGroup.name}:duration`,
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, {
+ value: convertMicroToMilli_(runtimeGroup.time),
+ diagnostics: optRelatedNameMaps ?
+ {samples: optRelatedNameMaps.durationBreakdown} : {}
+ }, {
+ binBoundaries: DURATION_CUSTOM_BOUNDARIES,
+ summaryOptions: SUMMARY_OPTIONS,
+ diagnostics: optRelatedNameMaps ?
+ {samples: optRelatedNameMaps.durationNames} : {}
+ });
+
+ histograms.createHistogram(
+ `${runtimeGroup.name}:count`,
+ tr.b.Unit.byName.count_smallerIsBetter, {
+ value: runtimeGroup.count,
+ diagnostics: optRelatedNameMaps ?
+ {samples: optRelatedNameMaps.countBreakdown} : {}
+ }, {
+ binBoundaries: COUNT_CUSTOM_BOUNDARIES,
+ summaryOptions: SUMMARY_OPTIONS,
+ diagnostics: optRelatedNameMaps ?
+ {samples: optRelatedNameMaps.countNames} : {}
+ });
+ }
+
+ function addDetailedHistogramsForRuntimeGroup(runtimeGroup) {
+ const durationNames = new tr.v.d.RelatedNameMap();
+ const durationBreakdown = new tr.v.d.Breakdown();
+ const countNames = new tr.v.d.RelatedNameMap();
+ const countBreakdown = new tr.v.d.Breakdown();
+
+ for (const entry of runtimeGroup.values) {
+ const durationSampleHistogram = histograms.createHistogram(
+ `${entry.name}:duration`,
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ convertMicroToMilli_(entry.time), {
+ binBoundaries: DURATION_CUSTOM_BOUNDARIES,
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ durationNames.set(entry.name, durationSampleHistogram.name);
+ durationBreakdown.set(entry.name, convertMicroToMilli_(entry.time));
+
+ const countSampleHistogram = histograms.createHistogram(
+ `${entry.name}:count`,
+ tr.b.Unit.byName.count_smallerIsBetter,
+ entry.count, {
+ binBoundaries: COUNT_CUSTOM_BOUNDARIES,
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ countNames.set(entry.name, countSampleHistogram.name);
+ countBreakdown.set(entry.name, entry.count);
+ }
+
+ addHistogramsForRuntimeGroup(runtimeGroup, {
+ durationNames,
+ durationBreakdown,
+ countNames,
+ countBreakdown
+ });
+ }
+
+ for (const runtimeGroup of runtimeGroupCollection.runtimeGroups) {
+ addHistogramsForRuntimeGroup(runtimeGroup);
+ }
+
+ const blinkGroupCollection = runtimeGroupCollection.blinkRCSGroupCollection;
+ if (blinkGroupCollection.totalTime > 0) {
+ blinkGroupCollection.runtimeGroups.forEach(
+ addDetailedHistogramsForRuntimeGroup);
+ }
+ }
+
+ // TODO(crbug.com/688342): Remove this metric and use runtimeStatsTotalMetric
+ // instead when the runtimeStatsTotalMetric is stable.
+ 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, 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();
+
+ // Compute runtimeStats in each of the UE buckets. Also record the
+ // histograms in RelatedNameMap. These histograms are added to the
+ // corresponding histograms in the total bucket as a diagnostic. This keeps
+ // the data grouped.
+ 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);
+ }
+ }
+
+ // Add V8 only time that is Total - Blink C++ duration.
+ addDurationHistogram(name, 'V8-Only', overallV8Time, histograms,
+ durationNamesByGroupName);
+ addCountHistogram(name, 'V8-Only', overallV8Count, histograms,
+ countNamesByGroupName);
+ }
+
+ // Add the runtimeStats for all the samples. Please note, the values in
+ // the UE buckets may not add upto the values computed here. Since UEs
+ // can overlap, we count some of the samples in multiple UE buckets.
+ 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));
+ }
+ }
+
+ // Add V8 only time that is Total - Blink C++ duration.
+ 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();
+ // User expectations can sometime overlap. So, certain v8 slices can be
+ // included in more than one expectation. We count such slices in each
+ // of the expectations. This is done so as to minimize the noise due to
+ // the differences in the extent of overlap between the runs.
+ 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;
+ // filterArray filters the array that intersects the range inclusively.
+ // Expectations are not inclusive i.e. expectations are like [0, 1),
+ // [1, 2). v8ThreadSlices that start at 1 should be counted only in [1,2)
+ // bucket. Filter out sample at the boundary so that they are not counted
+ // twice.
+ 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);
+ }
+ }
+
+ // Compute runtimeStats in each of the UE buckets and also compute
+ // runtimeStats on all of the samples. The values in UE buckets do not add
+ // up to the total of all samples, since we duplicate some of the samples in
+ // multiple buckets when the UEs overlap.
+ computeRuntimeStatsBucketOnUE(histograms, v8ThreadSlices,
+ v8SlicesBucketOnUEMap);
+ }
+
+ tr.metrics.MetricRegistry.register(runtimeStatsTotalMetric);
+ tr.metrics.MetricRegistry.register(runtimeStatsMetric);
+
+ return {
+ runtimeStatsMetric,
+ runtimeStatsTotalMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html
new file mode 100644
index 00000000000..91bc8b7a10e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html
@@ -0,0 +1,718 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import"
+ href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/metrics/v8/runtime_stats_metric.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function checkRuntimeHistogram_(histograms, name, count, duration,
+ breakdownHistograms) {
+ const countHistogram = histograms.getHistogramNamed(`${name}:count`);
+ assert.strictEqual(tr.b.getOnlyElement(countHistogram.sampleValues), count);
+ const durationHistogram = histograms.getHistogramNamed(`${name}:duration`);
+ assert.strictEqual(
+ tr.b.getOnlyElement(durationHistogram.sampleValues), duration);
+
+ if (breakdownHistograms === undefined) return;
+ const countBin = tr.b.getOnlyElement(countHistogram.allBins.filter(
+ bin => bin.diagnosticMaps.length > 0));
+ const durationBin = tr.b.getOnlyElement(durationHistogram.allBins.filter(
+ bin => bin.diagnosticMaps.length > 0));
+ for (const name of breakdownHistograms) {
+ assert.notEqual(tr.b.getOnlyElement(countBin.diagnosticMaps)
+ .get('samples').get(name + ':count'), undefined);
+ assert.notEqual(tr.b.getOnlyElement(durationBin.diagnosticMaps)
+ .get('samples').get(name + ':duration'), undefined);
+ }
+ }
+
+ test('runtimeStatsMetricUsingTTI', function() {
+ // The renderer thread timeline looks like:
+ //
+ // * [V8.NewInstance] * [BlinkRuntimeCallStats,V8.Execute] * ...[V8.Ignored]
+ // | | |
+ // | | |
+ // v v v
+ // First navigation FMP TTI
+ // 200 9200 15400
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 200,
+ duration: 5.0
+ }));
+ rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300,
+ {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 9200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 10000,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.newInstance',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 12555,
+ duration: 990,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 9180,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 9200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 9200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 9350,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 11150,
+ duration: 100,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 12550,
+ duration: 1000,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-v8.runtime_stats',
+ title: 'runtime-call-stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 13550,
+ duration: 1000,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [15, 60],
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 13555,
+ duration: 990,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ StoreIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99],
+ ParseBackgroundFunctionLiteral: [2, 22],
+ CompileBackgroundIgnition: [3, 33]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 14950,
+ duration: 500,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 22150,
+ duration: 10,
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Ignored',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 30000,
+ duration: 1000,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99],
+ ParseBackgroundFunctionLiteral: [2, 22],
+ CompileBackgroundIgnition: [3, 33]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-v8.runtime_stats',
+ title: 'V8.Ignored',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 30000,
+ duration: 1000,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [15, 60],
+ }
+ }
+ }));
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.v8.runtimeStatsMetric(histograms, model);
+ assert.strictEqual(histograms.length, 54);
+ assert.strictEqual(histograms.sourceHistograms.length, 52);
+
+ // A few of the top level ones.
+ checkRuntimeHistogram_(histograms, 'IC', 8, 0.088);
+ checkRuntimeHistogram_(histograms, 'API', 18, 0.198);
+ checkRuntimeHistogram_(histograms, 'Compile', 6, 0.066);
+ checkRuntimeHistogram_(histograms, 'Total', 95, 1.045);
+ // Lower level one for Blink.
+ checkRuntimeHistogram_(histograms, 'Blink_UpdateLayout', 15, 0.06);
+ });
+
+ test('runtimeStatsMetricUsingDomContentLoaded', function() {
+ // The renderer thread timeline looks like:
+ //
+ // * [V8.NewInstance] * [ V8.Execute ] * [V8.Ignored]
+ // | | |
+ // | | |
+ // v v v
+ // First navigation DCL DCL
+ // 200 1300 2400
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const rendererProcess = model.rendererProcess;
+ const mainThread = model.rendererMain;
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.newInstance',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 300,
+ duration: 990,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 1300,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 1400,
+ duration: 990,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ StoreIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 2400,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Ignored',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 2450,
+ duration: 1000,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99]
+ }
+ }
+ }));
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.v8.runtimeStatsMetric(histograms, model);
+ assert.strictEqual(histograms.length, 38);
+ assert.strictEqual(histograms.sourceHistograms.length, 38);
+
+ // A few of the top level ones.
+ checkRuntimeHistogram_(histograms, 'IC', 8, 0.088);
+ checkRuntimeHistogram_(histograms, 'API', 18, 0.198);
+ checkRuntimeHistogram_(histograms, 'Compile', 6, 0.066);
+ checkRuntimeHistogram_(histograms, 'Total', 90, 0.99);
+ });
+
+ test('runtimeStatsMetricBucketOnUE', function() {
+ // Test that v8 statistics are properly bucketed when UEs overlap.
+ // The renderer thread timeline looks like:
+ //
+ // * * [ V8 ] * [ V8 ] * [ V8 ] * [ V8 ] * [ V8 ] *
+ // | | | | | | |
+ // | | | | | | |
+ // v v v v v v v
+ // First LoadStart LoadEnd AnimStart RespEnd AnimEnd IdleEnd
+ // nav RespStart IdleStart
+ // 200 300 1000 2000 2100 3000 3500
+
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(1984);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ // Add User expectations.
+ model.userModel.expectations.push(new tr.model.um.LoadExpectation(
+ model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 1000));
+
+ model.userModel.expectations.push(new tr.model.um.ResponseExpectation(
+ model, tr.model.um.INITIATOR_TYPE.SCROLL, 1000, 1100));
+
+ model.userModel.expectations.push(new tr.model.um.AnimationExpectation(
+ model, tr.model.um.INITIATOR_TYPE.VIDEO, 2000, 1000));
+
+ model.userModel.expectations.push(new tr.model.um.IdleExpectation(
+ model, 3000, 500));
+
+ // Add V8 ThreadSlices corresponding to Load UE.
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.newInstance',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 300,
+ duration: 600,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 10],
+ HandleApiCall: [2, 11],
+ CompileFullCode: [3, 12],
+ LoadIC_Miss: [4, 13],
+ ParseLazy: [5, 14],
+ OptimizeCode: [6, 15],
+ FunctionCallback: [7, 16],
+ AllocateInTargetSpace: [8, 17],
+ API_Object_Get: [9, 18]
+ }
+ }
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 350,
+ duration: 400,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [30, 150],
+ Blink_Style_UpdateStyle: [20, 100],
+ }
+ }
+ }));
+
+ // Add V8 Thread slices corresponding to Response UE
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 1000,
+ duration: 800,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99]
+ }
+ }
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 1100,
+ duration: 600,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [10, 300],
+ Blink_Style_UpdateStyle: [15, 200],
+ }
+ }
+ }));
+
+ // V8 slices in the overlap range of animation + response
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 2000,
+ duration: 99,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 1],
+ HandleApiCall: [2, 2],
+ CompileFunctionLiteral: [3, 7],
+ LoadIC_Miss: [4, 4],
+ ParseLazy: [5, 5],
+ OptimizeCode: [6, 6],
+ FunctionCallback: [7, 7],
+ AllocateInTargetSpace: [8, 8],
+ API_Object_Get: [9, 9]
+ }
+ }
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 2010,
+ duration: 79,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [1, 10],
+ Blink_Style_UpdateStyle: [1, 10],
+ }
+ }
+ }));
+
+ // V8 slices in animation UE range.
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 2200,
+ duration: 700,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 1],
+ HandleApiCall: [2, 2],
+ CompileFullCode: [3, 3],
+ StoreIC_Miss: [4, 4],
+ ParseLazy: [5, 5],
+ OptimizeCode: [6, 6],
+ FunctionCallback: [7, 7],
+ AllocateInTargetSpace: [8, 8],
+ API_Object_Get: [9, 9]
+ }
+ }
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 2300,
+ duration: 600,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [20, 200],
+ Blink_Style_UpdateStyle: [15, 150],
+ }
+ }
+ }));
+
+ // Add V8 slices corresponding to Idle UE.
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 3001,
+ duration: 499,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseLazy: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Object_Get: [9, 99]
+ }
+ }
+ }));
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 3100,
+ duration: 300,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [1, 10],
+ Blink_Style_UpdateStyle: [10, 100],
+ }
+ }
+ }));
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.v8.runtimeStatsTotalMetric(histograms, model);
+ assert.strictEqual(histograms.length, 270);
+
+ // Check total:
+ checkRuntimeHistogram_(histograms, 'IC', 20, 0.109);
+ checkRuntimeHistogram_(histograms, 'API', 45, 0.234);
+ checkRuntimeHistogram_(histograms, 'Total', 225, 1.21);
+ checkRuntimeHistogram_(histograms, 'Blink C++', 35, 0.184);
+ checkRuntimeHistogram_(histograms, 'V8-Only', 190, 1.026);
+ checkRuntimeHistogram_(histograms, 'Blink_Layout', 62, 0.67);
+
+ // Check Load bucket:
+ checkRuntimeHistogram_(histograms, 'Load_Parse', 5, 0.014);
+ checkRuntimeHistogram_(histograms, 'Load_JavaScript', 1, 0.01);
+ checkRuntimeHistogram_(histograms, 'Load_Blink C++', 7, 0.016);
+ checkRuntimeHistogram_(histograms, 'Load_V8-Only', 38, 0.11);
+ checkRuntimeHistogram_(histograms, 'Load_Blink_Style', 20, 0.1);
+
+ // Check Response bucket:
+ checkRuntimeHistogram_(histograms, 'Response_Parse', 10, 0.06);
+ checkRuntimeHistogram_(histograms, 'Response_Compile', 6, 0.04);
+ checkRuntimeHistogram_(histograms, 'Response_Blink_Layout', 11, 0.31);
+
+ // Check Animation bucket:
+ checkRuntimeHistogram_(histograms, 'Animation_Parse', 10, 0.01);
+ checkRuntimeHistogram_(histograms, 'Animation_Blink_Style', 16, 0.16);
+
+ // Check Idle bucket:
+ checkRuntimeHistogram_(histograms, 'Idle_Parse', 5, 0.055);
+ checkRuntimeHistogram_(histograms, 'Idle_Blink_Parsing', 0, 0);
+ });
+
+ test('runtimeStatsMetricTotalNoUE', function() {
+ // Test that total v8 count works even without UE.
+ // The renderer thread timeline looks like:
+ //
+ // * [V8.NewInstance] * [ V8.Execute ] *
+ // |
+ // v
+ // First navigation
+ // 200
+
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const rendererProcess = model.getOrCreateProcess(1984);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'CrRendererMain';
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 200,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.newInstance',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 300,
+ duration: 600,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 10],
+ HandleApiCall: [2, 11],
+ CompileFullCode: [3, 12],
+ StoreIC_Miss: [4, 13],
+ ParseLazy: [5, 14],
+ OptimizeCode: [6, 15],
+ FunctionCallback: [7, 16],
+ AllocateInTargetSpace: [8, 17],
+ API_Object_Get: [9, 18]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 350,
+ duration: 400,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [5, 100],
+ Blink_Style_UpdateStyle: [2, 50],
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'V8.Execute',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 1100,
+ duration: 800,
+ args: {
+ 'runtime-call-stats': {
+ JS_Execution: [1, 11],
+ HandleApiCall: [2, 22],
+ CompileFullCode: [3, 33],
+ LoadIC_Miss: [4, 44],
+ ParseFunctionLiteral: [5, 55],
+ OptimizeCode: [6, 66],
+ FunctionCallback: [7, 77],
+ AllocateInTargetSpace: [8, 88],
+ API_Context_New: [9, 99]
+ }
+ }
+ }));
+
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'v8',
+ title: 'disabled-by-default-v8.runtime_stats',
+ type: tr.e.v8.V8ThreadSlice,
+ start: 1200,
+ duration: 500,
+ args: {
+ 'runtime-call-stats': {
+ Blink_Layout_UpdateLayout: [10, 200],
+ Blink_Style_UpdateStyle: [10, 100],
+ }
+ }
+ }));
+ });
+
+ const histograms = new tr.v.HistogramSet();
+ tr.metrics.v8.runtimeStatsTotalMetric(histograms, model);
+ assert.strictEqual(histograms.length, 54);
+
+ // Check total:
+ checkRuntimeHistogram_(histograms, 'IC', 8, 0.057);
+ checkRuntimeHistogram_(histograms, 'API', 18, 0.117);
+ checkRuntimeHistogram_(histograms, 'Parse', 10, 0.069);
+ checkRuntimeHistogram_(histograms, 'Total', 90, 0.621);
+ checkRuntimeHistogram_(histograms, 'Blink C++', 14, 0.093);
+ checkRuntimeHistogram_(histograms, 'V8-Only', 76, 0.528);
+ checkRuntimeHistogram_(histograms, 'Blink_Layout', 15, 0.3);
+ checkRuntimeHistogram_(histograms, 'Blink_Style', 12, 0.15);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html
new file mode 100644
index 00000000000..b9ec7ed80bd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html
@@ -0,0 +1,490 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/category_util.html">
+<link rel="import" href="/tracing/base/math/piecewise_linear_function.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.v8.utils', function() {
+ // The title of the idle task event.
+ const IDLE_TASK_EVENT = 'SingleThreadIdleTaskRunner::RunTask';
+
+ // V8 execution event.
+ const V8_EXECUTE = 'V8.Execute';
+
+ // GC events start with this prefix.
+ const GC_EVENT_PREFIX = 'V8.GC';
+
+ // Special handling is required for full GCs inside low memory notification.
+ const FULL_GC_EVENT = 'V8.GCCompactor';
+
+ const LOW_MEMORY_EVENT = 'V8.GCLowMemoryNotification';
+
+ const MAJOR_GC_EVENT = 'MajorGC';
+ const MINOR_GC_EVENT = 'MinorGC';
+
+ // Maps the top-level GC events in timeline to telemetry friendly names.
+ const TOP_GC_EVENTS = {
+ 'V8.GCCompactor': 'v8-gc-full-mark-compactor',
+ 'V8.GCFinalizeMC': 'v8-gc-latency-mark-compactor',
+ 'V8.GCFinalizeMCReduceMemory': 'v8-gc-memory-mark-compactor',
+ 'V8.GCIncrementalMarking': 'v8-gc-incremental-step',
+ 'V8.GCIncrementalMarkingFinalize': 'v8-gc-incremental-finalize',
+ 'V8.GCIncrementalMarkingStart': 'v8-gc-incremental-start',
+ 'V8.GCPhantomHandleProcessingCallback': 'v8-gc-phantom-handle-callback',
+ 'V8.GCScavenger': 'v8-gc-scavenger'
+ };
+
+ const MARK_COMPACTOR_EVENTS = new Set([
+ 'V8.GCCompactor',
+ 'V8.GCFinalizeMC',
+ 'V8.GCFinalizeMCReduceMemory',
+ 'V8.GCIncrementalMarking',
+ 'V8.GCIncrementalMarkingFinalize',
+ 'V8.GCIncrementalMarkingStart',
+ 'V8.GCPhantomHandleProcessingCallback'
+ ]);
+
+ const LOW_MEMORY_MARK_COMPACTOR = 'v8-gc-low-memory-mark-compactor';
+
+ /**
+ * Finds the first parent of the |event| for which the |predicate| holds.
+ */
+ function findParent(event, predicate) {
+ let parent = event.parentSlice;
+ while (parent) {
+ if (predicate(parent)) {
+ return parent;
+ }
+ parent = parent.parentSlice;
+ }
+ return null;
+ }
+
+ function isIdleTask(event) {
+ return event.title === IDLE_TASK_EVENT;
+ }
+
+ function isLowMemoryEvent(event) {
+ return event.title === LOW_MEMORY_EVENT;
+ }
+
+ function isV8Event(event) {
+ return event.title.startsWith('V8.');
+ }
+
+ function isV8ExecuteEvent(event) {
+ return event.title === V8_EXECUTE;
+ }
+
+ function isTopV8ExecuteEvent(event) {
+ return isV8ExecuteEvent(event) && findParent(isV8ExecuteEvent) === null;
+ }
+
+ function isGarbageCollectionEvent(event) {
+ // Low memory notification is handled specially because it contains
+ // several full mark compact events.
+ return event.title && event.title.startsWith(GC_EVENT_PREFIX) &&
+ event.title !== LOW_MEMORY_EVENT;
+ }
+
+ function isTopGarbageCollectionEvent(event) {
+ return event.title in TOP_GC_EVENTS;
+ }
+
+ function isForcedGarbageCollectionEvent(event) {
+ return findParent(event, isLowMemoryEvent) !== null;
+ }
+
+ function isSubGarbageCollectionEvent(event) {
+ // To reduce number of results, we return only the first level of GC
+ // subevents. Some subevents are nested in MajorGC or MinorGC events, so
+ // we have to check for it explicitly.
+ return isGarbageCollectionEvent(event) &&
+ event.parentSlice &&
+ (isTopGarbageCollectionEvent(event.parentSlice) ||
+ event.parentSlice.title === MAJOR_GC_EVENT ||
+ event.parentSlice.title === MINOR_GC_EVENT);
+ }
+
+ function isNotForcedTopGarbageCollectionEvent(event) {
+ // We exclude garbage collection events forced by benchmark runner,
+ // because they cannot happen in real world.
+ return tr.metrics.v8.utils.isTopGarbageCollectionEvent(event) &&
+ !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event);
+ }
+
+ function isNotForcedSubGarbageCollectionEvent(event) {
+ // We exclude garbage collection events forced by benchmark runner,
+ // because they cannot happen in real world.
+ return tr.metrics.v8.utils.isSubGarbageCollectionEvent(event) &&
+ !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event);
+ }
+
+ function isFullMarkCompactorEvent(event) {
+ return event.title === 'V8.GCCompactor';
+ }
+
+ function isMarkCompactorSummaryEvent(event) {
+ return event.title === 'V8.GCMarkCompactorSummary';
+ }
+
+ function isMarkCompactorMarkingSummaryEvent(event) {
+ return event.title === 'V8.GCMarkCompactorMarkingSummary';
+ }
+
+ function isIncrementalMarkingEvent(event) {
+ return event.title.startsWith('V8.GCIncrementalMarking');
+ }
+
+ function isLatencyMarkCompactorEvent(event) {
+ return event.title === 'V8.GCFinalizeMC';
+ }
+
+ function isMemoryMarkCompactorEvent(event) {
+ return event.title === 'V8.GCFinalizeMCReduceMemory';
+ }
+
+ function isScavengerEvent(event) {
+ return event.title === 'V8.GCScavenger';
+ }
+
+ function isCompileOptimizeRCSCategory(name) {
+ return name === 'Optimize';
+ }
+
+ function isCompileUnoptimizeRCSCategory(name) {
+ return name === 'Compile';
+ }
+
+ function isCompileParseRCSCategory(name) {
+ return name === 'Parse';
+ }
+
+ function isCompileRCSCategory(name) {
+ return name === 'Compile' || name === 'Optimize' || name === 'Parse';
+ }
+
+ function isV8RCSEvent(event) {
+ return event instanceof tr.e.v8.V8ThreadSlice;
+ }
+
+ function isMarkCompactorEvent(event) {
+ return MARK_COMPACTOR_EVENTS.has(event.title);
+ }
+
+ function isNotForcedMarkCompactorEvent(event) {
+ return !isForcedGarbageCollectionEvent(event) &&
+ isMarkCompactorEvent(event);
+ }
+
+ function forcedGCEventName() {
+ return LOW_MEMORY_EVENT;
+ }
+
+ function topGarbageCollectionEventName(event) {
+ if (event.title === FULL_GC_EVENT) {
+ // Full mark compact events inside a low memory notification
+ // are counted as low memory mark compacts.
+ 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';
+ // Remove redundant prefixes and convert to lower case.
+ const name = event.title.replace('V8.GC_MC_', '')
+ .replace('V8.GC_SCAVENGER_', '')
+ .replace('V8.GC_', '')
+ .replace(/_/g, '-').toLowerCase();
+ return prefix + '-' + name;
+ }
+
+ /**
+ * Filters events using the |filterCallback|, then groups events by the user
+ * the name computed using the |nameCallback|, and then invokes
+ * the |processCallback| with the grouped events.
+ * @param {Function} filterCallback Takes an event and returns a boolean.
+ * @param {Function} nameCallback Takes event and returns a string.
+ * @param {Function} processCallback Takes a name, and an array of events.
+ */
+ function groupAndProcessEvents(model, filterCallback,
+ nameCallback, processCallback) {
+ // Map: name -> [events].
+ 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);
+ }
+ }
+
+ /**
+ * Given a list of intervals, returns a new list with all overalapping
+ * intervals merged into a single interval.
+ */
+ 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);
+ // Empty range.
+ if (startOfFirstDumpWithV8 === Infinity) return new tr.b.math.Range();
+ return tr.b.math.Range.fromExplicitRange(startOfFirstDumpWithV8, Infinity);
+ }
+
+ /**
+ * An end-point of a window that is sliding from left to right
+ * over |points| starting from time |start|.
+ * It is intended to be used only by the |mutatorUtilization| function.
+ * @constructor
+ */
+ class WindowEndpoint {
+ constructor(start, points) {
+ this.points = points;
+ // The index of the last passed point.
+ this.lastIndex = -1;
+ // The position of the end-point in the time line.
+ this.position = start;
+ this.distanceUntilNextPoint = points[0].position - start;
+ // The cumulative duration of GC pauses until this position.
+ this.cummulativePause = 0;
+ // The number of entered GC intervals.
+ this.stackDepth = 0;
+ }
+
+ // Advance the end-point by the given |delta|.
+ advance(delta) {
+ if (delta < this.distanceUntilNextPoint) {
+ this.position += delta;
+ this.cummulativePause += this.stackDepth > 0 ? delta : 0;
+ this.distanceUntilNextPoint =
+ this.points[this.lastIndex + 1].position - this.position;
+ } else {
+ this.position += this.distanceUntilNextPoint;
+ this.cummulativePause +=
+ this.stackDepth > 0 ? this.distanceUntilNextPoint : 0;
+ this.distanceUntilNextPoint = 0;
+ this.lastIndex++;
+ if (this.lastIndex < this.points.length) {
+ this.stackDepth += this.points[this.lastIndex].delta;
+ if (this.lastIndex + 1 < this.points.length) {
+ this.distanceUntilNextPoint =
+ this.points[this.lastIndex + 1].position - this.position;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns mutator utilization as a piecewise linear function.
+ * Mutator utilization for a window size w is a function of time mu_w(t)
+ * that shows how much time in [t, t+w] is left for the mutator relative
+ * to the time window size.
+ * More formally:
+ * mu_w(t) = (w - total_time_spent_in_gc_in(t, t + w)) / w.
+ * The range of mu_w(t) is [0..1].
+ * See "A Parallel, Real-Time Garbage Collector" by Cheng et. al. for
+ * more info [https://www.cs.cmu.edu/~guyb/papers/gc2001.pdf].
+ *
+ * All parameters must use the same time unit.
+ * @param {number} start The start time of execution.
+ * @param {number} end The end time of execution.
+ * @param {number} timeWindow The size of the time window.
+ * @param {!Array<!{start: number, end: number}>} intervals The list of
+ * GC pauses.
+ */
+ function mutatorUtilization(start, end, timeWindow, intervals) {
+ const mu = new tr.b.math.PiecewiseLinearFunction();
+ // If the interval is smaller than the time window, then the function is
+ // empty.
+ if (end - start <= timeWindow) {
+ return mu;
+ }
+
+ // If there are GC pauses then the mutator utilization is 1.0.
+ if (intervals.length === 0) {
+ mu.push(start, 1.0, end - timeWindow, 1.0);
+ return mu;
+ }
+
+ intervals = unionOfIntervals(intervals);
+
+ // Create a point for the start and the end of each interval.
+ 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});
+
+ // The left and the right limit of the sliding window.
+ const left = new WindowEndpoint(start, points);
+ const right = new WindowEndpoint(start, points);
+
+ // Advance the right end-point until we get the correct window size.
+ // Allow the floating-point precision errors of this magnitude.
+ const EPSILON = 1e-6;
+ while (right.position - left.position < timeWindow - EPSILON) {
+ right.advance(timeWindow - (right.position - left.position));
+ }
+
+ while (right.lastIndex < points.length) {
+ // Advance the window end-points by the largest possible amount
+ // without jumping over a point.
+ const distanceUntilNextPoint =
+ Math.min(left.distanceUntilNextPoint, right.distanceUntilNextPoint);
+ const position1 = left.position;
+ const value1 = right.cummulativePause - left.cummulativePause;
+ left.advance(distanceUntilNextPoint);
+ right.advance(distanceUntilNextPoint);
+ // Add a new mutator utilization segment only if it is non-trivial.
+ 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;
+ }
+
+ /**
+ * Computes the minimum mutator utilization (MMU) metric for the given time
+ * windows and the given renderers. The results are added as histograms to
+ * the given histogram set.
+ *
+ * For example, passing 'v8-gc-mark-compactor-mmu' as the metric name and
+ * [16, 50, 100] as the time windows will produce the following:
+ * - v8-gc-mark-compactor-mmu-16ms_window
+ * - v8-gc-mark-compactor-mmu-50ms_window
+ * - v8-gc-mark-compactor-mmu-100ms_window
+ *
+ * @param {!string} metricName the name of the metric.
+ * @param {!function(tr.b.Event): boolean} eventFilter the predicate for
+ * filtering the events that will be used for computing the MMU.
+ * @param {!Array.<tr.model.helpers.ChromeRendererHelper>} rendererHelpers
+ * @param {!tr.v.HistogramSet} histograms
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html
new file mode 100644
index 00000000000..dc45524508a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html
@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/v8/utils.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function addDumpWithAllocator(model, process, allocator, start) {
+ const gmd = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump(
+ model, {ts: start});
+ const pmd = tr.model.MemoryDumpTestUtils.addProcessMemoryDump(gmd, process);
+ pmd.memoryAllocatorDumps =
+ [tr.model.MemoryDumpTestUtils.newAllocatorDump(pmd, allocator)];
+ }
+
+ const unionOfIntervals = tr.metrics.v8.utils.unionOfIntervals;
+
+ function interval(start, end) {
+ return {start, end};
+ }
+
+ /**
+ * Brute force computation of the mutator utilization.
+ * The function steps from the start to the end and for each step
+ * computes the mutator utilization.
+ */
+ function mutatorUtilizationSlow(start, end, timeWindow, intervals) {
+ const STEP = 0.001;
+ function timeToIndex(time) {
+ return Math.floor((time - start) / STEP);
+ }
+ const N = timeToIndex(end) + 1;
+ // bitmap[i] === true means that GC is active at time i.
+ const bitmap = new Array(N);
+ for (let i = 0; i < N; i++) {
+ bitmap[i] = false;
+ }
+ intervals.forEach(function(interval) {
+ const start = timeToIndex(interval.start);
+ const end = timeToIndex(interval.end);
+ for (let i = start; i < end; i++) {
+ bitmap[i] = true;
+ }
+ });
+ const pause = new Array(N);
+ for (let i = 0; i < N; i++) {
+ pause[i] = (i > 0 ? pause[i - 1] : 0) + (bitmap[i] ? 1 : 0);
+ }
+ const windowWidth = timeToIndex(timeWindow);
+ const mu = new Array(Math.max(N - windowWidth, 0));
+ for (let i = 0; i < mu.length; i++) {
+ const value = pause[i + windowWidth] - pause[i];
+ mu[i] = 1.0 - value / windowWidth;
+ }
+ mu.sort((a, b) => a - b);
+ return {
+ average: mu.reduce((acc, x) => (acc + x), 0) / mu.length,
+ min: mu.reduce((acc, x) => Math.min(acc, x), 0),
+ max: mu.reduce((acc, x) => Math.max(acc, x), 0),
+ percentile(percent) {
+ return mu[Math.floor(percent * (mu.length - 1))];
+ }
+ };
+ }
+
+ /**
+ * Constructs PiecewiseLinearFunction from pieces.
+ * @param {!Array<!{x1: number, y1: number, x2: number, y2: number}>} pieces
+ * The list of pieces ordered by the x coordinate.
+ */
+ function createExpectedFunction(pieces) {
+ const f = new tr.b.math.PiecewiseLinearFunction();
+ pieces.forEach(function(p) {
+ f.push(p.x1, p.y1, p.x2, p.y2);
+ });
+ return f;
+ }
+
+ test('unionOfIntervals', function() {
+ assert.deepEqual(unionOfIntervals([]), []);
+ assert.deepEqual(unionOfIntervals([interval(1, 1)]), [interval(1, 1)]);
+ assert.deepEqual(
+ unionOfIntervals([interval(0, 1), interval(1, 2), interval(2, 3)]),
+ [interval(0, 3)]);
+ assert.deepEqual(
+ unionOfIntervals([interval(0, 1), interval(1, 2), interval(3, 3)]),
+ [interval(0, 2), interval(3, 3)]);
+ assert.deepEqual(
+ unionOfIntervals([interval(0, 10), interval(1, 2), interval(3, 3)]),
+ [interval(0, 10)]);
+ assert.deepEqual(
+ unionOfIntervals([interval(0, 10), interval(1, 2), interval(3, 11)]),
+ [interval(0, 11)]);
+ assert.deepEqual(
+ unionOfIntervals([interval(3, 10), interval(1, 2), interval(11, 11)]),
+ [interval(1, 2), interval(3, 10), interval(11, 11)]);
+ });
+
+ test('basicMutatorUtilization', function() {
+ assert.deepEqual(
+ tr.metrics.v8.utils.mutatorUtilization(0, 40, 10, [interval(10, 20)]),
+ createExpectedFunction([
+ { x1: 0, y1: 1.0, x2: 10, y2: 0.0 },
+ { x1: 10, y1: 0.0, x2: 20, y2: 1.0 },
+ { x1: 20, y1: 1.0, x2: 30, y2: 1.0 }])
+ );
+ assert.deepEqual(
+ tr.metrics.v8.utils.mutatorUtilization(0, 40, 10, [interval(10, 15)]),
+ createExpectedFunction([
+ { x1: 0, y1: 1.0, x2: 5, y2: 0.5 },
+ { x1: 5, y1: 0.5, x2: 10, y2: 0.5 },
+ { x1: 10, y1: 0.5, x2: 15, y2: 1.0 },
+ { x1: 15, y1: 1.0, x2: 30, y2: 1.0 }])
+ );
+ assert.deepEqual(
+ tr.metrics.v8.utils.mutatorUtilization(0, 60, 20,
+ [interval(30, 35), interval(40, 45)]),
+ createExpectedFunction([
+ { x1: 0, y1: 1.0, x2: 10, y2: 1.0 },
+ { x1: 10, y1: 1.0, x2: 15, y2: 0.75 },
+ { x1: 15, y1: 0.75, x2: 20, y2: 0.75 },
+ { x1: 20, y1: 0.75, x2: 25, y2: 0.5 },
+ { x1: 25, y1: 0.5, x2: 30, y2: 0.5 },
+ { x1: 30, y1: 0.5, x2: 35, y2: 0.75 },
+ { x1: 35, y1: 0.75, x2: 40, y2: 0.75 }])
+ );
+ });
+
+ test('mutatorUtilization', function() {
+ const pauses = [
+ interval(10, 20),
+ interval(15, 23),
+ interval(30, 31),
+ interval(33, 34),
+ interval(60, 61),
+ interval(61, 63),
+ interval(80, 88)];
+ const actual = tr.metrics.v8.utils.mutatorUtilization(0, 100, 7, pauses);
+ const expected = mutatorUtilizationSlow(0, 100, 7, pauses);
+ assert.closeTo(expected.average, actual.average, 1e-3);
+ assert.closeTo(expected.max, actual.max, 1e-3);
+ assert.closeTo(expected.min, actual.min, 1e-3);
+ assert.closeTo(expected.percentile(0.5), actual.percentile(0.5), 1e-3);
+ assert.closeTo(expected.percentile(0.9), actual.percentile(0.9), 1e-3);
+ });
+
+ test('mutatorUtilizationMakesProgress', function() {
+ const start = 44173.32375717163;
+ const end = 80378.0457572937;
+ const pauses = [
+ interval(500, 700),
+ interval(900, 990)];
+ const actual = tr.metrics.v8.utils.mutatorUtilization(
+ start, end, 16.67, pauses);
+ assert.closeTo(0.9548, actual.average, 1e-3);
+ });
+
+
+ test('rangeForMemoryDumps', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ addDumpWithAllocator(model, process, 'dummy', 10);
+ addDumpWithAllocator(model, process, 'v8', 20);
+ });
+ const range = tr.metrics.v8.utils.rangeForMemoryDumps(model);
+ assert.isFalse(range.isEmpty);
+ assert.strictEqual(range.min, 20);
+ assert.strictEqual(range.max, Infinity);
+ });
+
+ test('rangeForMemoryDumpsEmpty', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ addDumpWithAllocator(model, process, 'dummy', 10);
+ addDumpWithAllocator(model, process, 'dummy', 20);
+ });
+ const range = tr.metrics.v8.utils.rangeForMemoryDumps(model);
+ assert.isTrue(range.isEmpty);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html
new file mode 100644
index 00000000000..507d23e6119
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/system_health/memory_metric.html">
+<link rel="import" href="/tracing/metrics/v8/execution_metric.html">
+<link rel="import" href="/tracing/metrics/v8/gc_metric.html">
+
+<script>
+'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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html
new file mode 100644
index 00000000000..1a3f4b4d5af
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'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);
+ }
+ 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));
+
+ // TODO(crbug/884259): Remove the following aliases after some time.
+ 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'));
+
+ // Scene update metrics.
+ 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));
+ // Draw metrics.
+ 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));
+
+ // Maps from the GUID of a UiRenderer::DrawUiView slice to each of its
+ // children slices.
+ 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) {
+ // Skip user expecations not of the right type or not inside the range of
+ // interest.
+ 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) {
+ // The events are traced on the GL thread in the browser process.
+ // Unfortunately, this thread has no name.
+ // TODO(tiborg): Give GL thread a name and reference the thread by
+ // the given name.
+ const glThreads = helper.process.findAllThreadsMatching(
+ thread => !thread.name);
+
+ for (const glThread of glThreads) {
+ for (const event of glThread.getDescendantEvents()) {
+ // Skip events that are neither in the user expecation, range of
+ // interest nor part of the frame cycle durations.
+ 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;
+ }
+
+ // There can be multiple UiElementRenderer::Draw... events per frame
+ // but this metric should calculate the across frame average of
+ // durations. Thus, collect UiElementRenderer::Draw... slices here
+ // in order to process them below.
+ if (event.parentSlice &&
+ event.parentSlice.title === 'UiRenderer::DrawUiView') {
+ const guid = event.parentSlice.guid;
+ if (!drawUiSubSlicesMap.has(guid)) {
+ drawUiSubSlicesMap.set(guid, []);
+ }
+ drawUiSubSlicesMap.get(guid).push(event);
+ continue;
+ }
+
+ const {wall: wallHist, cpu: cpuHist} =
+ histogramsByEventTitle.get(event.title);
+ wallHist.addSample(event.duration);
+ if (cpuHist !== undefined) {
+ cpuHist.addSample(event.cpuDuration);
+ }
+ }
+ }
+ }
+ }
+
+ // Calculate the average of the per frame sums of UiElementRenderer::Draw...
+ // events.
+ for (const subSlices of drawUiSubSlicesMap.values()) {
+ const eventMap = new Map();
+ for (const event of subSlices) {
+ if (!eventMap.has(event.title)) {
+ eventMap.set(event.title, {
+ wall: 0,
+ cpu: 0
+ });
+ }
+ eventMap.get(event.title).wall += event.duration;
+ eventMap.get(event.title).cpu += event.cpuDuration;
+ }
+ for (const [title, values] of eventMap.entries()) {
+ const {wall: wallHist, cpu: cpuHist} =
+ histogramsByEventTitle.get(title);
+ wallHist.addSample(values.wall);
+ if (cpuHist !== undefined) {
+ cpuHist.addSample(values.cpu);
+ }
+ }
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(frameCycleDurationMetric, {
+ supportsRangeOfInterest: true,
+ });
+
+ return {
+ frameCycleDurationMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html
new file mode 100644
index 00000000000..e6500713167
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html
@@ -0,0 +1,333 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/vr/frame_cycle_duration_metric.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ const TOLERANCE = 1e-6;
+
+ function createSubSlices(durations, currentTime, sliceGroup) {
+ if (durations === undefined) {
+ return [];
+ }
+ const slices = [];
+ for (const duration of durations) {
+ const option = {
+ cat: 'gpu',
+ title: duration.title,
+ start: currentTime,
+ end: currentTime + duration.wall,
+ };
+ if (duration.cpu !== undefined) {
+ option.cpuStart = currentTime;
+ option.cpuEnd = currentTime + duration.cpu;
+ }
+ const slice = tr.c.TestUtils.newSliceEx(option);
+ slice.subSlices = createSubSlices(
+ duration.sub, currentTime + 0.1, sliceGroup);
+ sliceGroup.pushSlice(slice);
+ slices.push(slice);
+ const maxDuration = duration.cpu === undefined ?
+ duration.wall : Math.max(duration.wall, duration.cpu);
+ currentTime += maxDuration + 1;
+ }
+ return slices;
+ }
+
+ test('frameCycleDurationMetric', function() {
+ const durations = [
+ {
+ title: 'Vr.DrawFrame',
+ wall: 390,
+ cpu: 340,
+ sub: [
+ {title: 'Vr.AcquireGvrFrame', wall: 2, cpu: 2},
+ {title: 'Vr.AcquireGvrFrame', wall: 2.5, cpu: 1.5},
+ {
+ title: 'UiScene::OnBeginFrame.UpdateAnimationsAndOpacity',
+ wall: 3,
+ cpu: 2
+ },
+ {
+ title: 'UiScene::OnBeginFrame.UpdateAnimationsAndOpacity',
+ wall: 2,
+ cpu: 1
+ },
+ {title: 'UiScene::OnBeginFrame.UpdateBindings', wall: 3, cpu: 2},
+ {title: 'UiScene::OnBeginFrame.UpdateBindings', wall: 2, cpu: 1},
+ {title: 'UiScene::OnBeginFrame.UpdateLayout', wall: 3, cpu: 2},
+ {title: 'UiScene::OnBeginFrame.UpdateLayout', wall: 2, cpu: 1},
+ {
+ title: 'UiScene::OnBeginFrame.UpdateWorldSpaceTransform',
+ wall: 3,
+ cpu: 2
+ },
+ {
+ title: 'UiScene::OnBeginFrame.UpdateWorldSpaceTransform',
+ wall: 2,
+ cpu: 1
+ },
+ {title: 'Vr.ProcessControllerInput', wall: 1, cpu: 0.5},
+ {title: 'Vr.ProcessControllerInput', wall: 0.5, cpu: 0.4},
+ {title: 'Vr.ProcessControllerInputForWebXr', wall: 1, cpu: 0.5},
+ {title: 'Vr.ProcessControllerInputForWebXr', wall: 1.5, cpu: 0.6},
+ {
+ title: 'UiRenderer::DrawUiView',
+ wall: 250,
+ cpu: 230,
+ sub: [
+ {title: 'UiElementRenderer::DrawTexturedQuad', wall: 1, cpu: 0.5},
+ {title: 'UiElementRenderer::DrawGradientQuad', wall: 3, cpu: 2},
+ {title: 'UiElementRenderer::DrawGradientQuad', wall: 4, cpu: 3},
+ {title: 'UiElementRenderer::DrawTexturedQuad', wall: 2, cpu: 1},
+ {
+ title: 'UiElementRenderer::DrawGradientGridQuad',
+ wall: 5,
+ cpu: 4
+ },
+ {
+ title: 'UiElementRenderer::DrawGradientGridQuad',
+ wall: 6,
+ cpu: 5
+ },
+ {title: 'UiElementRenderer::DrawController', wall: 7, cpu: 6},
+ {title: 'UiElementRenderer::DrawController', wall: 8, cpu: 7},
+ {title: 'UiElementRenderer::DrawLaser', wall: 9, cpu: 8},
+ {title: 'UiElementRenderer::DrawLaser', wall: 10, cpu: 9},
+ {title: 'UiElementRenderer::DrawReticle', wall: 11, cpu: 10},
+ {title: 'UiElementRenderer::DrawReticle', wall: 12, cpu: 11},
+ {title: 'UiElementRenderer::DrawShadow', wall: 13, cpu: 12},
+ {title: 'UiElementRenderer::DrawShadow', wall: 14, cpu: 13},
+ {title: 'UiElementRenderer::DrawStars', wall: 15, cpu: 14},
+ {title: 'UiElementRenderer::DrawStars', wall: 16, cpu: 15},
+ {title: 'UiElementRenderer::DrawBackground', wall: 17, cpu: 16},
+ {title: 'UiElementRenderer::DrawBackground', wall: 18, cpu: 17},
+ {title: 'UiElementRenderer::DrawKeyboard', wall: 19, cpu: 18},
+ {title: 'UiElementRenderer::DrawKeyboard', wall: 20, cpu: 19},
+ ]
+ },
+ {
+ title: 'UiRenderer::DrawUiView',
+ wall: 80,
+ cpu: 75,
+ sub: [
+ {title: 'UiElementRenderer::DrawTexturedQuad', wall: 2, cpu: 1},
+ {title: 'UiElementRenderer::DrawGradientQuad', wall: 3, cpu: 2},
+ {
+ title: 'UiElementRenderer::DrawGradientGridQuad',
+ wall: 4,
+ cpu: 3
+ },
+ {title: 'UiElementRenderer::DrawController', wall: 5, cpu: 4},
+ {title: 'UiElementRenderer::DrawLaser', wall: 6, cpu: 5},
+ {title: 'UiElementRenderer::DrawReticle', wall: 7, cpu: 6},
+ {title: 'UiElementRenderer::DrawShadow', wall: 8, cpu: 7},
+ {title: 'UiElementRenderer::DrawStars', wall: 9, cpu: 8},
+ {title: 'UiElementRenderer::DrawBackground', wall: 10, cpu: 9},
+ {title: 'UiElementRenderer::DrawKeyboard', wall: 11, cpu: 10},
+ ]
+ },
+ {title: 'Vr.SubmitFrameNow', wall: 3, cpu: 0.5},
+ {title: 'Vr.SubmitFrameNow', wall: 3.5, cpu: 0.5},
+ {title: 'Vr.PostSubmitDrawOnGpu', wall: 3},
+ {title: 'Vr.PostSubmitDrawOnGpu', wall: 4},
+ ]
+ },
+ {title: 'Vr.DrawFrame', wall: 20, cpu: 10},
+ // This event should be filtered out by the user expecation.
+ {title: 'Vr.DrawFrame', wall: 20, cpu: 10},
+ ];
+ const histograms = new tr.v.HistogramSet();
+ const model = tr.c.TestUtils.newModel(function(model) {
+ model.userModel.expectations.push(
+ new tr.model.um.AnimationExpectation(model,
+ tr.model.um.INITIATOR_TYPE.VR, 0, 411));
+ const browserProcess = model.getOrCreateProcess(0);
+ const browserMainThread = browserProcess.getOrCreateThread(0);
+ browserMainThread.name = 'CrBrowserMain';
+ const browserGlThread = browserProcess.getOrCreateThread(1);
+ const group = browserGlThread.sliceGroup;
+ createSubSlices(durations, 0, group);
+ group.createSubSlices();
+ });
+
+ tr.metrics.vr.frameCycleDurationMetric(histograms, model);
+
+ assert.closeTo(histograms.getHistogramNamed('draw_frame_wall').average,
+ 205, TOLERANCE);
+ assert.closeTo(histograms.getHistogramNamed('draw_frame_cpu').average,
+ 175, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('acquire_frame_wall').average,
+ 2.25, TOLERANCE);
+ assert.closeTo(histograms.getHistogramNamed('acquire_frame_cpu').average,
+ 1.75, TOLERANCE);
+
+ assert.isUndefined(
+ histograms.getHistogramNamed('post_submit_draw_on_gpu_cpu'));
+ assert.closeTo(
+ histograms.getHistogramNamed('post_submit_draw_on_gpu_wall').average,
+ 3.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('update_controller_wall').average,
+ 0.75, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('update_controller_cpu').average,
+ 0.45, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('update_controller_webxr_wall').average,
+ 1.25, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('update_controller_webxr_cpu').average,
+ 0.55, TOLERANCE);
+
+ assert.closeTo(histograms.getHistogramNamed('submit_frame_wall').average,
+ 3.25, TOLERANCE);
+ assert.closeTo(histograms.getHistogramNamed('submit_frame_cpu').average,
+ 0.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('update_animations_and_opacity_wall')
+ .average,
+ 2.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('update_animations_and_opacity_cpu')
+ .average,
+ 1.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('update_bindings_wall').average, 2.5,
+ TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('update_bindings_cpu').average, 1.5,
+ TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('update_layout_wall').average, 2.5,
+ TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('update_layout_cpu').average, 1.5,
+ TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('update_world_space_transforms_wall')
+ .average,
+ 2.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('update_world_space_transforms_cpu')
+ .average,
+ 1.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_ui_wall')
+ .average,
+ 165, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_ui_cpu')
+ .average,
+ 152.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_textured_quad_wall')
+ .average,
+ 2.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_textured_quad_cpu')
+ .average,
+ 1.25, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_gradient_quad_wall')
+ .average,
+ 5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_gradient_quad_cpu')
+ .average,
+ 3.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_gradient_grid_quad_wall')
+ .average,
+ 7.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_gradient_grid_quad_cpu')
+ .average,
+ 6, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_controller_wall')
+ .average,
+ 10, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_controller_cpu')
+ .average,
+ 8.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_laser_wall')
+ .average,
+ 12.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_laser_cpu')
+ .average,
+ 11, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_reticle_wall')
+ .average,
+ 15, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_reticle_cpu')
+ .average,
+ 13.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_shadow_wall')
+ .average,
+ 17.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_shadow_cpu')
+ .average,
+ 16.0, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_stars_wall')
+ .average,
+ 20, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_stars_cpu')
+ .average,
+ 18.5, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_background_wall')
+ .average,
+ 22.5, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_background_cpu')
+ .average,
+ 21, TOLERANCE);
+
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_keyboard_wall')
+ .average,
+ 25, TOLERANCE);
+ assert.closeTo(
+ histograms.getHistogramNamed('draw_keyboard_cpu')
+ .average,
+ 23.5, TOLERANCE);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html
new file mode 100644
index 00000000000..254267053ea
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.vr', function() {
+ function webvrMetric(histograms, model, opt_options) {
+ // Maps VR trace counters to histogram.
+ const WEBVR_COUNTERS = new Map([
+ ['gpu.WebVR FPS', {
+ name: 'webvr_fps',
+ unit: tr.b.Unit.byName.count_biggerIsBetter,
+ samples: {},
+ options: {
+ description: 'WebVR frame per second',
+ binBoundaries: tr.v.HistogramBinBoundaries.createLinear(20, 120, 25),
+ },
+ }],
+ ['gpu.WebVR frame time (ms)', {
+ name: 'webvr_frame_time',
+ unit: tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ samples: {},
+ options: {
+ description: 'WebVR frame time in ms',
+ binBoundaries: tr.v.HistogramBinBoundaries.createLinear(20, 120, 25),
+ },
+ }],
+ ['gpu.WebVR pose prediction (ms)', {
+ name: 'webvr_pose_prediction',
+ unit: tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ samples: {},
+ options: {
+ description: 'WebVR pose prediction in ms',
+ binBoundaries: tr.v.HistogramBinBoundaries.createLinear(20, 120, 25),
+ },
+ }],
+ ]);
+
+ for (const ue of model.userModel.expectations) {
+ const rangeOfInterestEnabled = opt_options && opt_options.rangeOfInterest;
+ if (rangeOfInterestEnabled &&
+ !opt_options.rangeOfInterest.intersectsExplicitRangeInclusive(
+ ue.start, ue.end)) {
+ continue;
+ }
+
+ // By default, only do calculations in the VR animation expectation, i.e.
+ // some time after we've entered VR, in order to avoid skewed results
+ // caused by VR entry, but allow calculation on the response expectation
+ // if we've manually selected it as a range of interest
+ 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 (!(WEBVR_COUNTERS.has(counter.id))) continue;
+
+ for (const series of counter.series) {
+ if (!(series.name in WEBVR_COUNTERS.get(counter.id).samples)) {
+ WEBVR_COUNTERS.get(counter.id).samples[series.name] = [];
+ }
+ for (const sample of series.samples) {
+ if (sample.timestamp < ue.start || sample.timestamp >= ue.end) {
+ continue;
+ }
+ if (rangeOfInterestEnabled &&
+ !opt_options.rangeOfInterest.intersectsExplicitRangeInclusive(
+ sample.timestamp, sample.timestamp)) {
+ continue;
+ }
+
+ WEBVR_COUNTERS.get(counter.id).samples[series.name].push(
+ sample.value);
+ }
+ }
+ }
+ }
+
+ // Make sure we always report a value for WebVR FPS so that failing to
+ // submit frames will show up as a regression
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html
new file mode 100644
index 00000000000..75d73aeeb8a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/metrics/vr/webvr_metric.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel() {
+ const model = tr.c.TestUtils.newModel(
+ function(model) {
+ model.userModel.expectations.push(
+ new tr.model.um.AnimationExpectation(model,
+ tr.model.um.INITIATOR_TYPE.VR, 0, 5));
+ const process = model.getOrCreateProcess(1);
+ const fpsCounter = process.getOrCreateCounter('gpu', 'WebVR FPS');
+ const fpsSeries = new tr.model.CounterSeries('value', 0);
+ fpsSeries.addCounterSample(1, 59);
+ fpsSeries.addCounterSample(3, 60);
+ fpsCounter.addSeries(fpsSeries);
+
+ const frameTimeCounter = process.getOrCreateCounter(
+ 'gpu', 'WebVR frame time (ms)');
+ const renderingSeries = new tr.model.CounterSeries('rendering', 0);
+ renderingSeries.addCounterSample(1, 3);
+ renderingSeries.addCounterSample(2, 4);
+ frameTimeCounter.addSeries(renderingSeries);
+ const javascriptSeries = new tr.model.CounterSeries('javascript', 0);
+ javascriptSeries.addCounterSample(1, 5);
+ javascriptSeries.addCounterSample(2, 6);
+ frameTimeCounter.addSeries(javascriptSeries);
+ });
+ return model;
+ }
+
+ test('webvrMetric', function() {
+ const histograms = new tr.v.HistogramSet();
+ const model = createModel();
+ tr.metrics.vr.webvrMetric(histograms, model);
+
+ const fpsValue = histograms.getHistogramNamed('webvr_fps');
+ assert.strictEqual(fpsValue.max, 60);
+ assert.strictEqual(fpsValue.min, 59);
+ assert.strictEqual(fpsValue.average, 59.5);
+
+ const renderingValue = histograms.getHistogramNamed(
+ 'webvr_frame_time_rendering');
+ assert.strictEqual(renderingValue.max, 4);
+ assert.strictEqual(renderingValue.min, 3);
+ assert.strictEqual(renderingValue.average, 3.5);
+
+ const javascriptValue = histograms.getHistogramNamed(
+ 'webvr_frame_time_javascript');
+ assert.strictEqual(javascriptValue.max, 6);
+ assert.strictEqual(javascriptValue.min, 5);
+ assert.strictEqual(javascriptValue.average, 5.5);
+ });
+
+ test('webvrMetric_alwaysReportFps', function() {
+ const histograms = new tr.v.HistogramSet();
+ const model = tr.c.TestUtils.newModel();
+ tr.metrics.vr.webvrMetric(histograms, model);
+
+ const fpsValue = histograms.getHistogramNamed('webvr_fps');
+ assert.strictEqual(fpsValue.max, 0);
+ assert.strictEqual(fpsValue.min, 0);
+
+ const renderingValue = histograms.getHistogramNamed(
+ 'webvr_frame_time_rendering');
+ assert.strictEqual(renderingValue, undefined);
+
+ const javascriptValue = histograms.getHistogramNamed(
+ 'webvr_frame_time_javascript');
+ assert.strictEqual(javascriptValue, undefined);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html
new file mode 100644
index 00000000000..a8414ec46d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html
@@ -0,0 +1,364 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/metrics/v8/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+
+<script>
+'use strict';
+
+tr.exportTo('tr.metrics.webrtc', function() {
+ const DISPLAY_HERTZ = 60.0;
+ const VSYNC_DURATION_US = 1e6 / DISPLAY_HERTZ;
+ // How much more severe is a 'Badly out of sync' render event compared to an
+ // 'Out of sync' one when calculating the smoothness score.
+ const SEVERITY = 3;
+ // How many vsyncs a frame should be displayed to be considered frozen.
+ const FROZEN_FRAME_VSYNC_COUNT_THRESHOLD = 6;
+
+ const WEB_MEDIA_PLAYER_UPDATE_TITLE = 'UpdateCurrentFrame';
+ // These four are args for WebMediaPlayerMS update events.
+ const IDEAL_RENDER_INSTANT_NAME = 'Ideal Render Instant';
+ const ACTUAL_RENDER_BEGIN_NAME = 'Actual Render Begin';
+ const ACTUAL_RENDER_END_NAME = 'Actual Render End';
+ // The events of interest have a 'Serial' argument which represents the
+ // stream ID.
+ 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
+ ];
+
+ // By default, we store a single value, so we only need one of the
+ // statistics to keep track. We choose the average for that.
+ 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;
+
+ /*
+ * Verify that the event is a valid event.
+ *
+ * An event is valid if it is a UpdateCurrentFrame event,
+ * and has all of the mandatory arguments. See MANDATORY above.
+ */
+ 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));
+ }
+ }
+ const eventsByStreamName = tr.b.groupIntoMap(
+ webMediaPlayerMSEvents,
+ event => event.args[STREAM_ID_NAME]
+ );
+ for (const [streamName, events] of eventsByStreamName) {
+ getTimeStats(histograms, streamName, events);
+ }
+ }
+
+ tr.metrics.MetricRegistry.register(webrtcRenderingMetric);
+
+ function getTimeStats(histograms, streamName, events) {
+ const frameHist = getFrameDistribution(histograms, events);
+ addFpsFromFrameDistribution(histograms, frameHist);
+ addFreezingScore(histograms, frameHist);
+
+ const driftTimeStats = getDriftStats(events);
+ histograms.createHistogram('WebRTCRendering_drift_time',
+ timeDurationInMs_smallerIsBetter, driftTimeStats.driftTime, {
+ summaryOptions: {
+ count: false,
+ min: false,
+ percentile: [0.75, 0.9],
+ },
+ });
+ histograms.createHistogram('WebRTCRendering_rendering_length_error',
+ percentage_smallerIsBetter,
+ driftTimeStats.renderingLengthError, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+
+ const smoothnessStats = getSmoothnessStats(driftTimeStats.driftTime);
+ histograms.createHistogram('WebRTCRendering_percent_badly_out_of_sync',
+ percentage_smallerIsBetter, smoothnessStats.percentBadlyOutOfSync, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ histograms.createHistogram('WebRTCRendering_percent_out_of_sync',
+ percentage_smallerIsBetter, smoothnessStats.percentOutOfSync, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ histograms.createHistogram('WebRTCRendering_smoothness_score',
+ percentage_biggerIsBetter, smoothnessStats.smoothnessScore, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ histograms.createHistogram('WebRTCRendering_frames_out_of_sync',
+ count_smallerIsBetter, smoothnessStats.framesOutOfSync, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ histograms.createHistogram('WebRTCRendering_frames_badly_out_of_sync',
+ count_smallerIsBetter, smoothnessStats.framesSeverelyOutOfSync, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ }
+
+ const FRAME_DISTRIBUTION_BIN_BOUNDARIES =
+ tr.v.HistogramBinBoundaries.createLinear(1, 50, 49);
+
+ /**
+ * Create the frame distribution.
+ *
+ * If the overall display distribution is A1:A2:..:An, this will tell how
+ * many times a frame stays displayed during Ak*VSYNC_DURATION_US, also known
+ * as 'source to output' distribution.
+ *
+ * In other terms, a distribution B where
+ * B[k] = number of frames that are displayed k times.
+ *
+ * @param {tr.v.HistogramSet} histograms
+ * @param {Array.<event>} events - An array of events.
+ * @returns {tr.v.Histogram} frameHist - The frame distribution.
+ */
+ function getFrameDistribution(histograms, events) {
+ const cadence = tr.b.runLengthEncoding(
+ events.map(e => e.args[IDEAL_RENDER_INSTANT_NAME]));
+ return histograms.createHistogram('WebRTCRendering_frame_distribution',
+ count_smallerIsBetter, cadence.map(ticks => ticks.count), {
+ binBoundaries: FRAME_DISTRIBUTION_BIN_BOUNDARIES,
+ summaryOptions: {
+ percentile: [0.75, 0.9],
+ },
+ });
+ }
+
+ /**
+ * Calculate the apparent FPS from frame distribution.
+ *
+ * Knowing the display frequency and the frame distribution, it is possible to
+ * calculate the video apparent frame rate as played by WebMediaPlayerMs
+ * module.
+ *
+ * @param {tr.v.HistogramSet} histograms
+ * @param {tr.v.Histogram} frameHist - The frame distribution. See
+ * getFrameDistribution.
+ */
+ function addFpsFromFrameDistribution(histograms, frameHist) {
+ let numberFrames = 0;
+ let numberVsyncs = 0;
+ for (let ticks = 1; ticks < frameHist.allBins.length; ++ticks) {
+ const count = frameHist.allBins[ticks].count;
+ numberFrames += count;
+ numberVsyncs += ticks * count;
+ }
+ const meanRatio = numberVsyncs / numberFrames;
+ histograms.createHistogram('WebRTCRendering_fps',
+ unitlessNumber_biggerIsBetter, DISPLAY_HERTZ / meanRatio, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ }
+
+ /**
+ * Returns the weighted penalty for a number of frozen frames.
+ *
+ * In a series of repeated frames of length > 5, all frames after the first
+ * are considered frozen. Conversely, no frames in a series of repeated frames
+ * of length <= 5 will be considered frozen.
+ *
+ * This means the weight for 0 to 4 frozen frames is 0.
+ *
+ * @param {Number} numberFrozenFrames - The number of frozen frames.
+ * @returns {Number} - The weight penalty for the number of frozen frames.
+ */
+ function frozenPenaltyWeight(numberFrozenFrames) {
+ const penalty = {
+ 5: 1,
+ 6: 5,
+ 7: 15,
+ 8: 25
+ };
+ return penalty[numberFrozenFrames] || (8 * (numberFrozenFrames - 4));
+ }
+
+ /**
+ * Adds the freezing score.
+ *
+ * @param {tr.v.HistogramSet} histograms
+ * @param {tr.v.Histogram} frameHist - The frame distribution.
+ * See getFrameDistribution.
+ */
+ function addFreezingScore(histograms, frameHist) {
+ let numberVsyncs = 0;
+ let freezingScore = 0;
+ let frozenFramesCount = 0;
+ for (let ticks = 1; ticks < frameHist.allBins.length; ++ticks) {
+ const count = frameHist.allBins[ticks].count;
+ numberVsyncs += ticks * count;
+ if (ticks >= FROZEN_FRAME_VSYNC_COUNT_THRESHOLD) {
+ // The first frame of the series is not considered frozen.
+ frozenFramesCount += count * (ticks - 1);
+ freezingScore += count * frozenPenaltyWeight(ticks - 1);
+ }
+ }
+ freezingScore = 1 - freezingScore / numberVsyncs;
+ if (freezingScore < 0) {
+ freezingScore = 0;
+ }
+ histograms.createHistogram('WebRTCRendering_frozen_frames_count',
+ count_smallerIsBetter, frozenFramesCount, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ histograms.createHistogram('WebRTCRendering_freezing_score',
+ percentage_biggerIsBetter, freezingScore, {
+ summaryOptions: SUMMARY_OPTIONS,
+ });
+ }
+
+ /**
+ * Get the drift time statistics.
+ *
+ * This method will calculate:
+ * - Drift Time: The difference between the Actual Render Begin and the Ideal
+ * Render Instant for each event.
+ * - Rendering Length Error: The alignment error of the Ideal Render
+ * Instants. The Ideal Render Instants should be equally spaced by
+ * intervals of length VSYNC_DURATION_US. The Rendering Length error
+ * measures how much they are misaligned.
+ *
+ * @param {Array.<event>} events - An array of events.
+ * @returns {Object.<Array.<Number>, Number>} - The drift time and rendering
+ * length error.
+ */
+ function getDriftStats(events) {
+ const driftTime = [];
+ const discrepancy = [];
+ let oldIdealRender = 0;
+ let expectedIdealRender = 0;
+
+ for (const event of events) {
+ const currentIdealRender = event.args[IDEAL_RENDER_INSTANT_NAME];
+ // The expected time of the next 'Ideal Render' event begins as the
+ // current 'Ideal Render' time and increases by VSYNC_DURATION_US on every
+ // frame.
+ expectedIdealRender += VSYNC_DURATION_US;
+ if (currentIdealRender === oldIdealRender) {
+ continue;
+ }
+ const actualRenderBegin = event.args[ACTUAL_RENDER_BEGIN_NAME];
+ // When was the frame rendered vs. when it would've been ideal.
+ driftTime.push(actualRenderBegin - currentIdealRender);
+ // The discrepancy is the absolute difference between the current Ideal
+ // Render and the expected Ideal Render.
+ discrepancy.push(Math.abs(currentIdealRender - expectedIdealRender));
+ expectedIdealRender = currentIdealRender;
+ oldIdealRender = currentIdealRender;
+ }
+
+ const discrepancySum = tr.b.math.Statistics.sum(discrepancy) -
+ discrepancy[0];
+ const lastIdealRender =
+ events[events.length - 1].args[IDEAL_RENDER_INSTANT_NAME];
+ const firstIdealRender = events[0].args[IDEAL_RENDER_INSTANT_NAME];
+ const idealRenderSpan = lastIdealRender - firstIdealRender;
+
+ const renderingLengthError = discrepancySum / idealRenderSpan;
+
+ return {driftTime, renderingLengthError};
+ }
+
+ /**
+ * Get the smoothness stats from the normalized drift time.
+ *
+ * This method will calculate the smoothness score, along with the percentage
+ * of frames badly out of sync and the percentage of frames out of sync.
+ * To be considered badly out of sync, a frame has to have missed rendering by
+ * at least 2 * VSYNC_DURATION_US.
+ * To be considered out of sync, a frame has to have missed rendering by at
+ * least one VSYNC_DURATION_US.
+ * The smoothness score is a measure of how out of sync the frames are.
+ *
+ * @param {Array.<Number>} driftTimes - See getDriftStats.
+ * @returns {Object.<Number, Number, Number>} - The percentBadlyOutOfSync,
+ * percentOutOfSync and smoothnesScore calculated from the driftTimes array.
+ */
+ function getSmoothnessStats(driftTimes) {
+ const meanDriftTime = tr.b.math.Statistics.mean(driftTimes);
+ const normDriftTimes = driftTimes.map(driftTime =>
+ Math.abs(driftTime - meanDriftTime));
+
+ // How many times is a frame later/earlier than T=2*VSYNC_DURATION_US. Time
+ // is in microseconds
+ const framesSeverelyOutOfSync = normDriftTimes
+ .filter(driftTime => driftTime > 2 * VSYNC_DURATION_US)
+ .length;
+ // How many times is a frame later/earlier than VSYNC_DURATION_US.
+ const framesOutOfSync = normDriftTimes
+ .filter(driftTime => driftTime > VSYNC_DURATION_US)
+ .length;
+
+ const percentBadlyOutOfSync = framesSeverelyOutOfSync /
+ driftTimes.length;
+ const percentOutOfSync = framesOutOfSync / driftTimes.length;
+
+ const framesOutOfSyncOnlyOnce = framesOutOfSync - framesSeverelyOutOfSync;
+
+ // Calculate smoothness metric. From the formula, we can see that smoothness
+ // score can be negative.
+ let smoothnessScore = 1 - (framesOutOfSyncOnlyOnce +
+ SEVERITY * framesSeverelyOutOfSync) / driftTimes.length;
+
+ // Minimum smoothness_score value allowed is zero.
+ if (smoothnessScore < 0) {
+ smoothnessScore = 0;
+ }
+
+ return {
+ framesOutOfSync,
+ framesSeverelyOutOfSync,
+ percentBadlyOutOfSync,
+ percentOutOfSync,
+ smoothnessScore
+ };
+ }
+
+ return {
+ webrtcRenderingMetric,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html
new file mode 100644
index 00000000000..17505faa9d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html
@@ -0,0 +1,456 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/metrics/webrtc/webrtc_rendering_metric.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const DISPLAY_HERTZ = 60.0;
+ const VSYNC_DURATION_US = 1e6 / DISPLAY_HERTZ;
+
+ /**
+ * @param {Array.<Number>} pair - An array of length 2, from which a valid
+ * WebMediaPlayerMS event will be generated.
+ * @returns {event} A valid WebMediaPlayerMS event where the Ideal Render
+ * Instant is the first element and the Actual Render Begin is the second
+ * element.
+ */
+ function eventFromPair(pair) {
+ return {
+ title: 'UpdateCurrentFrame',
+ start: pair[1],
+ duration: 1,
+ args: {
+ 'Ideal Render Instant': pair[0],
+ 'Actual Render Begin': pair[1],
+ 'Actual Render End': 0,
+ 'Serial': 0,
+ }
+ };
+ }
+
+ /**
+ * @param {Array.<Number>} driftTimes - An array with the desired driftTimes.
+ * @return {Array.<event>} An array of events such that the drift times
+ * computed by webrtcRenderingMetric is the same as driftTimes.
+ */
+ function eventsFromDriftTimes(driftTimes) {
+ const pairs = [];
+ for (let i = 1; i <= driftTimes.length; ++i) {
+ pairs.push([i, i + driftTimes[i - 1]]);
+ }
+ return pairs.map(eventFromPair);
+ }
+
+ /**
+ * @param {Array.<Number>} normDriftTimes - To decide if a frame is out of
+ * sync or badly out of sync, we use the normalized drift times, that we get
+ * by subtracting the mean from each entry of the drift times array. The sum
+ * of the normDriftTimes must equal 0.
+ * @return {Array.<event>) An array of events such that when we normalize the
+ * drift times computed by webrtcRenderingMetric, we get the normDriftTimes
+ * array.
+ */
+ function eventsFromNormDriftTimes(normDriftTimes) {
+ /* Let
+ * B[i] = normDriftTimes[i]
+ * A[i] = driftTimes[i] be the array we want to find.
+ *
+ * We require that:
+ * sum(B[i]) = 0
+ *
+ * Then
+ * B[i] = A[i] - mean(A)
+ * => B[i] - B[0] = A[i] - mean(A) - A[0] + mean(A)
+ * => B[i] - B[0] = A[i] - A[0]
+ * => A[i] = B[i] - B[0] + A[0]
+ *
+ * We can fix A[0] to any number we want.
+ *
+ * Let's make sure that the array A we found generates the array B when
+ * normalized:
+ * A[i] - mean(A)
+ * = A[i] - sum(A[j]) / n
+ * = B[i] - B[0] + A[0] - sum(B[j] - B[0] + A[0]) / n
+ * = B[i] - B[0] + A[0] - (sum(B[j]) - n B[0] / n + n A[0] / n)
+ * = B[i] - B[0] + A[0] - sum(B[j]) + B[0] - A[0]
+ * = B[i] - sum(B[j])
+ * = B[i] since we require sum(B[j]) = 0
+ */
+ const driftTimes = [10000];
+ for (let i = 1; i < normDriftTimes.length; ++i) {
+ driftTimes.push(normDriftTimes[i] - normDriftTimes[0] + driftTimes[0]);
+ }
+ return eventsFromDriftTimes(driftTimes);
+ }
+
+ /**
+ * @param {Array.<Array.<Number>>} frameDistribution - An array of pairs
+ * encoding the source to output distribution. That is an array where each
+ * [ticks, count] entry says that there are 'count' frames that are displayed
+ * 'ticks' times.
+ * @returns {Array.<events>} The events that give rise to the given
+ * frameDistribution.
+ */
+ function eventsFromFrameDistribution(frameDistribution) {
+ let frameId = 0;
+ const pairs = [];
+ for (const [ticks, count] of frameDistribution) {
+ // We need 'count' runs, each run consisting of 'ticks' repeated elements.
+ for (let i = 0; i < count; ++i) {
+ frameId += 1;
+ for (let j = 0; j < ticks; ++j) {
+ // Frames are decided by the Ideal Render Instant.
+ pairs.push([frameId, 0]);
+ }
+ }
+ }
+ return pairs.map(eventFromPair);
+ }
+
+ function newModel(fakeEvents) {
+ function customizeModelCallback(model) {
+ const rendererProcess = model.getOrCreateProcess(1);
+ const mainThread = rendererProcess.getOrCreateThread(2);
+ mainThread.name = 'Compositor';
+ for (const event of fakeEvents) {
+ mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(event));
+ }
+ }
+ return tr.c.TestUtils.newModelWithEvents([], {customizeModelCallback});
+ }
+
+ function runWebrtcRenderingMetric(fakeEvents) {
+ const histograms = new tr.v.HistogramSet();
+ const model = newModel(fakeEvents);
+ tr.metrics.webrtc.webrtcRenderingMetric(histograms, model);
+ return histograms;
+ }
+
+ test('frameDistribution', function() {
+ // These numbers don't mean anything, we just want to make sure we can
+ // recover them after running webrtcRenderingMetric.
+ const frameDistribution = [[10, 3], [5, 15], [3, 146], [1, 546], [2, 10]];
+ const frameHist = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ for (const [ticks, count] of frameDistribution) {
+ for (let i = 0; i < count; ++i) {
+ frameHist.addSample(ticks);
+ }
+ }
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ // We don't have access to the values stored in the histogram, so we check
+ // for equality in the summary statistics.
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frame_distribution');
+ assert.strictEqual(hist.sum, frameHist.sum);
+ assert.strictEqual(hist.numValues, frameHist.numValues);
+ assert.strictEqual(hist.running.min, frameHist.running.min);
+ assert.strictEqual(hist.running.max, frameHist.running.max);
+ assert.closeTo(hist.standardDeviation, frameHist.standardDeviation, 1e-2);
+ });
+
+ test('driftTime', function() {
+ // These numbers don't mean anything. We just want to make sure we can
+ // recover them after running the metric.
+ const fakeDriftTimes = [16700, 17640, 15000, 24470, 16700, 14399, 17675];
+ const fakeEvents = eventsFromDriftTimes(fakeDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ // We don't have access to the values stored in the histogram, so we check
+ // for equality in the summary statistics.
+ const hist = histograms.getHistogramNamed('WebRTCRendering_drift_time');
+ assert.strictEqual(hist.sum, tr.b.math.Statistics.sum(fakeDriftTimes));
+ assert.strictEqual(hist.numValues, fakeDriftTimes.length);
+ assert.strictEqual(hist.running.min,
+ tr.b.math.Statistics.min(fakeDriftTimes));
+ assert.strictEqual(hist.running.max,
+ tr.b.math.Statistics.max(fakeDriftTimes));
+ assert.closeTo(hist.standardDeviation,
+ tr.b.math.Statistics.stddev(fakeDriftTimes), 1e-2);
+ });
+
+ test('framesBadlyOutOfSyncPerfect', function() {
+ // None of these will exceed the threshold for badly out of sync events,
+ // which is about 33 333.
+ const normDriftTimes = [-16700, 17640, 15000, -17640, -15000, 16700];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frames_badly_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 0);
+ });
+
+ test('framesBadylOutOfSync', function() {
+ // Only 34 000 will exceed the threshold for badly out of sync events,
+ // which is about 33 333.
+ const normDriftTimes = [-34000, 10000, 10000, 10000, 4000];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frames_badly_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 1);
+ });
+
+ test('framesOutOfSyncPerfect', function() {
+ // None of these will exceed the threshold for badly out of sync, which is
+ // about 16 667.
+ const normDriftTimes = [-16600, 15640, 15000, -15640, -15000, 16600];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frames_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 0);
+ });
+
+ test('framesOutOfSync', function() {
+ // Only 17000 will exceed the threshold for badly out of sync, which is
+ // about 16 667.
+ const normDriftTimes = [-17000, 5000, 5000, 5000, 2000];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frames_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 1);
+ });
+
+ test('percentBadlyOutOfSyncPerfect', function() {
+ // None of these will exceed the threshold for badly out of sync events,
+ // which is about 33 333.
+ const normDriftTimes = [-16700, 17640, 15000, -17640, -15000, 16700];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_percent_badly_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 0);
+ });
+
+ test('percentBadylOutOfSync', function() {
+ // Only 34 000 will exceed the threshold for badly out of sync events,
+ // which is about 33 333.
+ const normDriftTimes = [-34000, 10000, 10000, 10000, 4000];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_percent_badly_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, .2);
+ });
+
+ test('percentOutOfSyncPerfect', function() {
+ // None of these will exceed the threshold for badly out of sync, which is
+ // about 16 667.
+ const normDriftTimes = [-16600, 15640, 15000, -15640, -15000, 16600];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_percent_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 0);
+ });
+
+ test('percentOutOfSync', function() {
+ // Only 17000 will exceed the threshold for badly out of sync, which is
+ // about 16 667.
+ const normDriftTimes = [-17000, 5000, 5000, 5000, 2000];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_percent_out_of_sync');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, .2);
+ });
+
+ test('smoothnessScorePerfect', function() {
+ // None of these will exceed the threshold for badly out of sync, which is
+ // about 16 667, so the smoothnessScore wil be perfect.
+ const normDriftTimes = [-16600, 15640, 15000, -15640, -15000, 16600];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_smoothness_score');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 1);
+ });
+
+ test('smoothnessScore', function() {
+ // One will exceed the threshold for frames badly out of sync (33 333) and
+ // another two the threshold for frames out of sync (16 667). So the
+ // smoothness score is
+ // 1 - (frames out of sync + 3 * frames badly out of sync) / n
+ // = 1 - (2 + 3) / 5 = 0
+ const normDriftTimes = [-17000, 34000, -17000, -10000, 10000];
+ assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0);
+ const fakeEvents = eventsFromNormDriftTimes(normDriftTimes);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_smoothness_score');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 0);
+ });
+
+ test('fpsPerfect', function() {
+ // Every frame is displayed once. This is a perfect FPS of 60.
+ const frameDistribution = [[1, 10]];
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist = histograms.getHistogramNamed('WebRTCRendering_fps');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 60);
+ });
+
+ test('fps', function() {
+ // Every frame is displayed 15 times. This means an FPS of 4.
+ const frameDistribution = [[15, 10]];
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist = histograms.getHistogramNamed('WebRTCRendering_fps');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 4);
+ });
+
+ test('frozenFramesCountPerfect', function() {
+ // 10 frames are displayed one time, and 10 frames are displayed twice.
+ // This means no frames exceed the threshold of 6, and so no frames are
+ // considered frozen.
+ const frameDistribution = [[1, 10], [2, 10]];
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frozen_frames_count');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 0);
+ });
+
+ test('frozenFramesCount', function() {
+ // 82 frames are displayed 1 time, 5 frames are displayed 2 times,
+ // and 1 frame is displayed 6 times.
+ // Only the drame displayed 6 times satisfies the threshold of 6. Since the
+ // first appearance is not considered frozen, there are 5 frozen frames.
+ const frameDistribution = [[1, 82], [2, 5], [6, 1]];
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_frozen_frames_count');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 5);
+ });
+
+ test('freezingScorePerfect', function() {
+ // Every frame is displayed 1 times. This means a perfect freezing score of
+ // 100.
+ const frameDistribution = [[1, 10]];
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist = histograms.getHistogramNamed('WebRTCRendering_freezing_score');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, 1);
+ });
+
+ test('freezingScore', function() {
+ // 82 frames are displayed 1 time, 5 frames are displayed 2 times,
+ // and 1 frame is displayed 8 times.
+ // This means that the total number of frames displayed is
+ // 82 * 1 + 5 * 2 + 1 * 8 = 100
+ // And the freezing score is
+ // 1 - 82 / 100 * weight[0]
+ // - 5 / 100 * weight[1]
+ // - 1 / 100 * weight[7]
+ // = 1 - .82 * 0 since weight[0] = 0
+ // - .05 * 0 since weight[1] = 0 too
+ // - .01 * 15 since weight[7] = 15
+ // = 1 - .15 = .85
+ // See frozenPenaltyWeight for information on the weights and
+ // addFreezingScore for the definition of the freezingScore.
+ const frameDistribution = [[1, 82], [2, 5], [8, 1]];
+ const fakeEvents = eventsFromFrameDistribution(frameDistribution);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist = histograms.getHistogramNamed('WebRTCRendering_freezing_score');
+ assert.strictEqual(hist.numValues, 1);
+ assert.strictEqual(hist.running.mean, .85);
+ });
+
+ test('renderingLengthErrorPerfect', function() {
+ const fakePairs = [];
+ for (let i = 1; i < 10; ++i) {
+ // Each frame's Ideal Render Instant is exactly VSYNC_DURATION_US after
+ // the previous one, so that the rendering length error is 0.
+ fakePairs.push([VSYNC_DURATION_US * i, 0]);
+ }
+ const fakeEvents = fakePairs.map(eventFromPair);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_rendering_length_error');
+ assert.strictEqual(hist.numValues, 1);
+ assert.closeTo(hist.running.mean, 0, 1e-3);
+ });
+
+ test('renderingLengthError', function() {
+ const errors = [1000, 3000, 0, 5000, 0, 2000];
+ const fakePairs = [[1, 0]];
+ for (let i = 0; i < errors.length; ++i) {
+ // Each frame's Ideal Render Instant is close to VSYNC_DURATION_US after
+ // the previous one, but with a known delay.
+ fakePairs.push([fakePairs[i][0] + VSYNC_DURATION_US + errors[i], 0]);
+ }
+
+ // The rendering length error is then the sum of the errors, normalized by
+ // the span between the first and the last Ideal Render Instants.
+ const idealRenderSpan = fakePairs[fakePairs.length - 1][0] -
+ fakePairs[0][0];
+ const expectedRenderingLengthError = tr.b.math.Statistics.sum(errors) /
+ idealRenderSpan;
+
+ const fakeEvents = fakePairs.map(eventFromPair);
+ const histograms = runWebrtcRenderingMetric(fakeEvents);
+
+ const hist =
+ histograms.getHistogramNamed('WebRTCRendering_rendering_length_error');
+ assert.strictEqual(hist.numValues, 1);
+ assert.closeTo(hist.running.mean, expectedRenderingLengthError, 1e-3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/activity.html b/chromium/third_party/catapult/tracing/tracing/model/activity.html
new file mode 100644
index 00000000000..2db257c7e06
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/activity.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/color_scheme.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class representing a user activity that is running
+ * in the process.
+ * On the Android platform, activities are mapped to Android Activities
+ * running in the foreground of the process.
+ * On Windows/OS X this could for example represent
+ * the currently active window of the process.
+ */
+tr.exportTo('tr.model', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ /**
+ * @constructor
+ * @param {String} name Name of the activity
+ * @param {String} category Category of the activities
+ * @param {String} range The time range where the activity was running
+ * @param {String} args Additional arguments
+ */
+ function Activity(name, category, range, args) {
+ tr.model.TimedEvent.call(this, range.min);
+ this.title = name;
+ this.category = category;
+ this.colorId = ColorScheme.getColorIdForGeneralPurposeString(name);
+ this.duration = range.duration;
+ this.args = args;
+ this.name = name;
+ }
+
+ Activity.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ shiftTimestampsForward(amount) {
+ this.start += amount;
+ },
+
+ addBoundsToRange(range) {
+ range.addValue(this.start);
+ range.addValue(this.end);
+ }
+ };
+ return {
+ Activity,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/alert.html b/chromium/third_party/catapult/tracing/tracing/model/alert.html
new file mode 100644
index 00000000000..a8955caec93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/alert.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ function Alert(info, start, opt_associatedEvents, opt_args) {
+ tr.model.TimedEvent.call(this, start);
+ this.info = info;
+ this.args = opt_args || {};
+ this.associatedEvents = new tr.model.EventSet(opt_associatedEvents);
+ this.associatedEvents.forEach(function(event) {
+ event.addAssociatedAlert(this);
+ }, this);
+ }
+
+ Alert.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ get title() {
+ return this.info.title;
+ },
+
+ get colorId() {
+ return this.info.colorId;
+ },
+
+ get userFriendlyName() {
+ return 'Alert ' + this.title + ' at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.start);
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ Alert,
+ {
+ name: 'alert',
+ pluralName: 'alerts'
+ });
+
+ return {
+ Alert,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/annotation.html b/chromium/third_party/catapult/tracing/tracing/model/annotation.html
new file mode 100644
index 00000000000..9a3c1a726ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/annotation.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/base/guid.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * Annotation is a base class that represents all annotation objects that
+ * can be drawn on the timeline.
+ *
+ * @constructor
+ */
+ function Annotation() {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.view_ = undefined;
+ }
+
+ Annotation.fromDictIfPossible = function(args) {
+ if (args.typeName === undefined) {
+ throw new Error('Missing typeName argument');
+ }
+
+ const typeInfo = Annotation.findTypeInfoMatching(function(typeInfo) {
+ return typeInfo.metadata.typeName === args.typeName;
+ });
+
+ if (typeInfo === undefined) return undefined;
+
+ return typeInfo.constructor.fromDict(args);
+ };
+
+ Annotation.fromDict = function() {
+ throw new Error('Not implemented');
+ };
+
+ Annotation.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ // Invoked by trace model when this annotation is removed.
+ onRemove() {
+ },
+
+ toDict() {
+ throw new Error('Not implemented');
+ },
+
+ getOrCreateView(viewport) {
+ if (!this.view_) {
+ this.view_ = this.createView_(viewport);
+ }
+ return this.view_;
+ },
+
+ createView_() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b. BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(Annotation, options);
+
+ Annotation.addEventListener('will-register', function(e) {
+ if (!e.typeInfo.constructor.hasOwnProperty('fromDict')) {
+ throw new Error('Must have fromDict method');
+ }
+
+ if (!e.typeInfo.metadata.typeName) {
+ throw new Error('Registered Annotations must provide typeName');
+ }
+ });
+
+ return {
+ Annotation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/annotation_test.html b/chromium/third_party/catapult/tracing/tracing/model/annotation_test.html
new file mode 100644
index 00000000000..f0d38843a3f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/annotation_test.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/comment_box_annotation.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/rect_annotation.html">
+<link rel="import" href="/tracing/model/x_marker_annotation.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('rectAnnotation', function() {
+ const fakeYComponents1 = [{stableId: '1.2', yPercentOffset: 0.3}];
+ const fakeYComponents2 = [{stableId: '1.2', yPercentOffset: 0.9}];
+ const start = new tr.model.Location(50, fakeYComponents1);
+ const end = new tr.model.Location(100, fakeYComponents2);
+ const rectAnnotation = new tr.model.RectAnnotation(start, end);
+ assert.strictEqual(rectAnnotation.startLocation, start);
+ assert.strictEqual(rectAnnotation.endLocation, end);
+ });
+
+ test('xMarkerAnnotation', function() {
+ const xMarkerAnnotation = new tr.model.XMarkerAnnotation(90);
+ assert.strictEqual(xMarkerAnnotation.timestamp, 90);
+ });
+
+ test('commentBoxAnnotation', function() {
+ const fakeYComponents = [{stableId: '1.2', yPercentOffset: 0.5}];
+ const location = new tr.model.Location(120, fakeYComponents);
+ const text = 'abc';
+ const commentBoxAnnotation =
+ new tr.model.CommentBoxAnnotation(location, text);
+ assert.strictEqual(commentBoxAnnotation.location, location);
+ assert.strictEqual(commentBoxAnnotation.text, text);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice.html
new file mode 100644
index 00000000000..9e395ab4cb7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the AsyncSlice class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A AsyncSlice represents an interval of time during which an
+ * asynchronous operation is in progress. An AsyncSlice consumes no CPU time
+ * itself and so is only associated with Threads at its start and end point.
+ *
+ * @constructor
+ */
+ function AsyncSlice(category, title, colorId, start, args, duration,
+ opt_isTopLevel, opt_cpuStart, opt_cpuDuration,
+ opt_argsStripped) {
+ tr.model.TimedEvent.call(this, start);
+
+ this.category = category || '';
+ // We keep the original title from the trace file in originalTitle since
+ // some sub-classes, e.g. NetAsyncSlice, change the title field.
+ this.originalTitle = title;
+ this.title = title;
+ this.colorId = colorId;
+ this.args = args;
+ this.startStackFrame = undefined;
+ this.endStackFrame = undefined;
+ this.didNotFinish = false;
+ this.important = false;
+ this.subSlices = [];
+ this.parentContainer_ = undefined;
+
+ this.id = undefined;
+ this.startThread = undefined;
+ this.endThread = undefined;
+ this.cpuStart = undefined;
+ this.cpuDuration = undefined;
+ this.argsStripped = false;
+
+ this.startStackFrame = undefined;
+ this.endStackFrame = undefined;
+
+ this.duration = duration;
+
+ // isTopLevel is set at import because only NESTABLE_ASYNC events might not
+ // be topLevel. All legacy async events are toplevel by definition.
+ this.isTopLevel = (opt_isTopLevel === true);
+
+ if (opt_cpuStart !== undefined) {
+ this.cpuStart = opt_cpuStart;
+ }
+
+ if (opt_cpuDuration !== undefined) {
+ this.cpuDuration = opt_cpuDuration;
+ }
+
+ if (opt_argsStripped !== undefined) {
+ this.argsStripped = opt_argsStripped;
+ }
+ }
+
+ AsyncSlice.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ get analysisTypeName() {
+ return this.title;
+ },
+
+ get parentContainer() {
+ return this.parentContainer_;
+ },
+
+ set parentContainer(parentContainer) {
+ this.parentContainer_ = parentContainer;
+ for (let i = 0; i < this.subSlices.length; i++) {
+ const subSlice = this.subSlices[i];
+ if (subSlice.parentContainer === undefined) {
+ subSlice.parentContainer = parentContainer;
+ }
+ }
+ },
+
+ get viewSubGroupTitle() {
+ return this.title;
+ },
+
+ // Certain AsyncSlices can provide a grouping key to group a set of
+ // independent tracks, while grouping by |viewSubGroupTitle| puts slices
+ // into a single (maybe multi-row but single) track.
+ get viewSubGroupGroupingKey() {
+ return undefined;
+ },
+
+ get userFriendlyName() {
+ return 'Async slice ' + this.title + ' at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.start);
+ },
+
+ get stableId() {
+ const parentAsyncSliceGroup = this.parentContainer.asyncSliceGroup;
+ return parentAsyncSliceGroup.stableId + '.' +
+ parentAsyncSliceGroup.slices.indexOf(this);
+ },
+
+ * findTopmostSlicesRelativeToThisSlice(eventPredicate, opt_this) {
+ if (eventPredicate(this)) {
+ yield this;
+ return;
+ }
+ for (const s of this.subSlices) {
+ yield* s.findTopmostSlicesRelativeToThisSlice(eventPredicate);
+ }
+ },
+
+ findDescendentSlice(targetTitle) {
+ if (!this.subSlices) return undefined;
+
+ for (let i = 0; i < this.subSlices.length; i++) {
+ if (this.subSlices[i].title === targetTitle) {
+ return this.subSlices[i];
+ }
+ const slice = this.subSlices[i].findDescendentSlice(targetTitle);
+ if (slice) return slice;
+ }
+ return undefined;
+ },
+
+ * enumerateAllDescendents() {
+ for (const slice of this.subSlices) {
+ yield slice;
+ }
+ for (const slice of this.subSlices) {
+ // Slices might contain sub-events different from AsyncSlice.
+ // We don't go any deeper in that case.
+ if (slice.enumerateAllDescendents !== undefined) {
+ yield* slice.enumerateAllDescendents();
+ }
+ }
+ },
+
+ compareTo(that) {
+ return this.title.localeCompare(that.title);
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ AsyncSlice,
+ {
+ name: 'asyncSlice',
+ pluralName: 'asyncSlices'
+ });
+
+
+ return {
+ AsyncSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html
new file mode 100644
index 00000000000..249e6396667
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html
@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/async_slice.html">
+<link rel="import" href="/tracing/model/event_container.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the AsyncSliceGroup class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * Group of AsyncSlices associated with a thread or an upper-level
+ * AsyncSliceGroup. Thread can have number of AsyncSliceGroups and these
+ * groups can have nested groups too. No further nested levels are allowed.
+ */
+ class AsyncSliceGroup extends tr.model.EventContainer {
+ /**
+ * @param {Thread} parentContainer Thread on which async slices start.
+ * @param {String} opt_name Optional name (no IDs please) for settings key.
+ */
+ constructor(parentContainer, opt_name) {
+ super();
+ this.parentContainer_ = parentContainer;
+ this.name_ = opt_name;
+ this.slices = [];
+ this.viewSubGroups_ = undefined;
+
+ // Default values for the root group.
+ // Nested groups will get these values reassigned.
+ this.nestedLevel_ = 0;
+ this.hasNestedSubGroups_ = true;
+ this.title_ = undefined;
+ }
+
+ get parentContainer() {
+ return this.parentContainer_;
+ }
+
+ get model() {
+ return this.parentContainer_.parent.model;
+ }
+
+ get stableId() {
+ return this.parentContainer_.stableId + '.AsyncSliceGroup';
+ }
+
+ get title() {
+ // Title isn't defined for the root group (nested level 0) because
+ // slices it contains aren't grouped on that level.
+ // All the nested groups have their title which is:
+ // - |slice.viewSubGroupGroupingKey| if defined (level 1 only), or
+ // - |slice.viewSubGroupTitle| otherwise (most cases).
+ if (this.nested_level_ === 0) {
+ return '<root>';
+ }
+ return this.title_;
+ }
+
+ getSettingsKey() {
+ if (this.name_ === undefined) {
+ return undefined;
+ }
+ const parentKey = this.parentContainer_.getSettingsKey();
+ if (parentKey === undefined) {
+ return undefined;
+ }
+ return parentKey + '.' + this.name_;
+ }
+
+ /**
+ * Helper function that pushes the provided slice onto the slices array.
+ */
+ push(slice) {
+ if (this.viewSubGroups_ !== undefined) {
+ throw new Error(
+ 'No new slices are allowed when view sub-groups already formed.');
+ }
+ slice.parentContainer = this.parentContainer;
+ this.slices.push(slice);
+ return slice;
+ }
+
+ /**
+ * @return {Number} The number of slices in this group.
+ */
+ get length() {
+ return this.slices.length;
+ }
+
+ /**
+ * Shifts all the timestamps inside this group forward by the amount
+ * specified, including all nested subSlices if there are any.
+ */
+ shiftTimestampsForward(amount) {
+ for (const slice of this.childEvents()) {
+ slice.start += amount;
+ }
+ }
+
+ /**
+ * Updates the bounds for this group based on the slices it contains.
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Closes any open slices.
+ * All open slices assumed as finished at the end of model's time bounds.
+ */
+ autoCloseOpenSlices() {
+ const maxTimestamp = this.parentContainer_.parent.model.bounds.max;
+ for (const slice of this.childEvents()) {
+ if (slice.didNotFinish) {
+ slice.duration = maxTimestamp - slice.start;
+ }
+ }
+ }
+
+ /**
+ * Get AsyncSlice sub-groups arranged by title and grouping key.
+ *
+ * @return {Array} An array of AsyncSliceGroups where each group has
+ * a title and sub-set of the original slices. Returns an empty array
+ * if group can't be sub-divided.
+ */
+ get viewSubGroups() {
+ // Only 2 nested levels are allowed (see class description).
+ // Also it's always known in advance whether group will be sub-divided or
+ // not (most of them). Root group is always divisible.
+ if (!this.hasNestedSubGroups_ || this.nestedLevel_ === 2) {
+ return [];
+ }
+ if (this.viewSubGroups_ !== undefined) {
+ return this.viewSubGroups_;
+ }
+
+ const subGroupsByTitle = new Map();
+
+ for (const slice of this.slices) {
+ // Group by title by default.
+ let subGroupTitle = slice.viewSubGroupTitle;
+ let hasNestedSubGroups = false;
+
+ // Apply custom grouping rules for special slice classes.
+ if (this.nestedLevel_ === 0 &&
+ slice.viewSubGroupGroupingKey !== undefined) {
+ subGroupTitle = slice.viewSubGroupGroupingKey;
+ hasNestedSubGroups = true;
+ }
+
+ let subGroup = subGroupsByTitle.get(subGroupTitle);
+ if (subGroup === undefined) {
+ let name;
+ if (this.name_ !== undefined) {
+ name = this.name_ + '.' + subGroupTitle;
+ } else {
+ name = subGroupTitle;
+ }
+ subGroup = new AsyncSliceGroup(this.parentContainer_, name);
+ subGroup.title_ = subGroupTitle;
+ subGroup.hasNestedSubGroups_ = hasNestedSubGroups;
+ subGroup.nestedLevel_ = this.nestedLevel_ + 1;
+ subGroupsByTitle.set(subGroupTitle, subGroup);
+ }
+ subGroup.push(slice);
+ }
+
+ this.viewSubGroups_ = Array.from(subGroupsByTitle.values());
+ this.viewSubGroups_.sort((a, b) => a.title.localeCompare(b.title));
+ return this.viewSubGroups_;
+ }
+
+ * findTopmostSlicesInThisContainer(eventPredicate, opt_this) {
+ for (const slice of this.slices) {
+ if (slice.isTopLevel) {
+ yield* slice.findTopmostSlicesRelativeToThisSlice(
+ eventPredicate, opt_this);
+ }
+ }
+ }
+
+ * childEvents() {
+ for (const slice of this.slices) {
+ yield slice;
+ yield* slice.enumerateAllDescendents();
+ }
+ }
+
+ * childEventContainers() {
+ }
+ }
+
+ return {
+ AsyncSliceGroup,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html
new file mode 100644
index 00000000000..f996a63f2b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome_config.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Process = tr.model.Process;
+ const Thread = tr.model.Thread;
+ const AsyncSlice = tr.model.AsyncSlice;
+ const AsyncSliceGroup = tr.model.AsyncSliceGroup;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+ const newModel = tr.c.TestUtils.newModel;
+
+ test('asyncSliceGroupBounds_Empty', function() {
+ const thread = {};
+ const g = new AsyncSliceGroup(thread);
+ g.updateBounds();
+ assert.isTrue(g.bounds.isEmpty);
+ });
+
+ test('asyncSliceGroupBounds_Basic', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = p1.getOrCreateThread(456);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(1, 1.5, t1, t1));
+ assert.strictEqual(g.length, 2);
+ g.updateBounds();
+ assert.strictEqual(g.bounds.min, 0);
+ assert.strictEqual(g.bounds.max, 2.5);
+ });
+
+ test('asyncSliceGroupChildEvents', function() {
+ const t1 = {}; // Fake thread.
+ const g = new AsyncSliceGroup(t1);
+ const sl1 = newAsyncSlice(0, 1, t1, t1);
+ const sl2 = newAsyncSlice(1, 3, t1, t1);
+ const sl2sub1 = newAsyncSlice(1, 2, t1, t1);
+ sl2.subSlices.push(sl2sub1);
+ const sl2sub1sub1 = newAsyncSlice(2, 1, t1, t1);
+ sl2sub1.subSlices.push(sl2sub1sub1);
+ const sl2sub2 = newAsyncSlice(3, 1, t1, t1);
+ sl2.subSlices.push(sl2sub2);
+ g.push(sl1);
+ g.push(sl2);
+
+ assert.sameMembers(
+ Array.from(g.childEvents()), [sl1, sl2, sl2sub1, sl2sub1sub1, sl2sub2]);
+ });
+
+ test('asyncSliceGroupShiftTimestamps', function() {
+ const t1 = {}; // Fake thread.
+ const g = new AsyncSliceGroup(t1);
+ const sl1 = newAsyncSlice(1, 2, t1, t1);
+ const sl2 = newAsyncSlice(3, 4, t1, t1);
+ const sl2sub1 = newAsyncSlice(3.5, 2, t1, t1);
+ sl2.subSlices.push(sl2sub1);
+ const sl2sub1sub1 = newAsyncSlice(4, 0.5, t1, t1);
+ sl2sub1.subSlices.push(sl2sub1sub1);
+ g.push(sl1);
+ g.push(sl2);
+
+ g.updateBounds();
+ assert.strictEqual(g.bounds.min, 1);
+ assert.strictEqual(g.bounds.max, 7);
+
+ g.shiftTimestampsForward(1.5);
+ g.updateBounds();
+ assert.strictEqual(g.bounds.min, 2.5);
+ assert.strictEqual(g.bounds.max, 8.5);
+ assert.strictEqual(sl2sub1.start, 5);
+ assert.strictEqual(sl2sub1.duration, 2);
+ assert.strictEqual(sl2sub1sub1.start, 5.5);
+ assert.strictEqual(sl2sub1sub1.duration, 0.5);
+ });
+
+ test('asyncSliceGroupViewSubGroups', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ p1.name = 'Renderer';
+ const t1 = p1.getOrCreateThread(321);
+ t1.name = 'MainThread';
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSliceEx(
+ { title: 'VeryBusy',
+ start: 0, duration: 1 }));
+ g.push(newAsyncSliceEx(
+ { cat: 'renderer.scheduler',
+ id: ':ptr:0xdeadbeef', title: 'FrameScheduler.Foo',
+ start: 0.5, duration: 0.1 }));
+ g.push(newAsyncSliceEx(
+ { cat: 'renderer.scheduler',
+ id: ':ptr:0xdeadbeef', title: 'FrameScheduler.Bar',
+ start: 0.55, duration: 0.2 }));
+ g.push(newAsyncSliceEx(
+ { cat: 'renderer.scheduler',
+ id: ':ptr:0x1ee7beef', title: 'FrameScheduler.Baz',
+ start: 0.3, duration: 0.3 }));
+ g.push(newAsyncSliceEx(
+ { cat: 'renderer.scheduler',
+ id: ':ptr:0x1ee7beef', title: 'FrameScheduler.Baz',
+ start: 0.7, duration: 0.2 }));
+ g.push(newAsyncSliceEx(
+ { title: 'VeryBusy',
+ start: 1, duration: 1.5 }));
+ g.push(newAsyncSliceEx(
+ { title: 'Loading',
+ start: 0, duration: 5 }));
+ assert.strictEqual(g.length, 7);
+
+ const vsg = g.viewSubGroups;
+ assert.strictEqual(vsg.length, 4);
+ // Groups must be sorted by title.
+ assert.strictEqual(vsg[0].title, 'Frame:ptr:0x1ee7beef');
+ assert.strictEqual(vsg[1].title, 'Frame:ptr:0xdeadbeef');
+ assert.strictEqual(vsg[2].title, 'Loading');
+ assert.strictEqual(vsg[3].title, 'VeryBusy');
+ // Check nested view sub-groups.
+ assert.strictEqual(vsg[2].viewSubGroups.length, 0);
+ assert.strictEqual(vsg[3].viewSubGroups.length, 0);
+ const wf1vsg = vsg[0].viewSubGroups;
+ assert.strictEqual(wf1vsg.length, 1);
+ assert.strictEqual(wf1vsg[0].title, 'Baz');
+ assert.strictEqual(wf1vsg[0].getSettingsKey(),
+ 'processes.Renderer.MainThread.Frame:ptr:0x1ee7beef.Baz');
+ const wf2vsg = vsg[1].viewSubGroups;
+ assert.strictEqual(wf2vsg.length, 2);
+ assert.strictEqual(wf2vsg[0].title, 'Bar');
+ assert.strictEqual(wf2vsg[0].getSettingsKey(),
+ 'processes.Renderer.MainThread.Frame:ptr:0xdeadbeef.Bar');
+ assert.strictEqual(wf2vsg[1].title, 'Foo');
+ assert.strictEqual(wf2vsg[1].getSettingsKey(),
+ 'processes.Renderer.MainThread.Frame:ptr:0xdeadbeef.Foo');
+ });
+
+ test('asyncSliceGroupStableId', function() {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new AsyncSliceGroup(thread);
+
+ assert.strictEqual(process.stableId, 123);
+ assert.strictEqual(thread.stableId, '123.456');
+ assert.strictEqual(group.stableId, '123.456.AsyncSliceGroup');
+ });
+
+ test('asyncSliceParentContainerSetAtPush', function() {
+ const m = newModel(function(m) {
+ m.process = m.getOrCreateProcess(123);
+ m.thread = m.process.getOrCreateThread(456);
+ m.group = new AsyncSliceGroup(m.thread);
+
+ m.sA = m.group.push(newAsyncSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ });
+
+ assert.deepEqual(m.sA.parentContainer, m.thread);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html
new file mode 100644
index 00000000000..1de8493035e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const Process = tr.model.Process;
+ const Thread = tr.model.Thread;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+ const newFakeThread = tr.c.TestUtils.newFakeThread;
+
+ test('stableId', function() {
+ const thread = newFakeThread();
+ const group = thread.asyncSliceGroup;
+
+ const sA = group.push(newAsyncSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.push(newAsyncSliceEx(
+ { title: 'sB', start: 10.0, duration: 20.0 }));
+ const sC = group.push(newAsyncSliceEx(
+ { title: 'sC', start: 20.0, duration: 30.0 }));
+
+ assert.strictEqual(group.stableId + '.0', sA.stableId);
+ assert.strictEqual(group.stableId + '.1', sB.stableId);
+ assert.strictEqual(group.stableId + '.2', sC.stableId);
+ });
+
+ test('setParentContainerForSubSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const asyncSlice = newAsyncSlice(0, 10, t1, t1);
+ const subSlice1 = newAsyncSlice(1, 5, t1, t1);
+ const subSlice2 = newAsyncSlice(6, 9, t1, t1);
+ const subSlice3 = newAsyncSlice(2, 3, t1, t1);
+ subSlice1.subSlices.push(subSlice3);
+ asyncSlice.subSlices.push(subSlice1);
+ asyncSlice.subSlices.push(subSlice2);
+ asyncSlice.parentContainer = t1;
+ assert.strictEqual(asyncSlice.subSlices.length, 2);
+ assert.strictEqual(subSlice1.subSlices.length, 1);
+ assert.deepEqual(asyncSlice.parentContainer, t1);
+ assert.deepEqual(subSlice1.parentContainer, t1);
+ assert.deepEqual(subSlice2.parentContainer, t1);
+ assert.deepEqual(subSlice3.parentContainer, t1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html
new file mode 100644
index 00000000000..cb9a30bad79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html
@@ -0,0 +1,467 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const ClockDomainId = {
+ BATTOR: 'BATTOR',
+
+ // NOTE: Exists for backwards compatibility with old Chrome traces which
+ // didn't explicitly specify the clock being used.
+ UNKNOWN_CHROME_LEGACY: 'UNKNOWN_CHROME_LEGACY',
+
+ LINUX_CLOCK_MONOTONIC: 'LINUX_CLOCK_MONOTONIC',
+ LINUX_FTRACE_GLOBAL: 'LINUX_FTRACE_GLOBAL',
+ MAC_MACH_ABSOLUTE_TIME: 'MAC_MACH_ABSOLUTE_TIME',
+ WIN_ROLLOVER_PROTECTED_TIME_GET_TIME:
+ 'WIN_ROLLOVER_PROTECTED_TIME_GET_TIME',
+ WIN_QPC: 'WIN_QPC',
+
+ // 'TELEMETRY' and 'SYSTRACE' aren't really clock domains because they
+ // actually can use one of several clock domains, just like Chrome. However,
+ // because there's a chance that they are running off of the same clock as
+ // Chrome (e.g. LINUX_CLOCK_MONOTONIC) but on a separate device (i.e. on a
+ // a host computer with Chrome running on an attached phone), there's a
+ // chance that Chrome and the controller will erroneously get put into
+ // the same clock domain. The solution for this is that clock domains should
+ // actually be some (unique_device_id, clock_id) tuple. For now, though,
+ // we'll hack around this by putting Telemetry into its own clock domain.
+ SYSTRACE: 'SYSTRACE',
+ TELEMETRY: 'TELEMETRY'
+ };
+
+ const POSSIBLE_CHROME_CLOCK_DOMAINS = new Set([
+ ClockDomainId.UNKNOWN_CHROME_LEGACY,
+ ClockDomainId.LINUX_CLOCK_MONOTONIC,
+ ClockDomainId.MAC_MACH_ABSOLUTE_TIME,
+ ClockDomainId.WIN_ROLLOVER_PROTECTED_TIME_GET_TIME,
+ ClockDomainId.WIN_QPC
+ ]);
+
+ // The number of milliseconds above which the BattOr sync is no longer
+ // considered "fast", and it's more accurate to use the sync start timestamp
+ // instead of the normal sync timestamp due to a bug in the Chrome serial code
+ // making serial reads too slow.
+ const BATTOR_FAST_SYNC_THRESHOLD_MS = 3;
+
+ /**
+ * A ClockSyncManager holds clock sync markers and uses them to shift
+ * timestamps from agents' clock domains onto the model's clock domain.
+ *
+ * In this context, a "clock domain" is a single perspective on the passage
+ * of time. A single computer can have multiple clock domains because it
+ * can have multiple methods of retrieving a timestamp (e.g.
+ * clock_gettime(CLOCK_MONOTONIC) and clock_gettime(CLOCK_REALTIME) on Linux).
+ * Another common reason for multiple clock domains within a single trace
+ * is that traces can span devices (e.g. a laptop collecting a Chrome trace
+ * can have its power consumption recorded by a second device and the two
+ * traces can be viewed alongside each other).
+ *
+ * For more information on how to synchronize multiple time domains using this
+ * method, see: http://bit.ly/1OVkqju.
+ *
+ * @constructor
+ */
+ function ClockSyncManager() {
+ // A set of all domains seen by the ClockSyncManager.
+ this.domainsSeen_ = new Set();
+ this.markersBySyncId_ = new Map();
+ // transformerMapByDomainId_[fromDomainId][toDomainId] returns the function
+ // that converts timestamps in the "from" domain to timestamps in the "to"
+ // domain.
+ this.transformerMapByDomainId_ = {};
+ }
+
+ ClockSyncManager.prototype = {
+ /**
+ * Adds a clock sync marker to the list of known markers.
+ *
+ * @param {string} domainId The clock domain that the marker is in.
+ * @param {string} syncId The identifier shared by both sides of the clock
+ * sync marker.
+ * @param {number} startTs The time (in ms) at which the sync started.
+ * @param {number=} opt_endTs The time (in ms) at which the sync ended. If
+ * unspecified, it's assumed to be the same as the start,
+ * indicating an instantaneous sync.
+ */
+ addClockSyncMarker(domainId, syncId, startTs, opt_endTs) {
+ this.onDomainSeen_(domainId);
+
+ if (Object.values(ClockDomainId).indexOf(domainId) < 0) {
+ throw new Error('"' + domainId + '" is not in the list of known ' +
+ 'clock domain IDs.');
+ }
+
+ if (this.modelDomainId_) {
+ throw new Error('Cannot add new clock sync markers after getting ' +
+ 'a model time transformer.');
+ }
+
+ 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);
+ },
+
+ // TODO(charliea): Remove this once the clockSyncMetric is no longer using
+ // it.
+ get markersBySyncId() {
+ return this.markersBySyncId_;
+ },
+
+ /** @return {Set<String>} The string IDs of the domains seen so far. */
+ get domainsSeen() {
+ return this.domainsSeen_;
+ },
+
+ /**
+ * Returns a function that, given a timestamp in the domain with |domainId|,
+ * returns a timestamp in the model's clock domain.
+ *
+ * NOTE: All clock sync markers should be added before calling this function
+ * for the first time. This is because the first time that this function is
+ * called, a model clock domain is selected. This clock domain must have
+ * syncs connecting it with all other clock domains. If multiple clock
+ * domains are viable candidates, the one with the clock domain ID that is
+ * the first alphabetically is selected.
+ */
+ getModelTimeTransformer(domainId) {
+ this.onDomainSeen_(domainId);
+
+ if (!this.modelDomainId_) {
+ this.selectModelDomainId_();
+ }
+
+ return this.getTimeTransformerRaw_(domainId, this.modelDomainId_).fn;
+ },
+
+ /**
+ * Returns the error associated with the transformation given by
+ * |getModelTimeTransformer(domainId)|.
+ */
+ 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 + '".');
+ }
+
+ return transformer;
+ },
+
+ /**
+ * Returns a function that, given a timestamp in the "from" domain, returns
+ * a timestamp in the "to" domain.
+ */
+ getTransformerBetween_(fromDomainId, toDomainId) {
+ // Do a breadth-first search from the "from" domain until we reach the
+ // "to" domain.
+ const visitedDomainIds = new Set();
+ // Keep a queue of nodes to visit, starting with the "from" domain.
+ const queue = [{
+ domainId: fromDomainId,
+ transformer: Transformer.IDENTITY
+ }];
+
+ while (queue.length > 0) {
+ // NOTE: Using a priority queue here would theoretically be much more
+ // efficient, but the actual performance difference is negligible given
+ // how few clock domains we have in a trace.
+ queue.sort((domain1, domain2) =>
+ domain1.transformer.error - domain2.transformer.error);
+
+ const current = queue.shift();
+
+ if (current.domainId === toDomainId) {
+ return current.transformer;
+ }
+
+ if (visitedDomainIds.has(current.domainId)) {
+ continue;
+ }
+ visitedDomainIds.add(current.domainId);
+
+ const outgoingTransformers =
+ this.transformerMapByDomainId_[current.domainId];
+
+ if (!outgoingTransformers) continue;
+
+ // Add all nodes that are directly connected to this one to the queue.
+ for (const outgoingDomainId in outgoingTransformers) {
+ // We have two transformers: one to get us from the "from" domain to
+ // the current domain, and another to get us from the current domain
+ // to the next domain. By composing those two transformers, we can
+ // create one that gets us from the "from" domain to the next domain.
+ const toNextDomainTransformer =
+ outgoingTransformers[outgoingDomainId];
+ const toCurrentDomainTransformer = current.transformer;
+
+ queue.push({
+ domainId: outgoingDomainId,
+ transformer: Transformer.compose(
+ toNextDomainTransformer, toCurrentDomainTransformer)
+ });
+ }
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Selects the domain to use as the model domain from among the domains
+ * with registered markers.
+ *
+ * This is necessary because some common domain must be chosen before all
+ * timestamps can be shifted onto the same domain.
+ *
+ * For the time being, preference is given to Chrome clock domains. If no
+ * Chrome clock domain is present, the first clock domain alphabetically
+ * is selected.
+ */
+ selectModelDomainId_() {
+ this.ensureAllDomainsAreConnected_();
+
+ // While we're migrating to the new clock sync system, we have to make
+ // sure to prefer the Chrome clock domain because legacy clock sync
+ // mechanisms assume that's the case.
+ for (const chromeDomainId of POSSIBLE_CHROME_CLOCK_DOMAINS) {
+ if (this.domainsSeen_.has(chromeDomainId)) {
+ this.modelDomainId_ = chromeDomainId;
+ return;
+ }
+ }
+
+ const domainsSeenArray = Array.from(this.domainsSeen_);
+ domainsSeenArray.sort();
+ this.modelDomainId_ = domainsSeenArray[0];
+ },
+
+ /** Throws an error if all domains are not connected. */
+ ensureAllDomainsAreConnected_() {
+ // NOTE: this is a ridiculously inefficient way to do this. Given how few
+ // clock domains we're likely to have, this shouldn't be a problem.
+ let firstDomainId = undefined;
+ for (const domainId of this.domainsSeen_) {
+ if (!firstDomainId) {
+ firstDomainId = domainId;
+ continue;
+ }
+
+ if (!this.getTransformerBetween_(firstDomainId, domainId)) {
+ throw new Error('Unable to select a master clock domain because no ' +
+ 'path can be found from "' + firstDomainId + '" to "' + domainId +
+ '".');
+ }
+ }
+
+ return true;
+ },
+
+ /** Observer called each time that a clock domain is seen. */
+ onDomainSeen_(domainId) {
+ if (domainId === ClockDomainId.UNKNOWN_CHROME_LEGACY &&
+ !this.domainsSeen_.has(ClockDomainId.UNKNOWN_CHROME_LEGACY)) {
+ // UNKNOWN_CHROME_LEGACY was just seen for the first time: collapse it
+ // and the other Chrome clock domains into one.
+ //
+ // This makes sure that we don't have two separate clock sync graphs:
+ // one attached to UNKNOWN_CHROME_LEGACY and the other attached to the
+ // real Chrome clock domain.
+ for (const chromeDomainId of POSSIBLE_CHROME_CLOCK_DOMAINS) {
+ if (chromeDomainId === ClockDomainId.UNKNOWN_CHROME_LEGACY) {
+ continue;
+ }
+
+ this.collapseDomains_(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY, chromeDomainId);
+ }
+ }
+
+ this.domainsSeen_.add(domainId);
+ },
+
+ /**
+ * Observer called when a complete sync is made involving |marker1| and
+ * |marker2|.
+ */
+ onSyncCompleted_(marker1, marker2) {
+ const forwardTransformer = Transformer.fromMarkers(marker1, marker2);
+ const backwardTransformer = Transformer.fromMarkers(marker2, marker1);
+
+ const existingTransformer =
+ this.getOrCreateTransformerMap_(marker1.domainId)[marker2.domainId];
+ if (!existingTransformer ||
+ forwardTransformer.error < existingTransformer.error) {
+ this.getOrCreateTransformerMap_(marker1.domainId)[marker2.domainId] =
+ forwardTransformer;
+ this.getOrCreateTransformerMap_(marker2.domainId)[marker1.domainId] =
+ backwardTransformer;
+ }
+ },
+
+ /** Makes timestamps in the two clock domains interchangeable. */
+ collapseDomains_(domain1Id, domain2Id) {
+ this.getOrCreateTransformerMap_(domain1Id)[domain2Id] =
+ this.getOrCreateTransformerMap_(domain2Id)[domain1Id] =
+ Transformer.IDENTITY;
+ },
+
+ /**
+ * Returns (and creates if it doesn't exist) the transformer map describing
+ * how to transform timestamps between directly connected clock domains.
+ */
+ getOrCreateTransformerMap_(domainId) {
+ if (!this.transformerMapByDomainId_[domainId]) {
+ this.transformerMapByDomainId_[domainId] = {};
+ }
+
+ return this.transformerMapByDomainId_[domainId];
+ },
+
+ /**
+ * @return {string} The clock sync graph represented in the DOT language.
+ * This is useful for debugging incorrect clock sync behavior.
+ */
+ computeDotGraph() {
+ let dotString = 'graph {\n';
+
+ const domainsSeen = [...this.domainsSeen_].sort();
+ for (const domainId of domainsSeen) {
+ dotString += ` ${domainId}[shape=box]\n`;
+ }
+
+ const markersBySyncIdEntries = [...this.markersBySyncId_.entries()].sort(
+ ([syncId1, markers1], [syncId2, markers2]) =>
+ syncId1.localeCompare(syncId2));
+
+ for (const [syncId, markers] of markersBySyncIdEntries) {
+ const sortedMarkers = markers.sort(
+ (a, b) => a.domainId.localeCompare(b.domainId));
+ for (const m of markers) {
+ dotString += ` "${syncId}" -- ${m.domainId} `;
+ dotString += `[label="[${m.startTs}, ${m.endTs}]"]\n`;
+ }
+ }
+
+ dotString += '}';
+
+ return dotString;
+ }
+ };
+
+ /**
+ * A ClockSyncMarker is an internal entity that represents a marker in a
+ * trace log indicating that a clock sync happened at a specified time.
+ *
+ * If no end timestamp argument is specified in the constructor, it's assumed
+ * that the end timestamp is the same as the start (i.e. the clock sync
+ * was instantaneous).
+ */
+ function ClockSyncMarker(domainId, startTs, opt_endTs) {
+ this.domainId = domainId;
+ this.startTs = startTs;
+ this.endTs = opt_endTs === undefined ? startTs : opt_endTs;
+ }
+
+ ClockSyncMarker.prototype = {
+ get duration() { return this.endTs - this.startTs; },
+ get ts() { return this.startTs + this.duration / 2; }
+ };
+
+ /**
+ * A Transformer encapsulates information about how to turn timestamps in one
+ * clock domain into timestamps in another. It also stores additional data
+ * about the maximum error involved in doing so.
+ */
+ function Transformer(fn, error) {
+ this.fn = fn;
+ this.error = error;
+ }
+
+ Transformer.IDENTITY = new Transformer((x => x), 0);
+
+ /**
+ * Given two transformers, creates a third that's a composition of the two.
+ *
+ * @param {function(Number): Number} aToB A function capable of converting a
+ * timestamp from domain A to domain B.
+ * @param {function(Number): Number} bToC A function capable of converting a
+ * timestamp from domain B to domain C.
+ *
+ * @return {function(Number): Number} A function capable of converting a
+ * timestamp from domain A to domain C.
+ */
+ Transformer.compose = function(aToB, bToC) {
+ return new Transformer(
+ (ts) => bToC.fn(aToB.fn(ts)), aToB.error + bToC.error);
+ };
+
+ /**
+ * Returns a function that, given a timestamp in |fromMarker|'s domain,
+ * returns a timestamp in |toMarker|'s domain.
+ */
+ Transformer.fromMarkers = function(fromMarker, toMarker) {
+ let fromTs = fromMarker.ts;
+ let toTs = toMarker.ts;
+
+ // TODO(charliea): Usually, we estimate that the clock sync marker is
+ // issued by the agent exactly in the middle of the controller's start and
+ // end timestamps. However, there's currently a bug in the Chrome serial
+ // code that's making the clock sync ack for BattOr take much longer to
+ // read than it should (by about 8ms). This is causing the above estimate
+ // of the controller's sync timestamp to be off by a substantial enough
+ // amount that it makes traces hard to read. For now, make an exception
+ // for BattOr and just use the controller's start timestamp as the sync
+ // time. In the medium term, we should fix the Chrome serial code in order
+ // to remove this special logic and get an even more accurate estimate.
+ if (fromMarker.domainId === ClockDomainId.BATTOR &&
+ toMarker.duration > BATTOR_FAST_SYNC_THRESHOLD_MS) {
+ toTs = toMarker.startTs;
+ } else if (toMarker.domainId === ClockDomainId.BATTOR &&
+ fromMarker.duration > BATTOR_FAST_SYNC_THRESHOLD_MS) {
+ fromTs = fromMarker.startTs;
+ }
+
+ const tsShift = toTs - fromTs;
+ return new Transformer(
+ (ts) => ts + tsShift, fromMarker.duration + toMarker.duration);
+ };
+
+ return {
+ ClockDomainId,
+ ClockSyncManager,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html
new file mode 100644
index 00000000000..a35049396de
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/clock_sync_manager.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ClockDomainId = tr.model.ClockDomainId;
+ const ClockSyncManager = tr.model.ClockSyncManager;
+
+ const testOptions = {
+ setUp() {
+ // Add a few testing clock domains to the list of permissible domains.
+ ClockDomainId.DOMAIN_1 = 'DOMAIN1';
+ ClockDomainId.DOMAIN_2 = 'DOMAIN2';
+ ClockDomainId.DOMAIN_3 = 'DOMAIN3';
+ ClockDomainId.DOMAIN_4 = 'DOMAIN4';
+ ClockDomainId.DOMAIN_5 = 'DOMAIN5';
+ },
+
+ tearDown() {
+ delete ClockDomainId.DOMAIN_1;
+ delete ClockDomainId.DOMAIN_2;
+ delete ClockDomainId.DOMAIN_3;
+ delete ClockDomainId.DOMAIN_4;
+ delete ClockDomainId.DOMAIN_5;
+ }
+ };
+
+ test('addClockSyncMarker_throwsWithUnknownDomain', function() {
+ const mgr = new ClockSyncManager();
+
+ assert.throws(function() {
+ mgr.addClockSyncMarker('unknown', 'sync1', 100, 200);
+ }, '"unknown" is not in the list of known clock domain IDs.');
+ }, testOptions);
+
+
+ test('addClockSyncMarker_throwsWhenSelfSyncing', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200);
+
+ assert.throws(function() {
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 200, 300);
+ }, 'A clock domain cannot sync with itself.');
+ }, testOptions);
+
+ test('addClockSyncMarker_throwsWhenAddingThirdSyncMarkerToSync', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100);
+
+ assert.throws(function() {
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync1', 100);
+ }, 'Clock sync with ID "sync1" is already complete - cannot add a third ' +
+ 'clock sync marker to it.');
+ }, testOptions);
+
+ test('addClockSyncMarker_throwsAfterGetModelTimeTransformer', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100);
+
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1);
+
+ assert.throws(function() {
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 100);
+ }, 'Cannot add new clock sync markers after getting a model time ' +
+ 'transformer.');
+ }, testOptions);
+
+ test('getModelTimeTransformer_noMarkers', function() {
+ const mgr = new ClockSyncManager();
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100);
+ }, testOptions);
+
+ test('getModelTimeTransformer_noMarkersSecondDomainThrows', function() {
+ const mgr = new ClockSyncManager();
+
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1);
+
+ assert.throws(function() {
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2);
+ }, 'No clock sync markers exist pairing clock domain "DOMAIN2" with' +
+ ' target clock domain "DOMAIN1".');
+ }, testOptions);
+
+ test('getModelTimeTransformer_noMarkersChromeLegacyFirst', function() {
+ const mgr = new ClockSyncManager();
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.UNKNOWN_CHROME_LEGACY)(100),
+ 100);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.LINUX_CLOCK_MONOTONIC)(100),
+ 100);
+ }, testOptions);
+
+ test('getModelTimeTransformer_noMarkersChromeLegacySecond', function() {
+ const mgr = new ClockSyncManager();
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.LINUX_CLOCK_MONOTONIC)(100),
+ 100);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.UNKNOWN_CHROME_LEGACY)(100),
+ 100);
+ }, testOptions);
+
+ test('getModelTimeTransformer_oneMarker', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100);
+ }, testOptions);
+
+ test('getModelTimeTransformer_oneCompleteSync', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(350), 100);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100);
+ }, testOptions);
+
+ test('getModelTimeTransformer_oneCompleteSyncWithChromeLegacyBefore',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_oneCompleteSyncWithChromeLegacyAfter',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_twoCompleteSyncs', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350);
+
+ const tx = mgr.getTransformerBetween_('DOMAIN1', 'DOMAIN3');
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 200);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 250);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(350), 100);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_3)(250), 200);
+ }, testOptions);
+
+ test('getModelTimeTransformer_twoSyncMarkersWithEndTs', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(350), 150);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(150), 150);
+ }, testOptions);
+
+ test('getModelTimeTransformer_indirectlyConnectedGraph',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 200);
+
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 200);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 300);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_3)(300), 100);
+ }, testOptions);
+
+ test('getModelTimeTransformer_usesPathWithLeastError', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 100, 150);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 100);
+
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(125), 125);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(100), 125);
+ }, testOptions);
+
+ // NOTE: This is the same test as the above, but reversed to ensure that
+ // the result didn't stem from some ordering coincidence.
+ test('getModelTimeTransformer_usesPathWithLeastErrorReverse', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 150);
+
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 100, 200);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(125), 125);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(100), 125);
+ }, testOptions);
+
+ test('getModelTimeTransformer_battorSyncUsesNormalTimestampWhenFast',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 102);
+ mgr.addClockSyncMarker(ClockDomainId.BATTOR, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(101),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_battorSyncUsesChromeLegacyStartTsWhenTooSlow',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200);
+ mgr.addClockSyncMarker(ClockDomainId.BATTOR, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_prefersChromeLegacyDomain', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100),
+ 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_collapsesUnknownChromeLegacyDomainLinux',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350);
+
+ mgr.addClockSyncMarker(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC, 'sync2', 350);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.LINUX_CLOCK_MONOTONIC)(350),
+ 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_collapsesUnknownChromeLegacyDomainMac',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350);
+
+ mgr.addClockSyncMarker(
+ ClockDomainId.MAC_MACH_ABSOLUTE_TIME, 'sync2', 350);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.MAC_MACH_ABSOLUTE_TIME)(350),
+ 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_collapsesUnknownChromeLegacyDomainWinLoRes',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350);
+
+ mgr.addClockSyncMarker(
+ ClockDomainId.WIN_ROLLOVER_PROTECTED_TIME_GET_TIME, 'sync2', 350);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.WIN_ROLLOVER_PROTECTED_TIME_GET_TIME)(350),
+ 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_collapsesChromeLegacyDomainWinHiRes',
+ function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350);
+
+ mgr.addClockSyncMarker(ClockDomainId.WIN_QPC, 'sync2', 350);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450);
+
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(ClockDomainId.WIN_QPC)(350), 350);
+ assert.strictEqual(
+ mgr.getModelTimeTransformer(
+ ClockDomainId.UNKNOWN_CHROME_LEGACY)(350),
+ 350);
+ }, testOptions);
+
+ test('getModelTimeTransformer_throwsWithTwoDistinctGraphs', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100);
+
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 100);
+
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_4, 'sync3', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_5, 'sync3', 100);
+
+ assert.throws(function() {
+ mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_5);
+ }, 'Unable to select a master clock domain because no path can be found ' +
+ 'from "DOMAIN1" to "DOMAIN4"');
+ }, testOptions);
+
+ test('computeDotGraph_noMarkers', function() {
+ const mgr = new ClockSyncManager();
+
+ assert.strictEqual(mgr.computeDotGraph(), [
+ 'graph {',
+ '}'
+ ].join('\n'));
+ }, testOptions);
+
+ test('computeDotGraph_oneMarker', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+
+ assert.strictEqual(mgr.computeDotGraph(), [
+ 'graph {',
+ ' DOMAIN1[shape=box]',
+ ' "sync1" -- DOMAIN1 [label="[100, 100]"]',
+ '}'
+ ].join('\n'));
+ }, testOptions);
+
+ test('computeDotGraph_oneCompleteSync', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350);
+
+ assert.strictEqual(mgr.computeDotGraph(), [
+ 'graph {',
+ ' DOMAIN1[shape=box]',
+ ' DOMAIN2[shape=box]',
+ ' "sync1" -- DOMAIN1 [label="[100, 100]"]',
+ ' "sync1" -- DOMAIN2 [label="[350, 350]"]',
+ '}'
+ ].join('\n'));
+ }, testOptions);
+
+ test('computeDotGraph_twoCompleteSyncs', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 0, 50);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 400);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350);
+
+ assert.strictEqual(mgr.computeDotGraph(), [
+ 'graph {',
+ ' DOMAIN1[shape=box]',
+ ' DOMAIN2[shape=box]',
+ ' DOMAIN3[shape=box]',
+ ' "sync1" -- DOMAIN1 [label="[100, 100]"]',
+ ' "sync1" -- DOMAIN2 [label="[350, 350]"]',
+ ' "sync2" -- DOMAIN2 [label="[0, 50]"]',
+ ' "sync2" -- DOMAIN3 [label="[400, 400]"]',
+ '}'
+ ].join('\n'));
+ }, testOptions);
+
+ test('computeDotGraph_oneCompleteSyncOneIncompleteSync', function() {
+ const mgr = new ClockSyncManager();
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350);
+ mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 450);
+
+ assert.strictEqual(mgr.computeDotGraph(), [
+ 'graph {',
+ ' DOMAIN1[shape=box]',
+ ' DOMAIN2[shape=box]',
+ ' DOMAIN3[shape=box]',
+ ' "sync1" -- DOMAIN1 [label="[100, 100]"]',
+ ' "sync1" -- DOMAIN2 [label="[350, 350]"]',
+ ' "sync2" -- DOMAIN3 [label="[450, 450]"]',
+ '}'
+ ].join('\n'));
+ }, testOptions);
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html b/chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html
new file mode 100644
index 00000000000..16d2cb098bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/annotation.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/model/rect_annotation.html">
+<link rel="import" href="/tracing/ui/annotations/comment_box_annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ function CommentBoxAnnotation(location, text) {
+ tr.model.Annotation.apply(this, arguments);
+
+ this.location = location;
+ this.text = text;
+ }
+
+ CommentBoxAnnotation.fromDict = function(dict) {
+ const args = dict.args;
+ const location =
+ new tr.model.Location(args.location.xWorld, args.location.yComponents);
+ return new tr.model.CommentBoxAnnotation(location, args.text);
+ };
+
+ CommentBoxAnnotation.prototype = {
+ __proto__: tr.model.Annotation.prototype,
+
+ onRemove() {
+ this.view_.removeTextArea();
+ },
+
+ toDict() {
+ return {
+ typeName: 'comment_box',
+ args: {
+ text: this.text,
+ location: this.location.toDict()
+ }
+ };
+ },
+
+ createView_(viewport) {
+ return new tr.ui.annotations.CommentBoxAnnotationView(viewport, this);
+ }
+ };
+
+ tr.model.Annotation.register(
+ CommentBoxAnnotation, {typeName: 'comment_box'});
+
+ return {
+ CommentBoxAnnotation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html b/chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html
new file mode 100644
index 00000000000..cefc2247095
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * Indicates how much of a compound-event is selected [if any].
+ *
+ * The CompoundEventSelectionState enum is used with events that are
+ * directly selectable, but also have associated events, too, that can be
+ * selected. In this situation, there are a variety of different
+ * selected states other than just "yes, no". This enum encodes those
+ * various possible states.
+ */
+ const CompoundEventSelectionState = {
+ // Basic bit states.
+ NOT_SELECTED: 0,
+ EVENT_SELECTED: 0x1,
+ SOME_ASSOCIATED_EVENTS_SELECTED: 0x2,
+ ALL_ASSOCIATED_EVENTS_SELECTED: 0x4,
+
+ // Common combinations.
+ EVENT_AND_SOME_ASSOCIATED_SELECTED: 0x1 | 0x2,
+ EVENT_AND_ALL_ASSOCIATED_SELECTED: 0x1 | 0x4
+ };
+
+ return {
+ CompoundEventSelectionState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/constants.html b/chromium/third_party/catapult/tracing/tracing/model/constants.html
new file mode 100644
index 00000000000..0bf134ec7fe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/constants.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ return {
+ // Since the PID of the browser process is not known to the child processes,
+ // we let them use "pid_ref = -1" to reference an object created in the
+ // browser process.
+ BROWSER_PROCESS_PID_REF: -1,
+
+ // The default scope of object events, when not explicitly specified.
+ OBJECT_DEFAULT_SCOPE: 'ptr',
+
+ // Event phases that have process-local IDs, unless a global ID is
+ // explicitly specified.
+ LOCAL_ID_PHASES: new Set(['N', 'D', 'O', '(', ')'])
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html
new file mode 100644
index 00000000000..1078b764862
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ContainerMemoryDump class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * The ContainerMemoryDump represents an abstract container memory dump.
+ * @constructor
+ */
+ function ContainerMemoryDump(start) {
+ tr.model.TimedEvent.call(this, start);
+
+ this.levelOfDetail = undefined;
+
+ this.memoryAllocatorDumps_ = undefined;
+ this.memoryAllocatorDumpsByFullName_ = undefined;
+ }
+
+ /**
+ * Memory dump level of detail. See base::trace_event::MemoryDumpLevelOfDetail
+ * in the Chromium repository.
+ *
+ * @enum
+ */
+ ContainerMemoryDump.LevelOfDetail = {
+ BACKGROUND: 0,
+ LIGHT: 1,
+ DETAILED: 2
+ };
+
+ ContainerMemoryDump.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ shiftTimestampsForward(amount) {
+ this.start += amount;
+ },
+
+ get memoryAllocatorDumps() {
+ return this.memoryAllocatorDumps_;
+ },
+
+ set memoryAllocatorDumps(memoryAllocatorDumps) {
+ this.memoryAllocatorDumps_ = memoryAllocatorDumps;
+ this.forceRebuildingMemoryAllocatorDumpByFullNameIndex();
+ },
+
+ getMemoryAllocatorDumpByFullName(fullName) {
+ if (this.memoryAllocatorDumps_ === undefined) return undefined;
+
+ // Lazily generate the index if necessary.
+ if (this.memoryAllocatorDumpsByFullName_ === undefined) {
+ const index = {};
+ function addDumpsToIndex(dumps) {
+ dumps.forEach(function(dump) {
+ index[dump.fullName] = dump;
+ addDumpsToIndex(dump.children);
+ });
+ }
+ addDumpsToIndex(this.memoryAllocatorDumps_);
+ this.memoryAllocatorDumpsByFullName_ = index;
+ }
+
+ return this.memoryAllocatorDumpsByFullName_[fullName];
+ },
+
+ forceRebuildingMemoryAllocatorDumpByFullNameIndex() {
+ // Clear the index and generate it lazily.
+ this.memoryAllocatorDumpsByFullName_ = undefined;
+ },
+
+ iterateRootAllocatorDumps(fn, opt_this) {
+ if (this.memoryAllocatorDumps === undefined) return;
+ this.memoryAllocatorDumps.forEach(fn, opt_this || this);
+ }
+ };
+
+ return {
+ ContainerMemoryDump,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html
new file mode 100644
index 00000000000..a6b2009c96e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ContainerMemoryDump = tr.model.ContainerMemoryDump;
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const Scalar = tr.b.Scalar;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+
+ test('memoryAllocatorDumps_undefined', function() {
+ const md = new ContainerMemoryDump(42);
+
+ assert.isUndefined(md.memoryAllocatorDumps);
+ assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc'));
+ });
+
+ test('memoryAllocatorDumps_zero', function() {
+ const md = new ContainerMemoryDump(42);
+ md.memoryAllocatorDumps = [];
+
+ assert.lengthOf(md.memoryAllocatorDumps, 0);
+ assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc'));
+ });
+
+ test('memoryAllocatorDumps_flat', function() {
+ const md = new ContainerMemoryDump(42);
+
+ const oilpanDump = newAllocatorDump(md, 'oilpan', {
+ size: 1024,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7),
+ inner_size: 768
+ });
+ const v8Dump = newAllocatorDump(md, 'v8', {
+ size: 2048,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15),
+ inner_size: 1999
+ });
+ md.memoryAllocatorDumps = [oilpanDump, v8Dump];
+
+ assert.lengthOf(md.memoryAllocatorDumps, 2);
+ assert.strictEqual(md.memoryAllocatorDumps[0], oilpanDump);
+ assert.strictEqual(md.memoryAllocatorDumps[1], v8Dump);
+
+ assert.strictEqual(
+ md.getMemoryAllocatorDumpByFullName('oilpan'), oilpanDump);
+ assert.strictEqual(md.getMemoryAllocatorDumpByFullName('v8'), v8Dump);
+ assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc'));
+ });
+
+ test('memoryAllocatorDumps_nested', function() {
+ const md = new ContainerMemoryDump(42);
+
+ const oilpanDump = newAllocatorDump(md, 'oilpan', {
+ size: 1024,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7),
+ inner_size: 768
+ });
+
+ const oilpanBucket1Dump = addChildDump(oilpanDump, 'bucket1', {
+ size: 512,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 3),
+ inner_size: 256
+ });
+
+ const oilpanBucket2Dump = addChildDump(oilpanDump, 'bucket2', {
+ size: 512,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4),
+ inner_size: 512
+ });
+
+ const oilpanBucket2StringsDump = addChildDump(
+ oilpanBucket2Dump, 'strings', {
+ size: 512,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4),
+ inner_size: 512
+ });
+
+ const v8Dump = newAllocatorDump(md, 'v8', {
+ size: 2048,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15),
+ inner_size: 1999
+ });
+
+ md.memoryAllocatorDumps = [oilpanDump, v8Dump];
+
+ assert.lengthOf(md.memoryAllocatorDumps, 2);
+ assert.strictEqual(md.memoryAllocatorDumps[0], oilpanDump);
+ assert.strictEqual(md.memoryAllocatorDumps[1], v8Dump);
+
+ assert.strictEqual(
+ md.getMemoryAllocatorDumpByFullName('oilpan'), oilpanDump);
+ assert.strictEqual(md.getMemoryAllocatorDumpByFullName('oilpan/bucket1'),
+ oilpanBucket1Dump);
+ assert.strictEqual(md.getMemoryAllocatorDumpByFullName('oilpan/bucket2'),
+ oilpanBucket2Dump);
+ assert.strictEqual(
+ md.getMemoryAllocatorDumpByFullName('oilpan/bucket2/strings'),
+ oilpanBucket2StringsDump);
+ assert.strictEqual(md.getMemoryAllocatorDumpByFullName('v8'), v8Dump);
+ assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc'));
+ });
+
+ test('iterateRootAllocatorDumps', function() {
+ const containerDump = new ContainerMemoryDump(42);
+
+ const oilpanDump = new MemoryAllocatorDump(containerDump, 'oilpan');
+ const v8Dump = new MemoryAllocatorDump(containerDump, 'v8');
+ addChildDump(v8Dump, 'heaps');
+
+ containerDump.memoryAllocatorDumps = [oilpanDump, v8Dump];
+
+ const visitedAllocatorDumps = [];
+ containerDump.iterateRootAllocatorDumps(
+ function(dump) { this.visitedAllocatorDumps.push(dump); },
+ { visitedAllocatorDumps });
+ assert.sameMembers(visitedAllocatorDumps, [oilpanDump, v8Dump]);
+ });
+
+ test('forceRebuildingMemoryAllocatorDumpByFullNameIndex', function() {
+ const containerDump = new ContainerMemoryDump(42);
+
+ const v8Dump = new MemoryAllocatorDump(containerDump, 'v8');
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps');
+ const v8HeapSmallDump = addChildDump(v8HeapsDump, 'S');
+
+ // Setting the memory allocator dumps should update the index properly.
+ containerDump.memoryAllocatorDumps = [v8Dump];
+ assert.strictEqual(
+ containerDump.getMemoryAllocatorDumpByFullName('v8'), v8Dump);
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps'), v8HeapsDump);
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps/S'), v8HeapSmallDump);
+
+ // Add a second grandchild (v8/heaps/L).
+ const v8HeapLargeDump = addChildDump(v8HeapsDump, 'L');
+
+ // Setting the memory allocator dumps again should update the index
+ // properly again.
+ containerDump.memoryAllocatorDumps = [v8Dump];
+ assert.strictEqual(
+ containerDump.getMemoryAllocatorDumpByFullName('v8'), v8Dump);
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps'), v8HeapsDump);
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps/S'), v8HeapSmallDump);
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps/L'), v8HeapLargeDump);
+
+ // Remove the first grandchild (v8/heaps/S).
+ v8HeapsDump.children.splice(0, 1);
+
+ // Force rebuilding the index and check that it was updated properly.
+ containerDump.forceRebuildingMemoryAllocatorDumpByFullNameIndex();
+ assert.strictEqual(
+ containerDump.getMemoryAllocatorDumpByFullName('v8'), v8Dump);
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps'), v8HeapsDump);
+ assert.isUndefined(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps/S'));
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ 'v8/heaps/L'), v8HeapLargeDump);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter.html b/chromium/third_party/catapult/tracing/tracing/model/counter.html
new file mode 100644
index 00000000000..5b276984fd2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/counter.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/event_container.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * A container holding all series of a given type of measurement.
+ *
+ * As an example, if we're measuring the throughput of data sent over several
+ * USB connections, the throughput of each cable might be added as a separate
+ * series to a single counter.
+ *
+ * @constructor
+ * @extends {EventContainer}
+ */
+ function Counter(parent, id, category, name) {
+ tr.model.EventContainer.call(this);
+
+ this.parent_ = parent;
+ this.id_ = id;
+ this.category_ = category || '';
+ this.name_ = name;
+
+ this.series_ = [];
+ this.totals = [];
+ }
+
+ Counter.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get parent() {
+ return this.parent_;
+ },
+
+ get id() {
+ return this.id_;
+ },
+
+ get category() {
+ return this.category_;
+ },
+
+ get name() {
+ return this.name_;
+ },
+
+ * childEvents() {
+ },
+
+ * childEventContainers() {
+ yield* this.series;
+ },
+
+ set timestamps(arg) {
+ throw new Error('Bad counter API. No cookie.');
+ },
+
+ set seriesNames(arg) {
+ throw new Error('Bad counter API. No cookie.');
+ },
+
+ set seriesColors(arg) {
+ throw new Error('Bad counter API. No cookie.');
+ },
+
+ set samples(arg) {
+ throw new Error('Bad counter API. No cookie.');
+ },
+
+ addSeries(series) {
+ series.counter = this;
+ series.seriesIndex = this.series_.length;
+ this.series_.push(series);
+ return series;
+ },
+
+ getSeries(idx) {
+ return this.series_[idx];
+ },
+
+ get series() {
+ return this.series_;
+ },
+
+ get numSeries() {
+ return this.series_.length;
+ },
+
+ get numSamples() {
+ if (this.series_.length === 0) return 0;
+ return this.series_[0].length;
+ },
+
+ get timestamps() {
+ if (this.series_.length === 0) return [];
+ return this.series_[0].timestamps;
+ },
+
+ /**
+ * Obtains min, max, avg, values, start, and end for different series for
+ * a given counter
+ * getSampleStatistics([0,1])
+ * The statistics objects that this returns are an array of objects, one
+ * object for each series for the counter in the form:
+ * {min: minVal, max: maxVal, avg: avgVal, start: startVal, end: endVal}
+ *
+ * @param {Array.<Number>} Indices to summarize.
+ * @return {Object} An array of statistics. Each element in the array
+ * has data for one of the series in the selected counter.
+ */
+ getSampleStatistics(sampleIndices) {
+ sampleIndices.sort();
+
+ const ret = [];
+ this.series_.forEach(function(series) {
+ ret.push(series.getStatistics(sampleIndices));
+ });
+ return ret;
+ },
+
+ /**
+ * Shifts all the timestamps inside this counter forward by the amount
+ * specified.
+ */
+ shiftTimestampsForward(amount) {
+ for (let i = 0; i < this.series_.length; ++i) {
+ this.series_[i].shiftTimestampsForward(amount);
+ }
+ },
+
+ /**
+ * Updates the bounds for this counter based on the samples it contains.
+ */
+ updateBounds() {
+ this.totals = [];
+ this.maxTotal = 0;
+ this.bounds.reset();
+
+ if (this.series_.length === 0) return;
+
+ const firstSeries = this.series_[0];
+ const lastSeries = this.series_[this.series_.length - 1];
+
+ this.bounds.addValue(firstSeries.getTimestamp(0));
+ this.bounds.addValue(lastSeries.getTimestamp(lastSeries.length - 1));
+
+ const numSeries = this.numSeries;
+ this.maxTotal = -Infinity;
+
+ // Sum the samples at each timestamp.
+ // Note, this assumes that all series have all timestamps.
+ for (let i = 0; i < firstSeries.length; ++i) {
+ let total = 0;
+ this.series_.forEach(function(series) {
+ total += series.getSample(i).value;
+ this.totals.push(total);
+ }.bind(this));
+
+ this.maxTotal = Math.max(total, this.maxTotal);
+ }
+ }
+ };
+
+ /**
+ * Comparison between counters that orders by parent.compareTo, then name.
+ */
+ Counter.compare = function(x, y) {
+ let tmp = x.parent.compareTo(y.parent);
+ if (tmp !== 0) return tmp;
+ tmp = x.name.localeCompare(y.name);
+ if (tmp === 0) return x.tid - y.tid;
+ return tmp;
+ };
+
+ return {
+ Counter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_sample.html b/chromium/third_party/catapult/tracing/tracing/model/counter_sample.html
new file mode 100644
index 00000000000..3da38c349c7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/counter_sample.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_registry.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * The value of a given measurement at a given time.
+ *
+ * As an example, if we're measuring the throughput of data sent over a USB
+ * connection, each counter sample might represent the instantaneous
+ * throughput of the connection at a given time.
+ *
+ * @constructor
+ * @extends {Event}
+ */
+ function CounterSample(series, timestamp, value) {
+ tr.model.Event.call(this);
+ this.series_ = series;
+ this.timestamp_ = timestamp;
+ this.value_ = value;
+ }
+
+ CounterSample.groupByTimestamp = function(samples) {
+ const samplesByTimestamp = tr.b.groupIntoMap(samples, s => s.timestamp);
+ const timestamps = Array.from(samplesByTimestamp.keys());
+ timestamps.sort();
+ const groups = [];
+ for (const ts of timestamps) {
+ const group = samplesByTimestamp.get(ts);
+ group.sort((x, y) => x.series.seriesIndex - y.series.seriesIndex);
+ groups.push(group);
+ }
+ return groups;
+ };
+
+ CounterSample.prototype = {
+ __proto__: tr.model.Event.prototype,
+
+ get series() {
+ return this.series_;
+ },
+
+ get timestamp() {
+ return this.timestamp_;
+ },
+
+ get value() {
+ return this.value_;
+ },
+
+ set timestamp(timestamp) {
+ this.timestamp_ = timestamp;
+ },
+
+ addBoundsToRange(range) {
+ range.addValue(this.timestamp);
+ },
+
+ getSampleIndex() {
+ return tr.b.findLowIndexInSortedArray(
+ this.series.timestamps,
+ function(x) { return x; },
+ this.timestamp_);
+ },
+
+ get userFriendlyName() {
+ return 'Counter sample from ' + this.series_.title + ' at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.timestamp);
+ }
+ };
+
+
+ tr.model.EventRegistry.register(
+ CounterSample,
+ {
+ name: 'counterSample',
+ pluralName: 'counterSamples'
+ });
+
+ return {
+ CounterSample,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html
new file mode 100644
index 00000000000..d4ad8fcb0d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/counter.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Counter = tr.model.Counter;
+ const CounterSeries = tr.model.CounterSeries;
+ const CounterSample = tr.model.CounterSample;
+
+ test('groupByTimestamp', function() {
+ const counter = new Counter();
+ const slice0 = counter.addSeries(new CounterSeries('x', 0));
+ const slice1 = counter.addSeries(new CounterSeries('y', 1));
+
+ const slice0Sample0 = slice0.addCounterSample(0, 100);
+ const slice0Sample1 = slice1.addCounterSample(0, 200);
+ const slice1Sample0 = slice0.addCounterSample(1, 100);
+ const slice1Sample1 = slice1.addCounterSample(1, 200);
+
+ const groups = CounterSample.groupByTimestamp([slice0Sample1, slice0Sample0,
+ slice1Sample1, slice1Sample0]);
+ assert.strictEqual(groups.length, 2);
+ assert.deepEqual(groups[0], [slice0Sample0, slice0Sample1]);
+ assert.deepEqual(groups[1], [slice1Sample0, slice1Sample1]);
+ });
+
+ test('getSampleIndex', function() {
+ const ctr = new Counter(null, 0, '', 'myCounter');
+ const slice0 = new CounterSeries('a', 0);
+ ctr.addSeries(slice0);
+
+ const slice0Sample0 = slice0.addCounterSample(0, 0);
+ const slice0Sample1 = slice0.addCounterSample(1, 100);
+ assert.strictEqual(slice0Sample0.getSampleIndex(), 0);
+ assert.strictEqual(slice0Sample1.getSampleIndex(), 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_series.html b/chromium/third_party/catapult/tracing/tracing/model/counter_series.html
new file mode 100644
index 00000000000..b2ab8f4fe58
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/counter_series.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/counter_sample.html">
+<link rel="import" href="/tracing/model/event_container.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const CounterSample = tr.model.CounterSample;
+
+ /**
+ * A container holding all samples of a given measurement over time.
+ *
+ * As an example, a counter series might measure the throughput of data sent
+ * over a USB connection, with each sample representing the instantaneous
+ * throughput of the connection.
+ *
+ * @constructor
+ * @extends {EventContainer}
+ */
+ function CounterSeries(name, color) {
+ tr.model.EventContainer.call(this);
+
+ this.name_ = name;
+ this.color_ = color;
+
+ this.timestamps_ = [];
+ this.samples_ = [];
+
+ // Set by counter.addSeries
+ this.counter = undefined;
+ this.seriesIndex = undefined;
+ }
+
+ CounterSeries.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get length() {
+ return this.timestamps_.length;
+ },
+
+ get name() {
+ return this.name_;
+ },
+
+ get color() {
+ return this.color_;
+ },
+
+ get samples() {
+ return this.samples_;
+ },
+
+ get timestamps() {
+ return this.timestamps_;
+ },
+
+ getSample(idx) {
+ return this.samples_[idx];
+ },
+
+ getTimestamp(idx) {
+ return this.timestamps_[idx];
+ },
+
+ addCounterSample(ts, val) {
+ const sample = new CounterSample(this, ts, val);
+ this.addSample(sample);
+ return sample;
+ },
+
+ addSample(sample) {
+ this.timestamps_.push(sample.timestamp);
+ this.samples_.push(sample);
+ },
+
+ getStatistics(sampleIndices) {
+ let sum = 0;
+ let min = Number.MAX_VALUE;
+ let max = -Number.MAX_VALUE;
+
+ for (let i = 0; i < sampleIndices.length; ++i) {
+ const sample = this.getSample(sampleIndices[i]).value;
+
+ sum += sample;
+ min = Math.min(sample, min);
+ max = Math.max(sample, max);
+ }
+
+ return {
+ min,
+ max,
+ avg: (sum / sampleIndices.length),
+ start: this.getSample(sampleIndices[0]).value,
+ end: this.getSample(sampleIndices.length - 1).value
+ };
+ },
+
+ shiftTimestampsForward(amount) {
+ for (let i = 0; i < this.timestamps_.length; ++i) {
+ this.timestamps_[i] += amount;
+ this.samples_[i].timestamp = this.timestamps_[i];
+ }
+ },
+
+ * childEvents() {
+ yield* this.samples_;
+ },
+
+ * childEventContainers() {
+ }
+ };
+
+ return {
+ CounterSeries,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_test.html b/chromium/third_party/catapult/tracing/tracing/model/counter_test.html
new file mode 100644
index 00000000000..5c64e7c273d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/counter_test.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Counter = tr.model.Counter;
+ const CounterSeries = tr.model.CounterSeries;
+ const CounterSample = tr.model.CounterSample;
+
+ const createCounterWithTwoSeries = function() {
+ const ctr = new Counter(null, 0, '', 'myCounter');
+ const aSeries = new CounterSeries('a', 0);
+ const bSeries = new CounterSeries('b', 0);
+ ctr.addSeries(aSeries);
+ ctr.addSeries(bSeries);
+
+ aSeries.addCounterSample(0, 5);
+ aSeries.addCounterSample(1, 6);
+ aSeries.addCounterSample(2, 5);
+ aSeries.addCounterSample(3, 7);
+
+ bSeries.addCounterSample(0, 10);
+ bSeries.addCounterSample(1, 15);
+ bSeries.addCounterSample(2, 12);
+ bSeries.addCounterSample(3, 16);
+
+ return ctr;
+ };
+
+ test('getSampleStatisticsWithSingleSelection', function() {
+ const ctr = createCounterWithTwoSeries();
+ const ret = ctr.getSampleStatistics([0]);
+
+ assert.strictEqual(ret[0].min, 5);
+ assert.strictEqual(ret[0].max, 5);
+ assert.strictEqual(ret[0].avg, 5);
+ assert.strictEqual(ret[0].start, 5);
+ assert.strictEqual(ret[0].end, 5);
+
+ assert.strictEqual(ret[1].min, 10);
+ assert.strictEqual(ret[1].max, 10);
+ assert.strictEqual(ret[1].avg, 10);
+ assert.strictEqual(ret[1].start, 10);
+ assert.strictEqual(ret[1].end, 10);
+ });
+
+ test('getSampleStatisticsWithMultipleSelections', function() {
+ const ctr = createCounterWithTwoSeries();
+ const ret = ctr.getSampleStatistics([0, 1]);
+
+ assert.strictEqual(ret[0].min, 5);
+ assert.strictEqual(ret[0].max, 6);
+ assert.strictEqual(ret[0].avg, (5 + 6) / 2);
+ assert.strictEqual(ret[0].start, 5);
+ assert.strictEqual(ret[0].end, 6);
+
+ assert.strictEqual(ret[1].min, 10);
+ assert.strictEqual(ret[1].max, 15);
+ assert.strictEqual(ret[1].avg, (10 + 15) / 2);
+ assert.strictEqual(ret[1].start, 10);
+ assert.strictEqual(ret[1].end, 15);
+ });
+
+ test('getSampleStatisticsWithOutofOrderIndices', function() {
+ const ctr = createCounterWithTwoSeries();
+ const ret = ctr.getSampleStatistics([1, 0]);
+
+ assert.strictEqual(ret[0].min, 5);
+ assert.strictEqual(ret[0].max, 6);
+ assert.strictEqual(ret[0].avg, (5 + 6) / 2);
+ assert.strictEqual(ret[0].start, 5);
+ assert.strictEqual(ret[0].end, 6);
+
+ assert.strictEqual(ret[1].min, 10);
+ assert.strictEqual(ret[1].max, 15);
+ assert.strictEqual(ret[1].avg, (10 + 15) / 2);
+ assert.strictEqual(ret[1].start, 10);
+ assert.strictEqual(ret[1].end, 15);
+ });
+
+ test('getSampleStatisticsWithAllSelections', function() {
+ const ctr = createCounterWithTwoSeries();
+ const ret = ctr.getSampleStatistics([1, 0, 2, 3]);
+
+ assert.strictEqual(ret[0].min, 5);
+ assert.strictEqual(ret[0].max, 7);
+ assert.strictEqual(ret[0].avg, (5 + 6 + 5 + 7) / 4);
+ assert.strictEqual(ret[0].start, 5);
+ assert.strictEqual(ret[0].end, 7);
+
+ assert.strictEqual(ret[1].min, 10);
+ assert.strictEqual(ret[1].max, 16);
+ assert.strictEqual(ret[1].avg, (10 + 15 + 12 + 16) / 4);
+ assert.strictEqual(ret[1].start, 10);
+ assert.strictEqual(ret[1].end, 16);
+ });
+
+ test('testCounterSortRemainInOrder', function() {
+ const model = new tr.Model();
+ const process = new tr.model.Process(model, 4);
+ const ctr1 = new Counter(process, 0, '', 'a');
+ const ctr2 = new Counter(process, 0, '', 'b');
+
+ const array = [ctr1, ctr2];
+ array.sort(tr.model.Counter.compare);
+
+ assert.strictEqual(array[0], ctr1);
+ assert.strictEqual(array[1], ctr2);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/cpu.html b/chromium/third_party/catapult/tracing/tracing/model/cpu.html
new file mode 100644
index 00000000000..beb2f63e7cf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/cpu.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/cpu_slice.html">
+<link rel="import" href="/tracing/model/process_base.html">
+<link rel="import" href="/tracing/model/thread_time_slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Cpu class.
+ */
+tr.exportTo('tr.model', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Counter = tr.model.Counter;
+ const CpuSlice = tr.model.CpuSlice;
+
+ /**
+ * The Cpu represents a Cpu from the kernel's point of view.
+ * @constructor
+ */
+ function Cpu(kernel, number) {
+ if (kernel === undefined || number === undefined) {
+ throw new Error('Missing arguments');
+ }
+ this.kernel = kernel;
+ this.cpuNumber = number;
+ this.slices = [];
+ this.counters = {};
+ this.bounds_ = new tr.b.math.Range();
+ this.samples_ = undefined; // Set during createSubSlices
+
+ // Start timestamp of the last active thread.
+ this.lastActiveTimestamp_ = undefined;
+
+ // Identifier of the last active thread. On Linux, it's a pid while on
+ // Windows it's a thread id.
+ this.lastActiveThread_ = undefined;
+
+ // Name and arguments of the last active thread.
+ this.lastActiveName_ = undefined;
+ this.lastActiveArgs_ = undefined;
+ }
+
+ Cpu.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get samples() {
+ return this.samples_;
+ },
+
+ get userFriendlyName() {
+ return 'CPU ' + this.cpuNumber;
+ },
+
+ * findTopmostSlicesInThisContainer(eventPredicate, opt_this) {
+ // All CpuSlices are toplevel since CpuSlices do not nest.
+ for (const s of this.slices) {
+ yield* s.findTopmostSlicesRelativeToThisSlice(
+ eventPredicate, opt_this);
+ }
+ },
+
+ * childEvents() {
+ yield* this.slices;
+
+ if (this.samples_) {
+ yield* this.samples_;
+ }
+ },
+
+ * childEventContainers() {
+ yield* Object.values(this.counters);
+ },
+
+ /**
+ * @return {Counter} The counter on this CPU with the given category/name
+ * combination, creating it if it doesn't exist.
+ */
+ getOrCreateCounter(cat, name) {
+ const id = cat + '.' + name;
+ if (!this.counters[id]) {
+ this.counters[id] = new Counter(this, id, cat, name);
+ }
+ return this.counters[id];
+ },
+
+ /**
+ * @return {Counter} the counter on this CPU with the given category/name
+ * combination, or undefined if it doesn't exist.
+ */
+ getCounter(cat, name) {
+ const id = cat + '.' + name;
+ if (!this.counters[id]) {
+ return undefined;
+ }
+ return this.counters[id];
+ },
+
+ /**
+ * Shifts all the timestamps inside this CPU forward by the amount
+ * specified.
+ */
+ shiftTimestampsForward(amount) {
+ for (let sI = 0; sI < this.slices.length; sI++) {
+ this.slices[sI].start = (this.slices[sI].start + amount);
+ }
+ for (const id in this.counters) {
+ this.counters[id].shiftTimestampsForward(amount);
+ }
+ },
+
+ /**
+ * Updates the range based on the current slices attached to the cpu.
+ */
+ updateBounds() {
+ this.bounds_.reset();
+ if (this.slices.length) {
+ this.bounds_.addValue(this.slices[0].start);
+ this.bounds_.addValue(this.slices[this.slices.length - 1].end);
+ }
+ for (const id in this.counters) {
+ this.counters[id].updateBounds();
+ this.bounds_.addRange(this.counters[id].bounds);
+ }
+ if (this.samples_ && this.samples_.length) {
+ this.bounds_.addValue(this.samples_[0].start);
+ this.bounds_.addValue(
+ this.samples_[this.samples_.length - 1].end);
+ }
+ },
+
+ createSubSlices() {
+ this.samples_ = this.kernel.model.samples.filter(function(sample) {
+ return sample.cpu === this;
+ }, this);
+ },
+
+ addCategoriesToDict(categoriesDict) {
+ for (let i = 0; i < this.slices.length; i++) {
+ categoriesDict[this.slices[i].category] = true;
+ }
+ for (const id in this.counters) {
+ categoriesDict[this.counters[id].category] = true;
+ }
+ for (let i = 0; i < this.samples_.length; i++) {
+ categoriesDict[this.samples_[i].category] = true;
+ }
+ },
+
+ /*
+ * Returns the index of the slice in the CPU's slices, or undefined.
+ */
+ indexOf(cpuSlice) {
+ const i = tr.b.findLowIndexInSortedArray(
+ this.slices,
+ function(slice) { return slice.start; },
+ cpuSlice.start);
+ if (this.slices[i] !== cpuSlice) return undefined;
+ return i;
+ },
+
+ /**
+ * Closes the thread running on the CPU. |endTimestamp| is the timestamp
+ * at which the thread was unscheduled. |args| is merged with the arguments
+ * specified when the thread was initially scheduled.
+ */
+ closeActiveThread(endTimestamp, args) {
+ // Don't generate a slice if the last active thread is the idle task.
+ if (this.lastActiveThread_ === undefined ||
+ this.lastActiveThread_ === 0) {
+ return;
+ }
+
+ if (endTimestamp < this.lastActiveTimestamp_) {
+ throw new Error('The end timestamp of a thread running on CPU ' +
+ this.cpuNumber + ' is before its start timestamp.');
+ }
+
+ // Merge |args| with |this.lastActiveArgs_|. If a key is in both
+ // dictionaries, the value from |args| is used.
+ for (const key in args) {
+ this.lastActiveArgs_[key] = args[key];
+ }
+
+ const duration = endTimestamp - this.lastActiveTimestamp_;
+ const slice = new tr.model.CpuSlice(
+ '', this.lastActiveName_,
+ ColorScheme.getColorIdForGeneralPurposeString(this.lastActiveName_),
+ this.lastActiveTimestamp_,
+ this.lastActiveArgs_,
+ duration);
+ slice.cpu = this;
+ this.slices.push(slice);
+
+ // Clear the last state.
+ this.lastActiveTimestamp_ = undefined;
+ this.lastActiveThread_ = undefined;
+ this.lastActiveName_ = undefined;
+ this.lastActiveArgs_ = undefined;
+ },
+
+ switchActiveThread(timestamp, oldThreadArgs, newThreadId,
+ newThreadName, newThreadArgs) {
+ // Close the previous active thread and generate a slice.
+ this.closeActiveThread(timestamp, oldThreadArgs);
+
+ // Keep track of the new thread.
+ this.lastActiveTimestamp_ = timestamp;
+ this.lastActiveThread_ = newThreadId;
+ this.lastActiveName_ = newThreadName;
+ this.lastActiveArgs_ = newThreadArgs;
+ },
+
+ /**
+ * Returns the frequency statistics for this CPU;
+ * the returned object contains the frequencies as keys,
+ * and the duration at this frequency in milliseconds as the value,
+ * for the range that was specified.
+ */
+ getFreqStatsForRange(range) {
+ const stats = {};
+
+ function addStatsForFreq(freqSample, index) {
+ // Counters don't have an explicit end or duration;
+ // calculate the end by looking at the starting point
+ // of the next value in the series, or if that doesn't
+ // exist, assume this frequency is held until the end.
+ const freqEnd = (index < freqSample.series_.length - 1) ?
+ freqSample.series_.samples_[index + 1].timestamp : range.max;
+
+ const freqRange = tr.b.math.Range.fromExplicitRange(
+ freqSample.timestamp, freqEnd);
+ const intersection = freqRange.findIntersection(range);
+ if (!(freqSample.value in stats)) {
+ stats[freqSample.value] = 0;
+ }
+ stats[freqSample.value] += intersection.duration;
+ }
+
+ const freqCounter = this.getCounter('', 'Clock Frequency');
+ if (freqCounter !== undefined) {
+ const freqSeries = freqCounter.getSeries(0);
+ if (!freqSeries) return;
+
+ tr.b.iterateOverIntersectingIntervals(freqSeries.samples_,
+ function(x) { return x.timestamp; },
+ function(x, index) {
+ if (index < freqSeries.length - 1) {
+ return freqSeries.samples_[index + 1].timestamp;
+ }
+ return range.max;
+ },
+ range.min,
+ range.max,
+ addStatsForFreq);
+ }
+
+ return stats;
+ }
+ };
+
+ /**
+ * Comparison between processes that orders by cpuNumber.
+ */
+ Cpu.compare = function(x, y) {
+ return x.cpuNumber - y.cpuNumber;
+ };
+
+
+ return {
+ Cpu,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html b/chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html
new file mode 100644
index 00000000000..6faf5d143fa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/thread_time_slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the CpuSlice class.
+ */
+tr.exportTo('tr.model', function() {
+ const Slice = tr.model.Slice;
+
+ /**
+ * A CpuSlice represents a slice of time on a CPU.
+ *
+ * @constructor
+ */
+ function CpuSlice(cat, title, colorId, start, args, opt_duration) {
+ Slice.apply(this, arguments);
+ this.threadThatWasRunning = undefined;
+ this.cpu = undefined;
+ }
+
+ CpuSlice.prototype = {
+ __proto__: Slice.prototype,
+
+ get analysisTypeName() {
+ return 'tr.ui.analysis.CpuSlice';
+ },
+
+ getAssociatedTimeslice() {
+ if (!this.threadThatWasRunning) {
+ return undefined;
+ }
+ const timeSlices = this.threadThatWasRunning.timeSlices;
+ for (let i = 0; i < timeSlices.length; i++) {
+ const timeSlice = timeSlices[i];
+ if (timeSlice.start !== this.start) {
+ continue;
+ }
+ if (timeSlice.duration !== this.duration) {
+ continue;
+ }
+ return timeSlice;
+ }
+ return undefined;
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ CpuSlice,
+ {
+ name: 'cpuSlice',
+ pluralName: 'cpuSlices'
+ });
+
+ return {
+ CpuSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/cpu_test.html b/chromium/third_party/catapult/tracing/tracing/model/cpu_test.html
new file mode 100644
index 00000000000..49f01dff4f3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/cpu_test.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Cpu = tr.model.Cpu;
+ const newThreadSlice = tr.c.TestUtils.newThreadSlice;
+
+ test('cpuBounds_Empty', function() {
+ const cpu = new Cpu({}, 1);
+ cpu.updateBounds();
+ assert.isUndefined(cpu.bounds.min);
+ assert.isUndefined(cpu.bounds.max);
+ });
+
+ test('cpuBounds_OneSlice', function() {
+ const cpu = new Cpu({}, 1);
+ cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3}));
+ cpu.updateBounds();
+ assert.strictEqual(cpu.bounds.min, 1);
+ assert.strictEqual(cpu.bounds.max, 4);
+ });
+
+ test('getOrCreateCounter', function() {
+ const cpu = new Cpu({}, 1);
+ const ctrBar = cpu.getOrCreateCounter('foo', 'bar');
+ const ctrBar2 = cpu.getOrCreateCounter('foo', 'bar');
+ assert.strictEqual(ctrBar, ctrBar2);
+ });
+
+ test('shiftTimestampsForward', function() {
+ const cpu = new Cpu({}, 1);
+ const ctr = cpu.getOrCreateCounter('foo', 'bar');
+ cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3}));
+ let shiftCount = 0;
+ ctr.shiftTimestampsForward = function(ts) {
+ if (ts === 0.32) {
+ shiftCount++;
+ }
+ };
+ cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3}));
+ cpu.shiftTimestampsForward(0.32);
+ assert.strictEqual(1, shiftCount);
+ assert.strictEqual(cpu.slices[0].start, 1.32);
+ });
+
+
+ function newCpuSliceNamed(cpu, name, start, duration, opt_thread) {
+ const s = new tr.model.CpuSlice(
+ 'cat', name, 0, start, {}, duration);
+ s.cpu = cpu;
+ if (opt_thread) {
+ s.threadThatWasRunning = opt_thread;
+ }
+ return s;
+ }
+
+ test('getTimesliceForCpuSlice', function() {
+ const m = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const cpu = m.kernel.getOrCreateCpu(1);
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ t2.timeSlices = [newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 0, 10, cpu),
+ newThreadSlice(t2, SCHEDULING_STATE.SLEEPING, 10, 10),
+ newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 20, 10, cpu)];
+ cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2),
+ newCpuSliceNamed(cpu, 'x', 20, 10, t2)];
+ assert.strictEqual(
+ cpu.slices[0].getAssociatedTimeslice(), t2.timeSlices[0]);
+ assert.strictEqual(
+ cpu.slices[1].getAssociatedTimeslice(), t2.timeSlices[2]);
+
+ assert.strictEqual(t2.timeSlices[0].getAssociatedCpuSlice(), cpu.slices[0]);
+ assert.isUndefined(t2.timeSlices[1].getAssociatedCpuSlice());
+ assert.strictEqual(t2.timeSlices[2].getAssociatedCpuSlice(), cpu.slices[1]);
+
+ assert.strictEqual(cpu.indexOf(cpu.slices[0]), 0);
+ assert.strictEqual(cpu.indexOf(cpu.slices[1]), 1);
+
+ assert.strictEqual(t2.indexOfTimeSlice(t2.timeSlices[0]), 0);
+ assert.strictEqual(t2.indexOfTimeSlice(t2.timeSlices[1]), 1);
+ assert.strictEqual(t2.indexOfTimeSlice(t2.timeSlices[2]), 2);
+ });
+
+ test('putToSleepFor', function() {
+ const m = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const cpu = m.kernel.getOrCreateCpu(1);
+
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const t3 = m.getOrCreateProcess(1).getOrCreateThread(3);
+ t2.timeSlices = [newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 0, 10, cpu),
+ newThreadSlice(t2, SCHEDULING_STATE.SLEEPING, 10, 10),
+ newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 20, 10, cpu)];
+ t3.timeSlices = [newThreadSlice(t3, SCHEDULING_STATE.RUNNING, 10, 5, cpu)];
+ cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2),
+ newCpuSliceNamed(cpu, 'x', 10, 5, t3),
+ newCpuSliceNamed(cpu, 'x', 20, 10, t2)];
+
+ // At timeslice 0, the thread is running.
+ assert.isUndefined(t2.timeSlices[0].getCpuSliceThatTookCpu());
+
+ // t2 lost the cpu to t3 at t=10
+ assert.strictEqual(
+ cpu.slices[1],
+ t2.timeSlices[1].getCpuSliceThatTookCpu());
+ });
+
+ test('putToSleepForNothing', function() {
+ const m = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const cpu = m.kernel.getOrCreateCpu(1);
+
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const t3 = m.getOrCreateProcess(1).getOrCreateThread(3);
+ t2.timeSlices = [newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 0, 10, cpu),
+ newThreadSlice(t2, SCHEDULING_STATE.SLEEPING, 10, 10),
+ newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 20, 10, cpu)];
+ t3.timeSlices = [newThreadSlice(t3, SCHEDULING_STATE.RUNNING, 15, 5, cpu)];
+ cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2),
+ newCpuSliceNamed(cpu, 'x', 15, 5, t3),
+ newCpuSliceNamed(cpu, 'x', 20, 10, t2)];
+ assert.isUndefined(t2.timeSlices[1].getCpuSliceThatTookCpu());
+ });
+
+ test('switchActiveThread', function() {
+ const m = new tr.Model();
+ const cpu = m.kernel.getOrCreateCpu(1);
+
+ cpu.switchActiveThread(5, {}, 0, 'idle thread', {});
+ cpu.switchActiveThread(10, {}, 1, 'thread one', {a: 1});
+ cpu.switchActiveThread(15, {b: 2}, 2, 'thread two', {c: 3});
+ cpu.switchActiveThread(30, {c: 4, d: 5}, 3, 'thread three', {e: 6});
+ cpu.closeActiveThread(40, {f: 7});
+ cpu.switchActiveThread(50, {}, 4, 'thread four', {g: 8});
+ cpu.switchActiveThread(60, {}, 1, 'thread one', {});
+ cpu.closeActiveThread(70, {});
+
+ assert.strictEqual(cpu.slices.length, 5);
+
+ assert.strictEqual(cpu.slices[0].title, 'thread one');
+ assert.strictEqual(cpu.slices[0].start, 10);
+ assert.strictEqual(cpu.slices[0].duration, 5);
+ assert.strictEqual(Object.keys(cpu.slices[0].args).length, 2);
+ assert.strictEqual(cpu.slices[0].args.a, 1);
+ assert.strictEqual(cpu.slices[0].args.b, 2);
+
+ assert.strictEqual(cpu.slices[1].title, 'thread two');
+ assert.strictEqual(cpu.slices[1].start, 15);
+ assert.strictEqual(cpu.slices[1].duration, 15);
+ assert.strictEqual(Object.keys(cpu.slices[1].args).length, 2);
+ assert.strictEqual(cpu.slices[1].args.c, 4);
+ assert.strictEqual(cpu.slices[1].args.d, 5);
+
+ assert.strictEqual(cpu.slices[2].title, 'thread three');
+ assert.strictEqual(cpu.slices[2].start, 30);
+ assert.strictEqual(cpu.slices[2].duration, 10);
+ assert.strictEqual(Object.keys(cpu.slices[2].args).length, 2);
+ assert.strictEqual(cpu.slices[2].args.e, 6);
+ assert.strictEqual(cpu.slices[2].args.f, 7);
+
+ assert.strictEqual(cpu.slices[3].title, 'thread four');
+ assert.strictEqual(cpu.slices[3].start, 50);
+ assert.strictEqual(cpu.slices[3].duration, 10);
+ assert.strictEqual(Object.keys(cpu.slices[3].args).length, 1);
+ assert.strictEqual(cpu.slices[3].args.g, 8);
+
+ assert.strictEqual(cpu.slices[4].title, 'thread one');
+ assert.strictEqual(cpu.slices[4].start, 60);
+ assert.strictEqual(cpu.slices[4].duration, 10);
+ assert.strictEqual(Object.keys(cpu.slices[4].args).length, 0);
+ });
+
+ test('getFrequencyStats', function() {
+ const m = new tr.Model();
+ const cpu = m.kernel.getOrCreateCpu(1);
+ const powerCounter = cpu.getOrCreateCounter('', 'Clock Frequency');
+ const series = powerCounter.addSeries(new tr.model.CounterSeries('state',
+ ColorScheme.getColorIdForGeneralPurposeString('test')));
+
+ series.addCounterSample(0, 100000);
+ series.addCounterSample(20, 300000);
+ series.addCounterSample(30, 100000);
+ series.addCounterSample(80, 500000);
+ series.addCounterSample(100, 300000);
+
+ const range = tr.b.math.Range.fromExplicitRange(10, 90);
+ const stats = cpu.getFreqStatsForRange(range);
+ assert.strictEqual(stats[100000], 60);
+ assert.strictEqual(stats[300000], 10);
+ assert.strictEqual(stats[500000], 10);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/device.html b/chromium/third_party/catapult/tracing/tracing/model/device.html
new file mode 100644
index 00000000000..5dca1e03151
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/device.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/cpu.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/power_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Device class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * Device represents the device-level objects in the model.
+ * @constructor
+ * @extends {tr.model.EventContainer}
+ */
+ function Device(model) {
+ if (!model) {
+ throw new Error('Must provide a model.');
+ }
+
+ tr.model.EventContainer.call(this);
+
+ this.powerSeries_ = undefined;
+ this.cpuUsageSeries_ = undefined;
+ this.vSyncTimestamps_ = [];
+ }
+
+ Device.compare = function(x, y) {
+ return x.guid - y.guid;
+ };
+
+ Device.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ compareTo(that) {
+ return Device.compare(this, that);
+ },
+
+ get userFriendlyName() {
+ return 'Device';
+ },
+
+ get userFriendlyDetails() {
+ return 'Device';
+ },
+
+ get stableId() {
+ return 'Device';
+ },
+
+ getSettingsKey() {
+ return 'device';
+ },
+
+ get powerSeries() {
+ return this.powerSeries_;
+ },
+
+ set powerSeries(powerSeries) {
+ this.powerSeries_ = powerSeries;
+ },
+
+ get cpuUsageSeries() {
+ return this.cpuUsageSeries_;
+ },
+
+ set cpuUsageSeries(cpuUsageSeries) {
+ this.cpuUsageSeries_ = cpuUsageSeries;
+ },
+
+ get vSyncTimestamps() {
+ return this.vSyncTimestamps_;
+ },
+
+ set vSyncTimestamps(value) {
+ this.vSyncTimestamps_ = value;
+ },
+
+ updateBounds() {
+ this.bounds.reset();
+ for (const child of this.childEventContainers()) {
+ child.updateBounds();
+ this.bounds.addRange(child.bounds);
+ }
+ },
+
+ shiftTimestampsForward(amount) {
+ for (const child of this.childEventContainers()) {
+ child.shiftTimestampsForward(amount);
+ }
+
+ for (let i = 0; i < this.vSyncTimestamps_.length; i++) {
+ this.vSyncTimestamps_[i] += amount;
+ }
+ },
+
+ addCategoriesToDict(categoriesDict) {
+ },
+
+ * childEventContainers() {
+ if (this.powerSeries_) {
+ yield this.powerSeries_;
+ }
+ if (this.cpuUsageSeries_) {
+ yield this.cpuUsageSeries_;
+ }
+ }
+ };
+
+ return {
+ Device,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/device_test.html b/chromium/third_party/catapult/tracing/tracing/model/device_test.html
new file mode 100644
index 00000000000..6e46cced01b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/device_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/device.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ test('updateBounds', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+
+ // Verify that the bounds match the lowest and highest timestamps.
+ device.powerSeries.addPowerSample(100, 5);
+ device.powerSeries.addPowerSample(200, 5);
+ device.updateBounds();
+
+ assert.strictEqual(device.bounds.min, 100);
+ assert.strictEqual(device.bounds.max, 200);
+
+ // Add a new sample and verify that the bounds change.
+ device.powerSeries.addPowerSample(700, 5);
+ device.updateBounds();
+
+ assert.strictEqual(device.bounds.min, 100);
+ assert.strictEqual(device.bounds.max, 700);
+ });
+
+ test('shiftTimestampsForward', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+
+ device.powerSeries.addPowerSample(100, 2);
+ device.powerSeries.addPowerSample(200, 2);
+ device.shiftTimestampsForward(2);
+
+ assert.strictEqual(device.powerSeries.samples[0].start, 102);
+ assert.strictEqual(device.powerSeries.samples[1].start, 202);
+ });
+
+ test('childEventContainers_noPowerSeries', function() {
+ const device = new Device(new Model());
+ const childEventContainers = [];
+ for (const container of device.childEventContainers()) {
+ childEventContainers.push(container);
+ }
+ assert.deepEqual(childEventContainers, []);
+ });
+
+ test('childEventContainers_powerSeries', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ const childEventContainers = [];
+ for (const container of device.childEventContainers()) {
+ childEventContainers.push(container);
+ }
+ assert.deepEqual(childEventContainers, [device.powerSeries]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event.html b/chromium/third_party/catapult/tracing/tracing/model/event.html
new file mode 100644
index 00000000000..99495fcc913
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Event class.
+ */
+tr.exportTo('tr.model', function() {
+ const SelectableItem = tr.model.SelectableItem;
+ const SelectionState = tr.model.SelectionState;
+ const IMMUTABLE_EMPTY_SET = tr.model.EventSet.IMMUTABLE_EMPTY_SET;
+
+ /**
+ * An Event is the base type for any non-container, selectable piece
+ * of data in the trace model.
+ *
+ * @constructor
+ * @extends {SelectableItem}
+ */
+ function Event() {
+ SelectableItem.call(this, this /* modelItem */);
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.selectionState = SelectionState.NONE;
+ this.info = undefined;
+ }
+
+ Event.prototype = {
+ __proto__: SelectableItem.prototype,
+
+ get guid() {
+ return this.guid_;
+ },
+
+ get stableId() {
+ return undefined;
+ },
+
+ get range() {
+ const range = new tr.b.math.Range();
+ this.addBoundsToRange(range);
+ return range;
+ },
+
+ // Empty by default. Lazily initialized on an instance in
+ // addAssociatedAlert(). See #1930.
+ associatedAlerts: IMMUTABLE_EMPTY_SET,
+
+ addAssociatedAlert(alert) {
+ if (this.associatedAlerts === IMMUTABLE_EMPTY_SET) {
+ this.associatedAlerts = new tr.model.EventSet();
+ }
+ this.associatedAlerts.push(alert);
+ },
+
+ // Adds the range of timestamps for this event to the specified range.
+ // If this is not overridden in subclass, it means that type of event
+ // doesn't have timestamps.
+ addBoundsToRange(range) {}
+ };
+
+ return {
+ Event,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_container.html b/chromium/third_party/catapult/tracing/tracing/model/event_container.html
new file mode 100644
index 00000000000..8c71eb4b688
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_container.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * EventContainer is a base class for any class in the trace model that
+ * contains child events or child EventContainers.
+ *
+ * For all EventContainers, updateBounds() must be called after modifying the
+ * container's events if an up-to-date bounds is expected.
+ *
+ * @constructor
+ */
+ function EventContainer() {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.important = true;
+ this.bounds_ = new tr.b.math.Range();
+ }
+
+ EventContainer.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ /**
+ * @return {String} A stable and unique identifier that describes this
+ * container's position in the event tree relative to the root. If an event
+ * container 'B' is a child to another event container 'A', then container
+ * B's stable ID would be 'A.B'.
+ */
+ get stableId() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Returns the bounds of the event container, which describe the range
+ * of timestamps for all ancestor events.
+ */
+ get bounds() {
+ return this.bounds_;
+ },
+
+ // TODO(charliea): A default implementation of this method could likely be
+ // provided that iterates throuch getDescendantEvents.
+ /**
+ * Updates the bounds of the event container. After updating, this.bounds
+ * will describe the range of timestamps of all ancestor events.
+ */
+ updateBounds() {
+ throw new Error('Not implemented');
+ },
+
+ // TODO(charliea): A default implementation of this method could likely be
+ // provided that iterates through getDescendantEvents.
+ /**
+ * Shifts the timestamps for ancestor events by 'amount' milliseconds.
+ */
+ shiftTimestampsForward(amount) {
+ throw new Error('Not implemented');
+ },
+
+
+ /**
+ * Returns an iterable of all child events.
+ */
+ * childEvents() {
+ },
+
+ /**
+ * Returns an iterable of all events in this and descendant
+ * event containers.
+ */
+ * getDescendantEvents() {
+ yield* this.childEvents();
+ for (const container of this.childEventContainers()) {
+ yield* container.getDescendantEvents();
+ }
+ },
+
+ /**
+ * Returns an iterable of all child event containers.
+ */
+ * childEventContainers() {
+ },
+
+ /**
+ * Returns an iterable containing this and all descendant event containers.
+ */
+ * getDescendantEventContainers() {
+ yield this;
+ for (const container of this.childEventContainers()) {
+ yield* container.getDescendantEventContainers();
+ }
+ },
+
+ /**
+ * Returns an iterable of all events in this and descendant event containers
+ * in a given set of ranges.
+ *
+ * This base class provides a default implementation with no assumptions
+ * about the order of events. A container can override this implementation
+ * with a more efficient one, for example if its events are sorted.
+ */
+ * getDescendantEventsInSortedRanges(ranges, opt_containerPredicate) {
+ if (opt_containerPredicate === undefined ||
+ opt_containerPredicate(this)) {
+ for (const event of this.childEvents()) {
+ const i = tr.b.findFirstTrueIndexInSortedArray(
+ ranges, range => event.start <= range.max);
+ if (i < ranges.length && event.end >= ranges[i].min) yield event;
+ }
+ }
+
+ for (const container of this.childEventContainers()) {
+ yield* container.getDescendantEventsInSortedRanges(
+ ranges, opt_containerPredicate);
+ }
+ },
+
+ /**
+ * Finds topmost slices in this container (see docstring for
+ * findTopmostSlices).
+ */
+ * findTopmostSlicesInThisContainer(eventPredicate, opt_this) {
+ },
+
+ /**
+ * The findTopmostSlices* series of helpers find all topmost slices
+ * satisfying the given predicates.
+ *
+ * As an example, suppose we are trying to find slices named 'C', with the
+ * following thread:
+ *
+ * -> |---C---| |-----D-----|
+ * |-C-| |---C---| <-
+ *
+ * findTopmostSlices would locate the pointed-to Cs, because the bottom C on
+ * the left is not the topmost C, and the right one is, even though it is
+ * not itself a top-level slice.
+ */
+ * findTopmostSlices(eventPredicate) {
+ for (const ec of this.getDescendantEventContainers()) {
+ yield* ec.findTopmostSlicesInThisContainer(eventPredicate);
+ }
+ },
+
+ * findTopmostSlicesNamed(name) {
+ yield* this.findTopmostSlices(e => e.title === name);
+ }
+ };
+
+ return {
+ EventContainer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_container_test.html b/chromium/third_party/catapult/tracing/tracing/model/event_container_test.html
new file mode 100644
index 00000000000..06b06a391c2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_container_test.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('getDescendantEventsInSortedRanges', function() {
+ class ContainerTypeX extends tr.model.EventContainer {
+ * childEvents() {
+ const event1 = new tr.model.TimedEvent(4);
+ event1.duration = 2;
+ event1.title = 'X-1';
+ yield event1;
+
+ const event2 = new tr.model.TimedEvent(1);
+ event2.duration = 2;
+ event2.title = 'X-2';
+ yield event2;
+ }
+ }
+
+ class ContainerTypeY extends tr.model.EventContainer {
+ constructor() {
+ super();
+ this.childContainer_ = new ContainerTypeX();
+ }
+
+ * childEvents() {
+ const event1 = new tr.model.TimedEvent(5);
+ event1.duration = 4;
+ event1.title = 'Y-1';
+ yield event1;
+
+ const event2 = new tr.model.TimedEvent(6);
+ event2.duration = 2;
+ event2.title = 'Y-2';
+ yield event2;
+ }
+
+ * childEventContainers() {
+ yield this.childContainer_;
+ }
+ }
+
+ // We have the following timed events:
+ // 1 2 3 4 5 6 7 8 9
+ // ContainerTypeY <----- Y-1 ----->
+ // <- Y2 ->
+ // ContainerTypeX <- X-2 -> <- X-1 ->
+ const container = new ContainerTypeY();
+
+ // [2, 5] intersect X-1, X-2, and Y-1.
+ const r1 = new tr.b.math.Range.fromExplicitRange(2, 5);
+ let slices = [...container.getDescendantEventsInSortedRanges([r1])];
+ slices = slices.map(s => s.title).sort();
+ assert.strictEqual(slices.length, 3);
+ assert.strictEqual(slices[0], 'X-1');
+ assert.strictEqual(slices[1], 'X-2');
+ assert.strictEqual(slices[2], 'Y-1');
+
+ // [2, 5], [7, 8] intersect X-1, X-2, Y-1, and Y-2.
+ const r2 = new tr.b.math.Range.fromExplicitRange(7, 8);
+ slices = [...container.getDescendantEventsInSortedRanges([r1, r2])];
+ slices = slices.map(s => s.title).sort();
+ assert.strictEqual(slices.length, 4);
+ assert.strictEqual(slices[0], 'X-1');
+ assert.strictEqual(slices[1], 'X-2');
+ assert.strictEqual(slices[2], 'Y-1');
+ assert.strictEqual(slices[3], 'Y-2');
+
+ // We should see events from ContainerTypeX only.
+ slices = [...container.getDescendantEventsInSortedRanges(
+ [r1], container => container instanceof ContainerTypeX)];
+ slices = slices.map(s => s.title).sort();
+ assert.strictEqual(slices.length, 2);
+ assert.strictEqual(slices[0], 'X-1');
+ assert.strictEqual(slices[1], 'X-2');
+
+ // We should see events from ContainerTypeY only.
+ slices = [...container.getDescendantEventsInSortedRanges(
+ [r1], container => container instanceof ContainerTypeY)];
+ slices = slices.map(s => s.title).sort();
+ assert.strictEqual(slices.length, 1);
+ assert.strictEqual(slices[0], 'Y-1');
+ });
+});
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_info.html b/chromium/third_party/catapult/tracing/tracing/model/event_info.html
new file mode 100644
index 00000000000..ecc309d471a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_info.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/color_scheme.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ /**
+ * EventInfo is an annotation added to Events in order to document
+ * what they represent, and override their title/colorId values.
+ *
+ * TODO(ccraik): eventually support more complex structure/paragraphs.
+ *
+ * @param {string} title A user-visible title for the event.
+ * @param {string} description A user-visible description of the event.
+ * @param {Array} docLinks A list of Objects, each of the form
+ * {label: str, textContent: str, href: str}
+ *
+ * @constructor
+ */
+ function EventInfo(title, description, docLinks) {
+ this.title = title;
+ this.description = description;
+ this.docLinks = docLinks;
+ this.colorId = ColorScheme.getColorIdForGeneralPurposeString(title);
+ }
+
+ return {
+ EventInfo,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_registry.html b/chromium/third_party/catapult/tracing/tracing/model/event_registry.html
new file mode 100644
index 00000000000..7913d7ffe80
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_registry.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the EventRegistry class.
+ */
+tr.exportTo('tr.model', function() {
+ // Create the type registry.
+ function EventRegistry() {
+ }
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(EventRegistry, options);
+
+ // Enforce all options objects have the right fields.
+ EventRegistry.addEventListener('will-register', function(e) {
+ const metadata = e.typeInfo.metadata;
+ if (metadata.name === undefined) {
+ throw new Error('Registered events must provide name metadata');
+ }
+ if (metadata.pluralName === undefined) {
+ throw new Error('Registered events must provide pluralName metadata');
+ }
+
+ // Add a subtype registry to every event so that all events can be
+ // extended
+ if (metadata.subTypes === undefined) {
+ metadata.subTypes = {};
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = e.typeInfo.constructor;
+ options.defaultConstructor = e.typeInfo.constructor;
+ tr.b.decorateExtensionRegistry(metadata.subTypes, options);
+ } else {
+ if (!metadata.subTypes.register) {
+ throw new Error('metadata.subTypes must be an extension registry.');
+ }
+ }
+
+ e.typeInfo.constructor.subTypes = metadata.subTypes;
+ });
+
+ // Helper: lookup Events indexed by type name.
+ let eventsByTypeName = undefined;
+ EventRegistry.getEventTypeInfoByTypeName = function(typeName) {
+ if (eventsByTypeName === undefined) {
+ eventsByTypeName = {};
+ EventRegistry.getAllRegisteredTypeInfos().forEach(function(typeInfo) {
+ eventsByTypeName[typeInfo.metadata.name] = typeInfo;
+ });
+ }
+ return eventsByTypeName[typeName];
+ };
+
+ // Ensure eventsByTypeName stays current.
+ EventRegistry.addEventListener('registry-changed', function() {
+ eventsByTypeName = undefined;
+ });
+
+ function convertCamelCaseToTitleCase(name) {
+ let result = name.replace(/[A-Z]/g, ' $&');
+ result = result.charAt(0).toUpperCase() + result.slice(1);
+ return result;
+ }
+
+ EventRegistry.getUserFriendlySingularName = function(typeName) {
+ const typeInfo = EventRegistry.getEventTypeInfoByTypeName(typeName);
+ const str = typeInfo.metadata.name;
+ return convertCamelCaseToTitleCase(str);
+ };
+
+ EventRegistry.getUserFriendlyPluralName = function(typeName) {
+ const typeInfo = EventRegistry.getEventTypeInfoByTypeName(typeName);
+ const str = typeInfo.metadata.pluralName;
+ return convertCamelCaseToTitleCase(str);
+ };
+
+ return {
+ EventRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_set.html b/chromium/third_party/catapult/tracing/tracing/model/event_set.html
new file mode 100644
index 00000000000..8805a6302ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_set.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_registry.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const EventRegistry = tr.model.EventRegistry;
+
+ const RequestSelectionChangeEvent = tr.b.Event.bind(
+ undefined, 'requestSelectionChange', true, false);
+
+ /**
+ * Represents a event set within a and its associated set of tracks.
+ * @constructor
+ */
+ function EventSet(opt_events) {
+ this.bounds_ = new tr.b.math.Range();
+ this.events_ = new Set();
+ this.guid_ = tr.b.GUID.allocateSimple();
+
+ if (opt_events) {
+ if (opt_events instanceof Array) {
+ for (const event of opt_events) {
+ this.push(event);
+ }
+ } else if (opt_events instanceof EventSet) {
+ this.addEventSet(opt_events);
+ } else {
+ this.push(opt_events);
+ }
+ }
+ }
+
+ EventSet.prototype = {
+ __proto__: Object.prototype,
+
+ get bounds() {
+ return this.bounds_;
+ },
+
+ get duration() {
+ if (this.bounds_.isEmpty) return 0;
+ return this.bounds_.max - this.bounds_.min;
+ },
+
+ get length() {
+ return this.events_.size;
+ },
+
+ get guid() {
+ return this.guid_;
+ },
+
+ * [Symbol.iterator]() {
+ for (const event of this.events_) {
+ yield event;
+ }
+ },
+
+ clear() {
+ this.bounds_ = new tr.b.math.Range();
+ this.events_.clear();
+ },
+
+ /**
+ * Pushes each argument onto the EventSet. Returns the number of
+ * arguments pushed.
+ */
+ push(...events) {
+ let numPushed;
+ for (const event of events) {
+ if (event.guid === undefined) {
+ throw new Error('Event must have a GUID');
+ }
+
+ if (!this.events_.has(event)) {
+ this.events_.add(event);
+ // Some uses of eventSet (e.g. in tests) have Events as objects that
+ // don't have addBoundsToRange as a function. Thus we need to handle
+ // this case.
+ if (event.addBoundsToRange) {
+ if (this.bounds_ !== undefined) {
+ event.addBoundsToRange(this.bounds_);
+ }
+ }
+ }
+ numPushed++;
+ }
+ return numPushed;
+ },
+
+ contains(event) {
+ if (this.events_.has(event)) return event;
+ return undefined;
+ },
+
+ addEventSet(eventSet) {
+ for (const event of eventSet) {
+ this.push(event);
+ }
+ },
+
+ intersectionIsEmpty(otherEventSet) {
+ return !this.some(event => otherEventSet.contains(event));
+ },
+
+ equals(that) {
+ if (this.length !== that.length) return false;
+ return this.every(event => that.contains(event));
+ },
+
+ sortEvents(compare) {
+ // Convert to array, then sort, then convert back
+ const ary = this.toArray();
+ ary.sort(compare);
+
+ this.clear();
+ for (const event of ary) {
+ this.push(event);
+ }
+ },
+
+ getEventsOrganizedByBaseType(opt_pruneEmpty) {
+ const allTypeInfos = EventRegistry.getAllRegisteredTypeInfos();
+
+ const events = this.getEventsOrganizedByCallback(function(event) {
+ let maxEventIndex = -1;
+ let maxEventTypeInfo = undefined;
+
+ allTypeInfos.forEach(function(eventTypeInfo, eventIndex) {
+ if (!(event instanceof eventTypeInfo.constructor)) return;
+
+ if (eventIndex > maxEventIndex) {
+ maxEventIndex = eventIndex;
+ maxEventTypeInfo = eventTypeInfo;
+ }
+ });
+
+ if (maxEventIndex === -1) {
+ throw new Error(`Unrecognized event type: ${event.constructor.name}`);
+ }
+
+ return maxEventTypeInfo.metadata.name;
+ });
+
+ if (!opt_pruneEmpty) {
+ allTypeInfos.forEach(function(eventTypeInfo) {
+ if (events[eventTypeInfo.metadata.name] === undefined) {
+ events[eventTypeInfo.metadata.name] = new EventSet();
+ }
+ });
+ }
+
+ return events;
+ },
+
+ getEventsOrganizedByTitle() {
+ return this.getEventsOrganizedByCallback(function(event) {
+ if (event.title === undefined) {
+ throw new Error('An event didn\'t have a title!');
+ }
+ return event.title;
+ });
+ },
+
+ /**
+ * @param {!function(!tr.model.Event):string} cb
+ * @param {*=} opt_this
+ * @return {!Object} TODO(#3432) Return Map.
+ */
+ getEventsOrganizedByCallback(cb, opt_this) {
+ const groupedEvents = tr.b.groupIntoMap(this, cb, opt_this || this);
+ const groupedEventsDict = {};
+ for (const [k, events] of groupedEvents) {
+ groupedEventsDict[k] = new EventSet(events);
+ }
+ return groupedEventsDict;
+ },
+
+ enumEventsOfType(type, func) {
+ for (const event of this) {
+ if (event instanceof type) {
+ func(event);
+ }
+ }
+ },
+
+ get userFriendlyName() {
+ if (this.length === 0) {
+ throw new Error('Empty event set');
+ }
+
+ const eventsByBaseType = this.getEventsOrganizedByBaseType(true);
+ const eventTypeName = Object.keys(eventsByBaseType)[0];
+
+ if (this.length === 1) {
+ const tmp = EventRegistry.getUserFriendlySingularName(eventTypeName);
+ return tr.b.getOnlyElement(this.events_).userFriendlyName;
+ }
+
+ const numEventTypes = Object.keys(eventsByBaseType).length;
+ if (numEventTypes !== 1) {
+ return this.length + ' events of various types';
+ }
+
+ const tmp = EventRegistry.getUserFriendlyPluralName(eventTypeName);
+ return this.length + ' ' + tmp;
+ },
+
+ filter(fn, opt_this) {
+ const res = new EventSet();
+ for (const event of this) {
+ if (fn.call(opt_this, event)) {
+ res.push(event);
+ }
+ }
+
+ return res;
+ },
+
+ toArray() {
+ const ary = [];
+ for (const event of this) {
+ ary.push(event);
+ }
+ return ary;
+ },
+
+ forEach(fn, opt_this) {
+ for (const event of this) {
+ fn.call(opt_this, event);
+ }
+ },
+
+ map(fn, opt_this) {
+ const res = [];
+ for (const event of this) {
+ res.push(fn.call(opt_this, event));
+ }
+ return res;
+ },
+
+ every(fn, opt_this) {
+ for (const event of this) {
+ if (!fn.call(opt_this, event)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ some(fn, opt_this) {
+ for (const event of this) {
+ if (fn.call(opt_this, event)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ asDict() {
+ const stableIds = [];
+ for (const event of this) {
+ stableIds.push(event.stableId);
+ }
+ return {'events': stableIds};
+ },
+
+ asSet() {
+ return this.events_;
+ }
+ };
+
+ EventSet.IMMUTABLE_EMPTY_SET = (function() {
+ const s = new EventSet();
+ s.push = function() {
+ throw new Error('Cannot push to an immutable event set');
+ };
+ s.addEventSet = function() {
+ throw new Error('Cannot add to an immutable event set');
+ };
+ Object.freeze(s);
+ return s;
+ })();
+
+ return {
+ EventSet,
+ RequestSelectionChangeEvent,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_set_test.html b/chromium/third_party/catapult/tracing/tracing/model/event_set_test.html
new file mode 100644
index 00000000000..9f597ed0984
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_set_test.html
@@ -0,0 +1,360 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newModel = tr.c.TestUtils.newModel;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('eventSetObject', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 5, {}, 1));
+
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(t1.sliceGroup.slices[0]);
+
+ assert.strictEqual(eventSet.bounds.min, 1);
+ assert.strictEqual(eventSet.bounds.max, 4);
+ assert.deepEqual(eventSet.asSet(), new tr.model.EventSet(
+ t1.sliceGroup.slices[0]).asSet());
+
+ eventSet.push(t1.sliceGroup.slices[1]);
+ assert.strictEqual(eventSet.bounds.min, 1);
+ assert.strictEqual(eventSet.bounds.max, 6);
+ assert.deepEqual(eventSet.asSet(), new tr.model.EventSet(
+ [t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]).asSet());
+
+ eventSet.clear();
+ assert.strictEqual(eventSet.length, 0);
+ });
+
+ test('push_noEvents', function() {
+ const eventSet = new tr.model.EventSet();
+ eventSet.push();
+ assert.strictEqual(eventSet.length, 0);
+ });
+
+ test('push_oneEvent', function() {
+ const eventSet = new tr.model.EventSet();
+ eventSet.push({guid: 1});
+ assert.strictEqual(eventSet.length, 1);
+ });
+
+ test('push_multipleEvents', function() {
+ const eventSet = new tr.model.EventSet();
+ eventSet.push({guid: 1}, {guid: 2}, {guid: 3});
+ assert.strictEqual(eventSet.length, 3);
+ });
+
+ test('push_multiplePushes', function() {
+ const eventSet = new tr.model.EventSet();
+ eventSet.push({guid: 1}, {guid: 2}, {guid: 3});
+ eventSet.push({guid: 4}, {guid: 5}, {guid: 6});
+ assert.strictEqual(eventSet.length, 6);
+ });
+
+ test('push_noGuidThrows', function() {
+ const eventSet = new tr.model.EventSet();
+ assert.throws(() => eventSet.push({guid: 1}, {badItem: 2}, {guid: 3}),
+ 'Event must have a GUID');
+ });
+
+ test('iteration', function() {
+ const eventSet = new tr.model.EventSet([{guid: 1}, {guid: 2}, {guid: 3}]);
+
+ let expectedId = 1;
+ for (const event of eventSet) {
+ assert.strictEqual(event.guid, expectedId++);
+ }
+ });
+
+ test('uniqueContents', function() {
+ const sample1 = {guid: 1};
+ const sample2 = {guid: 2};
+
+ const eventSet = new tr.model.EventSet();
+
+ eventSet.push(sample1);
+ eventSet.push(sample2);
+ assert.strictEqual(eventSet.length, 2);
+
+ eventSet.push(sample1);
+ assert.strictEqual(eventSet.length, 2);
+ });
+
+ test('userFriendlyNameSingular', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ const selection = new tr.model.EventSet(t1.sliceGroup.slices[0]);
+ assert.isDefined(selection.userFriendlyName);
+ });
+
+ test('userFriendlyNamePlural', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3));
+ const eventSet = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[1]
+ ]);
+ assert.isDefined(eventSet.userFriendlyName);
+ });
+
+ test('userFriendlyNameMixedPlural', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3));
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ const eventSet = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ s10
+ ]);
+ assert.isDefined(eventSet.userFriendlyName);
+ });
+
+ test('groupEventsByTitle', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 3, {}, 3));
+ const eventSet = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[2]
+ ]);
+
+ const eventsByTitle = eventSet.getEventsOrganizedByTitle();
+ assert.strictEqual(2, Object.keys(eventsByTitle).length);
+ assert.sameMembers(eventsByTitle.a.toArray(),
+ [t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]);
+ assert.sameMembers(eventsByTitle.b.toArray(),
+ [t1.sliceGroup.slices[2]]);
+ });
+
+ test('groupEventsByCallback', function() {
+ const a1 = new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3);
+ const a2 = new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3);
+ const b1 = new tr.model.ThreadSlice('', 'b', 0, 1, {}, 3);
+ const eventSet = new tr.model.EventSet([a1, a2, b1]);
+ function getEventKey(event) {
+ return 's' + event.start;
+ }
+ const eventsByCallback = eventSet.getEventsOrganizedByCallback(getEventKey);
+ assert.strictEqual(2, Object.keys(eventsByCallback).length);
+ assert.sameMembers(eventsByCallback.s1.toArray(), [a1, b1]);
+ assert.sameMembers(eventsByCallback.s2.toArray(), [a2]);
+ });
+
+ test('groupEventsByBaseType', function() {
+ const a = new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3);
+ const b = new tr.model.ThreadSlice('', 'b', 0, 1, {}, 3);
+ const c = new tr.model.AsyncSlice('', 'c', 0, 1, {}, 3);
+ const eventSet = new tr.model.EventSet([a, b, c]);
+ let eventsByBaseType = eventSet.getEventsOrganizedByBaseType(true);
+ assert.strictEqual(2, Object.keys(eventsByBaseType).length);
+ assert.sameMembers(eventsByBaseType.slice.toArray(), [a, b]);
+ assert.sameMembers(eventsByBaseType.asyncSlice.toArray(), [c]);
+
+ eventsByBaseType = eventSet.getEventsOrganizedByBaseType(false);
+ assert.isTrue(2 < Object.keys(eventsByBaseType).length);
+ assert.sameMembers(eventsByBaseType.slice.toArray(), [a, b]);
+ assert.sameMembers(eventsByBaseType.asyncSlice.toArray(), [c]);
+ for (const baseType in eventsByBaseType) {
+ if (baseType !== 'slice' && baseType !== 'asyncSlice') {
+ assert.strictEqual(0, eventsByBaseType[baseType].length);
+ }
+ }
+ });
+
+ test('intersectionIsEmpty1', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 3, {}, 3));
+
+ const set1 = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[2]
+ ]);
+ const set2 = new tr.model.EventSet([
+ t1.sliceGroup.slices[2]
+ ]);
+ assert.isFalse(set1.intersectionIsEmpty(set2));
+ });
+
+ test('intersectionIsNonEmpty2', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 3, {}, 3));
+
+ const set1 = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[1]
+ ]);
+ const set2 = new tr.model.EventSet([
+ t1.sliceGroup.slices[2]
+ ]);
+ assert.isTrue(set1.intersectionIsEmpty(set2));
+ });
+
+ test('equals', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 2, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'c', 0, 3, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'd', 0, 4, {}, 5));
+
+ const eventSet1 = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[2]
+ ]);
+ const eventSet2 = new tr.model.EventSet([
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[2]
+ ]);
+ const eventSet3 = new tr.model.EventSet([
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[3]
+ ]);
+ const eventSet4 = new tr.model.EventSet([
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[0]
+ ]);
+ assert.isTrue(eventSet1.equals(eventSet2));
+ assert.isFalse(eventSet1.equals(eventSet3));
+ assert.isFalse(eventSet1.equals(eventSet4));
+ });
+
+ test('filter', function() {
+ const m = newModel(function(m) {
+ const p = m.getOrCreateProcess(1);
+ const t = p.getOrCreateThread(1);
+
+ m.s0 = t.sliceGroup.pushSlice(newSliceEx(
+ { title: 's0', start: 0.0, duration: 1.0 }));
+ m.s1 = t.sliceGroup.pushSlice(newSliceEx(
+ { title: 's1', start: 0.0, duration: 1.0 }));
+ m.s2 = t.sliceGroup.pushSlice(newSliceEx(
+ { title: 's2', start: 0.0, duration: 1.0 }));
+
+ m.eventSet = new tr.model.EventSet([m.s0, m.s1, m.s2]);
+ });
+
+ const res = m.eventSet.filter(function(slice) {
+ return slice.title === 's0';
+ });
+
+ assert.isTrue(res.equals(new tr.model.EventSet([m.s0])));
+ });
+
+ test('toArray', function() {
+ const samples = [
+ {guid: 1},
+ {guid: 2}
+ ];
+ const eventSet = new tr.model.EventSet(samples);
+
+ assert.deepEqual(eventSet.toArray(), samples);
+ });
+
+ test('asDict', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 2, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'c', 0, 3, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'd', 0, 4, {}, 5));
+
+ const eventSet = new tr.model.EventSet([
+ t1.sliceGroup.slices[0],
+ t1.sliceGroup.slices[1],
+ t1.sliceGroup.slices[2]
+ ]);
+
+ assert.deepEqual(
+ {'events':
+ ['1.1.SliceGroup.0', '1.1.SliceGroup.1', '1.1.SliceGroup.2']},
+ eventSet.asDict());
+ });
+
+ test('immutableEmptySet', function() {
+ const s = tr.model.EventSet.IMMUTABLE_EMPTY_SET;
+ assert.lengthOf(s, 0);
+ assert.isTrue(s.bounds.isEmpty);
+
+ // Check that the iteration methods still work correctly.
+ function throwOnCall() {
+ throw new Error('This function should never be called!!!');
+ }
+ assert.deepEqual(s.map(throwOnCall), []);
+ s.forEach(throwOnCall);
+
+ // Check that the set is indeed immutable.
+ assert.throws(function() { s[0] = b; });
+ assert.throws(function() { s.push(b); });
+ assert.throws(function() { s.addEventSet(new tr.model.EventSet()); });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_test.html b/chromium/third_party/catapult/tracing/tracing/model/event_test.html
new file mode 100644
index 00000000000..efbf26b1d13
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/event_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/alert.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Alert = tr.model.Alert;
+ const Event = tr.model.Event;
+ const EventInfo = tr.model.EventInfo;
+ const EventSet = tr.model.EventSet;
+ const ImmutableEventSet = tr.model.ImmutableEventSet;
+
+ test('checkModelItem', function() {
+ const event = new Event;
+ assert.strictEqual(event.modelItem, event);
+ });
+
+ test('checkAssociatedAlerts', function() {
+ const event = new Event();
+ assert.strictEqual(event.associatedAlerts, EventSet.IMMUTABLE_EMPTY_SET);
+ assert.sameMembers(event.associatedAlerts.toArray(), []);
+
+ const info1 = new EventInfo('Critical', 'Critical alert!!!', []);
+ const alert1 = new Alert(info1, 7);
+ event.addAssociatedAlert(alert1);
+ assert.instanceOf(event.associatedAlerts, EventSet);
+ assert.sameMembers(event.associatedAlerts.toArray(), [alert1]);
+
+ const info2 = new EventInfo('Warning', 'Warning alert???', []);
+ const alert2 = new Alert(info2, 42);
+ event.addAssociatedAlert(alert2);
+ assert.instanceOf(event.associatedAlerts, EventSet);
+ assert.sameMembers(event.associatedAlerts.toArray(), [alert1, alert2]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/flow_event.html b/chromium/third_party/catapult/tracing/tracing/model/flow_event.html
new file mode 100644
index 00000000000..9b85217d6f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/flow_event.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Flow class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A Flow represents an interval of time plus parameters associated
+ * with that interval.
+ *
+ * @constructor
+ */
+ function FlowEvent(category, id, title, colorId, start, args, opt_duration) {
+ tr.model.TimedEvent.call(this, start);
+
+ this.category = category || '';
+ this.title = title;
+ this.colorId = colorId;
+ this.start = start;
+ this.args = args;
+
+ this.id = id;
+
+ this.startSlice = undefined;
+ this.endSlice = undefined;
+
+ this.startStackFrame = undefined;
+ this.endStackFrame = undefined;
+
+ if (opt_duration !== undefined) {
+ this.duration = opt_duration;
+ }
+ }
+
+ FlowEvent.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ get userFriendlyName() {
+ return 'Flow event named ' + this.title + ' at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.timestamp);
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ FlowEvent,
+ {
+ name: 'flowEvent',
+ pluralName: 'flowEvents'
+ });
+
+ return {
+ FlowEvent,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/frame.html b/chromium/third_party/catapult/tracing/tracing/model/frame.html
new file mode 100644
index 00000000000..6d1051a603d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/frame.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class describing rendered frames.
+ *
+ * Because a frame is produced by multiple threads, it does not inherit from
+ * TimedEvent, and has no duration.
+ */
+tr.exportTo('tr.model', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Statistics = tr.b.math.Statistics;
+
+ const FRAME_PERF_CLASS = {
+ GOOD: 'good',
+ BAD: 'bad',
+ TERRIBLE: 'terrible',
+ NEUTRAL: 'generic_work'
+ };
+
+ /**
+ * @constructor
+ * @param {Array} associatedEvents Selection of events composing the frame.
+ * @param {Array} threadTimeRanges Array of {thread, start, end}
+ * for each thread, describing the critical path of the frame.
+ */
+ function Frame(associatedEvents, threadTimeRanges, opt_args) {
+ tr.model.Event.call(this);
+
+ this.threadTimeRanges = threadTimeRanges;
+ this.associatedEvents = new tr.model.EventSet(associatedEvents);
+ this.args = opt_args || {};
+
+ this.title = 'Frame';
+ this.start = Statistics.min(
+ threadTimeRanges, function(x) { return x.start; });
+ this.end = Statistics.max(
+ threadTimeRanges, function(x) { return x.end; });
+ this.totalDuration = Statistics.sum(
+ threadTimeRanges, function(x) { return x.end - x.start; });
+
+ this.perfClass = FRAME_PERF_CLASS.NEUTRAL;
+ }
+
+ Frame.prototype = {
+ __proto__: tr.model.Event.prototype,
+
+ set perfClass(perfClass) {
+ this.colorId = ColorScheme.getColorIdForReservedName(perfClass);
+ this.perfClass_ = perfClass;
+ },
+
+ get perfClass() {
+ return this.perfClass_;
+ },
+
+ shiftTimestampsForward(amount) {
+ this.start += amount;
+ this.end += amount;
+
+ for (let i = 0; i < this.threadTimeRanges.length; i++) {
+ this.threadTimeRanges[i].start += amount;
+ this.threadTimeRanges[i].end += amount;
+ }
+ },
+
+ addBoundsToRange(range) {
+ range.addValue(this.start);
+ range.addValue(this.end);
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ Frame,
+ {
+ name: 'frame',
+ pluralName: 'frames'
+ });
+
+ return {
+ Frame,
+ FRAME_PERF_CLASS,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html
new file mode 100644
index 00000000000..058f52f14e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html
@@ -0,0 +1,849 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/event_registry.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the GlobalMemoryDump class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * The GlobalMemoryDump represents a simultaneous memory dump of all
+ * processes.
+ * @constructor
+ */
+ function GlobalMemoryDump(model, start) {
+ tr.model.ContainerMemoryDump.call(this, start);
+ this.model = model;
+ this.processMemoryDumps = {};
+ }
+
+ // Size numeric names.
+ const SIZE_NUMERIC_NAME = tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME;
+ const EFFECTIVE_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME;
+
+ // Size numeric info types.
+ 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 ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.start);
+ },
+
+ get containerName() {
+ return 'global space';
+ },
+
+ finalizeGraph() {
+ // 1. Transitively remove weak memory allocator dumps and all their
+ // owners and descendants from the model. This must be performed before
+ // any other steps.
+ this.removeWeakDumps();
+
+ // 2. Add ownership links from tracing MADs to descendants of malloc or
+ // winheap MADs so that tracing would be automatically discounted from
+ // them later (step 3).
+ this.setUpTracingOverheadOwnership();
+
+ // 3. Aggregate all other numerics of all MADs (*excluding* sizes and
+ // effective sizes) and propagate numerics from global MADs to their
+ // owners (*including* sizes and effective sizes). This step must be
+ // carried out before the sizes of all MADs are calculated (step 3).
+ // Otherwise, the propagated sizes of all MADs would not be aggregated.
+ this.aggregateNumerics();
+
+ // 4. Calculate the sizes of all memory allocator dumps (MADs). This step
+ // requires that the memory allocator dump graph has been finalized (step
+ // 1) and numerics were propagated from global MADs (step 2). Subsequent
+ // modifications of the graph will most likely break the calculation
+ // invariants.
+ this.calculateSizes();
+
+ // 5. Calculate the effective sizes of all MADs. This step requires that
+ // the sizes of all MADs have already been calculated (step 3).
+ this.calculateEffectiveSizes();
+
+ // 6. Discount tracing from VM regions stats. This steps requires that
+ // resident sizes (step 2) and sizes (step 3) of the tracing MADs have
+ // already been calculated.
+ this.discountTracingOverheadFromVmRegions();
+
+ // 7. The above steps (especially steps 1 and 3) can create new memory
+ // allocator dumps, so we force rebuilding the memory allocator dump
+ // indices of all container memory dumps.
+ this.forceRebuildingMemoryAllocatorDumpByFullNameIndices();
+ },
+
+ removeWeakDumps() {
+ // Mark all transitive owners and children of weak memory allocator dumps
+ // as weak.
+ 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) {
+ // The dump is weak, so remove it. This will implicitly remove all
+ // its descendants, which are also weak due to the initial marking
+ // step.
+ return false;
+ }
+
+ // This dump is non-weak, so keep it. Recursively remove its weak
+ // descendants and ownership links from weak dumps instead.
+ 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);
+ }
+ });
+ },
+
+ /**
+ * Calculate the size of all memory allocator dumps in the dump graph.
+ *
+ * The size refers to the allocated size of a (sub)component. It is a
+ * natural extension of the optional size numeric provided by
+ * MemoryAllocatorDump(s):
+ *
+ * - If a MAD provides a size numeric, then its size is assumed to be
+ * equal to it.
+ * - If a MAD does not provide a size numeric, then its size is assumed
+ * to be the maximum of (1) the size of the largest owner of the MAD
+ * and (2) the aggregated size of the MAD's children.
+ *
+ * Metric motivation: "How big is a (sub)system?"
+ *
+ * Please refer to the Memory Dump Graph Metric Calculation design document
+ * for more details (https://goo.gl/fKg0dt).
+ */
+ calculateSizes() {
+ this.traverseAllocatorDumpsInDepthFirstPostOrder(
+ this.calculateMemoryAllocatorDumpSize_.bind(this));
+ },
+
+ /**
+ * Calculate the size of the given MemoryAllocatorDump. This method assumes
+ * that the size of both the children and owners of the dump has already
+ * been calculated.
+ */
+ calculateMemoryAllocatorDumpSize_(dump) {
+ // This flag becomes true if the size numeric of the current dump should
+ // be defined, i.e. if (1) the current dump's size numeric is defined,
+ // (2) the size of at least one of its children is defined or (3) the
+ // size of at least one of its owners is defined.
+ let shouldDefineSize = false;
+
+ // This helper function returns the value of the size numeric of the
+ // given dependent memory allocator dump. If the numeric is defined, the
+ // shouldDefineSize flag above is also set to true (because condition
+ // (2) or (3) is satisfied). Otherwise, zero is returned (and the flag is
+ // left unchanged).
+ function getDependencySize(dependencyDump) {
+ const numeric = dependencyDump.numerics[SIZE_NUMERIC_NAME];
+ if (numeric === undefined) return 0;
+ shouldDefineSize = true;
+ return numeric.value;
+ }
+
+ // 1. Get the size provided by the dump. If present, define a function
+ // for checking dependent size consistency (a dump must always be bigger
+ // than all its children aggregated together and/or its largest owner).
+ const sizeNumeric = dump.numerics[SIZE_NUMERIC_NAME];
+ let size = 0;
+ let checkDependencySizeIsConsistent = function() { /* no-op */ };
+ if (sizeNumeric !== undefined) {
+ size = sizeNumeric.value;
+ shouldDefineSize = true;
+ if (sizeNumeric.unit !== tr.b.Unit.byName.sizeInBytes_smallerIsBetter) {
+ this.model.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Invalid unit of \'size\' numeric of memory allocator ' +
+ 'dump ' + dump.quantifiedName + ': ' +
+ sizeNumeric.unit.unitName + '.'
+ });
+ }
+ checkDependencySizeIsConsistent = function(
+ dependencySize, dependencyInfoType, dependencyName) {
+ if (size >= dependencySize) return;
+ this.model.importWarning({
+ type: 'memory_dump_parse_error',
+ message: 'Size provided by memory allocator dump \'' +
+ dump.fullName + '\'' +
+ tr.b.Unit.byName.sizeInBytes.format(size) +
+ ') is less than ' + dependencyName + ' (' +
+ tr.b.Unit.byName.sizeInBytes.format(dependencySize) + ').'
+ });
+ dump.infos.push({
+ type: dependencyInfoType,
+ providedSize: size,
+ dependencySize
+ });
+ }.bind(this);
+ }
+
+ // 2. Aggregate size of children. The recursive function traverses all
+ // descendants and ensures that double-counting due to ownership within a
+ // subsystem is avoided.
+ let aggregatedChildrenSize = 0;
+ // Owned child dump name -> (Owner child dump name -> overlapping size).
+ const allOverlaps = {};
+ dump.children.forEach(function(childDump) {
+ function aggregateDescendantDump(descendantDump) {
+ // Don't count this descendant dump if it owns another descendant of
+ // the current dump (would cause double-counting).
+ const ownedDumpLink = descendantDump.owns;
+ if (ownedDumpLink !== undefined &&
+ ownedDumpLink.target.isDescendantOf(dump)) {
+ // If the target owned dump is a descendant of a *different* child
+ // of the the current dump (i.e. not childDump), then we remember
+ // the ownership so that we could explain why the size of the
+ // current dump is not equal to the sum of its children.
+ let ownedChildDump = ownedDumpLink.target;
+ while (ownedChildDump.parent !== dump) {
+ ownedChildDump = ownedChildDump.parent;
+ }
+ if (childDump !== ownedChildDump) {
+ const ownedBySiblingSize = getDependencySize(descendantDump);
+ if (ownedBySiblingSize > 0) {
+ const previousTotalOwnedBySiblingSize =
+ ownedChildDump.ownedBySiblingSizes.get(childDump) || 0;
+ const updatedTotalOwnedBySiblingSize =
+ previousTotalOwnedBySiblingSize + ownedBySiblingSize;
+ ownedChildDump.ownedBySiblingSizes.set(
+ childDump, updatedTotalOwnedBySiblingSize);
+ }
+ }
+ return;
+ }
+
+ // If this descendant dump is a leaf node, add its size to the
+ // aggregated size.
+ if (descendantDump.children.length === 0) {
+ aggregatedChildrenSize += getDependencySize(descendantDump);
+ return;
+ }
+
+ // If this descendant dump is an intermediate node, recurse down into
+ // its children. Note that the dump's size is NOT added because it is
+ // an aggregate of its children (would cause double-counting).
+ descendantDump.children.forEach(aggregateDescendantDump);
+ }
+ aggregateDescendantDump(childDump);
+ });
+ checkDependencySizeIsConsistent(
+ aggregatedChildrenSize,
+ PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN,
+ 'the aggregated size of its children');
+
+ // 3. Calculate the largest owner size.
+ let largestOwnerSize = 0;
+ dump.ownedBy.forEach(function(ownershipLink) {
+ const owner = ownershipLink.source;
+ const ownerSize = getDependencySize(owner);
+ largestOwnerSize = Math.max(largestOwnerSize, ownerSize);
+ });
+ checkDependencySizeIsConsistent(
+ largestOwnerSize,
+ PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER,
+ 'the size of its largest owner');
+
+ // If neither the dump nor any of its dependencies (children and owners)
+ // provide a size, do NOT add a zero size numeric.
+ if (!shouldDefineSize) {
+ // The rest of the pipeline relies on size being either a valid
+ // Scalar, or undefined.
+ delete dump.numerics[SIZE_NUMERIC_NAME];
+ return;
+ }
+
+ // A dump must always be bigger than all its children aggregated
+ // together and/or its largest owner.
+ size = Math.max(size, aggregatedChildrenSize, largestOwnerSize);
+
+ dump.numerics[SIZE_NUMERIC_NAME] = new tr.b.Scalar(
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, size);
+
+ // Add a virtual child to make up for extra size of the dump with
+ // respect to its children (if applicable).
+ if (aggregatedChildrenSize < size &&
+ dump.children !== undefined && dump.children.length > 0) {
+ const virtualChild = new tr.model.MemoryAllocatorDump(
+ dump.containerMemoryDump, dump.fullName + '/<unspecified>');
+ virtualChild.parent = dump;
+ dump.children.unshift(virtualChild);
+ virtualChild.numerics[SIZE_NUMERIC_NAME] = new tr.b.Scalar(
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter,
+ size - aggregatedChildrenSize);
+ }
+ },
+
+ /**
+ * Calculate the effective size of all memory allocator dumps in the dump
+ * graph.
+ *
+ * The effective size refers to the amount of memory a particular component
+ * is using/consuming. In other words, every (reported) byte of used memory
+ * is uniquely attributed to exactly one component. Consequently, unlike
+ * size, effective size is cumulative, i.e. the sum of the effective sizes
+ * of (top-level) components is equal to the total amount of (reported)
+ * used memory.
+ *
+ * Metric motivation: "How much memory does a (sub)system use?" or "For how
+ * much memory should a (sub)system be 'charged'?"
+ *
+ * Please refer to the Memory Dump Graph Metric Calculation design document
+ * for more details (https://goo.gl/fKg0dt).
+ *
+ * This method assumes that the size of all contained memory allocator
+ * dumps has already been calculated [see calculateSizes()].
+ */
+ calculateEffectiveSizes() {
+ // 1. Calculate not-owned and not-owning sub-sizes of all MADs
+ // (depth-first post-order traversal).
+ this.traverseAllocatorDumpsInDepthFirstPostOrder(
+ this.calculateDumpSubSizes_.bind(this));
+
+ // 2. Calculate owned and owning coefficients of owned and owner MADs
+ // respectively (arbitrary traversal).
+ this.traverseAllocatorDumpsInDepthFirstPostOrder(
+ this.calculateDumpOwnershipCoefficient_.bind(this));
+
+ // 3. Calculate cumulative owned and owning coefficients of all MADs
+ // (depth-first pre-order traversal).
+ this.traverseAllocatorDumpsInDepthFirstPreOrder(
+ this.calculateDumpCumulativeOwnershipCoefficient_.bind(this));
+
+ // 4. Calculate the effective sizes of all MADs (depth-first post-order
+ // traversal).
+ this.traverseAllocatorDumpsInDepthFirstPostOrder(
+ this.calculateDumpEffectiveSize_.bind(this));
+ },
+
+ /**
+ * Calculate not-owned and not-owning sub-sizes of a memory allocator dump
+ * from its children's (sub-)sizes.
+ *
+ * Not-owned sub-size refers to the aggregated memory of all children which
+ * is not owned by other MADs. Conversely, not-owning sub-size is the
+ * aggregated memory of all children which do not own another MAD. The
+ * diagram below illustrates these two concepts:
+ *
+ * ROOT 1 ROOT 2
+ * size: 4 size: 5
+ * not-owned sub-size: 4 not-owned sub-size: 1 (!)
+ * not-owning sub-size: 0 (!) not-owning sub-size: 5
+ *
+ * ^ ^
+ * | |
+ *
+ * PARENT 1 ===== owns =====> PARENT 2
+ * size: 4 size: 5
+ * not-owned sub-size: 4 not-owned sub-size: 5
+ * not-owning sub-size: 4 not-owning sub-size: 5
+ *
+ * ^ ^
+ * | |
+ *
+ * CHILD 1 CHILD 2
+ * size [given]: 4 size [given]: 5
+ * not-owned sub-size: 4 not-owned sub-size: 5
+ * not-owning sub-size: 4 not-owning sub-size: 5
+ *
+ * This method assumes that (1) the size of the dump, its children, and its
+ * owners [see calculateSizes()] and (2) the not-owned and not-owning
+ * sub-sizes of both the children and owners of the dump have already been
+ * calculated [depth-first post-order traversal].
+ */
+ calculateDumpSubSizes_(dump) {
+ // Completely skip dumps with undefined size.
+ if (!hasSize(dump)) return;
+
+ // If the dump is a leaf node, then both sub-sizes are equal to the size.
+ if (dump.children === undefined || dump.children.length === 0) {
+ const size = getSize(dump);
+ dump.notOwningSubSize_ = size;
+ dump.notOwnedSubSize_ = size;
+ return;
+ }
+
+ // Calculate this dump's not-owning sub-size by summing up the not-owning
+ // sub-sizes of children MADs which do not own another MAD.
+ let notOwningSubSize = 0;
+ dump.children.forEach(function(childDump) {
+ if (childDump.owns !== undefined) return;
+ notOwningSubSize += optional(childDump.notOwningSubSize_, 0);
+ });
+ dump.notOwningSubSize_ = notOwningSubSize;
+
+ // Calculate this dump's not-owned sub-size.
+ let notOwnedSubSize = 0;
+ dump.children.forEach(function(childDump) {
+ // If the child dump is not owned, then add its not-owned sub-size.
+ if (childDump.ownedBy.length === 0) {
+ notOwnedSubSize += optional(childDump.notOwnedSubSize_, 0);
+ return;
+ }
+ // If the child dump is owned, then add the difference between its size
+ // and the largest owner.
+ let largestChildOwnerSize = 0;
+ childDump.ownedBy.forEach(function(ownershipLink) {
+ largestChildOwnerSize = Math.max(
+ largestChildOwnerSize, getSize(ownershipLink.source));
+ });
+ notOwnedSubSize += getSize(childDump) - largestChildOwnerSize;
+ });
+ dump.notOwnedSubSize_ = notOwnedSubSize;
+ },
+
+ /**
+ * Calculate owned and owning coefficients of a memory allocator dump and
+ * its owners.
+ *
+ * The owning coefficient refers to the proportion of a dump's not-owning
+ * sub-size which is attributed to the dump (only relevant to owning MADs).
+ * Conversely, the owned coefficient is the proportion of a dump's
+ * not-owned sub-size, which is attributed to it (only relevant to owned
+ * MADs).
+ *
+ * The not-owned size of the owned dump is split among its owners in the
+ * order of the ownership importance as demonstrated by the following
+ * example:
+ *
+ * memory allocator dumps
+ * OWNED OWNER1 OWNER2 OWNER3 OWNER4
+ * not-owned sub-size [given] 10 - - - -
+ * not-owning sub-size [given] - 6 7 5 8
+ * importance [given] - 2 2 1 0
+ * attributed not-owned sub-size 2 - - - -
+ * attributed not-owning sub-size - 3 4 0 1
+ * owned coefficient 2/10 - - - -
+ * owning coefficient - 3/6 4/7 0/5 1/8
+ *
+ * Explanation: Firstly, 6 bytes are split equally among OWNER1 and OWNER2
+ * (highest importance). OWNER2 owns one more byte, so its attributed
+ * not-owning sub-size is 6/2 + 1 = 4 bytes. OWNER3 is attributed no size
+ * because it is smaller than the owners with higher priority. However,
+ * OWNER4 is larger, so it's attributed the difference 8 - 7 = 1 byte.
+ * Finally, 2 bytes remain unattributed and are hence kept in the OWNED
+ * dump as attributed not-owned sub-size. The coefficients are then
+ * directly calculated as fractions of the sub-sizes and corresponding
+ * attributed sub-sizes.
+ *
+ * Note that we always assume that all ownerships of a dump overlap (e.g.
+ * OWNER3 is subsumed by both OWNER1 and OWNER2). Hence, the table could
+ * be alternatively represented as follows:
+ *
+ * owned memory range
+ * 0 1 2 3 4 5 6 7 8 9 10
+ * Priority 2 | OWNER1 + OWNER2 (split) | OWNER2 |
+ * Priority 1 | (already attributed) |
+ * Priority 0 | - - - (already attributed) - - - | OWNER4 |
+ * Remainder | - - - - - (already attributed) - - - - - - | OWNED |
+ *
+ * This method assumes that (1) the size of the dump [see calculateSizes()]
+ * and (2) the not-owned size of the dump and not-owning sub-sizes of its
+ * owners [see the first step of calculateEffectiveSizes()] have already
+ * been calculated. Note that the method doesn't make any assumptions about
+ * the order in which dumps are visited.
+ */
+ calculateDumpOwnershipCoefficient_(dump) {
+ // Completely skip dumps with undefined size.
+ if (!hasSize(dump)) return;
+
+ // We only need to consider owned dumps.
+ if (dump.ownedBy.length === 0) return;
+
+ // Sort the owners in decreasing order of ownership importance and
+ // increasing order of not-owning sub-size (in case of equal importance).
+ const owners = dump.ownedBy.map(function(ownershipLink) {
+ return {
+ dump: ownershipLink.source,
+ importance: optional(ownershipLink.importance, 0),
+ notOwningSubSize: optional(ownershipLink.source.notOwningSubSize_, 0)
+ };
+ });
+ owners.sort(function(a, b) {
+ if (a.importance === b.importance) {
+ return a.notOwningSubSize - b.notOwningSubSize;
+ }
+ return b.importance - a.importance;
+ });
+
+ // Loop over the list of owners and distribute the owned dump's not-owned
+ // sub-size among them according to their ownership importance and
+ // not-owning sub-size.
+ let currentImportanceStartPos = 0;
+ let alreadyAttributedSubSize = 0;
+ while (currentImportanceStartPos < owners.length) {
+ const currentImportance = owners[currentImportanceStartPos].importance;
+
+ // Find the position of the first owner with lower priority.
+ let nextImportanceStartPos = currentImportanceStartPos + 1;
+ while (nextImportanceStartPos < owners.length &&
+ owners[nextImportanceStartPos].importance ===
+ currentImportance) {
+ nextImportanceStartPos++;
+ }
+
+ // Visit the owners with the same importance in increasing order of
+ // not-owned sub-size, split the owned memory among them appropriately,
+ // and calculate their owning coefficients.
+ let attributedNotOwningSubSize = 0;
+ for (let pos = currentImportanceStartPos; pos < nextImportanceStartPos;
+ pos++) {
+ const owner = owners[pos];
+ const notOwningSubSize = owner.notOwningSubSize;
+ if (notOwningSubSize > alreadyAttributedSubSize) {
+ attributedNotOwningSubSize +=
+ (notOwningSubSize - alreadyAttributedSubSize) /
+ (nextImportanceStartPos - pos);
+ alreadyAttributedSubSize = notOwningSubSize;
+ }
+
+ let owningCoefficient = 0;
+ if (notOwningSubSize !== 0) {
+ owningCoefficient = attributedNotOwningSubSize / notOwningSubSize;
+ }
+ owner.dump.owningCoefficient_ = owningCoefficient;
+ }
+
+ currentImportanceStartPos = nextImportanceStartPos;
+ }
+
+ // Attribute the remainder of the owned dump's not-owned sub-size to
+ // the dump itself and calculate its owned coefficient.
+ const notOwnedSubSize = optional(dump.notOwnedSubSize_, 0);
+ const remainderSubSize = notOwnedSubSize - alreadyAttributedSubSize;
+ let ownedCoefficient = 0;
+ if (notOwnedSubSize !== 0) {
+ ownedCoefficient = remainderSubSize / notOwnedSubSize;
+ }
+ dump.ownedCoefficient_ = ownedCoefficient;
+ },
+
+ /**
+ * Calculate cumulative owned and owning coefficients of a memory allocator
+ * dump from its (non-cumulative) owned and owning coefficients and the
+ * cumulative coefficients of its parent and/or owned dump.
+ *
+ * The cumulative coefficients represent the total effect of all
+ * (non-strict) ancestor ownerships on a memory allocator dump. The
+ * cumulative owned coefficient of a MAD can be calculated simply as:
+ *
+ * cumulativeOwnedC(M) = ownedC(M) * cumulativeOwnedC(parent(M))
+ *
+ * This reflects the assumption that if a parent of a child MAD is
+ * (partially) owned, then the parent's owner also indirectly owns (a part
+ * of) the child MAD.
+ *
+ * The cumulative owning coefficient of a MAD depends on whether the MAD
+ * owns another dump:
+ *
+ * [if M doesn't own another MAD]
+ * / cumulativeOwningC(parent(M))
+ * cumulativeOwningC(M) =
+ * \ [if M owns another MAD]
+ * owningC(M) * cumulativeOwningC(owned(M))
+ *
+ * The reasoning behind the first case is similar to the one for cumulative
+ * owned coefficient above. The only difference is that we don't need to
+ * include the dump's (non-cumulative) owning coefficient because it is
+ * implicitly 1.
+ *
+ * The formula for the second case is derived as follows: Since the MAD
+ * owns another dump, its memory is not included in its parent's not-owning
+ * sub-size and hence shouldn't be affected by the parent's corresponding
+ * cumulative coefficient. Instead, the MAD indirectly owns everything
+ * owned by its owned dump (and so it should be affected by the
+ * corresponding coefficient).
+ *
+ * Note that undefined coefficients (and coefficients of non-existent
+ * dumps) are implicitly assumed to be 1.
+ *
+ * This method assumes that (1) the size of the dump [see calculateSizes()],
+ * (2) the (non-cumulative) owned and owning coefficients of the dump [see
+ * the second step of calculateEffectiveSizes()], and (3) the cumulative
+ * coefficients of the dump's parent and owned MADs (if present)
+ * [depth-first pre-order traversal] have already been calculated.
+ */
+ calculateDumpCumulativeOwnershipCoefficient_(dump) {
+ // Completely skip dumps with undefined size.
+ if (!hasSize(dump)) return;
+
+ let cumulativeOwnedCoefficient = optional(dump.ownedCoefficient_, 1);
+ const parent = dump.parent;
+ if (dump.parent !== undefined) {
+ cumulativeOwnedCoefficient *= dump.parent.cumulativeOwnedCoefficient_;
+ }
+ dump.cumulativeOwnedCoefficient_ = cumulativeOwnedCoefficient;
+
+ let cumulativeOwningCoefficient;
+ if (dump.owns !== undefined) {
+ cumulativeOwningCoefficient = dump.owningCoefficient_ *
+ dump.owns.target.cumulativeOwningCoefficient_;
+ } else if (dump.parent !== undefined) {
+ cumulativeOwningCoefficient = dump.parent.cumulativeOwningCoefficient_;
+ } else {
+ cumulativeOwningCoefficient = 1;
+ }
+ dump.cumulativeOwningCoefficient_ = cumulativeOwningCoefficient;
+ },
+
+ /**
+ * Calculate the effective size of a memory allocator dump.
+ *
+ * In order to simplify the (already complex) calculation, we use the fact
+ * that effective size is cumulative (unlike regular size), i.e. the
+ * effective size of a non-leaf node is equal to the sum of effective sizes
+ * of its children. The effective size of a leaf MAD is calculated as:
+ *
+ * effectiveSize(M) = size(M) * cumulativeOwningC(M) * cumulativeOwnedC(M)
+ *
+ * This method assumes that (1) the size of the dump and its children [see
+ * calculateSizes()] and (2) the cumulative owning and owned coefficients
+ * of the dump (if it's a leaf node) [see the third step of
+ * calculateEffectiveSizes()] or the effective sizes of its children (if
+ * it's a non-leaf node) [depth-first post-order traversal] have already
+ * been calculated.
+ */
+ calculateDumpEffectiveSize_(dump) {
+ // Completely skip dumps with undefined size. As a result, each dump will
+ // have defined effective size if and only if it has defined size.
+ if (!hasSize(dump)) {
+ // The rest of the pipeline relies on effective size being either a
+ // valid Scalar, or undefined.
+ delete dump.numerics[EFFECTIVE_SIZE_NUMERIC_NAME];
+ return;
+ }
+
+ let effectiveSize;
+ if (dump.children === undefined || dump.children.length === 0) {
+ // Leaf dump.
+ effectiveSize = getSize(dump) * dump.cumulativeOwningCoefficient_ *
+ dump.cumulativeOwnedCoefficient_;
+ } else {
+ // Non-leaf dump.
+ effectiveSize = 0;
+ dump.children.forEach(function(childDump) {
+ if (!hasSize(childDump)) return;
+ effectiveSize +=
+ childDump.numerics[EFFECTIVE_SIZE_NUMERIC_NAME].value;
+ });
+ }
+ dump.numerics[EFFECTIVE_SIZE_NUMERIC_NAME] = new tr.b.Scalar(
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, effectiveSize);
+ },
+
+ aggregateNumerics() {
+ // 1. Aggregate numerics in this global memory dump.
+ this.iterateRootAllocatorDumps(function(dump) {
+ dump.aggregateNumericsRecursively(this.model);
+ });
+
+ // 2. Propagate numerics and diagnostics from global memory allocator
+ // dumps to their owners.
+ this.iterateRootAllocatorDumps(
+ this.propagateNumericsAndDiagnosticsRecursively);
+
+ // 3. Aggregate numerics in the associated process memory dumps.
+ for (const processMemoryDump of Object.values(this.processMemoryDumps)) {
+ processMemoryDump.iterateRootAllocatorDumps(function(dump) {
+ dump.aggregateNumericsRecursively(this.model);
+ }, this);
+ }
+ },
+
+ propagateNumericsAndDiagnosticsRecursively(globalAllocatorDump) {
+ ['numerics', 'diagnostics'].forEach(function(field) {
+ for (const [name, value] of
+ Object.entries(globalAllocatorDump[field])) {
+ globalAllocatorDump.ownedBy.forEach(function(ownershipLink) {
+ const processAllocatorDump = ownershipLink.source;
+ if (processAllocatorDump[field][name] !== undefined) {
+ // Numerics and diagnostics provided by process memory allocator
+ // dumps themselves have precedence over numerics and diagnostics
+ // propagated from global memory allocator dumps.
+ return;
+ }
+ processAllocatorDump[field][name] = value;
+ });
+ }
+ });
+
+ // Recursively propagate numerics from all child memory allocator dumps.
+ globalAllocatorDump.children.forEach(
+ this.propagateNumericsAndDiagnosticsRecursively, this);
+ },
+
+ setUpTracingOverheadOwnership() {
+ for (const dump of Object.values(this.processMemoryDumps)) {
+ dump.setUpTracingOverheadOwnership(this.model);
+ }
+ },
+
+ discountTracingOverheadFromVmRegions() {
+ // TODO(petrcermak): Consider factoring out all the finalization code and
+ // constants to a single file.
+ for (const dump of Object.values(this.processMemoryDumps)) {
+ dump.discountTracingOverheadFromVmRegions(this.model);
+ }
+ },
+
+ forceRebuildingMemoryAllocatorDumpByFullNameIndices() {
+ this.iterateContainerDumps(function(containerDump) {
+ containerDump.forceRebuildingMemoryAllocatorDumpByFullNameIndex();
+ });
+ },
+
+ iterateContainerDumps(fn) {
+ fn.call(this, this);
+ for (const processDump of Object.values(this.processMemoryDumps)) {
+ fn.call(this, processDump);
+ }
+ },
+
+ iterateAllRootAllocatorDumps(fn) {
+ this.iterateContainerDumps(function(containerDump) {
+ containerDump.iterateRootAllocatorDumps(fn, this);
+ });
+ },
+
+ /**
+ * Traverse the memory dump graph in a depth first post-order, i.e.
+ * children and owners of a memory allocator dump are visited before the
+ * dump itself. This method will throw an exception if the graph contains
+ * a cycle.
+ */
+ traverseAllocatorDumpsInDepthFirstPostOrder(fn) {
+ const visitedDumps = new WeakSet();
+ const openDumps = new WeakSet();
+
+ function visit(dump) {
+ if (visitedDumps.has(dump)) return;
+
+ if (openDumps.has(dump)) {
+ throw new Error(dump.userFriendlyName + ' contains a cycle');
+ }
+ openDumps.add(dump);
+
+ // Visit owners before the dumps they own.
+ dump.ownedBy.forEach(function(ownershipLink) {
+ visit.call(this, ownershipLink.source);
+ }, this);
+
+ // Visit children before parents.
+ dump.children.forEach(visit, this);
+
+ // Actually visit the current memory allocator dump.
+ fn.call(this, dump);
+ visitedDumps.add(dump);
+
+ openDumps.delete(dump);
+ }
+
+ this.iterateAllRootAllocatorDumps(visit);
+ },
+
+ /**
+ * Traverse the memory dump graph in a depth first pre-order, i.e.
+ * children and owners of a memory allocator dump are visited after the
+ * dump itself. This method will not visit some dumps if the graph contains
+ * a cycle.
+ */
+ traverseAllocatorDumpsInDepthFirstPreOrder(fn) {
+ const visitedDumps = new WeakSet();
+
+ function visit(dump) {
+ if (visitedDumps.has(dump)) return;
+
+ // If this dumps owns another dump which hasn't been visited yet, then
+ // wait for this dump to be visited later.
+ if (dump.owns !== undefined && !visitedDumps.has(dump.owns.target)) {
+ return;
+ }
+
+ // If this dump's parent hasn't been visited yet, then wait for this
+ // dump to be visited later.
+ if (dump.parent !== undefined && !visitedDumps.has(dump.parent)) {
+ return;
+ }
+
+ // Actually visit the current memory allocator dump.
+ fn.call(this, dump);
+ visitedDumps.add(dump);
+
+ // Visit owners after the dumps they own.
+ dump.ownedBy.forEach(function(ownershipLink) {
+ visit.call(this, ownershipLink.source);
+ }, this);
+
+ // Visit children after parents.
+ dump.children.forEach(visit, this);
+ }
+
+ this.iterateAllRootAllocatorDumps(visit);
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ GlobalMemoryDump,
+ {
+ name: 'globalMemoryDump',
+ pluralName: 'globalMemoryDumps'
+ });
+
+ return {
+ GlobalMemoryDump,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html
new file mode 100644
index 00000000000..219ce0f28d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html
@@ -0,0 +1,3954 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const MemoryAllocatorDumpLink = tr.model.MemoryAllocatorDumpLink;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+ const checkDumpNumericsAndDiagnostics =
+ tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics;
+ const SIZE_DELTA = tr.model.MemoryDumpTestUtils.SIZE_DELTA;
+ 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 buildArgPusher(array) {
+ return function(arg) { array.push(arg); };
+ }
+
+ function assertEqualUniqueMembers(actualArray, expectedArray) {
+ assert.lengthOf(actualArray, expectedArray.length);
+ assert.sameMembers(actualArray, expectedArray);
+ }
+
+ function assertUndefinedNumeric(dump, numericName) {
+ const numeric = dump.numerics[numericName];
+ assert.isUndefined(numeric, 'expected numeric \'' + numericName +
+ '\' of memory allocator dump \'' + dump.fullName + '\' in ' +
+ dump.containerMemoryDump.userFriendlyName + ' to be undefined');
+ }
+
+ function assertDefinedNumeric(dump, numericName, expectedUnit, expectedValue,
+ opt_delta) {
+ const numeric = dump.numerics[numericName];
+ const errorMessagePrefix = 'expected numeric \'' + numericName +
+ '\' of memory allocator dump \'' + dump.fullName + '\' in ' +
+ dump.containerMemoryDump.userFriendlyName + ' to ';
+
+ assert.instanceOf(numeric, Scalar,
+ errorMessagePrefix + 'be an instance of Scalar');
+ assert.strictEqual(numeric.unit, expectedUnit,
+ errorMessagePrefix + 'have unit \'' + expectedUnit.unitName +
+ '\' but got \'' + numeric.unit.unitName + '\' instead');
+
+ const valueErrorMessage = errorMessagePrefix + 'have value \'' +
+ expectedValue + '\' but got \'' + numeric.value + '\'';
+ if (opt_delta !== undefined) {
+ assert.closeTo(
+ numeric.value, expectedValue, opt_delta, valueErrorMessage);
+ } else {
+ assert.strictEqual(numeric.value, expectedValue, valueErrorMessage);
+ }
+ }
+
+ function assertSizeNumeric(dump, sizeName, expectedValue) {
+ if (expectedValue === undefined) {
+ assertUndefinedNumeric(dump, sizeName);
+ } else {
+ assertDefinedNumeric(dump, sizeName, sizeInBytes_smallerIsBetter,
+ expectedValue, SIZE_DELTA);
+ }
+ }
+
+ function assertDumpSizes(dump, expectedSize, expectedEffectiveSize,
+ opt_expectedInfos, opt_expectedOwnedBySiblingSizes) {
+ // Check the 'size' numeric.
+ assertSizeNumeric(dump, 'size', expectedSize);
+
+ // Check the 'effective_size' numeric.
+ assertSizeNumeric(dump, 'effective_size', expectedEffectiveSize);
+
+ // Check the 'infos' list.
+ const expectedInfos = opt_expectedInfos || [];
+ const actualInfos = dump.infos;
+ assert.lengthOf(actualInfos, expectedInfos.length,
+ 'expected memory allocator dump \'' + dump.fullName + '\' in ' +
+ dump.containerMemoryDump.userFriendlyName + ' to have ' +
+ expectedInfos.length + ' infos but got ' + actualInfos.length);
+ for (let k = 0; k < actualInfos.length; k++) {
+ assert.deepEqual(actualInfos[k], expectedInfos[k],
+ 'info ' + k + ' of memory allocator dump \'' + dump.fullName +
+ '\' in ' + dump.containerMemoryDump.userFriendlyName +
+ ' doesn\'t match the expected info');
+ }
+
+ // Checked the 'ownedBySiblingSizes' map.
+ const expectedOwnedBySiblingSizes = opt_expectedOwnedBySiblingSizes || {};
+ const actualOwnedBySiblingSizes = {};
+ for (const siblingDump of dump.ownedBySiblingSizes.keys()) {
+ assert.strictEqual(siblingDump.parent, dump.parent);
+ actualOwnedBySiblingSizes[siblingDump.name] =
+ dump.ownedBySiblingSizes.get(siblingDump);
+ }
+ assert.deepEqual(actualOwnedBySiblingSizes, expectedOwnedBySiblingSizes,
+ 'ownedBySiblingSizes of memory allocator dump \'' + dump.fullName +
+ '\' in ' + dump.containerMemoryDump.userFriendlyName +
+ ' doesn\'t contain the expected values');
+ }
+
+ function createContainerDumps(processMemoryDumpCount, opt_model) {
+ let model = opt_model;
+ if (model === undefined) {
+ model = new Model();
+ }
+
+ const gmd = new GlobalMemoryDump(model, 0);
+ model.globalMemoryDumps.push(gmd);
+
+ const pmds = [];
+ for (let i = 0; i < processMemoryDumpCount; i++) {
+ const process = model.getOrCreateProcess(i);
+ const pmd = new ProcessMemoryDump(gmd, process, 0);
+ gmd.processMemoryDumps[i] = pmd;
+ process.memoryDumps.push(pmd);
+ pmds.push(pmd);
+ }
+
+ return [gmd].concat(pmds);
+ }
+
+ /**
+ * Build container memory dumps from tree recipes. This function returns
+ * a list containing a global memory dump and zero or more process memory
+ * dumps constructed from the provided function argument as follows:
+ *
+ * allTreeRecipes (argument):
+ *
+ * [
+ * [tree recipe GA, tree recipe GB, ...],
+ * [tree recipe P1A, tree recipe P1B, ...],
+ * [tree recipe P2A, tree recipe P2B ...],
+ * ...
+ * ]
+ *
+ * return value:
+ *
+ * [
+ * GlobalMemoryDump with root MemoryAllocatorDump(s) [GA, GB, ...],
+ * ProcessMemoryDump with root MemoryAllocatorDump(s) [P1A, P1B, ...],
+ * ProcessMemoryDump with root MemoryAllocatorDump(s) [P2A, P2B, ...],
+ * ...
+ * ]
+ *
+ * where a tree recipe is an object (a recursive data structure) with the
+ * following fields:
+ *
+ * name: Name of the resulting MAD.
+ * guid: GUID of the resulting MAD (can be undefined).
+ * owns: GUID of another MAD owned by the resulting MAD (no owned MAD if
+ * undefined).
+ * importance: Importance of the above ownership (can be undefined).
+ * size: Value of the 'size' numeric of the resulting MAD (no 'size'
+ * numeric if undefined).
+ * numerics: Extra numerics of the resulting MAD (dictionary).
+ * diagnostics: Extra diagnostics of the resulting MAD (dictionary).
+ * weak: Whether the resulting MAD should be weak (undefined implies
+ * non-weak).
+ * children: List of tree recipes for child MADs (no children if undefined).
+ * skip_build: If this optional property is set to true, this function will
+ * skip the corresponding tree recipe node and will not create a MAD
+ * for it (not allowed in root recipes).
+ *
+ * Other fields (most importantly 'expected_size') of a tree recipe are
+ * ignored by this function.
+ */
+ function buildDumpTrees(allTreeRecipes, opt_model) {
+ assert.isAbove(allTreeRecipes.length, 0);
+
+ // owned GUID -> {dump: owner, importance: optional}.
+ const ownerDumps = {};
+ const ownableDumps = {}; // ownable GUID -> ownable dump.
+
+ function buildAndAddDumpTrees(containerDump, treeRecipes) {
+ if (treeRecipes === undefined) return;
+
+ function buildDumpTreeRecursively(treeRecipe, namePrefix) {
+ const skipBuild = treeRecipe.skip_build;
+ const name = treeRecipe.name;
+ const guid = treeRecipe.guid;
+ const owns = treeRecipe.owns;
+ const size = treeRecipe.size;
+ const numerics = treeRecipe.numerics;
+ const diagnostics = treeRecipe.diagnostics;
+ const importance = treeRecipe.importance;
+ const weak = treeRecipe.weak;
+
+ assert.notStrictEqual(skipBuild, true);
+ assert.isDefined(name);
+
+ const fullName = namePrefix + name;
+ const dump = new MemoryAllocatorDump(containerDump, fullName, guid);
+
+ if (size !== undefined) {
+ dump.addNumeric(
+ 'size', new Scalar(sizeInBytes_smallerIsBetter, size));
+ }
+ if (guid !== undefined) {
+ assert.notProperty(guid, ownableDumps);
+ ownableDumps[guid] = dump;
+ }
+ if (owns !== undefined) {
+ if (!(owns in ownerDumps)) {
+ ownerDumps[owns] = [];
+ }
+ ownerDumps[owns].push({dump, importance});
+ } else {
+ assert.isUndefined(importance); // Test sanity check.
+ }
+
+ if (treeRecipe.children !== undefined) {
+ treeRecipe.children.forEach(function(childTreeRecipe) {
+ // Virtual children are added during size calculation.
+ if (childTreeRecipe.skip_build === true) return;
+ const childDump =
+ buildDumpTreeRecursively(childTreeRecipe, fullName + '/');
+ childDump.parent = dump;
+ dump.children.push(childDump);
+ });
+ }
+
+ if (numerics !== undefined) {
+ for (const [name, item] of Object.entries(numerics)) {
+ dump.addNumeric(name, item);
+ }
+ }
+ if (diagnostics !== undefined) {
+ for (const [name, item] of Object.entries(diagnostics)) {
+ dump.addDiagnostic(name, item);
+ }
+ }
+
+ if (weak) dump.weak = true;
+
+ return dump;
+ }
+
+ const memoryAllocatorDumps = treeRecipes.map(function(treeRecipe) {
+ return buildDumpTreeRecursively(treeRecipe, '');
+ });
+ containerDump.memoryAllocatorDumps = memoryAllocatorDumps;
+ }
+
+ // Recursively build memory allocator dump trees for all container dumps.
+ const containerDumps = createContainerDumps(
+ allTreeRecipes.length - 1, opt_model);
+ for (let i = 0; i < allTreeRecipes.length; i++) {
+ buildAndAddDumpTrees(containerDumps[i], allTreeRecipes[i]);
+ }
+
+ // Hook up ownership links.
+ for (const [ownedGuid, ownershipInfos] of Object.entries(ownerDumps)) {
+ const ownedDump = ownableDumps[ownedGuid];
+ assert.isDefined(ownedDump, 'Tree recipes don\'t contain a memory ' +
+ 'allocator dump with guid \'' + ownedGuid + '\'');
+
+ ownershipInfos.forEach(function(ownershipInfo) {
+ addOwnershipLink(
+ ownershipInfo.dump, ownedDump, ownershipInfo.importance);
+ });
+ }
+
+ return containerDumps;
+ }
+
+ // Check that the buildDumpTrees testing helper method above builds a
+ // hierarchy of container and memory allocator dumps from tree recipes
+ // correctly.
+ test('testSanityCheck_buildDumpTrees', function() {
+ const containerDumps = buildDumpTrees([
+ [ // GMD.
+ {
+ 'name': 'globalSharedDump1',
+ 'size': 123
+ },
+ {
+ 'name': 'globalSharedDump2',
+ 'subsystem_size': 999,
+ 'owns': 7,
+ 'importance': -1
+ }
+ ],
+ undefined, // PMD1.
+ [ // PMD2.
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'isolate1',
+ 'guid': 7,
+ 'weak': true
+ },
+ {
+ 'name': 'isolate2',
+ 'skip_build': true
+ },
+ {
+ 'name': 'isolate3',
+ 'size': 54,
+ 'guid': 60,
+ 'children': [
+ {
+ 'name': 'obj1',
+ 'size': 89,
+ 'guid': 3
+ },
+ {
+ 'name': 'obj2',
+ 'owns': 3,
+ 'weak': true
+ },
+ {
+ 'name': 'obj3',
+ 'owns': 3,
+ 'importance': 2
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+ assert.lengthOf(containerDumps, 3);
+ const gmd = containerDumps[0];
+ const pmd1 = containerDumps[1];
+ const pmd2 = containerDumps[2];
+
+ function checkDump(dump, expectedGuid, expectedFullName, expectedParent,
+ expectedChildrenCount, expectedSize, expectedIsOwner,
+ expectedOwnersCount, expectedContainerDump, opt_expectedWeak) {
+ assert.isDefined(dump);
+ assert.instanceOf(dump, MemoryAllocatorDump);
+ assert.strictEqual(dump.guid, expectedGuid);
+ assert.strictEqual(dump.fullName, expectedFullName);
+ assert.strictEqual(dump.parent, expectedParent);
+ assert.lengthOf(dump.children, expectedChildrenCount);
+
+ assertSizeNumeric(dump, 'size', expectedSize);
+ assertSizeNumeric(dump, 'subsystem_size', undefined);
+
+ if (expectedIsOwner) {
+ assert.isDefined(dump.owns);
+ } else {
+ assert.isUndefined(dump.owns);
+ }
+ assert.lengthOf(dump.ownedBy, expectedOwnersCount);
+
+ assert.strictEqual(dump.containerMemoryDump, expectedContainerDump);
+ assert.strictEqual(expectedContainerDump.getMemoryAllocatorDumpByFullName(
+ expectedFullName), dump);
+ assert.strictEqual(dump.weak, !!opt_expectedWeak);
+ }
+
+ function checkOwnershipLink(expectedSourceDump, expectedTargetDump,
+ expectedImportance) {
+ const link = expectedSourceDump.owns;
+ assert.isDefined(link);
+ assert.instanceOf(link, MemoryAllocatorDumpLink);
+ assert.strictEqual(link.source, expectedSourceDump);
+ assert.strictEqual(link.target, expectedTargetDump);
+ assert.strictEqual(link.importance, expectedImportance);
+ assert.include(expectedTargetDump.ownedBy, link);
+ }
+
+ // GMD memory allocator dumps.
+ assert.lengthOf(gmd.memoryAllocatorDumps, 2);
+ const globalSharedDump1 = gmd.memoryAllocatorDumps[0];
+ checkDump(globalSharedDump1, undefined, 'globalSharedDump1', undefined, 0,
+ 123, false, 0, gmd);
+ const globalSharedDump2 = gmd.memoryAllocatorDumps[1];
+ checkDump(globalSharedDump2, undefined, 'globalSharedDump2', undefined, 0,
+ undefined, true, 0, gmd);
+
+ // PMD1 memory allocator dumps.
+ assert.isUndefined(pmd1.memoryAllocatorDumps);
+
+ // PMD2 memory allocator dumps.
+ assert.lengthOf(pmd2.memoryAllocatorDumps, 1);
+ const v8Dump = pmd2.memoryAllocatorDumps[0];
+ checkDump(v8Dump, undefined, 'v8', undefined, 2, undefined, false, 0,
+ pmd2);
+ const isolate1Dump = v8Dump.children[0];
+ checkDump(isolate1Dump, 7, 'v8/isolate1', v8Dump, 0, undefined, false, 1,
+ pmd2, true /* weak dump */);
+ const isolate3Dump = v8Dump.children[1];
+ checkDump(isolate3Dump, 60, 'v8/isolate3', v8Dump, 3, 54, false, 0, pmd2);
+ const obj1Dump = isolate3Dump.children[0];
+ checkDump(obj1Dump, 3, 'v8/isolate3/obj1', isolate3Dump, 0, 89, false, 2,
+ pmd2);
+ const obj2Dump = isolate3Dump.children[1];
+ checkDump(obj2Dump, undefined, 'v8/isolate3/obj2', isolate3Dump, 0,
+ undefined, true, 0, pmd2, true /* weak dump */);
+ const obj3Dump = isolate3Dump.children[2];
+ checkDump(obj3Dump, undefined, 'v8/isolate3/obj3', isolate3Dump, 0,
+ undefined, true, 0, pmd2);
+
+ // Ownership links.
+ checkOwnershipLink(globalSharedDump2, isolate1Dump, -1);
+ checkOwnershipLink(obj2Dump, obj1Dump, undefined);
+ checkOwnershipLink(obj3Dump, obj1Dump, 2);
+ });
+
+ /**
+ * Check that container memory dumps have the expected structure with sizes
+ * as described by tree recipes. The fields of a tree recipe are used by this
+ * function to check the properties of a MemoryAllocatorDump as follows (see
+ * the buildDumpTrees documentation for more details about the structure of
+ * tree recipes):
+ *
+ * name: Expected name of the MAD.
+ * expected_removed: If provided and true, it is expected that there is no
+ * dump for the recipe.
+ * expected_size: Expected value of the 'size' numeric of the MAD (no
+ * 'size' numeric expected if undefined).
+ * expected_effective_size: Expected value of the 'effective_size'
+ * numeric of the MAD (no 'effective_size' numeric expected if
+ * undefined).
+ * expected_infos: List of expected MAD infos (zero infos expected if
+ * undefined).
+ * weak: Whether the MAD is expected to be weak (non-weak if undefined).
+ * owns: Expected GUID of the dump owned by the MAD.
+ * importance: Expected importance of the owhership from this MAD.
+ * expected_owned_by_links_count: Expected number of 'ownedBy' links of the
+ * MAD.
+ * children: List of tree recipes for child MADs (no children expected if
+ * undefined).
+ *
+ * Other fields of a tree recipe (including 'skip_build') are ignored by this
+ * function.
+ */
+ function checkDumpTrees(containerDumps, allTreeRecipes) {
+ assert.lengthOf(containerDumps, allTreeRecipes.length);
+
+ for (let i = 0; i < containerDumps.length; i++) {
+ const containerDump = containerDumps[i];
+ const treeRecipes = allTreeRecipes[i];
+
+ const memoryAllocatorDumps = containerDump.memoryAllocatorDumps;
+ if (treeRecipes === undefined) {
+ assert.isUndefined(memoryAllocatorDumps,
+ 'expected undefined memory allocator dumps in ' +
+ containerDump.userFriendlyName);
+ continue;
+ }
+
+ const expectedTreeRecipes = treeRecipes.filter(function(treeRecipe) {
+ return !treeRecipe.expected_removed;
+ });
+
+ assert.isDefined(memoryAllocatorDumps,
+ 'expected defined memory allocator dumps in ' +
+ containerDump.userFriendlyName);
+ assert.lengthOf(memoryAllocatorDumps, expectedTreeRecipes.length,
+ 'expected ' + expectedTreeRecipes.length + ' root memory allocator ' +
+ 'dumps but got ' + memoryAllocatorDumps.length + ' in ' +
+ containerDump.userFriendlyName);
+
+ function checkDumpTree(dump, treeRecipe, expectedParent, namePrefix) {
+ // Test sanity check.
+ assert.isFalse(!!treeRecipe.expected_removed);
+
+ // Check full name, parent, and container dump.
+ const expectedFullName = namePrefix + treeRecipe.name;
+ const quantifiedName = dump.quantifiedName;
+ assert.strictEqual(dump.fullName, expectedFullName,
+ quantifiedName + ' has invalid full name');
+ assert.strictEqual(dump.parent, expectedParent,
+ quantifiedName + ' has invalid parent');
+ assert.strictEqual(dump.containerMemoryDump, containerDump,
+ quantifiedName + ' has invalid container memory dump');
+ assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName(
+ expectedFullName), dump, quantifiedName +
+ 'is not indexed in its container memory dump');
+
+ // Check the guid of the dump.
+ assert.strictEqual(dump.guid, treeRecipe.guid,
+ quantifiedName + ' has invalid guid');
+
+ // Check that the 'weak' flag is correct.
+ assert.strictEqual(dump.weak, !!treeRecipe.weak,
+ quantifiedName + ' has invalid weak flag');
+
+ // Check that sizes were calculated correctly.
+ assertDumpSizes(dump,
+ treeRecipe.expected_size,
+ treeRecipe.expected_effective_size,
+ treeRecipe.expected_infos,
+ treeRecipe.expected_owned_by_sibling_sizes);
+
+ // Check that the 'owns' link is correct.
+ if (treeRecipe.owns === undefined) {
+ assert.isUndefined(dump.owns,
+ quantifiedName + ' was expected not to own another dump');
+ } else {
+ const ownershipLink = dump.owns;
+ assert.isDefined(dump.owns, quantifiedName +
+ ' was expected to have an \'owns\' link');
+ assert.strictEqual(ownershipLink.source, dump,
+ 'the \'owns\' link of ' + quantifiedName + ' has invalid source');
+ const expectedImportance = treeRecipe.importance;
+ assert.strictEqual(ownershipLink.importance, expectedImportance,
+ 'expected the importance of the \'owns\' link of ' +
+ quantifiedName + ' to be ' + expectedImportance +
+ ' but got ' + ownershipLink.importance);
+ const ownedDump = ownershipLink.target;
+ assert.strictEqual(ownedDump.guid, treeRecipe.owns,
+ 'the \'owns\' link of ' + quantifiedName +
+ ' has an invalid target');
+ assert.include(ownedDump.ownedBy, ownershipLink,
+ 'the target of the \'owns\' link of ' + quantifiedName +
+ ' doesn\'t have the link in its \'ownedBy\' list');
+ }
+
+ // Check that the number of 'ownedBy' links is correct.
+ const expectedOwnedByLinksCount =
+ treeRecipe.expected_owned_by_links_count;
+ if (expectedOwnedByLinksCount !== undefined) {
+ assert.lengthOf(dump.ownedBy, expectedOwnedByLinksCount,
+ 'expected ' + quantifiedName + ' to have ' +
+ expectedOwnedByLinksCount + ' \'ownedBy\' links but got ' +
+ dump.ownedBy.length);
+ }
+
+ // Check children recursively.
+ const actualChildren = dump.children;
+ const expectedChildren = (treeRecipe.children || []).filter(
+ function(childRecipe) {
+ return !childRecipe.expected_removed;
+ });
+ assert.lengthOf(actualChildren, expectedChildren.length,
+ 'expected ' + quantifiedName + ' to have ' +
+ expectedChildren.length + ' children but got ' +
+ actualChildren.length);
+ for (let k = 0; k < actualChildren.length; k++) {
+ checkDumpTree(actualChildren[k], expectedChildren[k], dump,
+ expectedFullName + '/');
+ }
+ }
+
+ for (let j = 0; j < memoryAllocatorDumps.length; j++) {
+ checkDumpTree(
+ memoryAllocatorDumps[j], expectedTreeRecipes[j], undefined, '');
+ }
+ }
+ }
+
+ // Check that the checkDumpTrees testing helper method above actually
+ // performs the expected checks. Since it will be used heavily throughout
+ // this file (where it is expected to pass), we only need to verify that it
+ // does indeed fail in several cases where it should.
+ test('testSanityCheck_checkDumpTrees_invalidName', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const v8Dump = new MemoryAllocatorDump(gmd, 'v8');
+ const heapsDump =
+ new MemoryAllocatorDump(gmd, 'heaps'); // Should be 'v8/heaps'.
+ v8Dump.children.push(heapsDump);
+ heapsDump.parent = v8Dump;
+ gmd.memoryAllocatorDumps = [v8Dump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'heaps'
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /'heaps'.*invalid full name/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidGuid', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ gmd.memoryAllocatorDumps = [new MemoryAllocatorDump(gmd, 'v8', 42)];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'v8',
+ 'guid': 43 // This should be 42.
+ }
+ ]
+ ]);
+ }, /'v8'.*\binvalid guid\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidStructure', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const rootDump = new MemoryAllocatorDump(gmd, 'root');
+ addChildDump(rootDump, 'child1');
+ gmd.memoryAllocatorDumps = [rootDump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'child1'
+ },
+ {
+ // This child is not present in the dump.
+ 'name': 'child2',
+ 'skip_build': true
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /expected.*\b2\b.*children.*got.*\b1\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidParentLink', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const rootDump = new MemoryAllocatorDump(gmd, 'root');
+ const parentDump = addChildDump(rootDump, 'parent');
+ const childDump = addChildDump(parentDump, 'child');
+ childDump.parent = rootDump; // This should correctly be parentDump.
+ gmd.memoryAllocatorDumps = [rootDump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+ }, 'invalid parent');
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidSize', function() {
+ const containerDumps = createContainerDumps(1);
+ const gmd = containerDumps[0];
+ const pmd = containerDumps[1];
+ const rootDump = newAllocatorDump(pmd, 'root', {numerics: {size: 100}});
+ addChildDump(rootDump, 'parent', {numerics: {size: 49}});
+ pmd.memoryAllocatorDumps = [rootDump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ undefined,
+ [
+ {
+ 'name': 'root',
+ 'expected_size': 100,
+ 'children': [
+ {
+ 'name': 'parent',
+ 'expected_size': 50 // This should be 49.
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /expected.*'size'.*value.*\b50\b.*got.*\b49\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidEffectiveSize', function() {
+ const containerDumps = createContainerDumps(1);
+ const gmd = containerDumps[0];
+ const pmd = containerDumps[1];
+ const rootDump = newAllocatorDump(pmd, 'root',
+ {numerics: {effective_size: 99}});
+ addChildDump(rootDump, 'parent', {numerics: {effective_size: 50}});
+ pmd.memoryAllocatorDumps = [rootDump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ undefined,
+ [
+ {
+ 'name': 'root',
+ 'expected_effective_size': 100, // This should be 99.
+ 'children': [
+ {
+ 'name': 'parent',
+ 'expected_effective_size': 50
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /expected.*'effective_size'.*value.*\b100\b.*got.*\b99\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidInfoCount', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ gmd.memoryAllocatorDumps = [
+ newAllocatorDump(gmd, 'v8', {numerics: {size: 50}})
+ ];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'v8',
+ 'expected_size': 50,
+ 'expected_infos': [
+ {
+ type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN,
+ providedSize: 50,
+ dependencySize: 60
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /expected.*'v8'.*\b1 infos\b.*\bgot\b.*\b0\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidInfo', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const v8Dump = newAllocatorDump(gmd, 'v8', {numerics: {size: 50}});
+ v8Dump.infos.push({
+ type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN,
+ providedSize: 40,
+ dependencySize: 50
+ });
+ gmd.memoryAllocatorDumps = [v8Dump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'v8',
+ 'expected_size': 50,
+ 'expected_infos': [
+ {
+ // Should be PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN below.
+ type: PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER,
+ providedSize: 40,
+ dependencySize: 50
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /\binfo 0\b.*'v8'.*\bexpected\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidOwnedBySiblingSizes', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const v8Dump = new MemoryAllocatorDump(gmd, 'v8');
+ addChildDump(v8Dump, 'child1', {guid: 42});
+ addChildDump(v8Dump, 'child2');
+ gmd.memoryAllocatorDumps = [v8Dump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'child1',
+ 'guid': 42
+ },
+ {
+ 'name': 'child2',
+ 'expected_owned_by_sibling_sizes': {
+ 'child1': 40 // This should be 30.
+ }
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /\bownedBySiblingSizes\b.*'v8\/child2'.*\bexpected\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidWeakFlag',
+ function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const parentDump = new MemoryAllocatorDump(gmd, 'parent');
+ const childDump = addChildDump(parentDump, 'child');
+ childDump.weak = true;
+ gmd.memoryAllocatorDumps = [parentDump];
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child',
+ // Missing "'weak': true".
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /'parent\/child'.*\binvalid weak flag\b/);
+
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'parent',
+ 'weak': true, // This should be false (or not provided).
+ 'children': [
+ {
+ 'name': 'child',
+ 'weak': true
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /'parent'.*\binvalid weak flag\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_dumpNotRemoved', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+ const parentDump = new MemoryAllocatorDump(gmd, 'parent');
+ for (let i = 1; i <= 3; i++) {
+ addChildDump(parentDump, 'child' + i);
+ }
+ const otherDump = new MemoryAllocatorDump(gmd, 'other');
+ gmd.memoryAllocatorDumps = [parentDump, otherDump];
+
+ // Child MAD not removed.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child1',
+ },
+ {
+ 'name': 'child2',
+ 'expected_removed': true
+ },
+ {
+ 'name': 'child3',
+ }
+ ]
+ },
+ {
+ 'name': 'other'
+ }
+ ]
+ ]);
+ }, /\bexpected\b.*'parent'.*\b2 children\b.*\bgot 3\b/);
+
+ // Root MAD not removed.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child1',
+ },
+ {
+ 'name': 'child2'
+ },
+ {
+ 'name': 'child3',
+ }
+ ]
+ },
+ {
+ 'name': 'other',
+ 'expected_removed': true
+ }
+ ]
+ ]);
+ }, /\bexpected\b.*\b1 root memory allocator dumps\b.*\bgot 2\b/);
+ });
+
+ test('testSanityCheck_checkDumpTrees_invalidOwnership', function() {
+ const containerDumps = createContainerDumps(1);
+ const gmd = containerDumps[0];
+ const pmd1 = containerDumps[1];
+ const ownedDump = new MemoryAllocatorDump(gmd, 'owned', 42);
+ const ownerDump1 = new MemoryAllocatorDump(pmd1, 'owner1');
+ const link1 = addOwnershipLink(ownerDump1, ownedDump);
+ const ownerDump2 = new MemoryAllocatorDump(pmd1, 'owner2');
+ const link2 = addOwnershipLink(ownerDump2, ownedDump, 3);
+ const nonOwnerDump = new MemoryAllocatorDump(pmd1, 'non-owner', 90);
+ gmd.memoryAllocatorDumps = [ownedDump];
+ pmd1.memoryAllocatorDumps = [ownerDump1, ownerDump2, nonOwnerDump];
+
+ // Missing 'owns' link.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42
+ }
+ ],
+ [
+ {
+ 'name': 'owner1',
+ 'owns': 42
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 3,
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90,
+ 'owns': 42 // This should not be here.
+ }
+ ]
+ ]);
+ }, /'non-owner'.*\bwas expected to have\b.*'owns' link\b/);
+
+ // Extra 'owns' link.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42
+ }
+ ],
+ [
+ {
+ 'name': 'owner1'
+ // Missing: "'owns': 42".
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 3,
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90
+ }
+ ]
+ ]);
+ }, /'owner1'.*\bwas expected not to own\b/);
+
+ // Invalid ownership importance.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42
+ }
+ ],
+ [
+ {
+ 'name': 'owner1',
+ 'owns': 42
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 2, // This should be 3.
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90
+ }
+ ]
+ ]);
+ }, /\bexpected\b.*\bimportance\b.*'owner2'.*\b2 but got 3\b/);
+
+ // Invalid ownership target.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42
+ }
+ ],
+ [
+ {
+ 'name': 'owner1',
+ 'owns': 90 // This should be 42.
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 3,
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90
+ }
+ ]
+ ]);
+ }, /'owner1'.*\binvalid target\b/);
+
+ // Invalid 'ownedBy' ownership links count.
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42,
+ 'expected_owned_by_links_count': 3 // This should be 2.
+ }
+ ],
+ [
+ {
+ 'name': 'owner1',
+ 'owns': 42
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 3,
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90
+ }
+ ]
+ ]);
+ }, /'owned'.*\bhave 3 'ownedBy' links\b.*\bgot 2\b/);
+
+ // Invalid ownership source.
+ link1.source = ownerDump2;
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42
+ }
+ ],
+ [
+ {
+ 'name': 'owner1',
+ 'owns': 42
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 3,
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90
+ }
+ ]
+ ]);
+ }, /'owns' link\b.*'owner1'.*\binvalid source\b/);
+ link1.source = ownerDump1;
+
+ // Ownership link not in target's 'ownedBy' list.
+ ownedDump.ownedBy.pop();
+ assert.throws(function() {
+ checkDumpTrees(containerDumps, [
+ [
+ {
+ 'name': 'owned',
+ 'guid': 42
+ }
+ ],
+ [
+ {
+ 'name': 'owner1',
+ 'owns': 42
+ },
+ {
+ 'name': 'owner2',
+ 'importance': 3,
+ 'owns': 42
+ },
+ {
+ 'name': 'non-owner',
+ 'guid': 90
+ }
+ ]
+ ]);
+ }, /\btarget of\b.*'owner2'.*'ownedBy' list\b/);
+ ownedDump.ownedBy.push(link2);
+ });
+
+ /**
+ * Build container memory dumps from tree recipes, let the resulting
+ * GlobalMemoryDump calculate sizes and effective sizes, and then check that
+ * the augmented container memory dumps have the expected structure with
+ * correct sizes and effective sizes (as described by the same tree recipes).
+ *
+ * See the documentation for buildDumpTrees and checkDumpTrees for more
+ * details about the structure of tree recipes.
+ */
+ function testSizesCalculation(allTreeRecipes) {
+ const m = new Model();
+ const io = new tr.importer.ImportOptions();
+ io.showImportWarnings = false;
+ m.importOptions = io;
+
+ const containerDumps = buildDumpTrees(allTreeRecipes, m);
+ const gmd = containerDumps[0];
+ gmd.calculateSizes();
+ gmd.calculateEffectiveSizes();
+ gmd.forceRebuildingMemoryAllocatorDumpByFullNameIndices();
+ checkDumpTrees(containerDumps, allTreeRecipes);
+ }
+
+ // Check that the testSizesCalculation testing helper method above
+ // actually performs the expected checks. Since it will be used heavily
+ // throughout this file (where it is expected to pass), we only need to
+ // verify that it does indeed fail when it should.
+ test('testSanityCheck_testSizesCalculation', function() {
+ assert.throws(function() {
+ testSizesCalculation([
+ [],
+ undefined,
+ [
+ {
+ 'name': 'winheap'
+ },
+ {
+ 'name': 'malloc',
+ 'expected_size': 100,
+ 'children': [
+ {
+ 'name': 'allocated_objects',
+ 'size': 100,
+ 'expected_size': 100
+ },
+ {
+ 'name': 'extra',
+ 'size': 20,
+ 'expected_size': 20
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /expected.*'size'.*value.*\b100\b.*got.*\b120\b/);
+ });
+
+ function calculationTest(caseName, treeRecipes) {
+ test('calculateSizes_' + caseName, function() {
+ testSizesCalculation(treeRecipes);
+ });
+ }
+
+ /**
+ * Build container memory dumps from tree recipes, let the resulting
+ * GlobalMemoryDump remove weak memory dumps, and then check that the updated
+ * container memory dumps have the expected structure (as described by the
+ * same tree recipes).
+ *
+ * See the documentation for buildDumpTrees and checkDumpTrees for more
+ * details about the structure of tree recipes.
+ */
+ function testWeakDumpRemoval(allTreeRecipes) {
+ const m = new tr.Model();
+ const io = new tr.importer.ImportOptions();
+ io.showImportWarnings = false;
+ m.importOptions = io;
+
+ const containerDumps = buildDumpTrees(allTreeRecipes, m);
+ const gmd = containerDumps[0];
+ gmd.removeWeakDumps();
+ gmd.forceRebuildingMemoryAllocatorDumpByFullNameIndices();
+ checkDumpTrees(containerDumps, allTreeRecipes);
+ }
+
+ // Similarly to testSanityCheck_testSizesCalculation, check that the
+ // testWeakDumpRemoval testing helper method above actually performs the
+ // expected checks.
+ test('testSanityCheck_testWeakDumpRemoval', function() {
+ assert.throws(function() {
+ testWeakDumpRemoval([
+ [],
+ undefined,
+ [
+ {
+ 'name': 'winheap'
+ },
+ {
+ 'name': 'malloc',
+ 'children': [
+ {
+ 'name': 'allocated_objects'
+ },
+ {
+ 'name': 'directly_weak',
+ 'guid': 42,
+ 'weak': true,
+ 'expected_removed': true
+ },
+ {
+ 'name': 'indirectly_weak',
+ 'owns': 42
+ // Missing: "'expected_removed': true".
+ }
+ ]
+ }
+ ]
+ ]);
+ }, /expected.*'malloc'.*\b2 children\b.*\bgot 1\b/);
+ });
+
+ function weakDumpRemovalTest(caseName, treeRecipes) {
+ test('removeWeakDumps_' + caseName, function() {
+ testWeakDumpRemoval(treeRecipes);
+ });
+ }
+
+ /////////////////////////////////////////////////////////////////////////////
+ // Actual tests begin here.
+ /////////////////////////////////////////////////////////////////////////////
+
+ test('iterateContainerDumps_withoutProcessMemoryDumps', function() {
+ const containerDumps = createContainerDumps(0);
+ const gmd = containerDumps[0];
+
+ const visitedContainerDumps = [];
+ gmd.iterateContainerDumps(buildArgPusher(visitedContainerDumps));
+ assertEqualUniqueMembers(visitedContainerDumps, containerDumps);
+ });
+
+ test('iterateContainerDumps_withProcessMemoryDumps', function() {
+ const containerDumps = createContainerDumps(2);
+ const gmd = containerDumps[0];
+
+ const visitedContainerDumps = [];
+ gmd.iterateContainerDumps(buildArgPusher(visitedContainerDumps));
+ assertEqualUniqueMembers(visitedContainerDumps, containerDumps);
+ });
+
+ test('iterateAllRootAllocatorDumps', function() {
+ const containerDumps = buildDumpTrees([
+ [ // GMD.
+ {
+ 'name': 'globalSharedDump1'
+ },
+ {
+ 'name': 'globalSharedDump2'
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'isolate'
+ }
+ ]
+ }
+ ]
+ ]);
+ const gmd = containerDumps[0];
+ const pmd = containerDumps[1];
+
+ const visitedAllocatorDumps = [];
+ gmd.iterateAllRootAllocatorDumps(buildArgPusher(visitedAllocatorDumps));
+ assertEqualUniqueMembers(visitedAllocatorDumps, [
+ gmd.getMemoryAllocatorDumpByFullName('globalSharedDump1'),
+ gmd.getMemoryAllocatorDumpByFullName('globalSharedDump2'),
+ pmd.getMemoryAllocatorDumpByFullName('v8')
+ ]);
+ });
+
+ test('traverseAllocatorDumpsInDepthFirstOrder_oneTreeWithoutOwners',
+ function() {
+ const containerDumps = buildDumpTrees([
+ [ // GMD.
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child1'
+ },
+ {
+ 'name': 'child2'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+ const gmd = containerDumps[0];
+
+ // Post-order.
+ let visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPostOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, [
+ gmd.getMemoryAllocatorDumpByFullName('root/parent/child1'),
+ gmd.getMemoryAllocatorDumpByFullName('root/parent/child2'),
+ gmd.getMemoryAllocatorDumpByFullName('root/parent'),
+ gmd.getMemoryAllocatorDumpByFullName('root')
+ ]);
+
+ // Pre-order.
+ visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPreOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, [
+ gmd.getMemoryAllocatorDumpByFullName('root'),
+ gmd.getMemoryAllocatorDumpByFullName('root/parent'),
+ gmd.getMemoryAllocatorDumpByFullName('root/parent/child1'),
+ gmd.getMemoryAllocatorDumpByFullName('root/parent/child2')
+ ]);
+ });
+
+ test('traverseAllocatorDumpsInDepthFirstOrder_oneTreeWithOwners',
+ function() {
+ const containerDumps = buildDumpTrees([
+ [], // GMD.
+ [ // PMD.
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child1',
+ 'owns': 0
+ },
+ {
+ 'name': 'child2',
+ 'guid': 0
+ },
+ {
+ 'name': 'child3',
+ 'owns': 0
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+ const gmd = containerDumps[0];
+ const pmd = containerDumps[1];
+
+ // Post-order.
+ let visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPostOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, [
+ pmd.getMemoryAllocatorDumpByFullName('root/parent/child1'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent/child3'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent/child2'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent'),
+ pmd.getMemoryAllocatorDumpByFullName('root')
+ ]);
+
+ // Pre-order.
+ visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPreOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, [
+ pmd.getMemoryAllocatorDumpByFullName('root'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent/child2'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent/child1'),
+ pmd.getMemoryAllocatorDumpByFullName('root/parent/child3')
+ ]);
+ });
+
+ test('traverseAllocatorDumpsInDepthFirstOrder_multipleTrees', function() {
+ const containerDumps = buildDumpTrees([
+ [ // GMD.
+ {
+ 'name': 'shared',
+ 'children': [
+ {
+ 'name': 'pool1',
+ 'guid': 1
+ },
+ {
+ 'name': 'pool2',
+ 'owns': 3
+ }
+ ]
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'oilpan',
+ 'children': [
+ {
+ 'name': 'objects'
+ },
+ {
+ 'name': 'heaps',
+ 'owns': 1,
+ 'children': [
+ {
+ 'name': 'small',
+ 'guid': 2
+ },
+ {
+ 'name': 'large'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'isolate1',
+ 'owns': 2
+ },
+ {
+ 'name': 'isolate2',
+ 'guid': 3
+ }
+ ]
+ }
+ ]
+ ]);
+ const gmd = containerDumps[0];
+ const pmd = containerDumps[1];
+
+ // Post-order.
+ let visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPostOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, [
+ pmd.getMemoryAllocatorDumpByFullName('v8/isolate1'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/small'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/large'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps'),
+ gmd.getMemoryAllocatorDumpByFullName('shared/pool1'),
+ gmd.getMemoryAllocatorDumpByFullName('shared/pool2'),
+ gmd.getMemoryAllocatorDumpByFullName('shared'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/objects'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan'),
+ pmd.getMemoryAllocatorDumpByFullName('v8/isolate2'),
+ pmd.getMemoryAllocatorDumpByFullName('v8')
+ ]);
+
+ // Pre-order.
+ visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPreOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, [
+ gmd.getMemoryAllocatorDumpByFullName('shared'),
+ gmd.getMemoryAllocatorDumpByFullName('shared/pool1'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/objects'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/small'),
+ pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/large'),
+ pmd.getMemoryAllocatorDumpByFullName('v8'),
+ pmd.getMemoryAllocatorDumpByFullName('v8/isolate1'),
+ pmd.getMemoryAllocatorDumpByFullName('v8/isolate2'),
+ gmd.getMemoryAllocatorDumpByFullName('shared/pool2')
+ ]);
+ });
+
+ test('traverseAllocatorDumpsInDepthFirstPostOrder_cycle', function() {
+ const containerDumps = buildDumpTrees([
+ [ // GMD.
+ {
+ 'name': 'shared',
+ 'owns': 2,
+ 'children': [
+ {
+ 'name': 'pool',
+ 'guid': 1
+ }
+ ]
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'oilpan',
+ 'owns': 1,
+ 'children': [
+ {
+ 'name': 'objects',
+ 'guid': 2
+ }
+ ]
+ }
+ ]
+ ]);
+ const gmd = containerDumps[0];
+
+ // Post-order.
+ assert.throws(function() {
+ gmd.traverseAllocatorDumpsInDepthFirstPostOrder(function() {});
+ }, /contains.*cycle/);
+
+ // Pre-order.
+ const visitedAllocatorDumps = [];
+ gmd.traverseAllocatorDumpsInDepthFirstPreOrder(
+ buildArgPusher(visitedAllocatorDumps));
+ assert.deepEqual(visitedAllocatorDumps, []);
+ });
+
+ // Just check that the method doesn't crash upon encountering empty and/or
+ // undefined memory allocator dumps.
+ calculationTest('noDumps', [
+ undefined, // GMD.
+ [], // PMD1.
+ undefined // PMD2.
+ ]);
+
+ calculationTest('flatDumps', [
+ [ // GMD.
+ {
+ 'name': 'shared',
+ 'size': 1024,
+ 'expected_size': 1024,
+ 'expected_effective_size': 1024
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'gpu'
+ }
+ ]
+ ]);
+
+ calculationTest('zeroSizes', [
+ [ // GMD.
+ {
+ 'name': 'shared',
+ 'size': 0,
+ 'expected_size': 0,
+ 'expected_effective_size': 0
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'gpu',
+ 'expected_size': 0,
+ 'expected_effective_size': 0,
+ 'children': [
+ {
+ 'name': 'zero',
+ 'size': 0,
+ 'expected_size': 0,
+ 'expected_effective_size': 0
+ }
+ ]
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'gpu',
+ 'expected_size': 0,
+ 'expected_effective_size': 0,
+ 'children': [
+ {
+ 'name': 'zero',
+ 'size': 0,
+ 'expected_size': 0,
+ 'expected_effective_size': 0
+ },
+ {
+ 'name': 'undefined'
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_allSizesUndefined', [
+ [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'child1'
+ },
+ {
+ 'name': 'child2'
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_parentSizeUndefined', [
+ [
+ {
+ 'name': 'parent',
+ 'expected_size': 384,
+ 'expected_effective_size': 384,
+ 'children': [
+ {
+ 'name': 'child1',
+ 'size': 128,
+ 'expected_size': 128,
+ 'expected_effective_size': 128
+ },
+ {
+ 'name': 'child2',
+ 'size': 256,
+ 'expected_size': 256,
+ 'expected_effective_size': 256
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_parentSizeDefined_childrenAddUp', [
+ [
+ {
+ 'name': 'parent',
+ 'size': 0,
+ 'expected_size': 0,
+ 'expected_effective_size': 0,
+ 'children': [
+ {
+ 'name': 'child1'
+ },
+ {
+ 'name': 'child2'
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_parentSizeDefined_childrenDontAddUp', [
+ [
+ {
+ 'name': 'parent',
+ 'size': 2048,
+ 'expected_size': 2048,
+ 'expected_effective_size': 2048,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 2048,
+ 'expected_effective_size': 2048
+ },
+ {
+ 'name': 'child1'
+ },
+ {
+ 'name': 'child2'
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_oneChildSizeUndefined_childrenAddUp', [
+ [
+ {
+ 'name': 'parent',
+ 'size': 4096,
+ 'expected_size': 4096,
+ 'expected_effective_size': 4096,
+ 'children': [
+ {
+ 'name': 'child1'
+ },
+ {
+ 'name': 'child2',
+ 'size': 4096,
+ 'expected_size': 4096,
+ 'expected_effective_size': 4096
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_oneChildSizeUndefined_childrenDontAddUp', [
+ [
+ {
+ 'name': 'parent',
+ 'size': 6144,
+ 'expected_size': 6144,
+ 'expected_effective_size': 6144,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 2048,
+ 'expected_effective_size': 2048
+ },
+ {
+ 'name': 'child1'
+ },
+ {
+ 'name': 'child2',
+ 'size': 4096,
+ 'expected_size': 4096,
+ 'expected_effective_size': 4096
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_allSizesDefined_childrenAddUp', [
+ [
+ {
+ 'name': 'parent',
+ 'size': 100,
+ 'expected_size': 100,
+ 'expected_effective_size': 100,
+ 'children': [
+ {
+ 'name': 'child1',
+ 'size': 70,
+ 'expected_size': 70,
+ 'expected_effective_size': 70
+ },
+ {
+ 'name': 'child2',
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_allSizesDefined_childrenDontUp', [
+ [
+ {
+ 'name': 'parent',
+ 'size': 150,
+ 'expected_size': 150,
+ 'expected_effective_size': 150,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 50,
+ 'expected_effective_size': 50
+ },
+ {
+ 'name': 'child1',
+ 'size': 70,
+ 'expected_size': 70,
+ 'expected_effective_size': 70
+ },
+ {
+ 'name': 'child2',
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_oneChildSizeDefined', [
+ [
+ {
+ 'name': 'parent',
+ 'expected_size': 49,
+ 'expected_effective_size': 49,
+ 'children': [
+ {
+ 'name': 'child1',
+ 'size': 49,
+ 'expected_size': 49,
+ 'expected_effective_size': 49
+ },
+ {
+ 'name': 'child2'
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('children_multipleLevels', [
+ [], // GMD.
+ [ // PMD.
+ {
+ 'name': 'v8',
+ 'expected_size': 36,
+ 'expected_effective_size': 36,
+ 'children': [
+ {
+ 'name': 'isolate1',
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 10,
+ 'children': [
+ {
+ 'skip_build': true,
+ 'name': '<unspecified>',
+ 'expected_size': 3,
+ 'expected_effective_size': 3
+ },
+ {
+ 'name': 'objects',
+ 'size': 3,
+ 'expected_size': 3,
+ 'expected_effective_size': 3
+ },
+ {
+ 'name': 'heaps',
+ 'size': 4,
+ 'expected_size': 4,
+ 'expected_effective_size': 4
+ }
+ ]
+ },
+ {
+ 'name': 'isolate2',
+ 'size': 12,
+ 'expected_size': 12,
+ 'expected_effective_size': 12,
+ 'children': [
+ {
+ 'name': 'objects',
+ 'size': 5,
+ 'expected_size': 5,
+ 'expected_effective_size': 5
+ },
+ {
+ 'name': 'heaps',
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 7
+ }
+ ]
+ },
+ {
+ 'name': 'isolate3',
+ 'expected_size': 14,
+ 'expected_effective_size': 14,
+ 'children': [
+ {
+ 'name': 'objects',
+ 'size': 14,
+ 'expected_size': 14,
+ 'expected_effective_size': 14
+ },
+ {
+ 'name': 'heaps'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('owners_allSizesUndefined', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'owns': 7,
+ 'importance': 1
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_ownedSizeDefined', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'size': 15,
+ 'expected_size': 15,
+ 'expected_effective_size': 15,
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'owns': 7
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_ownedSizeUndefined', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'expected_size': 9,
+ 'expected_effective_size': 0,
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'size': 5,
+ 'expected_size': 5,
+ 'expected_effective_size': 2.5,
+ 'owns': 7
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'size': 9,
+ 'expected_size': 9,
+ 'expected_effective_size': 6.5,
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_oneOwnerSizeDefined', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'expected_size': 16,
+ 'expected_effective_size': 0,
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'size': 16,
+ 'expected_size': 16,
+ 'expected_effective_size': 16,
+ 'owns': 7
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_oneOwnerSizeUndefined', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 2,
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'owns': 7
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'size': 18,
+ 'expected_size': 18,
+ 'expected_effective_size': 18,
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_allSizesDefined', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'size': 60,
+ 'expected_size': 60,
+ 'expected_effective_size': 31,
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'size': 29,
+ 'expected_size': 29,
+ 'expected_effective_size': 19.5,
+ 'owns': 7
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'size': 19,
+ 'expected_size': 19,
+ 'expected_effective_size': 9.5,
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_hierarchy', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'expected_size': 50,
+ 'expected_effective_size': 0,
+ 'guid': 7
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile',
+ 'expected_size': 50,
+ 'expected_effective_size': 0,
+ 'owns': 7,
+ 'guid': 0
+ },
+ {
+ 'name': 'object1',
+ 'size': 30,
+ 'owns': 0,
+ 'expected_size': 30,
+ 'expected_effective_size': 9
+ },
+ {
+ 'name': 'object2',
+ 'owns': 0
+ },
+ {
+ 'name': 'object3',
+ 'size': 50,
+ 'owns': 0,
+ 'expected_size': 50,
+ 'expected_effective_size': 21
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'chunk',
+ 'size': 40,
+ 'expected_size': 40,
+ 'expected_effective_size': 20,
+ 'owns': 7
+ }
+ ]
+ ]);
+
+ calculationTest('owners_withChildren', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'guid': 7,
+ 'expected_size': 48,
+ 'expected_effective_size': 17,
+ 'children': [
+ {
+ 'name': 'subbitmap1',
+ 'size': 32,
+ 'expected_size': 32,
+ 'expected_effective_size': 17 * (32 / 48)
+ },
+ {
+ 'name': 'subbitmap2',
+ 'size': 16,
+ 'expected_size': 16,
+ 'expected_effective_size': 17 * (16 / 48)
+ }
+ ]
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'tile',
+ 'expected_size': 31,
+ 'expected_effective_size': 0,
+ 'guid': 8,
+ 'owns': 7,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 7,
+ 'expected_effective_size': 0
+ },
+ {
+ 'name': 'subtile',
+ 'size': 24,
+ 'expected_size': 24,
+ 'expected_effective_size': 0
+ }
+ ]
+ },
+ {
+ 'name': 'cc',
+ 'owns': 8,
+ 'size': 31,
+ 'expected_size': 31,
+ 'expected_effective_size': 31
+ }
+ ]
+ ]);
+
+ calculationTest('owners_withParents', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'size': 96,
+ 'expected_size': 96,
+ 'expected_effective_size': 32,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 32,
+ 'expected_effective_size': 32
+ },
+ {
+ 'name': 'subbitmap',
+ 'guid': 2,
+ 'expected_size': 64,
+ 'expected_effective_size': 0
+ }
+ ]
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'tile',
+ 'expected_size': 64,
+ 'expected_effective_size': 0,
+ 'children': [
+ {
+ 'name': 'subtile',
+ 'guid': 1,
+ 'owns': 2,
+ 'expected_size': 64,
+ 'expected_effective_size': 0
+ }
+ ]
+ },
+ {
+ 'name': 'cc',
+ 'owns': 1,
+ 'size': 64,
+ 'expected_size': 64,
+ 'expected_effective_size': 64
+ }
+ ]
+ ]);
+
+ calculationTest('owners_multipleLevels', [
+ [ // GMD.
+ {
+ 'name': 'bitmap',
+ 'size': 96,
+ 'expected_size': 96,
+ 'expected_effective_size': 32,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 32,
+ 'expected_effective_size': 32
+ },
+ {
+ 'name': 'subbitmap',
+ 'guid': 2,
+ 'expected_size': 64,
+ 'expected_effective_size': 0
+ }
+ ]
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'tile',
+ 'expected_size': 64,
+ 'expected_effective_size': 0,
+ 'owns': 2,
+ 'children': [
+ {
+ 'name': 'subtile',
+ 'guid': 1,
+ 'expected_size': 64,
+ 'expected_effective_size': 0
+ }
+ ]
+ },
+ {
+ 'name': 'cc',
+ 'owns': 1,
+ 'size': 64,
+ 'expected_size': 64,
+ 'expected_effective_size': 64
+ }
+ ]
+ ]);
+
+ calculationTest('views_allSizesUndefined', [
+ [
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'v8/heaps',
+ 'guid': 1
+ },
+ {
+ 'name': 'v8/objects',
+ 'owns': 1
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_ownedSizeDefined', [
+ [
+ {
+ 'name': 'v8',
+ 'expected_size': 10,
+ 'expected_effective_size': 10,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 10
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_ownerSizeDefined', [
+ [
+ {
+ 'name': 'v8',
+ 'expected_size': 20,
+ 'expected_effective_size': 20,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'expected_size': 20,
+ 'expected_effective_size': 0,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 20
+ }
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 20
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_parentSizeUndefined', [
+ [
+ {
+ 'name': 'v8',
+ 'expected_size': 30,
+ 'expected_effective_size': 30,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 10,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 20
+ }
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 20
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_ownerSizeUndefined_childrenAddUp', [
+ [
+ {
+ 'name': 'v8',
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_ownerSizeUndefined_childrenDontAddUp', [
+ [
+ {
+ 'name': 'v8',
+ 'size': 40,
+ 'expected_size': 40,
+ 'expected_effective_size': 40,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 10,
+ 'expected_effective_size': 10
+ },
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_ownedSizeUndefined_childrenAddUp', [
+ [
+ {
+ 'name': 'v8',
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'expected_size': 30,
+ 'expected_effective_size': 0,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 30
+ }
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_ownedSizeUndefined_childrenDontAddUp', [
+ [
+ {
+ 'name': 'v8',
+ 'size': 40,
+ 'expected_size': 40,
+ 'expected_effective_size': 40,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 10,
+ 'expected_effective_size': 10
+ },
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'expected_size': 30,
+ 'expected_effective_size': 0,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 30
+ }
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_allSizesDefined_childrenAddUp', [
+ [
+ {
+ 'name': 'v8',
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 30,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 16,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 14
+ }
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 14,
+ 'expected_size': 14,
+ 'expected_effective_size': 14
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_allSizesDefined_childrenDontAddUp', [
+ [
+ {
+ 'name': 'v8',
+ 'size': 35,
+ 'expected_size': 35,
+ 'expected_effective_size': 35,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 5,
+ 'expected_effective_size': 5
+ },
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 30,
+ 'expected_size': 30,
+ 'expected_effective_size': 16,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 14
+ }
+ },
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 14,
+ 'expected_size': 14,
+ 'expected_effective_size': 14
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_deep', [
+ [
+ {
+ 'name': 'root',
+ 'expected_size': 17,
+ 'expected_effective_size': 17,
+ 'children': [
+ {
+ 'name': 'parent1',
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 5,
+ 'expected_owned_by_sibling_sizes': {
+ 'parent2': 5
+ },
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 2,
+ 'expected_effective_size': 2
+ },
+ {
+ 'name': 'child',
+ 'guid': 1,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 3
+ }
+ ]
+ },
+ {
+ 'name': 'parent2',
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 5,
+ 'expected_owned_by_sibling_sizes': {
+ 'parent3': 3
+ },
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 3,
+ 'expected_effective_size': 3
+ },
+ {
+ 'name': 'child',
+ 'guid': 2,
+ 'owns': 1,
+ 'size': 5,
+ 'expected_size': 5,
+ 'expected_effective_size': 2
+ }
+ ]
+ },
+ {
+ 'name': 'parent3',
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 7,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 4,
+ 'expected_effective_size': 4
+ },
+ {
+ 'name': 'child',
+ 'owns': 2,
+ 'size': 3,
+ 'expected_size': 3,
+ 'expected_effective_size': 3
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('views_nested', [
+ [
+ {
+ 'name': 'system',
+ 'expected_size': 7,
+ 'expected_effective_size': 7,
+ 'children': [
+ {
+ 'name': 'subsystem-A',
+ 'owns': 15,
+ 'expected_size': 5,
+ 'expected_effective_size': 5,
+ 'children': [
+ {
+ 'name': 'objects',
+ 'owns': 30,
+ 'size': 3,
+ 'expected_size': 3,
+ 'expected_effective_size': 3
+ },
+ {
+ 'name': 'heaps',
+ 'guid': 30,
+ 'size': 5,
+ 'expected_size': 5,
+ 'expected_effective_size': 2,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 3
+ }
+ }
+ ]
+ },
+ {
+ 'name': 'subsystem-B',
+ 'guid': 15,
+ 'expected_size': 7,
+ 'expected_effective_size': 2,
+ 'expected_owned_by_sibling_sizes': {
+ 'subsystem-A': 5
+ },
+ 'children': [
+ {
+ 'name': 'objects',
+ 'owns': 40,
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 2
+ },
+ {
+ 'name': 'heaps',
+ 'guid': 40,
+ 'expected_size': 7,
+ 'expected_effective_size': 0,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 7
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('importance_equal', [
+ [ // GMD (both importances undefined and equal sizes).
+ {
+ 'name': 'owned',
+ 'guid': 1,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 4
+ },
+ {
+ 'name': 'owner1',
+ 'owns': 1,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 3
+ },
+ {
+ 'name': 'owner2',
+ 'owns': 1,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 3
+ }
+ ],
+ [ // PMD1 (only one importance defined and different sizes).
+ {
+ 'name': 'owned',
+ 'guid': 2,
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 5
+ },
+ {
+ 'name': 'owner1',
+ 'owns': 2,
+ 'importance': 0,
+ 'size': 15,
+ 'expected_size': 15,
+ 'expected_effective_size': 10 / 2 + 5
+ },
+ {
+ 'name': 'owner2',
+ 'owns': 2,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 10 / 2
+ }
+ ],
+ [ // PMD2 (all importances defined and different sizes).
+ {
+ 'name': 'owned',
+ 'guid': 3,
+ 'size': 15,
+ 'expected_size': 15,
+ 'expected_effective_size': 5
+ },
+ {
+ 'name': 'owner1',
+ 'owns': 3,
+ 'importance': 3,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 8 / 3
+ },
+ {
+ 'name': 'owner2',
+ 'owns': 3,
+ 'importance': 3,
+ 'size': 9,
+ 'expected_size': 9,
+ 'expected_effective_size': 8 / 3 + 1 / 2
+ },
+ {
+ 'name': 'owner3',
+ 'owns': 3,
+ 'importance': 3,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 8 / 3 + 1 / 2 + 1
+ }
+ ]
+ ]);
+
+ calculationTest('importance_notEqual', [
+ [ // GMD (one importance undefined and equal sizes).
+ {
+ 'name': 'owned',
+ 'guid': 1,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 4
+ },
+ {
+ 'name': 'owner1',
+ 'owns': 1,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 0
+ },
+ {
+ 'name': 'owner2',
+ 'owns': 1,
+ 'importance': 1,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 6
+ }
+ ],
+ [ // PMD1 (one importance undefined and different sizes).
+ {
+ 'name': 'owned',
+ 'guid': 2,
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 4
+ },
+ {
+ 'name': 'owner1',
+ 'owns': 2,
+ 'importance': -1,
+ 'size': 16,
+ 'expected_size': 16,
+ 'expected_effective_size': 6
+ },
+ {
+ 'name': 'owner2',
+ 'owns': 2,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 10
+ }
+ ],
+ [ // PMD2 (all importances defined and different sizes).
+ {
+ 'name': 'owned',
+ 'guid': 3,
+ 'size': 15,
+ 'expected_size': 15,
+ 'expected_effective_size': 5
+ },
+ {
+ 'name': 'owner1',
+ 'owns': 3,
+ 'importance': 4,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 8
+ },
+ {
+ 'name': 'owner2',
+ 'owns': 3,
+ 'importance': 3,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 0
+ },
+ {
+ 'name': 'owner3',
+ 'owns': 3,
+ 'importance': 2,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 2
+ }
+ ]
+ ]);
+
+ // Example taken from GlobalMemoryDump.calculateDumpOwnershipCoefficient_()
+ // documentation.
+ calculationTest('importance_manyOwners', [
+ [ // GMD.
+ {
+ 'name': 'owned',
+ 'guid': 4,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 2
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'owner1',
+ 'owns': 4,
+ 'importance': 2,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 6 / 2
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'some_parent',
+ 'expected_size': 7,
+ 'expected_effective_size': 6 / 2 + 1,
+ 'children': [
+ {
+ 'name': 'owner2',
+ 'owns': 4,
+ 'importance': 2,
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 6 / 2 + 1
+ }
+ ]
+ }
+ ],
+ [ // PMD3.
+ {
+ 'name': 'owner3',
+ 'owns': 4,
+ 'importance': 1,
+ 'size': 5,
+ 'expected_size': 5,
+ 'expected_effective_size': 0
+ },
+ {
+ 'name': 'owner4',
+ 'owns': 4,
+ 'importance': 0,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 1
+ }
+ ]
+ ]);
+
+ calculationTest('importance_chainOwnerships', [
+ [ // GMD.
+ {
+ 'name': 'owned',
+ 'guid': 5,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 2
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'owner1',
+ 'owns': 5,
+ 'importance': 2,
+ 'guid': 6,
+ 'size': 6,
+ 'expected_size': 6,
+ 'expected_effective_size': 2
+ },
+ {
+ 'name': 'subowner1',
+ 'owns': 6,
+ 'size': 4,
+ 'expected_size': 4,
+ 'expected_effective_size': 4
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'owner2',
+ 'owns': 5,
+ 'importance': 1,
+ 'guid': 8,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 2 - 2 / 4
+ },
+ {
+ 'name': 'subowner2',
+ 'owns': 8,
+ 'size': 2,
+ 'expected_size': 2,
+ 'expected_effective_size': 2 / 4
+ }
+ ]
+ ]);
+
+ calculationTest('importance_nested', [
+ [
+ {
+ 'name': 'grey',
+ 'guid': 15,
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 6
+ },
+ {
+ 'name': 'blue',
+ 'guid': 18,
+ 'owns': 15,
+ 'importance': 1,
+ 'size': 14,
+ 'expected_size': 14,
+ 'expected_effective_size': 1
+ },
+ {
+ 'name': 'purple',
+ 'owns': 15,
+ 'importance': 2,
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 7
+ },
+ {
+ 'name': 'yellow',
+ 'owns': 21,
+ 'importance': 3,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 3
+ },
+ {
+ 'name': 'red',
+ 'guid': 21,
+ 'owns': 18,
+ 'size': 12,
+ 'expected_size': 12,
+ 'expected_effective_size': 1
+ },
+ {
+ 'name': 'green',
+ 'owns': 21,
+ 'importance': 3,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 2
+ }
+ ]
+ ]);
+
+ calculationTest('importance_singleRefinement', [
+ [
+ {
+ 'name': 'v8',
+ 'expected_size': 13,
+ 'expected_effective_size': 13,
+ 'children': [
+ {
+ 'name': 'objects',
+ 'owns': 1,
+ 'size': 11,
+ 'expected_size': 11,
+ 'expected_effective_size': 11,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 4,
+ 'expected_effective_size': 4
+ },
+ {
+ 'name': 'object1',
+ 'owns': 2,
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 7
+ }
+ ]
+ },
+ {
+ 'name': 'heaps',
+ 'guid': 1,
+ 'size': 13,
+ 'expected_size': 13,
+ 'expected_effective_size': 2,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 11
+ },
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 3,
+ 'expected_effective_size': 1
+ },
+ {
+ 'name': 'heap1',
+ 'guid': 2,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 1,
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ calculationTest('importance_sharedRefinement', [
+ [ // GMD.
+ {
+ 'name': 'shared_bitmap',
+ 'guid': 0,
+ 'size': 23,
+ 'expected_size': 23,
+ 'expected_effective_size': 5,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 13,
+ 'expected_effective_size': 13 * 5 / (13 + 3)
+ },
+ {
+ 'name': 'bitmap0x7',
+ 'guid': 999,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 3 * 5 / (13 + 3),
+ }
+ ]
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'tile_manager',
+ 'owns': 0,
+ 'importance': 2,
+ 'size': 12,
+ 'expected_size': 12,
+ 'expected_effective_size': 5 + 2,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 5,
+ 'expected_effective_size': 5
+ },
+ {
+ 'name': 'tile42',
+ 'owns': 999,
+ 'importance': 1,
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 2,
+ }
+ ]
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'gpu',
+ 'owns': 0,
+ 'importance': 1,
+ 'size': 16,
+ 'expected_size': 16,
+ 'expected_effective_size': 6 + 5,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 11,
+ 'expected_effective_size': 6
+ },
+ {
+ 'name': 'chunk-3.14',
+ 'owns': 999,
+ 'importance': 2,
+ 'size': 5,
+ 'expected_size': 5,
+ 'expected_effective_size': 5,
+ }
+ ]
+ }
+ ]
+ ]);
+
+ // Example taken from https://goo.gl/fKg0dt.
+ calculationTest('documentationExample', [
+ [ // GMD, Global (shared) memory.
+ {
+ 'name': 'unknown',
+ 'guid': 2,
+ 'expected_size': 16,
+ 'expected_effective_size': 0,
+ }
+ ],
+ [ // PMD1, Browser process.
+ {
+ 'name': 'sharedbitmap',
+ 'size': 17,
+ 'expected_size': 17,
+ 'expected_effective_size': 9,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 1,
+ 'expected_effective_size': 1
+ },
+ {
+ 'name': '0x7',
+ 'size': 16,
+ 'expected_size': 16,
+ 'expected_effective_size': 8,
+ 'owns': 2,
+ 'importance': 1,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 16,
+ 'expected_effective_size': 8
+ },
+ {
+ 'name': 'y'
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ [ // PMD2, Renderer process.
+ {
+ 'name': 'v8',
+ 'expected_size': 13,
+ 'expected_effective_size': 13,
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'guid': 100,
+ 'expected_size': 12,
+ 'expected_effective_size': 3,
+ 'expected_owned_by_sibling_sizes': {
+ 'objects': 9
+ },
+ 'children': [
+ {
+ 'name': '1',
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 2,
+ 'owns': 2,
+ 'importance': 2
+ },
+ {
+ 'name': '2',
+ 'expected_size': 4,
+ 'expected_effective_size': 1,
+ 'size': 4
+ }
+ ]
+ },
+ {
+ 'name': 'objects',
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 10,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 1,
+ 'expected_effective_size': 1
+ },
+ {
+ 'name': 'strings',
+ 'size': 9,
+ 'expected_size': 9,
+ 'expected_effective_size': 9,
+ 'owns': 100
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ // This should never happen. Nevertheless, this test checks that we can
+ // handle invalid sizes (parent dump being smaller than its aggregated
+ // children and owned dump being smaller than its largest owner) gracefully.
+ calculationTest('invalidSizes', [
+ [
+ {
+ 'name': 'root1',
+ 'size': 24,
+ 'expected_size': 24,
+ 'expected_effective_size': 4,
+ 'children': [
+ {
+ 'name': '<unspecified>',
+ 'skip_build': true,
+ 'expected_size': 4,
+ 'expected_effective_size': 4
+ },
+ {
+ 'name': 'parent',
+ 'guid': 2,
+ 'size': 17, // Invalid: child has larger size.
+ 'expected_size': 20,
+ 'expected_infos': [
+ {
+ type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN,
+ providedSize: 17,
+ dependencySize: 20
+ },
+ {
+ type: PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER,
+ providedSize: 17,
+ dependencySize: 18
+ }
+ ],
+ 'expected_effective_size': 0,
+ 'children': [
+ {
+ 'name': 'child',
+ 'guid': 1,
+ 'size': 10, // Invalid: owner has larger size.
+ 'expected_size': 20,
+ 'expected_infos': [
+ {
+ type: PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER,
+ providedSize: 10,
+ dependencySize: 20
+ }
+ ],
+ 'expected_effective_size': 0,
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'name': 'root2',
+ 'owns': 1,
+ 'size': 20,
+ 'expected_size': 20,
+ 'expected_effective_size': 20
+ },
+ {
+ 'name': 'root3',
+ 'owns': 2,
+ 'importance': -1,
+ 'size': 18,
+ 'expected_size': 18,
+ 'expected_effective_size': 18
+ }
+ ]
+ ]);
+
+ calculationTest('multipleInfos', [
+ [
+ {
+ 'name': 'root',
+ 'expected_size': 10,
+ 'expected_effective_size': 10,
+ 'children': [
+ {
+ 'name': 'parent1',
+ 'size': 5,
+ 'expected_size': 10,
+ 'expected_infos': [
+ {
+ type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN,
+ providedSize: 5,
+ dependencySize: 10
+ }
+ ],
+ 'expected_effective_size': 1,
+ 'expected_owned_by_sibling_sizes': {
+ 'parent2': 17,
+ 'parent3': 7
+ },
+ 'children': [
+ {
+ 'name': 'child',
+ 'guid': 3,
+ 'size': 10,
+ 'expected_size': 10,
+ 'expected_effective_size': 1,
+ }
+ ]
+ },
+ {
+ 'name': 'parent2',
+ // NOTE(petrcermak): The expected size here is a little strange
+ // because the children both own the same dump (namely
+ // root/parent1/child). It would, therefore, probably make more
+ // sense for the calculated size to be 9. Since this is an unlikely
+ // case and would complicate the (already complex) size
+ // calculation, we will now keep the algorithm as is.
+ 'expected_size': 17,
+ 'expected_effective_size': 14 / 3 + 2,
+ 'children': [
+ {
+ 'name': 'child1',
+ 'owns': 3,
+ 'size': 9,
+ 'expected_size': 9,
+ 'expected_effective_size': 7 / 3 + 1 / 2 + 1,
+ },
+ {
+ 'name': 'child2',
+ 'owns': 3,
+ 'size': 8,
+ 'expected_size': 8,
+ 'expected_effective_size': 7 / 3 + 1 / 2,
+ }
+ ]
+ },
+ {
+ 'name': 'parent3',
+ 'size': 7,
+ 'expected_size': 7,
+ 'expected_effective_size': 7 / 3,
+ 'owns': 3
+ }
+ ]
+ }
+ ]
+ ]);
+
+ // Check that size calculation is NOT preceded by numeric aggregation, which
+ // would recursively sum up size numerics.
+ test('finalizeGraph_aggregation', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ buildDumpTrees([
+ undefined, // GMD.
+ [ // PMD.
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'owner_child',
+ 'owns': 9,
+ 'size': 7
+ },
+ {
+ 'name': 'owned_child',
+ 'guid': 9,
+ 'size': 20
+ }
+ ]
+ }
+ ]
+ ], model);
+ });
+ const pmd = model.getProcess(0).memoryDumps[0];
+
+ const rootDump = pmd.getMemoryAllocatorDumpByFullName('root');
+ assertDumpSizes(rootDump, 20, 20);
+
+ const ownerChildDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'root/owner_child');
+ assertDumpSizes(ownerChildDump, 7, 7);
+
+ const ownedChildDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'root/owned_child');
+ assertDumpSizes(ownedChildDump, 20, 13, [] /* expectedInfos */,
+ {'owner_child': 7} /* expectedOwnedBySiblingSizes */);
+ });
+
+ // Check that numeric and diagnostics propagation and aggregation are
+ // performed in the correct order.
+ test('finalizeGraph_propagation', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ buildDumpTrees([
+ [ // GMD.
+ {
+ 'name': 'owned_root',
+ 'guid': 1,
+ 'size': 10,
+ 'diagnostics': {
+ 'url': 'https://hello.world.com:42'
+ },
+ 'children': [
+ {
+ 'name': 'owned_child1',
+ 'numerics': {
+ 'summed': new Scalar(sizeInBytes_smallerIsBetter, 12)
+ },
+ 'diagnostics': {
+ 'url2': 'http://not.aggregated.to/owned/parent/dump'
+ }
+ },
+ {
+ 'name': 'owned_child2',
+ 'numerics': {
+ 'summed': new Scalar(sizeInBytes_smallerIsBetter, 15)
+ }
+ }
+ ]
+ }
+ ],
+ [ // PMD.
+ {
+ 'name': 'direct_owner',
+ 'owns': 1,
+ 'guid': 2,
+ 'diagnostics': {
+ 'url': 'file://not_overriden.html'
+ }
+ },
+ {
+ 'name': 'parent_owner',
+ 'children': [
+ {
+ 'name': 'child_owner',
+ 'owns': 1
+ },
+ {
+ 'name': 'sibling',
+ 'size': 5,
+ 'numerics': {
+ 'summed': new Scalar(sizeInBytes_smallerIsBetter, 13)
+ }
+ }
+ ]
+ },
+ {
+ 'name': 'precedent_owner',
+ 'owns': 1,
+ 'numerics': {
+ 'summed': new Scalar(sizeInBytes_smallerIsBetter, 0)
+ }
+ },
+ {
+ 'name': 'indirect_owner',
+ 'owns': 2
+ }
+ ]
+ ], model);
+ });
+ const pmd = model.getProcess(0).memoryDumps[0];
+
+ checkDumpNumericsAndDiagnostics(
+ pmd.getMemoryAllocatorDumpByFullName('direct_owner'),
+ {
+ size: 10,
+ effective_size: 3.3333,
+ summed: 27
+ },
+ {
+ url: 'file://not_overriden.html'
+ });
+ checkDumpNumericsAndDiagnostics(
+ pmd.getMemoryAllocatorDumpByFullName('parent_owner/child_owner'),
+ {
+ size: 10,
+ effective_size: 3.3333,
+ summed: 27
+ },
+ {
+ url: 'https://hello.world.com:42'
+ });
+ checkDumpNumericsAndDiagnostics(
+ pmd.getMemoryAllocatorDumpByFullName('parent_owner'),
+ {
+ size: 15,
+ effective_size: 8.3333,
+ summed: 40
+ }, {});
+ checkDumpNumericsAndDiagnostics(
+ pmd.getMemoryAllocatorDumpByFullName('precedent_owner'),
+ {
+ size: 10,
+ effective_size: 3.3333,
+ summed: 0
+ },
+ {
+ url: 'https://hello.world.com:42'
+ });
+ checkDumpNumericsAndDiagnostics(
+ pmd.getMemoryAllocatorDumpByFullName('indirect_owner'), {}, {});
+ });
+
+ // Check that weak dumps are removed before size size calculation.
+ test('finalizeGraph_weakDumpRemoval', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ buildDumpTrees([
+ undefined, // GMD.
+ [ // PMD.
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'directly_weak_child',
+ 'weak': true,
+ 'guid': 5,
+ 'owns': 10,
+ 'size': 100
+ },
+ {
+ 'name': 'strong_child',
+ 'guid': 10,
+ 'size': 120
+ },
+ {
+ 'name': 'indirectly_weak_child',
+ 'owns': 5,
+ 'size': 70
+ },
+ {
+ 'name': 'separate_weak_child',
+ 'weak': true,
+ 'size': 300
+ }
+ ]
+ }
+ ]
+ ], model);
+ });
+ const pmd = model.getProcess(0).memoryDumps[0];
+
+ const rootDump = pmd.getMemoryAllocatorDumpByFullName('root');
+ assertDumpSizes(rootDump, 120, 120);
+ assert.lengthOf(rootDump.children, 1);
+
+ const strongChildDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'root/strong_child');
+ assertDumpSizes(strongChildDump, 120, 120);
+ assert.lengthOf(strongChildDump.ownedBy, 0);
+
+ assert.isUndefined(pmd.getMemoryAllocatorDumpByFullName(
+ 'root/directly_weak_child'));
+ assert.isUndefined(pmd.getMemoryAllocatorDumpByFullName(
+ 'root/indirectly_weak_child'));
+ assert.isUndefined(pmd.getMemoryAllocatorDumpByFullName(
+ 'root/separate_weak_child'));
+ });
+
+ test('indicesUpdatedCorrectly', function() {
+ let gmd;
+ let rootDump;
+ let childDump;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ gmd = new GlobalMemoryDump(model, 10);
+ model.globalMemoryDumps.push(gmd);
+
+ rootDump = newAllocatorDump(gmd, 'root', {numerics: {size: 64}});
+ childDump = addChildDump(rootDump, 'child', {numerics: {size: 48}});
+
+ gmd.memoryAllocatorDumps = [rootDump];
+
+ // Before model is finalized.
+ assert.strictEqual(
+ gmd.getMemoryAllocatorDumpByFullName('root'), rootDump);
+ assert.strictEqual(
+ gmd.getMemoryAllocatorDumpByFullName('root/child'), childDump);
+ assert.isUndefined(
+ gmd.getMemoryAllocatorDumpByFullName('root/<unspecified>'));
+ });
+
+ // Test sanity check.
+ assert.isDefined(gmd);
+ assert.isDefined(rootDump);
+ assert.isDefined(childDump);
+
+ // After model is finalized.
+ assert.strictEqual(gmd.getMemoryAllocatorDumpByFullName('root'), rootDump);
+ assert.strictEqual(
+ gmd.getMemoryAllocatorDumpByFullName('root/child'), childDump);
+ const unspecifiedDump =
+ gmd.getMemoryAllocatorDumpByFullName('root/<unspecified>');
+ assert.strictEqual(unspecifiedDump.fullName, 'root/<unspecified>');
+ assert.strictEqual(unspecifiedDump.parent, rootDump);
+ assert.strictEqual(rootDump.children[0], unspecifiedDump);
+ });
+
+ weakDumpRemovalTest('allDumpsNonWeak', [
+ [ // GMD.
+ {
+ 'name': 'malloc',
+ 'children': [
+ {
+ 'name': 'allocated_objects',
+ 'children': [
+ {
+ 'name': 'obj42',
+ 'guid': 5,
+ 'expected_owned_by_links_count': 2
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ undefined, // PMD1.
+ [ // PMD2.
+ {
+ 'name': 'oilpan'
+ },
+ {
+ 'name': 'v8',
+ 'children': [
+ {
+ 'name': 'heaps',
+ 'children': [
+ {
+ 'name': 'S',
+ 'owns': 5
+ },
+ {
+ 'name': 'L',
+ 'owns': 5
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ weakDumpRemovalTest('weakRootDump', [
+ [], // GMD.
+ [ // PMD1.
+ {
+ 'name': 'strong1'
+ },
+ {
+ 'name': 'weak',
+ 'weak': true,
+ 'expected_removed': true
+ },
+ {
+ 'name': 'strong2'
+ }
+ ]
+ ]);
+
+ weakDumpRemovalTest('weakChildDump', [
+ [ // GMD.
+ {
+ 'name': 'root',
+ 'children': [
+ {
+ 'name': 'parent',
+ 'children': [
+ {
+ 'name': 'strong1'
+ },
+ {
+ 'name': 'weak',
+ 'weak': true,
+ 'expected_removed': true,
+ 'children': [
+ {
+ 'name': 'implicitly-removed'
+ }
+ ]
+ },
+ {
+ 'name': 'strong2'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]);
+
+ weakDumpRemovalTest('transitiveOwnerRemoval', [
+ [ // GMD.
+ {
+ 'name': 'not-removed-strong-dump',
+ 'guid': 0,
+ 'expected_owned_by_links_count': 1
+ },
+ {
+ 'name': 'weak-owned-dump',
+ 'guid': 1,
+ 'owns': 0,
+ 'weak': true,
+ 'expected_removed': true
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'direct-owner-dump',
+ 'guid': 2,
+ 'owns': 1,
+ 'expected_removed': true
+ },
+ {
+ 'name': 'also-not-removed-strong-dump',
+ 'owns': 0
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'indirect-owner-dump',
+ 'owns': 2,
+ 'expected_removed': true
+ }
+ ]
+ ]);
+
+ weakDumpRemovalTest('transitiveDescendantRemoval', [
+ [ // GMD.
+ {
+ 'name': 'A',
+ 'owns': 10,
+ // A =owns=> B -child-of-> C -> D => E -> F -> G (weak).
+ 'expected_removed': true
+ },
+ {
+ 'name': 'D',
+ 'owns': 5,
+ 'expected_removed': true, // D =owns=> E -child-of-> F -> G (weak).
+ 'children': [
+ {
+ 'name': 'C',
+ 'children': [
+ {
+ 'name': 'B',
+ 'guid': 10
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ undefined, // PMD1.
+ [ // PMD2.
+ {
+ 'name': 'first-retained-dump',
+ 'children': [
+ {
+ 'name': 'G',
+ 'weak': true,
+ 'expected_removed': true,
+ 'children': [
+ {
+ 'name': 'F',
+ 'children': [
+ {
+ 'name': 'E',
+ 'guid': 5
+ }
+ ]
+ },
+ {
+ 'name': 'H',
+ 'children': [
+ {
+ 'name': 'I',
+ 'children': [
+ {
+ 'name': 'J',
+ 'owns': 2
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ [ // PMD3.
+ {
+ 'name': 'second-retained-dump',
+ 'guid': 2,
+ // The only owner (J) is removed because J -child-of-> I -> H ->
+ // G (weak).
+ 'expected_owned_by_links_count': 0
+ }
+ ]
+ ]);
+
+ weakDumpRemovalTest('subownerships', [
+ [ // GMD.
+ {
+ 'name': 'root1',
+ 'owns': 20,
+ 'expected_removed': true, // root1 =owns=> root2 (weak).
+ 'children': [
+ {
+ 'name': 'child1',
+ 'owns': 2
+ }
+ ]
+ },
+ {
+ 'name': 'root2',
+ 'guid': 20,
+ 'owns': 30,
+ 'weak': true,
+ 'expected_removed': true,
+ 'children': [
+ {
+ 'name': 'child2',
+ 'guid': 2,
+ 'owns': 3
+ }
+ ]
+ },
+ {
+ 'name': 'root3',
+ 'guid': 30,
+ 'owns': 40,
+ 'expected_owned_by_links_count': 0,
+ 'children': [
+ {
+ 'name': 'child3',
+ 'guid': 3,
+ 'owns': 4,
+ 'weak': true,
+ 'expected_removed': true
+ }
+ ]
+ }
+ ],
+ [ // PMD1.
+ {
+ 'name': 'root4',
+ 'guid': 40,
+ 'expected_owned_by_links_count': 1,
+ 'children': [
+ {
+ 'name': 'child4',
+ 'guid': 4,
+ 'expected_owned_by_links_count': 0
+ }
+ ]
+ }
+ ],
+ [ // PMD2.
+ {
+ 'name': 'root5',
+ 'owns': 60,
+ 'expected_removed': true, // root5 =owns=> root6 => root7 (weak).
+ 'children': [
+ {
+ 'name': 'child5',
+ 'owns': 6
+ }
+ ]
+ },
+ {
+ 'name': 'root6',
+ 'guid': 60,
+ 'owns': 70,
+ 'expected_removed': true, // root6 =owns=> root7 (weak).
+ 'children': [
+ {
+ 'name': 'child6',
+ 'guid': 6,
+ 'owns': 7
+ }
+ ]
+ },
+ {
+ 'name': 'root7',
+ 'guid': 70,
+ 'owns': 40,
+ 'weak': true,
+ 'expected_removed': true,
+ 'children': [
+ {
+ 'name': 'child7',
+ 'guid': 7,
+ 'owns': 4
+ }
+ ]
+ }
+ ]
+ ]);
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/heap_dump.html b/chromium/third_party/catapult/tracing/tracing/model/heap_dump.html
new file mode 100644
index 00000000000..ec81a382b6f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/heap_dump.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * HeapEntry represents a single value describing the state of the heap of an
+ * allocator in a single process.
+ *
+ * An entry specifies how much space (e.g. 19 MiB) was allocated in a
+ * particular context, which consists of a codepath (e.g. drawQuad <- draw <-
+ * MessageLoop::RunTask) and an object type (e.g. HTMLImportLoader).
+ *
+ * If |valuesAreTotals| is true the size and count of this entry are totals
+ * for this and all more specific entries, otherwise they are values just for
+ * this specific entry.
+ *
+ * @{constructor}
+ */
+ function HeapEntry(
+ heapDump, leafStackFrame, objectTypeName, size, count, valuesAreTotals) {
+ this.heapDump = heapDump;
+
+ // The leaf stack frame of the associated backtrace (e.g. drawQuad for the
+ // drawQuad <- draw <- MessageLoop::RunTask backtrace). If undefined, the
+ // backtrace is empty.
+ this.leafStackFrame = leafStackFrame;
+
+ // The name of the allocated object type (e.g. 'HTMLImportLoader'). If
+ // undefined, the entry represents the sum over all object types.
+ this.objectTypeName = objectTypeName;
+
+ this.size = size;
+ this.count = count;
+ this.valuesAreTotals = valuesAreTotals;
+ }
+
+ /**
+ * HeapDump represents a dump of the heap of an allocator in a single process
+ * at a particular timestamp.
+ *
+ * @{constructor}
+ */
+ function HeapDump(processMemoryDump, allocatorName, isComplete) {
+ this.processMemoryDump = processMemoryDump;
+ this.allocatorName = allocatorName;
+ this.isComplete = isComplete;
+ this.entries = [];
+ }
+
+ HeapDump.prototype = {
+ addEntry(
+ leafStackFrame, objectTypeName, size, count, opt_valuesAreTotals) {
+ if (opt_valuesAreTotals === undefined) opt_valuesAreTotals = true;
+ const valuesAreTotals = opt_valuesAreTotals;
+ const entry = new HeapEntry(
+ this, leafStackFrame, objectTypeName, size, count, valuesAreTotals);
+ this.entries.push(entry);
+ return entry;
+ }
+ };
+
+ return {
+ HeapEntry,
+ HeapDump,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html
new file mode 100644
index 00000000000..9ce01725c03
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/stack_frame.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const StackFrame = tr.model.StackFrame;
+ const HeapEntry = tr.model.HeapEntry;
+ const HeapDump = tr.model.HeapDump;
+
+ test('heapDumps', function() {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(89);
+ const gmd = new GlobalMemoryDump(model, 42);
+ model.globalMemoryDumps.push(gmd);
+ const pmd = new ProcessMemoryDump(gmd, process, 42);
+ process.memoryDumps.push(pmd);
+
+ const rootFrame = new StackFrame(
+ undefined, tr.b.GUID.allocateSimple(), undefined);
+ const childFrame = new StackFrame(
+ rootFrame, tr.b.GUID.allocateSimple(), 'draw');
+ rootFrame.addChild(childFrame);
+
+ const dump = new HeapDump(pmd);
+ assert.strictEqual(dump.processMemoryDump, pmd);
+ assert.lengthOf(dump.entries, 0);
+
+ const entry1 = dump.addEntry(
+ childFrame, 'HTMLImportLoader', 1024, undefined);
+ assert.strictEqual(entry1.heapDump, dump);
+ assert.strictEqual(entry1.leafStackFrame, childFrame);
+ assert.strictEqual(entry1.objectTypeName, 'HTMLImportLoader');
+ assert.strictEqual(entry1.size, 1024);
+ assert.isUndefined(entry1.count);
+
+ const entry2 = dump.addEntry(undefined, undefined, 1048576, 42);
+ assert.strictEqual(entry2.heapDump, dump);
+ assert.isUndefined(entry2.leafStackFrame);
+ assert.isUndefined(entry2.objectTypeName);
+ assert.strictEqual(entry2.size, 1048576);
+ assert.strictEqual(entry2.count, 42);
+
+ assert.deepEqual(dump.entries, [entry1, entry2]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html
new file mode 100644
index 00000000000..9cabb504606
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html
@@ -0,0 +1,344 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/frame.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for managing android-specific model meta data,
+ * such as rendering apps, and frames rendered.
+ */
+tr.exportTo('tr.model.helpers', function() {
+ const Frame = tr.model.Frame;
+ const Statistics = tr.b.math.Statistics;
+
+ const UI_DRAW_TYPE = {
+ NONE: 'none',
+ LEGACY: 'legacy',
+ MARSHMALLOW: 'marshmallow'
+ };
+
+ const UI_THREAD_DRAW_NAMES = {
+ 'performTraversals': UI_DRAW_TYPE.LEGACY,
+ 'Choreographer#doFrame': UI_DRAW_TYPE.MARSHMALLOW
+ };
+
+ const RENDER_THREAD_DRAW_NAME = 'DrawFrame';
+ const RENDER_THREAD_INDEP_DRAW_NAME = 'doFrame';
+ const RENDER_THREAD_QUEUE_NAME = 'queueBuffer';
+ const RENDER_THREAD_SWAP_NAME = 'eglSwapBuffers';
+ const THREAD_SYNC_NAME = 'syncFrameState';
+
+ function getSlicesForThreadTimeRanges(threadTimeRanges) {
+ const ret = [];
+ threadTimeRanges.forEach(function(threadTimeRange) {
+ const slices = [];
+
+ threadTimeRange.thread.sliceGroup.iterSlicesInTimeRange(
+ function(slice) { slices.push(slice); },
+ threadTimeRange.start, threadTimeRange.end);
+ ret.push.apply(ret, slices);
+ });
+ return ret;
+ }
+
+ function makeFrame(threadTimeRanges, surfaceFlinger) {
+ const args = {};
+ if (surfaceFlinger && surfaceFlinger.hasVsyncs) {
+ const start = Statistics.min(threadTimeRanges,
+ function(threadTimeRanges) { return threadTimeRanges.start; });
+ args.deadline = surfaceFlinger.getFrameDeadline(start);
+ args.frameKickoff = surfaceFlinger.getFrameKickoff(start);
+ }
+ const events = getSlicesForThreadTimeRanges(threadTimeRanges);
+ return new Frame(events, threadTimeRanges, args);
+ }
+
+ function findOverlappingDrawFrame(renderThread, uiDrawSlice) {
+ if (!renderThread) return undefined;
+
+ // of all top level renderthread slices, find the one that has a 'sync'
+ // within the uiDrawSlice
+ let overlappingDrawFrame;
+ const slices = tr.b.iterateOverIntersectingIntervals(
+ renderThread.sliceGroup.slices,
+ function(range) { return range.start; },
+ function(range) { return range.end; },
+ uiDrawSlice.start,
+ uiDrawSlice.end,
+ function(rtDrawSlice) {
+ if (rtDrawSlice.title === RENDER_THREAD_DRAW_NAME) {
+ const rtSyncSlice = rtDrawSlice.findDescendentSlice(
+ THREAD_SYNC_NAME);
+ if (rtSyncSlice &&
+ rtSyncSlice.start >= uiDrawSlice.start &&
+ rtSyncSlice.end <= uiDrawSlice.end) {
+ // sync observed which overlaps ui draw. This means the RT draw
+ // corresponds to the UI draw
+ overlappingDrawFrame = rtDrawSlice;
+ }
+ }
+ });
+ return overlappingDrawFrame;
+ }
+
+ /**
+ * Builds an array of {start, end} ranges grouping common work of a frame
+ * that occurs just before performTraversals().
+ *
+ * Only necessary before Choreographer#doFrame tracing existed.
+ */
+ function getPreTraversalWorkRanges(uiThread) {
+ if (!uiThread) return [];
+
+ // gather all frame work that occurs outside of performTraversals
+ const preFrameEvents = [];
+ uiThread.sliceGroup.slices.forEach(function(slice) {
+ if (slice.title === 'obtainView' ||
+ slice.title === 'setupListItem' ||
+ slice.title === 'deliverInputEvent' ||
+ slice.title === 'RV Scroll') {
+ preFrameEvents.push(slice);
+ }
+ });
+ uiThread.asyncSliceGroup.slices.forEach(function(slice) {
+ if (slice.title === 'deliverInputEvent') {
+ preFrameEvents.push(slice);
+ }
+ });
+
+ return tr.b.math.mergeRanges(
+ tr.b.math.convertEventsToRanges(preFrameEvents),
+ 3,
+ function(events) {
+ return {
+ start: events[0].min,
+ end: events[events.length - 1].max
+ };
+ });
+ }
+
+ function getFrameStartTime(traversalStart, preTraversalWorkRanges) {
+ const preTraversalWorkRange =
+ tr.b.findClosestIntervalInSortedIntervals(
+ preTraversalWorkRanges,
+ function(range) { return range.start; },
+ function(range) { return range.end; },
+ traversalStart,
+ 3);
+
+ if (preTraversalWorkRange) {
+ return preTraversalWorkRange.start;
+ }
+ return traversalStart;
+ }
+
+ function getRtFrameEndTime(rtDrawSlice) {
+ // First try and get time that frame is queued:
+ const rtQueueSlice = rtDrawSlice.findDescendentSlice(
+ RENDER_THREAD_QUEUE_NAME);
+ if (rtQueueSlice) {
+ return rtQueueSlice.end;
+ }
+ // failing that, end of swapbuffers:
+ const rtSwapSlice = rtDrawSlice.findDescendentSlice(
+ RENDER_THREAD_SWAP_NAME);
+ if (rtSwapSlice) {
+ return rtSwapSlice.end;
+ }
+ // failing that, end of renderthread frame trace
+ return rtDrawSlice.end;
+ }
+
+ function getUiThreadDrivenFrames(app) {
+ if (!app.uiThread) return [];
+
+ let preTraversalWorkRanges = [];
+ if (app.uiDrawType === UI_DRAW_TYPE.LEGACY) {
+ preTraversalWorkRanges = getPreTraversalWorkRanges(app.uiThread);
+ }
+
+ const frames = [];
+ app.uiThread.sliceGroup.slices.forEach(function(slice) {
+ if (!(slice.title in UI_THREAD_DRAW_NAMES)) {
+ return;
+ }
+
+ const threadTimeRanges = [];
+ const uiThreadTimeRange = {
+ thread: app.uiThread,
+ start: getFrameStartTime(slice.start, preTraversalWorkRanges),
+ end: slice.end
+ };
+ threadTimeRanges.push(uiThreadTimeRange);
+
+ // on SDK 21+ devices with RenderThread,
+ // account for time taken on RenderThread
+ const rtDrawSlice = findOverlappingDrawFrame(
+ app.renderThread, slice);
+ if (rtDrawSlice) {
+ const rtSyncSlice = rtDrawSlice.findDescendentSlice(THREAD_SYNC_NAME);
+ if (rtSyncSlice) {
+ // Generally, the UI thread is only on the critical path
+ // until the start of sync.
+ uiThreadTimeRange.end = Math.min(uiThreadTimeRange.end,
+ rtSyncSlice.start);
+ }
+
+ threadTimeRanges.push({
+ thread: app.renderThread,
+ start: rtDrawSlice.start,
+ end: getRtFrameEndTime(rtDrawSlice)
+ });
+ }
+ frames.push(makeFrame(threadTimeRanges, app.surfaceFlinger));
+ });
+ return frames;
+ }
+
+ function getRenderThreadDrivenFrames(app) {
+ if (!app.renderThread) return [];
+
+ const frames = [];
+ app.renderThread.sliceGroup.getSlicesOfName(RENDER_THREAD_INDEP_DRAW_NAME)
+ .forEach(function(slice) {
+ const threadTimeRanges = [{
+ thread: app.renderThread,
+ start: slice.start,
+ end: slice.end
+ }];
+ frames.push(makeFrame(threadTimeRanges, app.surfaceFlinger));
+ });
+ return frames;
+ }
+
+ function getUiDrawType(uiThread) {
+ if (!uiThread) {
+ return UI_DRAW_TYPE.NONE;
+ }
+
+ const slices = uiThread.sliceGroup.slices;
+ for (let i = 0; i < slices.length; i++) {
+ if (slices[i].title in UI_THREAD_DRAW_NAMES) {
+ return UI_THREAD_DRAW_NAMES[slices[i].title];
+ }
+ }
+ return UI_DRAW_TYPE.NONE;
+ }
+
+ function getInputSamples(process) {
+ let samples = undefined;
+ for (const counterName in process.counters) {
+ if (/^android\.aq\:pending/.test(counterName) &&
+ process.counters[counterName].numSeries === 1) {
+ samples = process.counters[counterName].series[0].samples;
+ break;
+ }
+ }
+
+ if (!samples) return [];
+
+ // output rising edges only, since those are user inputs
+ const inputSamples = [];
+ let lastValue = 0;
+ samples.forEach(function(sample) {
+ if (sample.value > lastValue) {
+ inputSamples.push(sample);
+ }
+ lastValue = sample.value;
+ });
+ return inputSamples;
+ }
+
+ function getAnimationAsyncSlices(uiThread) {
+ if (!uiThread) return [];
+
+ const slices = [];
+ for (const slice of uiThread.asyncSliceGroup.getDescendantEvents()) {
+ if (/^animator\:/.test(slice.title)) {
+ slices.push(slice);
+ }
+ }
+ return slices;
+ }
+
+ /**
+ * Model for Android App specific data.
+ * @constructor
+ */
+ function AndroidApp(process, uiThread, renderThread, surfaceFlinger,
+ uiDrawType) {
+ this.process = process;
+ this.uiThread = uiThread;
+ this.renderThread = renderThread;
+ this.surfaceFlinger = surfaceFlinger;
+ this.uiDrawType = uiDrawType;
+
+ this.frames_ = undefined;
+ this.inputs_ = undefined;
+ }
+
+ AndroidApp.createForProcessIfPossible = function(process, surfaceFlinger) {
+ let uiThread = process.getThread(process.pid);
+ const uiDrawType = getUiDrawType(uiThread);
+ if (uiDrawType === UI_DRAW_TYPE.NONE) {
+ uiThread = undefined;
+ }
+ const renderThreads = process.findAllThreadsNamed('RenderThread');
+ const renderThread = (renderThreads.length === 1 ?
+ renderThreads[0] : undefined);
+
+ if (uiThread || renderThread) {
+ return new AndroidApp(process, uiThread, renderThread, surfaceFlinger,
+ uiDrawType);
+ }
+ };
+
+ AndroidApp.prototype = {
+ /**
+ * Returns a list of all frames in the trace for the app,
+ * constructed on first query.
+ */
+ getFrames() {
+ if (!this.frames_) {
+ const uiFrames = getUiThreadDrivenFrames(this);
+ const rtFrames = getRenderThreadDrivenFrames(this);
+ this.frames_ = uiFrames.concat(rtFrames);
+
+ // merge frames by sorting by end timestamp
+ this.frames_.sort(function(a, b) { a.end - b.end; });
+ }
+ return this.frames_;
+ },
+
+ /**
+ * Returns list of CounterSamples for each input event enqueued to the app.
+ */
+ getInputSamples() {
+ if (!this.inputs_) {
+ this.inputs_ = getInputSamples(this.process);
+ }
+ return this.inputs_;
+ },
+
+ getAnimationAsyncSlices() {
+ if (!this.animations_) {
+ this.animations_ = getAnimationAsyncSlices(this.uiThread);
+ }
+ return this.animations_;
+ }
+ };
+
+ return {
+ AndroidApp,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html
new file mode 100644
index 00000000000..76239a31942
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/model/helpers/android_app.html">
+<link rel="import" href="/tracing/model/helpers/android_surface_flinger.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for managing android-specific model meta data,
+ * such as rendering apps, frames rendered, and SurfaceFlinger.
+ */
+tr.exportTo('tr.model.helpers', function() {
+ const AndroidApp = tr.model.helpers.AndroidApp;
+ const AndroidSurfaceFlinger = tr.model.helpers.AndroidSurfaceFlinger;
+
+ const IMPORTANT_SURFACE_FLINGER_SLICES = {
+ 'doComposition': true,
+ 'updateTexImage': true,
+ 'postFramebuffer': true
+ };
+ const IMPORTANT_UI_THREAD_SLICES = {
+ 'Choreographer#doFrame': true,
+ 'performTraversals': true,
+ 'deliverInputEvent': true
+ };
+ const IMPORTANT_RENDER_THREAD_SLICES = {
+ 'doFrame': true
+ };
+
+ function iterateImportantThreadSlices(thread, important, callback) {
+ if (!thread) return;
+
+ thread.sliceGroup.slices.forEach(function(slice) {
+ if (slice.title in important) {
+ callback(slice);
+ }
+ });
+ }
+
+ /**
+ * Model for Android-specific data.
+ * @constructor
+ */
+ function AndroidModelHelper(model) {
+ this.model = model;
+ this.apps = [];
+ this.surfaceFlinger = undefined;
+
+ const processes = model.getAllProcesses();
+ for (let i = 0; i < processes.length && !this.surfaceFlinger; i++) {
+ this.surfaceFlinger =
+ AndroidSurfaceFlinger.createForProcessIfPossible(processes[i]);
+ }
+
+ model.getAllProcesses().forEach(function(process) {
+ const app = AndroidApp.createForProcessIfPossible(
+ process, this.surfaceFlinger);
+ if (app) {
+ this.apps.push(app);
+ }
+ }, this);
+ }
+
+ AndroidModelHelper.guid = tr.b.GUID.allocateSimple();
+
+ AndroidModelHelper.supportsModel = function(model) {
+ return true;
+ };
+
+ AndroidModelHelper.prototype = {
+ iterateImportantSlices(callback) {
+ if (this.surfaceFlinger) {
+ iterateImportantThreadSlices(
+ this.surfaceFlinger.thread,
+ IMPORTANT_SURFACE_FLINGER_SLICES,
+ callback);
+ }
+
+ this.apps.forEach(function(app) {
+ iterateImportantThreadSlices(
+ app.uiThread,
+ IMPORTANT_UI_THREAD_SLICES,
+ callback);
+ iterateImportantThreadSlices(
+ app.renderThread,
+ IMPORTANT_RENDER_THREAD_SLICES,
+ callback);
+ });
+ }
+ };
+
+ return {
+ AndroidModelHelper,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html
new file mode 100644
index 00000000000..0add58195af
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/android/android_auditor.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AndroidModelHelper = tr.model.helpers.AndroidModelHelper;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newCounterNamed = tr.c.TestUtils.newCounterNamed;
+ const newCounterSeries = tr.c.TestUtils.newCounterSeries;
+
+ function createSurfaceFlinger(model, vsyncCallback) {
+ if (model.getProcess(2)) {
+ throw new Error('process already exists');
+ }
+
+ const sfProcess = model.getOrCreateProcess(2);
+ const sfThread = sfProcess.getOrCreateThread(2); // main thread, tid = pid
+ sfThread.name = '/system/bin/surfaceflinger';
+
+ // ensure slicegroup has data
+ sfThread.sliceGroup.pushSlice(newSliceEx({
+ title: 'doComposition',
+ start: 8,
+ duration: 2
+ }));
+
+ vsyncCallback(sfProcess);
+ }
+
+ function createSurfaceFlingerWithVsync(model) {
+ createSurfaceFlinger(model, function(sfProcess) {
+ const counter = sfProcess.getOrCreateCounter('android', 'VSYNC');
+ const series = newCounterSeries();
+ for (let i = 0; i <= 10; i++) {
+ series.addCounterSample(i * 10, i % 2);
+ }
+ counter.addSeries(series);
+ });
+ }
+
+ /*
+ * List of customizeModelCallbacks which produce different 80ms frames,
+ * each starting at 10ms, and with a single important slice
+ */
+ const SINGLE_FRAME_CUSTOM_MODELS = [
+ function(model) {
+ // UI thread only
+ const uiThread = model.getOrCreateProcess(120).getOrCreateThread(120);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 10, duration: 80}));
+
+ model.uiThread = uiThread;
+ },
+
+ function(model) {
+ // RenderThread only
+ const renderThread = model.getOrCreateProcess(120).getOrCreateThread(200);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'doFrame', start: 10, duration: 80}));
+
+ model.renderThread = renderThread;
+ },
+
+ function(model) {
+ const uiThread = model.getOrCreateProcess(120).getOrCreateThread(120);
+
+ // UI thread time - 19 (from 10 to 29, ignored after)
+ uiThread.asyncSliceGroup.push(
+ newAsyncSliceNamed('deliverInputEvent', 10, 9, uiThread, uiThread));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 20, duration: 110}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'draw', start: 20, duration: 108}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'Record View#draw()', start: 20, duration: 8}));
+
+ // RenderThread time - 61 (from 29 to 90, ignored after)
+ const renderThread = model.getOrCreateProcess(120).getOrCreateThread(200);
+ renderThread.name = 'RenderThread';
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'DrawFrame', start: 29, duration: 70}));
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'syncFrameState', start: 29, duration: 1}));
+ renderThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'queueBuffer', start: 89, duration: 1}));
+
+ model.uiThread = uiThread;
+ model.renderThread = renderThread;
+ }
+ ];
+
+ test('getThreads', function() {
+ SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) {
+ const model = tr.c.TestUtils.newModel(customizeModelCallback);
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ assert.strictEqual(helper.apps[0].uiThread, model.uiThread);
+ assert.strictEqual(helper.apps[0].renderThread, model.renderThread);
+ });
+ });
+
+ test('iterateImportantSlices', function() {
+ SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) {
+ const model = tr.c.TestUtils.newModel(customizeModelCallback);
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+
+ let seen = 0;
+ helper.iterateImportantSlices(function(importantSlice) {
+ assert.isTrue(importantSlice instanceof tr.model.Slice);
+ seen++;
+ });
+ assert.strictEqual(seen, 1);
+ });
+ });
+
+ test('getFrames', function() {
+ SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) {
+ const model = tr.c.TestUtils.newModel(customizeModelCallback);
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ assert.strictEqual(helper.apps.length, 1);
+
+ const frames = helper.apps[0].getFrames();
+ assert.strictEqual(frames.length, 1);
+ assert.closeTo(frames[0].totalDuration, 80, 1e-5);
+
+ assert.closeTo(frames[0].start, 10, 1e-5);
+ assert.closeTo(frames[0].end, 90, 1e-5);
+ });
+ });
+
+ test('surfaceFlingerVsyncs', function() {
+ const model = tr.c.TestUtils.newModel(createSurfaceFlingerWithVsync);
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ assert.isTrue(helper.surfaceFlinger.hasVsyncs);
+
+ // test querying the vsyncs
+ assert.closeTo(helper.surfaceFlinger.getFrameKickoff(5), 0, 1e-5);
+ assert.closeTo(helper.surfaceFlinger.getFrameDeadline(95), 100, 1e-5);
+
+ assert.closeTo(helper.surfaceFlinger.getFrameKickoff(10), 10, 1e-5);
+ assert.closeTo(helper.surfaceFlinger.getFrameDeadline(90), 100, 1e-5);
+
+ // test undefined behavior outside of vsyncs.
+ assert.isUndefined(helper.surfaceFlinger.getFrameKickoff(-5));
+ assert.isUndefined(helper.surfaceFlinger.getFrameDeadline(105));
+ });
+
+ test('surfaceFlingerShiftedVsyncs', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ createSurfaceFlinger(model, function(sfProcess) {
+ const appSeries = newCounterSeries();
+ const sfSeries = newCounterSeries();
+ for (let i = 0; i <= 10; i++) {
+ // SF vsync is 4ms after app
+ appSeries.addCounterSample(i * 16, i % 2);
+ sfSeries.addCounterSample(i * 16 + 4, i % 2);
+ }
+ sfProcess.getOrCreateCounter('android', 'VSYNC-sf')
+ .addSeries(sfSeries);
+ sfProcess.getOrCreateCounter('android', 'VSYNC-app')
+ .addSeries(appSeries);
+ });
+ });
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ assert.isTrue(helper.surfaceFlinger.hasVsyncs);
+
+ // test querying the vsyncs - Frames should have 20ms window
+ assert.closeTo(helper.surfaceFlinger.getFrameKickoff(0), 0, 1e-5);
+ assert.closeTo(helper.surfaceFlinger.getFrameDeadline(0), 20, 1e-5);
+
+ assert.closeTo(helper.surfaceFlinger.getFrameKickoff(16), 16, 1e-5);
+ assert.closeTo(helper.surfaceFlinger.getFrameDeadline(16), 36, 1e-5);
+
+ // test undefined behavior outside of vsyncs.
+ assert.isUndefined(helper.surfaceFlinger.getFrameKickoff(-5));
+ assert.isUndefined(helper.surfaceFlinger.getFrameDeadline(165));
+ });
+
+ test('frameVsyncInterop', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ // app - 3 good, 3 bad frames
+ const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 1, duration: 8}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 10, duration: 8}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 20, duration: 8}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 31, duration: 11}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 45, duration: 6}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 60, duration: 20}));
+
+ // surface flinger - vsync every 10ms
+ createSurfaceFlingerWithVsync(model);
+ });
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+
+ const frames = helper.apps[0].getFrames();
+ assert.strictEqual(frames.length, 6);
+ for (let i = 0; i < 6; i++) {
+ const shouldMissDeadline = i >= 3;
+ const missedDeadline = frames[i].args.deadline < frames[i].end;
+ assert.strictEqual(shouldMissDeadline, missedDeadline);
+ }
+ });
+
+ test('appInputs', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(120);
+ const uiThread = process.getOrCreateThread(120);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 20, duration: 4}));
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 40, duration: 4}));
+
+ const counter = process.getOrCreateCounter('android', 'aq:pending:foo');
+ const series = newCounterSeries();
+ series.addCounterSample(10, 1);
+ series.addCounterSample(20, 0);
+ series.addCounterSample(30, 1);
+ series.addCounterSample(40, 2);
+ series.addCounterSample(50, 0);
+ counter.addSeries(series);
+ });
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ assert.strictEqual(helper.apps.length, 1);
+
+ const inputSamples = helper.apps[0].getInputSamples();
+ assert.strictEqual(inputSamples.length, 3);
+ assert.strictEqual(inputSamples[0].timestamp, 10);
+ assert.strictEqual(inputSamples[1].timestamp, 30);
+ assert.strictEqual(inputSamples[2].timestamp, 40);
+ });
+
+ test('appAnimations', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(120);
+ const uiThread = process.getOrCreateThread(120);
+ uiThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'performTraversals', start: 10, duration: 10}));
+ uiThread.asyncSliceGroup.push(newAsyncSliceNamed('animator:foo', 0, 10,
+ uiThread, uiThread));
+ });
+ const helper = model.getOrCreateHelper(AndroidModelHelper);
+ assert.strictEqual(helper.apps.length, 1);
+
+ const animations = helper.apps[0].getAnimationAsyncSlices();
+ assert.strictEqual(animations.length, 1);
+ assert.strictEqual(animations[0].start, 0);
+ assert.strictEqual(animations[0].end, 10);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html
new file mode 100644
index 00000000000..7595be059b3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for representing SurfaceFlinger process and its Vsyncs.
+ */
+tr.exportTo('tr.model.helpers', function() {
+ const findLowIndexInSortedArray = tr.b.findLowIndexInSortedArray;
+
+ const VSYNC_SF_NAME = 'android.VSYNC-sf';
+ const VSYNC_APP_NAME = 'android.VSYNC-app';
+ const VSYNC_FALLBACK_NAME = 'android.VSYNC';
+
+ // when sampling vsync, push samples back by this much to ensure
+ // frame start samples *between* vsyncs
+ const TIMESTAMP_FUDGE_MS = 0.01;
+
+ function getVsyncTimestamps(process, counterName) {
+ let vsync = process.counters[counterName];
+ if (!vsync) {
+ vsync = process.counters[VSYNC_FALLBACK_NAME];
+ }
+
+ if (vsync && vsync.numSeries === 1 && vsync.numSamples > 1) {
+ return vsync.series[0].timestamps;
+ }
+ return undefined;
+ }
+
+ /**
+ * Model for SurfaceFlinger specific data.
+ * @constructor
+ */
+ function AndroidSurfaceFlinger(process, thread) {
+ this.process = process;
+ this.thread = thread;
+
+ this.appVsync_ = undefined;
+ this.sfVsync_ = undefined;
+
+ this.appVsyncTimestamps_ = getVsyncTimestamps(process, VSYNC_APP_NAME);
+ this.sfVsyncTimestamps_ = getVsyncTimestamps(process, VSYNC_SF_NAME);
+
+ // separation of vsync of app vs sf - assume app has at least window of 5ms
+ this.deadlineDelayMs_ =
+ this.appVsyncTimestamps_ !== this.sfVsyncTimestamps_ ?
+ 5 : TIMESTAMP_FUDGE_MS;
+ }
+
+ AndroidSurfaceFlinger.createForProcessIfPossible = function(process) {
+ const mainThread = process.getThread(process.pid);
+
+ // newer versions - main thread, lowercase name, preceeding forward slash
+ if (mainThread && mainThread.name &&
+ /surfaceflinger/.test(mainThread.name)) {
+ return new AndroidSurfaceFlinger(process, mainThread);
+ }
+
+ // older versions - another thread is named SurfaceFlinger
+ const primaryThreads = process.findAllThreadsNamed('SurfaceFlinger');
+ if (primaryThreads.length === 1) {
+ return new AndroidSurfaceFlinger(process, primaryThreads[0]);
+ }
+ return undefined;
+ };
+
+ AndroidSurfaceFlinger.prototype = {
+ get hasVsyncs() {
+ return !!this.appVsyncTimestamps_ && !!this.sfVsyncTimestamps_;
+ },
+
+ getFrameKickoff(timestamp) {
+ if (!this.hasVsyncs) {
+ throw new Error('cannot query vsync info without vsyncs');
+ }
+
+ const firstGreaterIndex =
+ findLowIndexInSortedArray(this.appVsyncTimestamps_,
+ function(x) { return x; },
+ timestamp + TIMESTAMP_FUDGE_MS);
+
+ if (firstGreaterIndex < 1) return undefined;
+ return this.appVsyncTimestamps_[firstGreaterIndex - 1];
+ },
+
+ getFrameDeadline(timestamp) {
+ if (!this.hasVsyncs) {
+ throw new Error('cannot query vsync info without vsyncs');
+ }
+
+ const firstGreaterIndex =
+ findLowIndexInSortedArray(this.sfVsyncTimestamps_,
+ function(x) { return x; },
+ timestamp + this.deadlineDelayMs_);
+ if (firstGreaterIndex >= this.sfVsyncTimestamps_.length) {
+ return undefined;
+ }
+ return this.sfVsyncTimestamps_[firstGreaterIndex];
+ }
+ };
+
+ return {
+ AndroidSurfaceFlinger,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html
new file mode 100644
index 00000000000..9c9cb6e473b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_process_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+tr.exportTo('tr.model.helpers', function() {
+ function ChromeBrowserHelper(modelHelper, process) {
+ tr.model.helpers.ChromeProcessHelper.call(this, modelHelper, process);
+ this.mainThread_ = process.findAtMostOneThreadNamed('CrBrowserMain');
+ if (!process.name) {
+ process.name = ChromeBrowserHelper.PROCESS_NAME;
+ }
+ }
+
+ ChromeBrowserHelper.PROCESS_NAME = 'Browser';
+
+ ChromeBrowserHelper.isBrowserProcess = function(process) {
+ return !!process.findAtMostOneThreadNamed('CrBrowserMain');
+ };
+
+ ChromeBrowserHelper.prototype = {
+ __proto__: tr.model.helpers.ChromeProcessHelper.prototype,
+
+ // TODO(petrcermak): Pass browser name in a metadata event (see
+ // crbug.com/605088).
+ get browserName() {
+ const hasInProcessRendererThread = this.process.findAllThreadsNamed(
+ 'Chrome_InProcRendererThread').length > 0;
+ return hasInProcessRendererThread ? 'webview' : 'chrome';
+ },
+
+ get mainThread() {
+ return this.mainThread_;
+ },
+
+ get rendererHelpers() {
+ return this.modelHelper.rendererHelpers;
+ },
+
+ getLoadingEventsInRange(rangeOfInterest) {
+ return this.getAllAsyncSlicesMatching(function(slice) {
+ return slice.title.indexOf('WebContentsImpl Loading') === 0 &&
+ rangeOfInterest.intersectsExplicitRangeInclusive(
+ slice.start, slice.end);
+ });
+ },
+
+ getCommitProvisionalLoadEventsInRange(rangeOfInterest) {
+ return this.getAllAsyncSlicesMatching(function(slice) {
+ return slice.title === 'RenderFrameImpl::didCommitProvisionalLoad' &&
+ rangeOfInterest.intersectsExplicitRangeInclusive(
+ slice.start, slice.end);
+ });
+ },
+
+ get hasLatencyEvents() {
+ let hasLatency = false;
+ for (const thread of this.modelHelper.model.getAllThreads()) {
+ for (const event of thread.getDescendantEvents()) {
+ if (!event.isTopLevel) continue;
+ if (!(event instanceof tr.e.cc.InputLatencyAsyncSlice)) {
+ continue;
+ }
+ hasLatency = true;
+ }
+ }
+ return hasLatency;
+ },
+
+ getLatencyEventsInRange(rangeOfInterest) {
+ return this.getAllAsyncSlicesMatching(function(slice) {
+ return (slice.title.indexOf('InputLatency') === 0) &&
+ rangeOfInterest.intersectsExplicitRangeInclusive(
+ slice.start, slice.end);
+ });
+ },
+
+ getAllAsyncSlicesMatching(pred, opt_this) {
+ const events = [];
+ this.iterAllThreads(function(thread) {
+ for (const slice of thread.getDescendantEvents()) {
+ if (pred.call(opt_this, slice)) {
+ events.push(slice);
+ }
+ }
+ });
+ return events;
+ },
+
+ iterAllThreads(func, opt_this) {
+ for (const thread of Object.values(this.process.threads)) {
+ func.call(opt_this, thread);
+ }
+
+ for (const rendererHelper of Object.values(this.rendererHelpers)) {
+ const rendererProcess = rendererHelper.process;
+ for (const thread of Object.values(rendererProcess.threads)) {
+ func.call(opt_this, thread);
+ }
+ }
+ }
+ };
+
+ return {
+ ChromeBrowserHelper,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html
new file mode 100644
index 00000000000..a53f31127ea
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/helpers/chrome_browser_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
+
+ function getRange(min, max) {
+ const range = new tr.b.math.Range();
+ range.min = min;
+ range.max = max;
+ return range;
+ }
+
+ test('LoadingEvent', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { });
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ tr.e.chrome.ChromeTestUtils.addLoadingEvent(model, {start: 1, end: 10});
+ assert.strictEqual(1, modelHelper.browserHelper.getLoadingEventsInRange(
+ getRange(0, 100)).length);
+ });
+
+ test('ProvisionalLoadEvent', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { });
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ tr.e.chrome.ChromeTestUtils.addCommitLoadEvent(model, {start: 1, end: 10});
+ assert.strictEqual(1,
+ modelHelper.browserHelper.getCommitProvisionalLoadEventsInRange(
+ getRange(0, 100)).length);
+ });
+
+ test('LatencyEvent', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { });
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ tr.e.chrome.ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.UNKNOWN, {start: 1, end: 10});
+ assert.strictEqual(1, modelHelper.browserHelper.getLatencyEventsInRange(
+ getRange(0, 100)).length);
+ });
+
+ test('browserName_chrome', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { });
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ assert.strictEqual(modelHelper.browserHelper.browserName, 'chrome');
+ });
+
+ test('browserName_webview', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { });
+ model.browserProcess.getOrCreateThread(42).name =
+ 'Chrome_InProcRendererThread';
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ assert.strictEqual(modelHelper.browserHelper.browserName, 'webview');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html
new file mode 100644
index 00000000000..cf3857f1636
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/model/helpers/chrome_process_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing the Chrome GPU Process.
+ */
+tr.exportTo('tr.model.helpers', function() {
+ // TODO(charliea): This method should probably throw if this isn't a Chrome
+ // GPU process.
+ function ChromeGpuHelper(modelHelper, process) {
+ tr.model.helpers.ChromeProcessHelper.call(this, modelHelper, process);
+ if (!process.name) {
+ process.name = ChromeGpuHelper.PROCESS_NAME;
+ }
+ }
+
+ ChromeGpuHelper.PROCESS_NAME = 'GPU Process';
+
+ ChromeGpuHelper.isGpuProcess = function(process) {
+ // In some Android builds the GPU thread is not in a separate process.
+ if (process.findAtMostOneThreadNamed('CrBrowserMain') ||
+ process.findAtMostOneThreadNamed('CrRendererMain')) {
+ return false;
+ }
+
+ // On Android, there can sometimes be GPU processes with multiple main
+ // threads. We need to recognize those processes as GPU processes.
+ return process.findAllThreadsNamed('CrGpuMain').length > 0;
+ };
+
+ ChromeGpuHelper.prototype = {
+ __proto__: tr.model.helpers.ChromeProcessHelper.prototype
+ };
+
+ return {
+ ChromeGpuHelper,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html
new file mode 100644
index 00000000000..11f3be603b0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_gpu_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChromeGpuHelper = tr.model.helpers.ChromeGpuHelper;
+
+ test('constructor_doesntThrowIfMultipleMainThreads', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain';
+ model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrGpuMain';
+ });
+
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+
+ new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1));
+ });
+
+ test('constructor_doesntThrowIfNoMainThread', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1);
+ });
+
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+
+ new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1));
+ });
+
+ test('constructor_namesProcessIfUnnamed', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1);
+ });
+
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+
+ new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1));
+ assert.strictEqual(model.getOrCreateProcess(1).name, 'GPU Process');
+ });
+
+ test('constructor_doesntNameProcessIfNamed', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1).name = 'Example process name';
+ });
+
+ const modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+
+ new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1));
+ assert.strictEqual(
+ model.getOrCreateProcess(1).name, 'Example process name');
+ });
+
+ test('isGpuProcess_falseIfNoMainThread', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1);
+ });
+
+ assert.isFalse(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1)));
+ });
+
+ test('isGpuProcess', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain';
+ });
+
+ assert.isTrue(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1)));
+ });
+
+ test('isGpuProcess_trueIfMultipleMainThreads', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain';
+ model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrGpuMain';
+ });
+
+ assert.isTrue(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1)));
+ });
+
+ test('isGpuProcess_falseIfBrowserProcess', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain';
+ model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrBrowserMain';
+ });
+
+ assert.isFalse(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1)));
+ });
+
+ test('isGpuProcess_falseIfRendererProcess', function() {
+ const model = tr.c.TestUtils.newModel(model => {
+ model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain';
+ model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrRendererMain';
+ });
+
+ assert.isFalse(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1)));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html
new file mode 100644
index 00000000000..156a96b1304
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_browser_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_gpu_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html">
+<link rel="import" href="/tracing/model/helpers/telemetry_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+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];
+ }
+
+ function findTelemetrySurfaceFlingerProcess(model) {
+ const surfaceFlingerProcesses = model.getAllProcesses(
+ process => (process.name === 'SurfaceFlinger'));
+ if (surfaceFlingerProcesses.length !== 1) return undefined;
+ return surfaceFlingerProcesses[0];
+ }
+
+ function ChromeModelHelper(model) {
+ this.model_ = model;
+
+ // Find browserHelpers.
+ const browserProcesses = findChromeBrowserProcesses(model);
+ this.browserHelpers_ = browserProcesses.map(
+ p => new tr.model.helpers.ChromeBrowserHelper(this, p));
+
+ // Find gpuHelper.
+ const gpuProcess = findChromeGpuProcess(model);
+ if (gpuProcess) {
+ this.gpuHelper_ = new tr.model.helpers.ChromeGpuHelper(
+ this, gpuProcess);
+ } else {
+ this.gpuHelper_ = undefined;
+ }
+
+ // Find rendererHelpers.
+ const rendererProcesses_ = findChromeRenderProcesses(model);
+
+ this.rendererHelpers_ = {};
+ rendererProcesses_.forEach(function(renderProcess) {
+ const rendererHelper = new tr.model.helpers.ChromeRendererHelper(
+ this, renderProcess);
+ this.rendererHelpers_[rendererHelper.pid] = rendererHelper;
+ }, this);
+
+ this.surfaceFlingerProcess_ = findTelemetrySurfaceFlingerProcess(model);
+
+ this.chromeBounds_ = undefined;
+
+ this.telemetryHelper_ = new tr.model.helpers.TelemetryHelper(this);
+ }
+
+ ChromeModelHelper.guid = tr.b.GUID.allocateSimple();
+
+ ChromeModelHelper.supportsModel = function(model) {
+ if (findChromeBrowserProcesses(model).length) return true;
+ if (findChromeRenderProcesses(model).length) return true;
+ return false;
+ };
+
+ ChromeModelHelper.prototype = {
+ get pid() {
+ throw new Error('woah');
+ },
+
+ get process() {
+ throw new Error('woah');
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ // TODO: Make all users of ChromeModelHelper support multiple browsers and
+ // remove this getter (see #2119).
+ get browserProcess() {
+ if (this.browserHelper === undefined) return undefined;
+ return this.browserHelper.process;
+ },
+
+ // TODO: Make all users of ChromeModelHelper support multiple browsers and
+ // remove this getter (see #2119).
+ get browserHelper() {
+ return this.browserHelpers_[0];
+ },
+
+ get browserHelpers() {
+ return this.browserHelpers_;
+ },
+
+ get gpuHelper() {
+ return this.gpuHelper_;
+ },
+
+ get rendererHelpers() {
+ return this.rendererHelpers_;
+ },
+
+ get surfaceFlingerProcess() {
+ return this.surfaceFlingerProcess_;
+ },
+
+ /**
+ * Returns the minimal bounds that includes all Chrome-related slices, or
+ * undefined if no such minimal bounds could be established. This relies on
+ * the assumption that all Chrome-relevant traces are bounded by the browser
+ * process.
+ */
+ get chromeBounds() {
+ if (!this.chromeBounds_) {
+ this.chromeBounds_ = new tr.b.math.Range();
+ for (const browserHelper of Object.values(this.browserHelpers)) {
+ this.chromeBounds_.addRange(browserHelper.process.bounds);
+ }
+
+ for (const rendererHelper of Object.values(this.rendererHelpers)) {
+ this.chromeBounds_.addRange(rendererHelper.process.bounds);
+ }
+
+ if (this.gpuHelper) {
+ this.chromeBounds_.addRange(this.gpuHelper.process.bounds);
+ }
+ }
+
+ if (this.chromeBounds_.isEmpty) {
+ return undefined;
+ }
+
+ return this.chromeBounds_;
+ },
+
+ get telemetryHelper() {
+ return this.telemetryHelper_;
+ }
+ };
+
+ return {
+ ChromeModelHelper,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html
new file mode 100644
index 00000000000..e347cd883ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html
@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/helpers/chrome_browser_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+
+ test('getLatencyData', function() {
+ const m = tr.e.chrome.ChromeTestUtils.newChromeModel(function(m) {
+ m.browserMain.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'InputLatency::GestureScrollUpdate',
+ cat: 'benchmark',
+ start: 0,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ args: {
+ data: {
+ INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {'time': 0},
+ INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT: {time: 10}
+ }
+ }
+ }));
+ });
+
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ const latencyEvents = modelHelper.browserHelper.getLatencyEventsInRange(
+ m.bounds);
+ assert.strictEqual(latencyEvents.length, 1);
+ });
+
+ test('getFrametime', function() {
+ let frameTs;
+ const events = [];
+ // Browser process 3507
+ events.push({'cat': '__metadata', 'pid': 3507, 'tid': 3507, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'CrBrowserMain'}}); // @suppress longLineCheck
+
+ // Renderer process 3508
+ events.push({'cat': '__metadata', 'pid': 3508, 'tid': 3508, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'CrRendererMain'}}); // @suppress longLineCheck
+ // Compositor thread 3510
+ events.push({'cat': '__metadata', 'pid': 3508, 'tid': 3510, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'Compositor'}}); // @suppress longLineCheck
+
+ // Renderer process 3509
+ events.push({'cat': '__metadata', 'pid': 3509, 'tid': 3509, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'CrRendererMain'}}); // @suppress longLineCheck
+
+ // Compositor thread 3511
+ events.push({'cat': '__metadata', 'pid': 3509, 'tid': 3511, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'Compositor'}}); // @suppress longLineCheck
+
+ frameTs = 0;
+ // Add impl rendering stats for browser process 3507
+ for (let i = 0; i < 10; i++) {
+ events.push({'cat': 'benchmark', 'pid': 3507, 'tid': 3507, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::ImplThreadRenderingStats', 's': 't'}); // @suppress longLineCheck
+ frameTs += 16000 + 1000 * (i % 2);
+ }
+
+ frameTs = 0;
+ // Add main rendering stats for renderer process 3508
+ for (let i = 0; i < 10; i++) {
+ events.push({'cat': 'benchmark', 'pid': 3508, 'tid': 3508, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::MainThreadRenderingStats', 's': 't'}); // @suppress longLineCheck
+ frameTs += 16000 + 1000 * (i % 2);
+ }
+ events.push({'cat': 'benchmark', 'pid': 3508, 'tid': 3510, 'ts': 1600, 'ph': 'i', 'name': 'KeepAlive', 's': 't'}); // @suppress longLineCheck
+
+ frameTs = 0;
+ // Add impl and main rendering stats for renderer process 3509
+ for (let i = 0; i < 10; i++) {
+ events.push({'cat': 'benchmark', 'pid': 3509, 'tid': 3511, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::ImplThreadRenderingStats', 's': 't'}); // @suppress longLineCheck
+ events.push({'cat': 'benchmark', 'pid': 3509, 'tid': 3509, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::MainThreadRenderingStats', 's': 't'}); // @suppress longLineCheck
+ frameTs += 16000 + 1000 * (i % 2);
+ }
+
+ const m = tr.c.TestUtils.newModelWithEvents([events]);
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+
+ // Testing browser impl and main rendering stats.
+ let frameEvents = modelHelper.browserHelper.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, m.bounds);
+ let frametimeData = tr.model.helpers.getFrametimeDataFromEvents(
+ frameEvents);
+ assert.strictEqual(frametimeData.length, 9);
+ for (let i = 0; i < frametimeData.length; i++) {
+ assert.strictEqual(frametimeData[i].frametime, 16 + i % 2);
+ }
+ // No main rendering stats.
+ frameEvents = modelHelper.browserHelper.getFrameEventsInRange(
+ tr.model.helpers.MAIN_FRAMETIME_TYPE, m.bounds);
+ assert.strictEqual(frameEvents.length, 0);
+
+
+ // Testing renderer 3508 impl and main rendering stats.
+ frameEvents = modelHelper.rendererHelpers[3508].getFrameEventsInRange(
+ tr.model.helpers.MAIN_FRAMETIME_TYPE, m.bounds);
+ frametimeData = tr.model.helpers.getFrametimeDataFromEvents(frameEvents);
+ assert.strictEqual(frametimeData.length, 9);
+ for (let i = 0; i < frametimeData.length; i++) {
+ assert.strictEqual(frametimeData[i].frametime, 16 + i % 2);
+ }
+
+ // No impl rendering stats.
+ frameEvents = modelHelper.rendererHelpers[3508].getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, m.bounds);
+ assert.strictEqual(frameEvents.length, 0);
+
+
+ // Testing renderer 3509 impl and main rendering stats.
+ frameEvents = modelHelper.rendererHelpers[3509].getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, m.bounds);
+ frametimeData = tr.model.helpers.getFrametimeDataFromEvents(frameEvents);
+ assert.strictEqual(frametimeData.length, 9);
+ for (let i = 0; i < frametimeData.length; i++) {
+ assert.strictEqual(frametimeData[i].frametime, 16 + i % 2);
+ }
+
+ frameEvents = modelHelper.rendererHelpers[3509].getFrameEventsInRange(
+ tr.model.helpers.MAIN_FRAMETIME_TYPE, m.bounds);
+ frametimeData = tr.model.helpers.getFrametimeDataFromEvents(frameEvents);
+ assert.strictEqual(frametimeData.length, 9);
+ for (let i = 0; i < frametimeData.length; i++) {
+ assert.strictEqual(frametimeData[i].frametime, 16 + i % 2);
+ }
+ });
+
+ test('multipleBrowsers', function() {
+ const m = tr.c.TestUtils.newModel(function(model) {
+ const browserProcess1 = model.getOrCreateProcess(1);
+ browserProcess1.getOrCreateThread(2).name = 'CrBrowserMain';
+
+ const browserProcess2 = model.getOrCreateProcess(3);
+ browserProcess2.getOrCreateThread(4);
+ browserProcess2.getOrCreateThread(5).name = 'CrBrowserMain';
+
+ const nonBrowserProcess = model.getOrCreateProcess(6);
+ nonBrowserProcess.getOrCreateThread(7);
+
+ const browserProcess3 = model.getOrCreateProcess(8);
+ browserProcess3.getOrCreateThread(9).name = 'CrBrowserMain';
+ browserProcess3.getOrCreateThread(10);
+ });
+
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ const browserHelpers = modelHelper.browserHelpers;
+
+ // Check that the correct processes were marked as Chrome browser processes.
+ assert.sameMembers(browserHelpers.map(h => h.process.pid), [1, 3, 8]);
+
+ // Check that the browser helpers have the correct structure.
+ browserHelpers.forEach(function(helper) {
+ assert.instanceOf(helper, tr.model.helpers.ChromeBrowserHelper);
+ assert.strictEqual(helper.modelHelper, modelHelper);
+ });
+ });
+
+ test('chromeBounds_considersAllChromeProcesses', function() {
+ const model1 = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: 100,
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 200,
+ duration: 100,
+ }));
+ });
+
+ const model2 = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ const gpuProcess = model.getOrCreateProcess(42);
+ const gpuMainThread = gpuProcess.getOrCreateThread(1);
+ gpuMainThread.name = 'CrGpuMain';
+ gpuMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: 50,
+ }));
+ model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 100,
+ duration: 50,
+ }));
+ });
+
+ const modelHelper1 =
+ model1.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ assert.strictEqual(modelHelper1.chromeBounds.min, 0);
+ assert.strictEqual(modelHelper1.chromeBounds.max, 300);
+
+ const modelHelper2 =
+ model2.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ assert.strictEqual(modelHelper2.chromeBounds.min, 0);
+ assert.strictEqual(modelHelper2.chromeBounds.max, 150);
+ });
+
+ test('chromeBounds_onlyConsidersChromeProcesses', function() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) {
+ model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 0,
+ duration: 100,
+ }));
+
+ // The bounds of this process should not be included in chrome bounds.
+ const nonChromeProcess = model.getOrCreateProcess(1234);
+ nonChromeProcess.name = 'Telemetry';
+ const nonChromeThread = nonChromeProcess.getOrCreateThread(1);
+ nonChromeThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: 200,
+ duration: 100,
+ }));
+ });
+
+ const modelHelper =
+ model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ assert.strictEqual(modelHelper.chromeBounds.min, 0);
+ assert.strictEqual(modelHelper.chromeBounds.max, 100);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html
new file mode 100644
index 00000000000..208a4131c34
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+tr.exportTo('tr.model.helpers', function() {
+ const MAIN_FRAMETIME_TYPE = 'main_frametime_type';
+ const IMPL_FRAMETIME_TYPE = 'impl_frametime_type';
+
+ const MAIN_RENDERING_STATS =
+ 'BenchmarkInstrumentation::MainThreadRenderingStats';
+ const IMPL_RENDERING_STATS =
+ 'BenchmarkInstrumentation::ImplThreadRenderingStats';
+
+
+ function getSlicesIntersectingRange(rangeOfInterest, slices) {
+ const slicesInFilterRange = [];
+ for (let i = 0; i < slices.length; i++) {
+ const slice = slices[i];
+ if (rangeOfInterest.intersectsExplicitRangeInclusive(
+ slice.start, slice.end)) {
+ slicesInFilterRange.push(slice);
+ }
+ }
+ return slicesInFilterRange;
+ }
+
+
+ function ChromeProcessHelper(modelHelper, process) {
+ this.modelHelper = modelHelper;
+ this.process = process;
+ this.telemetryInternalRanges_ = undefined;
+ }
+
+ ChromeProcessHelper.prototype = {
+ get pid() {
+ return this.process.pid;
+ },
+
+ isTelemetryInternalEvent(slice) {
+ if (this.telemetryInternalRanges_ === undefined) {
+ this.findTelemetryInternalRanges_();
+ }
+ for (const range of this.telemetryInternalRanges_) {
+ if (range.containsExplicitRangeInclusive(slice.start, slice.end)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ findTelemetryInternalRanges_() {
+ this.telemetryInternalRanges_ = [];
+ let start = 0;
+ for (const thread of Object.values(this.process.threads)) {
+ for (const slice of thread.asyncSliceGroup.getDescendantEvents()) {
+ if (/^telemetry\.internal\..*\.start$/.test(slice.title)) {
+ start = slice.start;
+ } else if (/^telemetry\.internal\..*\.end$/.test(slice.title) &&
+ start !== undefined) {
+ this.telemetryInternalRanges_.push(
+ tr.b.math.Range.fromExplicitRange(start, slice.end));
+ start = undefined;
+ }
+ }
+ }
+ },
+
+ getFrameEventsInRange(frametimeType, range) {
+ const titleToGet = (frametimeType === MAIN_FRAMETIME_TYPE ?
+ MAIN_RENDERING_STATS : IMPL_RENDERING_STATS);
+
+ const frameEvents = [];
+ for (const event of this.process.getDescendantEvents()) {
+ if (event.title === titleToGet) {
+ if (range.intersectsExplicitRangeInclusive(event.start, event.end)) {
+ frameEvents.push(event);
+ }
+ }
+ }
+
+ frameEvents.sort(function(a, b) {return a.start - b.start;});
+ return frameEvents;
+ }
+ };
+
+ function getFrametimeDataFromEvents(frameEvents) {
+ const frametimeData = [];
+ for (let i = 1; i < frameEvents.length; i++) {
+ const diff = frameEvents[i].start - frameEvents[i - 1].start;
+ frametimeData.push({
+ 'x': frameEvents[i].start,
+ 'frametime': diff
+ });
+ }
+ return frametimeData;
+ }
+
+ return {
+ ChromeProcessHelper,
+
+ MAIN_FRAMETIME_TYPE,
+ IMPL_FRAMETIME_TYPE,
+ MAIN_RENDERING_STATS,
+ IMPL_RENDERING_STATS,
+
+ getSlicesIntersectingRange,
+ getFrametimeDataFromEvents,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html
new file mode 100644
index 00000000000..a10e7eaff01
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/helpers/chrome_process_helper.html">
+<link rel="import" href="/tracing/model/helpers/chrome_thread_helper.html">
+
+<script>
+'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';
+
+ // Returns true if there is either a main thread or a compositor thread.
+ 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,
+
+ // May be undefined.
+ get mainThread() {
+ return this.mainThread_;
+ },
+
+ // May be undefined.
+ get compositorThread() {
+ return this.compositorThread_;
+ },
+
+ // May be empty.
+ get rasterWorkerThreads() {
+ return this.rasterWorkerThreads_;
+ },
+
+ get isChromeTracingUI() {
+ return ChromeRendererHelper.isTracingProcess(this.process);
+ },
+ };
+
+ return {
+ ChromeRendererHelper,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html
new file mode 100644
index 00000000000..8ca62f70d25
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html
new file mode 100644
index 00000000000..39d6159ec60
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html
new file mode 100644
index 00000000000..49dbdd63938
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_thread_helper.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Process = tr.model.Process;
+ const Thread = tr.model.Thread;
+ const ChromeThreadHelper = tr.model.helpers.ChromeThreadHelper;
+
+ test('getNetworkEvents_empty', function() {
+ const model = new tr.Model();
+ const t = new Thread(new tr.model.Process(model, 7), 1);
+ const threadHelper = new ChromeThreadHelper(t);
+ assert.sameDeepMembers([],
+ threadHelper.getNetworkEvents());
+ });
+
+ test('getNetworkEvents_withIrrelevantEvents', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ const netEvent1 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'netlog',
+ title: 'Generic Network event',
+ start: 100,
+ duration: 200,
+ });
+ t.asyncSliceGroup.push(netEvent1);
+ const netEvent2 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'net',
+ title: 'Generic Network event',
+ start: 101,
+ duration: 201,
+ });
+ t.asyncSliceGroup.push(netEvent2);
+ t.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'irrelevant async event',
+ title: 'irrelevant async event',
+ start: 100,
+ duration: 200,
+ }));
+ t.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'irrelevant sync event',
+ title: 'irrelevant sync event',
+ start: 100,
+ duration: 200,
+ }));
+ const threadHelper = new ChromeThreadHelper(t);
+ assert.sameDeepMembers([netEvent1, netEvent2],
+ threadHelper.getNetworkEvents());
+ });
+
+ test('getNetworkEvents_allTypes', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ const netEvent1 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'netlog',
+ title: 'Generic Network event',
+ start: 100,
+ duration: 200,
+ });
+ t.asyncSliceGroup.push(netEvent1);
+ const netEvent2 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'net',
+ title: 'Generic Network event',
+ start: 101,
+ duration: 201,
+ });
+ t.asyncSliceGroup.push(netEvent2);
+ const netEvent3 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'disabled-by-default-netlog',
+ title: 'Generic Network event',
+ start: 101,
+ duration: 201,
+ });
+ t.asyncSliceGroup.push(netEvent3);
+ const netEvent4 = tr.c.TestUtils.newAsyncSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'Generic Network event',
+ start: 101,
+ duration: 201,
+ });
+ t.asyncSliceGroup.push(netEvent4);
+ const threadHelper = new ChromeThreadHelper(t);
+ assert.sameDeepMembers([netEvent1, netEvent2, netEvent3, netEvent4],
+ threadHelper.getNetworkEvents());
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html
new file mode 100644
index 00000000000..0c4486f8b54
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+
+<script>
+'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;
+ }
+
+ get renderersWithIR() {
+ this.findIRs_();
+ return this.renderersWithIR_;
+ }
+
+ get segments() {
+ this.findIRs_();
+ return this.segments_;
+ }
+
+ get uiSegments() {
+ this.findIRs_();
+ return this.uiSegments_;
+ }
+
+ /**
+ * Finds interesting segments we want to compute metrics in. We use trace
+ * events produced by Telemetry. One drawback of this method is that we
+ * cannot compute the metric from Chrome traces that are not produced by
+ * Telemetry. Alternatively, we could use the user model to detect
+ * interesting segments, like animation segments in the following way:
+ *
+ * const animationSegments = model.userModel.segments.filter(
+ * segment => segment.expectations.find(
+ * ue => ue instanceof tr.model.um.AnimationExpectation));
+ *
+ * However, the user model cannot detect all types of animations, yet. For
+ * more discussion about using test generated interaction records vs the
+ * user model please refer to http://bit.ly/ir-tbmv2. TODO(chiniforooshan):
+ * Improve the user model detection of animations.
+ *
+ * Also, some of the metrics we compute here are not animations specific.
+ */
+ 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;
+ }
+ }
+ }
+ if (foundIR && ChromeRendererHelper.isRenderProcess(process) &&
+ !ChromeRendererHelper.isTracingProcess(process)) {
+ this.renderersWithIR_.push(
+ new ChromeRendererHelper(this.modelHelper, process));
+ }
+ }
+
+ // Convert interaction record slices to segments. If an interaction record
+ // contains a gesture whose time range overlaps with the interaction
+ // record's range, use the gesture's time range.
+ //
+ // The synthetic gesture controller inserts a trace marker to precisely
+ // demarcate when the gesture was running. We check for overlap, not
+ // inclusion, because gesture actions can start/end slightly outside the
+ // telemetry markers on Windows.
+ 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);
+ }
+ }
+
+ return {
+ TelemetryHelper,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html
new file mode 100644
index 00000000000..1434603886e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('renderersWithIR', function() {
+ const m = tr.c.TestUtils.newModel((m) => {
+ m.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain';
+
+ // There is no IR in this renderer process.
+ const r1 = m.getOrCreateProcess(1).getOrCreateThread(1);
+ r1.name = 'CrRendererMain';
+ r1.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('AnAsyncSlice', 1, 1));
+
+ // This is the renderer created by Telemetry.
+ const r2 = m.getOrCreateProcess(2).getOrCreateThread(2);
+ r2.name = 'CrRendererMain';
+ r2.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+
+ // This is the Telemetry process, not a real renderer process.
+ const r3 = m.getOrCreateProcess(3).getOrCreateThread(3);
+ r3.name = 'CrRendererMain';
+ r3.parent.labels = ['chrome://tracing'];
+ r3.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1));
+ });
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ const renderers = modelHelper.telemetryHelper.renderersWithIR;
+ assert.strictEqual(1, renderers.length);
+ assert.strictEqual(2, renderers[0].process.pid);
+ });
+
+ test('segments', function() {
+ const m = tr.c.TestUtils.newModel((m) => {
+ // There is no IR in this renderer process.
+ const r = m.getOrCreateProcess(1).getOrCreateThread(1);
+ r.name = 'CrRendererMain';
+ r.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceNamed(
+ 'SyntheticGestureController::running', 6, 2));
+ r.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.A', 1, 1));
+ r.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Gesture_C', 5, 2));
+ r.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.ui_B', 3, 1));
+ });
+
+ // Async slices are:
+ //
+ // 1 2 3 4 5 6 7 8
+ // Interactions <-- A --> <-- B --> <------ C ------>
+ // Gestures <--------------->
+ //
+ // Segments should be: [1, 2], [3, 4], and [6, 8].
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ const segments = modelHelper.telemetryHelper.segments;
+ assert.strictEqual(3, segments.length);
+ assert.deepEqual([1, 2], [segments[0].start, segments[0].end]);
+ assert.deepEqual([3, 4], [segments[1].start, segments[1].end]);
+ assert.deepEqual([6, 8], [segments[2].start, segments[2].end]);
+ });
+
+ test('uiSegments', function() {
+ const m = tr.c.TestUtils.newModel((m) => {
+ // There is no IR in this renderer process.
+ const r = m.getOrCreateProcess(1).getOrCreateThread(1);
+ r.name = 'CrRendererMain';
+ r.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceNamed(
+ 'SyntheticGestureController::running', 6, 2));
+ r.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.A', 1, 1));
+ r.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.Gesture_C', 5, 2));
+ r.asyncSliceGroup.push(
+ tr.c.TestUtils.newAsyncSliceNamed('Interaction.ui_B', 3, 1));
+ });
+
+ // Async slices are:
+ //
+ // 1 2 3 4 5 6 7 8
+ // Interactions <-- A --> <-- B --> <------ C ------>
+ // Gestures <--------------->
+ //
+ // The only UI segment is [3, 4].
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ const uiSegments = modelHelper.telemetryHelper.uiSegments;
+ assert.strictEqual(1, uiSegments.length);
+ assert.deepEqual([3, 4], [uiSegments[0].start, uiSegments[0].end]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/instant_event.html b/chromium/third_party/catapult/tracing/tracing/model/instant_event.html
new file mode 100644
index 00000000000..d2f6cfbfd0b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/instant_event.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const InstantEventType = {
+ GLOBAL: 1,
+ PROCESS: 2
+ };
+
+ /**
+ * An InstantEvent is a zero-duration event.
+ *
+ * @constructor
+ */
+ 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
+ };
+
+ /**
+ * A GlobalInstantEvent is a zero-duration event that's not tied to any
+ * particular process.
+ *
+ * An example is a trace event that's issued when a new USB device is plugged
+ * into the machine.
+ *
+ * @constructor
+ */
+ function GlobalInstantEvent(category, title, colorId, start, args) {
+ 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);
+ }
+ };
+
+ /**
+ * A ProcessInstantEvent is a zero-duration event that's tied to a
+ * particular process.
+ *
+ * An example is a trace event that's issued when a kill signal is received.
+ *
+ * @constructor
+ */
+ function ProcessInstantEvent(category, title, colorId, start, args) {
+ 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,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html b/chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html
new file mode 100644
index 00000000000..8380bcb0849
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+ const CompoundEventSelectionState = tr.model.CompoundEventSelectionState;
+
+ function createModel(opt_customizeModelCallback) {
+ return TestUtils.newModel(function(model) {
+ model.p1 = model.getOrCreateProcess(1);
+ model.t2 = model.p1.getOrCreateThread(2);
+
+ model.s1 = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ title: 'a', start: 10, end: 20
+ }));
+ model.s2 = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ title: 'b', start: 20, end: 30
+ }));
+
+ model.ir1 = new tr.model.um.StubExpectation({
+ parentModel: model,
+ start: 100, end: 200,
+ typeName: 'Response',
+ normalizedEfficiency: 1.,
+ normalizedUserComfort: 0.0
+ });
+ model.userModel.expectations.push(model.ir1);
+ model.ir1.associatedEvents.push(model.s1);
+ model.ir1.associatedEvents.push(model.s2);
+
+ if (opt_customizeModelCallback) {
+ opt_customizeModelCallback(model);
+ }
+ });
+ }
+ test('notSelected', function() {
+ const model = createModel();
+
+ const sel = new tr.model.EventSet();
+
+ assert.strictEqual(CompoundEventSelectionState.NOT_SELECTED,
+ model.ir1.computeCompoundEvenSelectionState(sel));
+ });
+
+ test('directSelected', function() {
+ const model = createModel();
+
+ const sel = new tr.model.EventSet();
+ sel.push(model.ir1);
+
+ assert.strictEqual(CompoundEventSelectionState.EVENT_SELECTED,
+ model.ir1.computeCompoundEvenSelectionState(sel));
+ });
+
+ test('directAndSomeAssociatedSelected', function() {
+ const model = createModel();
+
+ const sel = new tr.model.EventSet();
+ sel.push(model.ir1);
+ sel.push(model.s1);
+
+ assert.strictEqual(
+ CompoundEventSelectionState.EVENT_AND_SOME_ASSOCIATED_SELECTED,
+ model.ir1.computeCompoundEvenSelectionState(sel));
+ });
+
+ test('allAssociatedEventsSelected', function() {
+ const model = createModel();
+
+ const sel = new tr.model.EventSet();
+ sel.push(model.s1);
+ sel.push(model.s2);
+
+ assert.strictEqual(
+ CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED,
+ model.ir1.computeCompoundEvenSelectionState(sel));
+ });
+
+ test('directAndAllAssociated', function() {
+ const model = createModel();
+
+ const sel = new tr.model.EventSet();
+ sel.push(model.ir1);
+ sel.push(model.s1);
+ sel.push(model.s2);
+
+ assert.strictEqual(
+ CompoundEventSelectionState.EVENT_AND_ALL_ASSOCIATED_SELECTED,
+ model.ir1.computeCompoundEvenSelectionState(sel));
+ });
+
+ test('stableId', function() {
+ const model = TestUtils.newModel();
+
+ const ir1 = TestUtils.newInteractionRecord(model, 0, 10);
+ const ir2 = TestUtils.newInteractionRecord(model, 10, 10);
+ const ir3 = TestUtils.newInteractionRecord(model, 20, 10);
+
+ assert.strictEqual('UserExpectation.' + ir1.guid, ir1.stableId);
+ assert.strictEqual('UserExpectation.' + ir2.guid, ir2.stableId);
+ assert.strictEqual('UserExpectation.' + ir3.guid, ir3.stableId);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html
new file mode 100644
index 00000000000..2b8264ac276
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ function getAssociatedEvents(irs) {
+ const allAssociatedEvents = new tr.model.EventSet();
+ irs.forEach(function(ir) {
+ ir.associatedEvents.forEach(function(event) {
+ // FlowEvents don't have parentContainers or cpuDurations, and it's
+ // annoying to highlight them.
+ if (event instanceof tr.model.FlowEvent) return;
+ allAssociatedEvents.push(event);
+ });
+ });
+ return allAssociatedEvents;
+ }
+
+ function getUnassociatedEvents(model, associatedEvents) {
+ const unassociatedEvents = new tr.model.EventSet();
+ // The set of unassociated events contains only events that are not in
+ // the set of associated events.
+ // Only add event to the set of unassociated events if it is not in
+ // the set of associated events.
+ for (const proc of model.getAllProcesses()) {
+ for (const thread of Object.values(proc.threads)) {
+ for (const event of thread.sliceGroup.getDescendantEvents()) {
+ if (!associatedEvents.contains(event)) {
+ unassociatedEvents.push(event);
+ }
+ }
+ }
+ }
+ return unassociatedEvents;
+ }
+
+ function getTotalCpuDuration(events) {
+ let cpuMs = 0;
+ events.forEach(function(event) {
+ // Add up events' cpu self time if they have any.
+ if (event.cpuSelfTime) {
+ cpuMs += event.cpuSelfTime;
+ }
+ });
+ return cpuMs;
+ }
+
+ function getIRCoverageFromModel(model) {
+ const associatedEvents = getAssociatedEvents(model.userModel.expectations);
+
+ if (!associatedEvents.length) return undefined;
+
+ const unassociatedEvents = getUnassociatedEvents(
+ model, associatedEvents);
+
+ const associatedCpuMs = getTotalCpuDuration(associatedEvents);
+ const unassociatedCpuMs = getTotalCpuDuration(unassociatedEvents);
+
+ const totalEventCount = associatedEvents.length + unassociatedEvents.length;
+ const totalCpuMs = associatedCpuMs + unassociatedCpuMs;
+ let coveredEventsCpuTimeRatio = undefined;
+ if (totalCpuMs !== 0) {
+ coveredEventsCpuTimeRatio = associatedCpuMs / totalCpuMs;
+ }
+
+ return {
+ associatedEventsCount: associatedEvents.length,
+ unassociatedEventsCount: unassociatedEvents.length,
+ associatedEventsCpuTimeMs: associatedCpuMs,
+ unassociatedEventsCpuTimeMs: unassociatedCpuMs,
+ coveredEventsCountRatio: associatedEvents.length / totalEventCount,
+ coveredEventsCpuTimeRatio
+ };
+ }
+
+ return {
+ getIRCoverageFromModel,
+ getAssociatedEvents,
+ getUnassociatedEvents,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html
new file mode 100644
index 00000000000..4a3b471ae6f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/ir_coverage.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ return tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(2);
+ const s0 = thread.sliceGroup.pushSlice(newSliceEx(
+ {title: 's0', start: 0.0, duration: 1.0}));
+ s0.isTopLevel = true;
+ const unassociatedEvent = thread.sliceGroup.pushSlice(newSliceEx(
+ {title: 's1', start: 6.0, duration: 1.0}));
+ unassociatedEvent.isTopLevel = true;
+ const s2 = thread.sliceGroup.pushSlice(newSliceEx(
+ {title: 's2', start: 2.0, duration: 1.0}));
+ s2.isTopLevel = true;
+ const f0 = tr.c.TestUtils.newFlowEventEx({
+ title: 'test1',
+ start: 0,
+ end: 10,
+ startSlice: s0,
+ endSlice: s2,
+ id: '0x100'
+ });
+ model.flowEvents.push(f0);
+ const as1 = tr.c.TestUtils.newAsyncSliceEx({
+ title: 'InputLatency::GestureTap',
+ cat: 'benchmark,latencyInfo',
+ start: 2,
+ end: 10,
+ id: '0x100',
+ isTopLevel: true,
+ startThread: thread
+ });
+ thread.asyncSliceGroup.push(as1);
+ const ir = new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 7});
+ ir.associatedEvents.push(as1);
+ ir.associatedEvents.push(s0);
+ ir.associatedEvents.push(s2);
+ ir.associatedEvents.push(f0);
+ model.userModel.expectations.push(ir);
+ });
+ }
+
+ test('computeCoverage', function() {
+ const model = createModel();
+ for (const event of model.getDescendantEvents()) {
+ if (event.title === 's0' || event.title === 's2') {
+ event.cpuSelfTime = 0.4;
+ } else if (event.title === 's1') {
+ event.cpuSelfTime = 0.8;
+ }
+ }
+
+ const coverage = tr.model.getIRCoverageFromModel(model);
+ assert.strictEqual(3, coverage.associatedEventsCount);
+ assert.strictEqual(1, coverage.unassociatedEventsCount);
+ assert.closeTo(0.75, coverage.coveredEventsCountRatio, 1e-3);
+ assert.closeTo(0.8, coverage.associatedEventsCpuTimeMs, 1e-3);
+ assert.closeTo(0.8, coverage.unassociatedEventsCpuTimeMs, 1e-3);
+ assert.closeTo(0.5, coverage.coveredEventsCpuTimeRatio, 1e-3);
+ });
+
+ test('zeroCPU', function() {
+ const model = createModel();
+ const coverage = tr.model.getIRCoverageFromModel(model);
+ assert.strictEqual(3, coverage.associatedEventsCount);
+ assert.strictEqual(1, coverage.unassociatedEventsCount);
+ assert.closeTo(0.75, coverage.coveredEventsCountRatio, 1e-3);
+ assert.closeTo(0.0, coverage.associatedEventsCpuTimeMs, 1e-3);
+ assert.closeTo(0.0, coverage.unassociatedEventsCpuTimeMs, 1e-3);
+ assert.strictEqual(undefined, coverage.coveredEventsCpuTimeRatio, 1e-3);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/kernel.html b/chromium/third_party/catapult/tracing/tracing/model/kernel.html
new file mode 100644
index 00000000000..834a91eea06
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/kernel.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/cpu.html">
+<link rel="import" href="/tracing/model/process_base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Process class.
+ */
+tr.exportTo('tr.model', function() {
+ const Cpu = tr.model.Cpu;
+ const ProcessBase = tr.model.ProcessBase;
+
+ /**
+ * The Kernel represents kernel-level objects in the model.
+ * @constructor
+ */
+ function Kernel(model) {
+ ProcessBase.call(this, model);
+
+ this.cpus = {};
+ this.softwareMeasuredCpuCount_ = undefined;
+ }
+
+ /**
+ * Comparison between kernels is pretty meaningless.
+ */
+ 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';
+ },
+
+ /**
+ * @return {Cpu} Gets a specific Cpu or creates one if
+ * it does not exist.
+ */
+ 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;
+ },
+
+ /**
+ * Estimates how many cpus are in the system, for use in system load
+ * estimation.
+ *
+ * If kernel trace was provided, uses that data. Otherwise, uses the
+ * software measured cpu count.
+ */
+ get bestGuessAtCpuCount() {
+ const realCpuCount = Object.keys(this.cpus).length;
+ if (realCpuCount !== 0) {
+ return realCpuCount;
+ }
+ return this.softwareMeasuredCpuCount;
+ },
+
+ updateBounds() {
+ ProcessBase.prototype.updateBounds.call(this);
+ for (const cpuNumber in this.cpus) {
+ const cpu = this.cpus[cpuNumber];
+ cpu.updateBounds();
+ this.bounds.addRange(cpu.bounds);
+ }
+ },
+
+ createSubSlices() {
+ ProcessBase.prototype.createSubSlices.call(this);
+ for (const cpuNumber in this.cpus) {
+ const cpu = this.cpus[cpuNumber];
+ cpu.createSubSlices();
+ }
+ },
+
+ addCategoriesToDict(categoriesDict) {
+ ProcessBase.prototype.addCategoriesToDict.call(this, categoriesDict);
+ for (const cpuNumber in this.cpus) {
+ this.cpus[cpuNumber].addCategoriesToDict(categoriesDict);
+ }
+ },
+
+ getSettingsKey() {
+ return 'kernel';
+ },
+
+ * childEventContainers() {
+ yield* ProcessBase.prototype.childEventContainers.call(this);
+ yield* Object.values(this.cpus);
+ },
+ };
+
+ return {
+ Kernel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/kernel_test.html b/chromium/third_party/catapult/tracing/tracing/model/kernel_test.html
new file mode 100644
index 00000000000..ffe145adef6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/kernel_test.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/kernel.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function newModel(events, callback) {
+ return tr.c.TestUtils.newModelWithEvents([events], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback: callback
+ });
+ }
+
+ test('bestGuessAtCpuCountWithNoData', function() {
+ const m = newModel([]);
+ assert.isUndefined(m.kernel.bestGuessAtCpuCount);
+ });
+
+ test('bestGuessAtCpuCountWithCpuData', function() {
+ const m = newModel([], function(m) {
+ const c1 = m.kernel.getOrCreateCpu(1);
+ const c2 = m.kernel.getOrCreateCpu(2);
+ });
+ assert.strictEqual(m.kernel.bestGuessAtCpuCount, 2);
+ });
+
+ test('bestGuessAtCpuCountWithSoftwareCpuCount', function() {
+ const m = newModel([], function(m) {
+ m.kernel.softwareMeasuredCpuCount = 2;
+ });
+ assert.strictEqual(m.kernel.bestGuessAtCpuCount, 2);
+ });
+
+ test('kernelStableId', function() {
+ const model = newModel([]);
+ assert.strictEqual(model.kernel.stableId, 'Kernel');
+ });
+
+ test('kernelTimeShifting', function() {
+ const m = newModel([]);
+ const ctr1 = m.kernel.getOrCreateCounter('cat', 'ctr1');
+ const series1 = new tr.model.CounterSeries('a', 0);
+ series1.addCounterSample(100, 5);
+ series1.addCounterSample(200, 5);
+ ctr1.addSeries(series1);
+
+ const ctr2 = m.kernel.getOrCreateCpu(1).getOrCreateCounter('cat', 'ctr2');
+ const series2 = new tr.model.CounterSeries('b', 0);
+ series2.addCounterSample(300, 5);
+ series2.addCounterSample(400, 5);
+ ctr2.addSeries(series2);
+
+ m.kernel.shiftTimestampsForward(2);
+
+ assert.strictEqual(ctr1.series[0].samples[0].timestamp, 102);
+ assert.strictEqual(ctr1.series[0].samples[1].timestamp, 202);
+
+ assert.strictEqual(ctr2.series[0].samples[0].timestamp, 302);
+ assert.strictEqual(ctr2.series[0].samples[1].timestamp, 402);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/location.html b/chromium/third_party/catapult/tracing/tracing/model/location.html
new file mode 100644
index 00000000000..b6403ae4c3f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/location.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * YComponent is a class that handles storing the stableId and the percentage
+ * offset in the y direction of all tracks within a specific viewX and viewY
+ * coordinate.
+ * @constructor
+ */
+ function YComponent(stableId, yPercentOffset) {
+ this.stableId = stableId;
+ this.yPercentOffset = yPercentOffset;
+ }
+
+ YComponent.prototype = {
+ toDict() {
+ return {
+ stableId: this.stableId,
+ yPercentOffset: this.yPercentOffset
+ };
+ }
+ };
+
+ /**
+ * Location is a class that represents a spatial location on the timeline
+ * that is specified by percent offsets within tracks rather than specific
+ * points.
+ *
+ * @constructor
+ */
+ function Location(xWorld, yComponents) {
+ this.xWorld_ = xWorld;
+ this.yComponents_ = yComponents;
+ }
+
+ /**
+ * Returns a new Location given by x and y coordinates with respect to
+ * the timeline's drawing canvas.
+ */
+ Location.fromViewCoordinates = function(viewport, viewX, viewY) {
+ const dt = viewport.currentDisplayTransform;
+ const xWorld = dt.xViewToWorld(viewX);
+ const yComponents = [];
+
+ // Since we're given coordinates within the timeline canvas, we need to
+ // convert them to document coordinates to get the element.
+ let elem = document.elementFromPoint(
+ viewX + viewport.modelTrackContainer.canvas.offsetLeft,
+ viewY + viewport.modelTrackContainer.canvas.offsetTop);
+ // Build yComponents by calculating percentage offset with respect to
+ // each parent track.
+ while (elem instanceof tr.ui.tracks.Track) {
+ if (elem.eventContainer) {
+ const boundRect = elem.getBoundingClientRect();
+ const yPercentOffset = (viewY - boundRect.top) / boundRect.height;
+ yComponents.push(
+ new YComponent(elem.eventContainer.stableId, yPercentOffset));
+ }
+ elem = elem.parentElement;
+ }
+
+ if (yComponents.length === 0) return;
+ return new Location(xWorld, yComponents);
+ };
+
+ Location.fromStableIdAndTimestamp = function(viewport, stableId, ts) {
+ const xWorld = ts;
+ const yComponents = [];
+
+ // The y components' percentage offsets will be calculated with respect to
+ // the boundingRect's top of containing track.
+ const containerToTrack = viewport.containerToTrackMap;
+ let elem = containerToTrack.getTrackByStableId(stableId);
+ if (!elem) return;
+
+ const firstY = elem.getBoundingClientRect().top;
+ while (elem instanceof tr.ui.tracks.Track) {
+ if (elem.eventContainer) {
+ const boundRect = elem.getBoundingClientRect();
+ const yPercentOffset = (firstY - boundRect.top) / boundRect.height;
+ yComponents.push(
+ new YComponent(elem.eventContainer.stableId, yPercentOffset));
+ }
+ elem = elem.parentElement;
+ }
+
+ if (yComponents.length === 0) return;
+ return new Location(xWorld, yComponents);
+ };
+
+ Location.prototype = {
+
+ get xWorld() {
+ return this.xWorld_;
+ },
+
+ /**
+ * Returns the first valid containing track based on the
+ * internal yComponents.
+ */
+ getContainingTrack(viewport) {
+ const containerToTrack = viewport.containerToTrackMap;
+ for (const i in this.yComponents_) {
+ const yComponent = this.yComponents_[i];
+ const track = containerToTrack.getTrackByStableId(yComponent.stableId);
+ if (track !== undefined) return track;
+ }
+ },
+
+ /**
+ * Calculates and returns x and y coordinates of the current location with
+ * respect to the timeline's canvas.
+ */
+ toViewCoordinates(viewport) {
+ const dt = viewport.currentDisplayTransform;
+ const containerToTrack = viewport.containerToTrackMap;
+ const viewX = dt.xWorldToView(this.xWorld_);
+
+ let viewY = -1;
+ for (const index in this.yComponents_) {
+ const yComponent = this.yComponents_[index];
+ const track = containerToTrack.getTrackByStableId(yComponent.stableId);
+ if (track !== undefined) {
+ const boundRect = track.getBoundingClientRect();
+ viewY = yComponent.yPercentOffset * boundRect.height + boundRect.top;
+ break;
+ }
+ }
+
+ return {
+ viewX,
+ viewY
+ };
+ },
+
+ toDict() {
+ return {
+ xWorld: this.xWorld_,
+ yComponents: this.yComponents_
+ };
+ }
+ };
+
+ return {
+ Location,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html
new file mode 100644
index 00000000000..0a723b8a9d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the MemoryAllocatorDump class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * @constructor
+ */
+ function MemoryAllocatorDump(containerMemoryDump, fullName, opt_guid) {
+ this.fullName = fullName;
+ this.parent = undefined;
+ this.children = [];
+
+ // String -> Scalar.
+ this.numerics = {};
+
+ // String -> string.
+ this.diagnostics = {};
+
+ // The associated container memory dump.
+ this.containerMemoryDump = containerMemoryDump;
+
+ // Ownership relationship between memory allocator dumps.
+ this.owns = undefined;
+ this.ownedBy = [];
+
+ // Map from sibling dumps (other children of this dump's parent) to the
+ // proportion of this dump's size which they (or their descendants) own.
+ this.ownedBySiblingSizes = new Map();
+
+ // Retention relationship between memory allocator dumps.
+ this.retains = [];
+ this.retainedBy = [];
+
+ // Weak memory allocator dumps are removed from the model after import in
+ // tr.model.GlobalMemoryDump.removeWeakDumps(). See
+ // base::trace_event::MemoryAllocatorDump::Flags::WEAK in the Chromium
+ // codebase.
+ this.weak = false;
+
+ // A list of information about the memory allocator dump (e.g. about how
+ // its fields were calculated). Each item should be an object with
+ // a mandatory 'type' property and type-specific extra arguments (see
+ // MemoryAllocatorDumpInfoType).
+ this.infos = [];
+
+ // For debugging purposes.
+ this.guid = opt_guid;
+ }
+
+ /**
+ * Size numeric names. Please refer to the Memory Dump Graph Metric
+ * Calculation design document for more details (https://goo.gl/fKg0dt).
+ */
+ MemoryAllocatorDump.SIZE_NUMERIC_NAME = 'size';
+ MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME = 'effective_size';
+ MemoryAllocatorDump.RESIDENT_SIZE_NUMERIC_NAME = 'resident_size';
+ MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME =
+ MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME;
+
+ MemoryAllocatorDump.prototype = {
+ get name() {
+ return this.fullName.substring(this.fullName.lastIndexOf('/') + 1);
+ },
+
+ get quantifiedName() {
+ return '\'' + this.fullName + '\' in ' +
+ this.containerMemoryDump.containerName;
+ },
+
+ getDescendantDumpByFullName(fullName) {
+ return this.containerMemoryDump.getMemoryAllocatorDumpByFullName(
+ this.fullName + '/' + fullName);
+ },
+
+ isDescendantOf(otherDump) {
+ if (this === otherDump) return true;
+ if (this.parent === undefined) return false;
+ return this.parent.isDescendantOf(otherDump);
+ },
+
+ addNumeric(name, numeric) {
+ if (!(numeric instanceof tr.b.Scalar)) {
+ throw new Error('Numeric value must be an instance of Scalar.');
+ }
+ if (name in this.numerics) {
+ throw new Error('Duplicate numeric name: ' + name + '.');
+ }
+ this.numerics[name] = numeric;
+ },
+
+ addDiagnostic(name, text) {
+ if (typeof text !== 'string') {
+ throw new Error('Diagnostic text must be a string.');
+ }
+ if (name in this.diagnostics) {
+ throw new Error('Duplicate diagnostic name: ' + name + '.');
+ }
+ this.diagnostics[name] = text;
+ },
+
+ aggregateNumericsRecursively(opt_model) {
+ const numericNames = new Set();
+
+ // Aggregate descendants's numerics recursively and gather children's
+ // numeric names.
+ this.children.forEach(function(child) {
+ child.aggregateNumericsRecursively(opt_model);
+ for (const [item, value] of Object.entries(child.numerics)) {
+ numericNames.add(item, value);
+ }
+ }, this);
+
+ // Aggregate children's numerics.
+ numericNames.forEach(function(numericName) {
+ if (numericName === MemoryAllocatorDump.SIZE_NUMERIC_NAME ||
+ numericName === MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME ||
+ this.numerics[numericName] !== undefined) {
+ // Don't aggregate size and effective size numerics. These are
+ // calculated in GlobalMemoryDump.prototype.calculateSizes() and
+ // GlobalMemoryDump.prototype.calculateEffectiveSizes respectively.
+ // Also don't aggregate numerics that the parent already has.
+ return;
+ }
+
+ this.numerics[numericName] = MemoryAllocatorDump.aggregateNumerics(
+ this.children.map(function(child) {
+ return child.numerics[numericName];
+ }), opt_model);
+ }, this);
+ }
+ };
+
+ // TODO(petrcermak): Consider moving this to tr.v.Histogram.
+ MemoryAllocatorDump.aggregateNumerics = function(numerics, opt_model) {
+ let shouldLogWarning = !!opt_model;
+ let aggregatedUnit = undefined;
+ let aggregatedValue = 0;
+
+ // Aggregate the units and sum up the values of the numerics.
+ numerics.forEach(function(numeric) {
+ if (numeric === undefined) return;
+
+ const unit = numeric.unit;
+ if (aggregatedUnit === undefined) {
+ aggregatedUnit = unit;
+ } else if (aggregatedUnit !== unit) {
+ if (shouldLogWarning) {
+ opt_model.importWarning({
+ type: 'numeric_parse_error',
+ message: 'Multiple units provided for numeric: \'' +
+ aggregatedUnit.unitName + '\' and \'' + unit.unitName + '\'.'
+ });
+ shouldLogWarning = false; // Don't log multiple warnings.
+ }
+ // Use the most generic unit when the numerics don't agree (best
+ // effort).
+ aggregatedUnit = tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ }
+
+ aggregatedValue += numeric.value;
+ }, this);
+
+ if (aggregatedUnit === undefined) return undefined;
+
+ return new tr.b.Scalar(aggregatedUnit, aggregatedValue);
+ };
+
+ /**
+ * @constructor
+ */
+ function MemoryAllocatorDumpLink(source, target, opt_importance) {
+ this.source = source;
+ this.target = target;
+ this.importance = opt_importance;
+ this.size = undefined;
+ }
+
+ /**
+ * Types of size numeric information.
+ *
+ * @enum
+ */
+ const MemoryAllocatorDumpInfoType = {
+ // The provided size of a MemoryAllocatorDump was less than the aggregated
+ // size of its children.
+ //
+ // Mandatory extra args:
+ // * providedSize: The inconsistent provided size.
+ // * dependencySize: The aggregated size of the children.
+ PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN: 0,
+
+ // The provided size of a MemoryAllocatorDump was less than the size of its
+ // largest owner.
+ //
+ // Mandatory extra args:
+ // * providedSize: The inconsistent provided size.
+ // * dependencySize: The size of the largest owner.
+ PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER: 1
+ };
+
+ return {
+ MemoryAllocatorDump,
+ MemoryAllocatorDumpLink,
+ MemoryAllocatorDumpInfoType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html
new file mode 100644
index 00000000000..7cc100dce25
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ContainerMemoryDump = tr.model.ContainerMemoryDump;
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const MemoryAllocatorDumpLink = tr.model.MemoryAllocatorDumpLink;
+ const Scalar = tr.b.Scalar;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const sizeInBytes = tr.b.Unit.byName.sizeInBytes;
+ const powerInWatts = tr.b.Unit.byName.powerInWatts;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+ const checkDumpNumericsAndDiagnostics =
+ tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics;
+
+ test('memoryAllocatorDump_instantiate', function() {
+ const containerDump = new ContainerMemoryDump(42);
+ containerDump.containerName = 'super dump';
+ const dump = new MemoryAllocatorDump(containerDump, 'v8/objects/object7');
+
+ assert.strictEqual(dump.name, 'object7');
+ assert.strictEqual(dump.fullName, 'v8/objects/object7');
+ assert.strictEqual(dump.containerMemoryDump, containerDump);
+ assert.strictEqual(
+ dump.quantifiedName, '\'v8/objects/object7\' in super dump');
+ });
+
+ test('memoryAllocatorDumps_aggregateNumericsRecursively', function() {
+ const md = new ContainerMemoryDump(42);
+
+ const oilpanDump = newAllocatorDump(md, 'oilpan', {numerics: {
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7)
+ }});
+
+ addChildDump(oilpanDump, 'bucket1', {numerics: {
+ size: 512,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 3),
+ inner_size: 256,
+ outer_size: 1024
+ }});
+
+ const oilpanBucket2Dump = addChildDump(oilpanDump, 'bucket2');
+
+ const oilpanBucket2StringsDump = addChildDump(
+ oilpanBucket2Dump, 'strings', {numerics: {
+ size: 512,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4),
+ inner_size: 512,
+ outer_size: 2048
+ }});
+
+ oilpanDump.aggregateNumericsRecursively();
+
+ // oilpan has *some* numerics aggregated.
+ checkDumpNumericsAndDiagnostics(oilpanDump, {
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7),
+ inner_size: 768,
+ outer_size: 3072
+ }, {});
+
+ // oilpan/bucket2 has *all* numerics aggregated (except for size).
+ checkDumpNumericsAndDiagnostics(oilpanBucket2Dump, {
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4),
+ inner_size: 512,
+ outer_size: 2048
+ }, {});
+
+ // oilpan/bucket2/strings has *no* numerics aggregated.
+ checkDumpNumericsAndDiagnostics(oilpanBucket2StringsDump, {
+ size: 512,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4),
+ inner_size: 512,
+ outer_size: 2048
+ }, {});
+ });
+
+ test('memoryAllocatorDump_aggregateNumerics', function() {
+ function checkAggregateNumerics(numerics, expectedValue, expectedUnit,
+ opt_expectedWarningCount) {
+ function checkResult(result) {
+ if (expectedValue === undefined) {
+ assert.isUndefined(result);
+ assert.isUndefined(expectedUnit); // Test sanity check.
+ } else {
+ assert.instanceOf(result, Scalar);
+ assert.strictEqual(result.value, expectedValue);
+ assert.strictEqual(result.unit, expectedUnit);
+ }
+ }
+
+ // Without model parameter.
+ const result1 = MemoryAllocatorDump.aggregateNumerics(numerics);
+ checkResult(result1);
+
+ // With model parameter.
+ const mockModel = {
+ warnings: [],
+ importWarning(warning) {
+ this.warnings.push(warning);
+ }
+ };
+ const result2 = MemoryAllocatorDump.aggregateNumerics(
+ numerics, mockModel);
+ checkResult(result2);
+ assert.lengthOf(mockModel.warnings, opt_expectedWarningCount || 0);
+ }
+
+ // No defined numerics.
+ checkAggregateNumerics([], undefined);
+ checkAggregateNumerics([undefined], undefined);
+ checkAggregateNumerics([undefined, undefined], undefined);
+
+ // Consistent units.
+ checkAggregateNumerics(
+ [new Scalar(unitlessNumber_smallerIsBetter, 10)],
+ 10, unitlessNumber_smallerIsBetter);
+ checkAggregateNumerics(
+ [new Scalar(sizeInBytes, 10),
+ new Scalar(sizeInBytes, 20),
+ new Scalar(sizeInBytes, 40)],
+ 70, sizeInBytes);
+ checkAggregateNumerics(
+ [undefined,
+ new Scalar(sizeInBytes, 16),
+ undefined,
+ new Scalar(sizeInBytes, 32),
+ undefined],
+ 48, sizeInBytes);
+
+ // Inconsistent units.
+ checkAggregateNumerics(
+ [new Scalar(sizeInBytes, 10),
+ new Scalar(powerInWatts, 20)],
+ 30, unitlessNumber_smallerIsBetter, 1 /* opt_expectedWarningCount */);
+ checkAggregateNumerics(
+ [undefined,
+ new Scalar(powerInWatts, 16),
+ undefined,
+ new Scalar(unitlessNumber_smallerIsBetter, 32),
+ undefined,
+ new Scalar(sizeInBytes, 64),
+ undefined],
+ 112, unitlessNumber_smallerIsBetter, 1 /* opt_expectedWarningCount */);
+ });
+
+ test('memoryAllocatorDumps_isDescendantOf', function() {
+ const md = new ContainerMemoryDump(42);
+
+ const v8Dump = new MemoryAllocatorDump(md, 'v8');
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps');
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects');
+ const v8Object1Dump = addChildDump(v8ObjectsDump, 'obj1');
+ const v8Object2Dump = addChildDump(v8ObjectsDump, 'obj2');
+
+ const oilpanDump = new MemoryAllocatorDump(md, 'oilpan');
+
+ assert.isTrue(v8Dump.isDescendantOf(v8Dump));
+ assert.isTrue(v8HeapsDump.isDescendantOf(v8Dump));
+ assert.isTrue(v8ObjectsDump.isDescendantOf(v8Dump));
+ assert.isTrue(v8Object1Dump.isDescendantOf(v8Dump));
+ assert.isTrue(v8Object2Dump.isDescendantOf(v8Dump));
+ assert.isTrue(v8ObjectsDump.isDescendantOf(v8ObjectsDump));
+ assert.isTrue(v8Object1Dump.isDescendantOf(v8ObjectsDump));
+ assert.isTrue(v8Object2Dump.isDescendantOf(v8ObjectsDump));
+ assert.isTrue(oilpanDump.isDescendantOf(oilpanDump));
+
+ assert.isFalse(v8Dump.isDescendantOf(oilpanDump));
+ assert.isFalse(v8Dump.isDescendantOf(v8HeapsDump));
+ assert.isFalse(v8Dump.isDescendantOf(v8ObjectsDump));
+ assert.isFalse(v8Dump.isDescendantOf(v8Object1Dump));
+ assert.isFalse(v8Dump.isDescendantOf(v8Object2Dump));
+ assert.isFalse(v8Object1Dump.isDescendantOf(v8Object2Dump));
+ assert.isFalse(v8Object2Dump.isDescendantOf(v8Object1Dump));
+ });
+
+ test('memoryAllocatorDumps_getDescendantDumpByFullName', function() {
+ const containerDump = new ContainerMemoryDump(42);
+
+ const gpuDump = new MemoryAllocatorDump(containerDump, 'gpu');
+ containerDump.memoryAllocatorDumps = [gpuDump];
+
+ const memtrackDump = addChildDump(gpuDump, 'android_memtrack');
+ const glDump = addChildDump(memtrackDump, 'gl');
+ const gfxDump = addChildDump(memtrackDump, 'gfx');
+ const tileDump = addChildDump(gfxDump, 'tile');
+
+ assert.strictEqual(gpuDump.getDescendantDumpByFullName(
+ 'android_memtrack'), memtrackDump);
+ assert.strictEqual(gpuDump.getDescendantDumpByFullName(
+ 'android_memtrack/gfx/tile'), tileDump);
+ assert.strictEqual(memtrackDump.getDescendantDumpByFullName('gl'), glDump);
+ assert.strictEqual(memtrackDump.getDescendantDumpByFullName(
+ 'gfx/tile'), tileDump);
+ });
+
+ test('memoryAllocatorDumpLink_instantiate', function() {
+ const d1 = new MemoryAllocatorDump('v8/isolate1');
+ const d2 = new MemoryAllocatorDump('oilpan/document1');
+ const link = new MemoryAllocatorDumpLink(d1, d2, 3);
+
+ assert.strictEqual(link.source, d1);
+ assert.strictEqual(link.target, d2);
+ assert.strictEqual(link.importance, 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html b/chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html
new file mode 100644
index 00000000000..de3413f7f44
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html
@@ -0,0 +1,220 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for tests involving memory dumps.
+ */
+tr.exportTo('tr.model', function() {
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const MemoryAllocatorDumpLink = tr.model.MemoryAllocatorDumpLink;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+
+ function castToScalar(value) {
+ if (typeof value === 'number') {
+ return new Scalar(sizeInBytes_smallerIsBetter, value);
+ }
+ assert.instanceOf(value, Scalar);
+ return value;
+ }
+
+ function getOption(opt_options, key, opt_defaultValue) {
+ if (opt_options && (key in opt_options)) {
+ return opt_options[key];
+ }
+ return opt_defaultValue;
+ }
+
+ function MemoryDumpTestUtils() {
+ throw new Error('Static class');
+ }
+
+ MemoryDumpTestUtils.SIZE_DELTA = 0.0001;
+
+ /**
+ * Create a new global memory dump and add it to a model.
+ *
+ * @param {!tr.Model} model The trace model to which the new global dump
+ * should be added.
+ * @param {!{
+ * ts: (number|undefined),
+ * duration: (number|undefined),
+ * levelOfDetail: (!tr.model.ContainerMemoryDump.LevelOfDetail|undefined)
+ * }=} opt_options Options for creating the new global dump.
+ * @return {!tr.model.GlobalMemoryDump} The newly created global memory dump.
+ */
+ MemoryDumpTestUtils.addGlobalMemoryDump = function(model, opt_options) {
+ const timestamp = getOption(opt_options, 'ts', 0);
+ const gmd = new GlobalMemoryDump(model, timestamp);
+ gmd.levelOfDetail = getOption(opt_options, 'levelOfDetail', LIGHT);
+ gmd.duration = getOption(opt_options, 'duration', 0);
+ model.globalMemoryDumps.push(gmd);
+ return gmd;
+ };
+
+ /**
+ * Create a new process memory dump and add it to a global memory dump.
+ *
+ * @param {!tr.model.GlobalMemoryDump} gmd The global dump to which the new
+ * process dump should be added.
+ * @param {!tr.model.Process} pmd The process associated with the process
+ * dump.
+ * @param {!{
+ * ts: (number|undefined)
+ * }=} opt_options Options for creating the new process dump.
+ * @return {!tr.model.ProcessMemoryDump} The newly created process memory
+ * dump.
+ */
+ MemoryDumpTestUtils.addProcessMemoryDump =
+ function(gmd, process, opt_options) {
+ const timestamp = getOption(opt_options, 'ts', gmd.start);
+ const pmd = new ProcessMemoryDump(gmd, process, timestamp);
+ process.memoryDumps.push(pmd);
+ if (process.pid in gmd.processMemoryDumps) {
+ // Test sanity check.
+ throw new Error('Process memory dump for process with pid=' +
+ process.pid + ' has already been provided');
+ }
+ gmd.processMemoryDumps[process.pid] = pmd;
+ return pmd;
+ };
+
+ /**
+ * Create a new memory allocator dump.
+ *
+ * @param {!tr.model.ContainerMemoryDump} containerDump The container dump
+ * associated with the new allocator dump.
+ * @param {string} fullName The full name of the new allocator dump
+ * (including ancestors).
+ * @param {!{
+ * guid: (number|undefined),
+ * numerics: (!Object<string, (number|!tr.b.Scalar)>|undefined)
+ * }=} opt_options Options for creating the new allocator dump.
+ * @return {!tr.model.MemoryAllocatorDump} The newly created memory allocator
+ * dump.
+ */
+ MemoryDumpTestUtils.newAllocatorDump = function(
+ containerDump, fullName, opt_options) {
+ const dump = new MemoryAllocatorDump(containerDump, fullName,
+ getOption(opt_options, 'guid'));
+ const numerics = getOption(opt_options, 'numerics');
+ if (numerics) {
+ for (const [numericName, value] of Object.entries(numerics)) {
+ dump.addNumeric(numericName, castToScalar(value));
+ }
+ }
+ const children = getOption(opt_options, 'children');
+ if (children) dump.children = children;
+ return dump;
+ };
+
+ /**
+ * Create a new child memory allocator dump and add it to a parent memory
+ * allocator dump.
+ *
+ * @param {!tr.model.MemoryAllocatorDump} parentDump The parent allocator
+ * dump.
+ * @param {string} name The name of the child allocator dump (excluding
+ * ancestors).
+ * @param {!{
+ * guid: (number|undefined),
+ * numerics: (!Object<string, (number|!tr.b.Scalar)>|undefined)
+ * }=} opt_options Options for creating the child allocator dump.
+ * @return {!tr.model.MemoryAllocatorDump} The newly created child memory
+ * allocator dump.
+ */
+ MemoryDumpTestUtils.addChildDump = function(parentDump, name, opt_options) {
+ const childDump = MemoryDumpTestUtils.newAllocatorDump(
+ parentDump.containerMemoryDump, parentDump.fullName + '/' + name,
+ opt_options);
+ childDump.parent = parentDump;
+ parentDump.children.push(childDump);
+ return childDump;
+ };
+
+ MemoryDumpTestUtils.addOwnershipLink = function(
+ ownerDump, ownedDump, opt_importance) {
+ assert.isUndefined(ownerDump.owns); // Sanity check.
+ const ownershipLink =
+ new MemoryAllocatorDumpLink(ownerDump, ownedDump, opt_importance);
+ ownerDump.owns = ownershipLink;
+ ownedDump.ownedBy.push(ownershipLink);
+ return ownershipLink;
+ };
+
+ MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics =
+ function(dump, expectedNumerics, expectedDiagnostics) {
+ const actualNumerics = dump.numerics;
+ assert.sameMembers(
+ Object.keys(actualNumerics), Object.keys(expectedNumerics));
+ for (const numericName in actualNumerics) {
+ const actualNumeric = actualNumerics[numericName];
+ const expectedNumeric = castToScalar(expectedNumerics[numericName]);
+ assert.instanceOf(actualNumeric, tr.b.Scalar);
+ assert.strictEqual(actualNumeric.unit, expectedNumeric.unit);
+ assert.closeTo(actualNumeric.value, expectedNumeric.value,
+ MemoryDumpTestUtils.SIZE_DELTA);
+ }
+
+ assert.deepEqual(dump.diagnostics, expectedDiagnostics);
+ };
+
+ MemoryDumpTestUtils.checkVMRegions = function(vmRegions, expectedRegions) {
+ if (vmRegions instanceof VMRegionClassificationNode) {
+ vmRegions = vmRegions.allRegionsForTesting;
+ }
+
+ const expectedRegionsMap = new Map();
+ expectedRegions.forEach(function(region) {
+ if (!(region instanceof VMRegion)) {
+ region = VMRegion.fromDict(region);
+ }
+ expectedRegionsMap.set(region.uniqueIdWithinProcess, region);
+ });
+ const actualRegionsMap = new Map();
+ vmRegions.forEach(function(region) {
+ actualRegionsMap.set(region.uniqueIdWithinProcess, region);
+ });
+
+ assert.strictEqual(actualRegionsMap.size, expectedRegionsMap.size);
+ for (const id of expectedRegionsMap.keys()) {
+ const expectedRegion = expectedRegionsMap.get(id);
+ const actualRegion = actualRegionsMap.get(id);
+
+ assert.instanceOf(actualRegion, VMRegion);
+ assert.strictEqual(actualRegion.startAddress,
+ expectedRegion.startAddress);
+ assert.strictEqual(actualRegion.sizeInBytes, expectedRegion.sizeInBytes);
+ assert.strictEqual(actualRegion.protectionFlags,
+ expectedRegion.protectionFlags);
+ assert.strictEqual(actualRegion.mappedFile, expectedRegion.mappedFile);
+ assert.deepEqual(actualRegion.byteStats, expectedRegion.byteStats);
+ }
+ };
+
+ return {
+ MemoryDumpTestUtils,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model.html b/chromium/third_party/catapult/tracing/tracing/model/model.html
new file mode 100644
index 00000000000..022b7cf7fbe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model.html
@@ -0,0 +1,670 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/interval_tree.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/alert.html">
+<link rel="import" href="/tracing/model/clock_sync_manager.html">
+<link rel="import" href="/tracing/model/constants.html">
+<link rel="import" href="/tracing/model/device.html">
+<link rel="import" href="/tracing/model/flow_event.html">
+<link rel="import" href="/tracing/model/frame.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/instant_event.html">
+<link rel="import" href="/tracing/model/kernel.html">
+<link rel="import" href="/tracing/model/model_indices.html">
+<link rel="import" href="/tracing/model/model_stats.html">
+<link rel="import" href="/tracing/model/object_snapshot.html">
+<link rel="import" href="/tracing/model/process.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/model/stack_frame.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Model is a parsed representation of the
+ * TraceEvents obtained from base/trace_event in which the begin-end
+ * tokens are converted into a hierarchy of processes, threads,
+ * subrows, and slices.
+ *
+ * The building block of the model is a slice. A slice is roughly
+ * equivalent to function call executing on a specific thread. As a
+ * result, slices may have one or more subslices.
+ *
+ * A thread contains one or more subrows of slices. Row 0 corresponds to
+ * the "root" slices, e.g. the topmost slices. Row 1 contains slices that
+ * are nested 1 deep in the stack, and so on. We use these subrows to draw
+ * nesting tasks.
+ *
+ */
+tr.exportTo('tr', function() {
+ const Process = tr.model.Process;
+ const Device = tr.model.Device;
+ const Kernel = tr.model.Kernel;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const GlobalInstantEvent = tr.model.GlobalInstantEvent;
+ const FlowEvent = tr.model.FlowEvent;
+ const Alert = tr.model.Alert;
+ const Sample = tr.model.Sample;
+
+ /**
+ * @constructor
+ */
+ function Model() {
+ tr.model.EventContainer.call(this);
+ tr.b.EventTarget.decorate(this);
+
+ this.timestampShiftToZeroAmount_ = 0;
+
+ this.faviconHue = 'blue'; // Should be a key from favicons.html
+
+ this.device = new Device(this);
+ this.kernel = new Kernel(this);
+ this.processes = {};
+ this.metadata = [];
+ this.categories = [];
+ this.instantEvents = [];
+ this.flowEvents = [];
+ this.clockSyncManager = new tr.model.ClockSyncManager();
+ this.intrinsicTimeUnit_ = undefined;
+
+ this.stackFrames = {};
+ this.samples = [];
+
+ this.alerts = [];
+ this.userModel = new tr.model.um.UserModel(this);
+
+ this.flowIntervalTree = new tr.b.IntervalTree((f) => f.start, (f) => f.end);
+ this.globalMemoryDumps = [];
+
+ this.userFriendlyCategoryDrivers_ = [];
+
+ this.annotationsByGuid_ = {};
+ this.modelIndices = undefined;
+
+ this.stats = new tr.model.ModelStats();
+
+ this.importWarnings_ = [];
+ this.reportedImportWarnings_ = {};
+
+ this.isTimeHighResolution_ = true;
+
+ this.patchupsToApply_ = [];
+
+ this.doesHelperGUIDSupportThisModel_ = {};
+ this.helpersByConstructorGUID_ = {};
+ this.eventsByStableId_ = undefined;
+ }
+
+ Model.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ getEventByStableId(stableId) {
+ if (this.eventsByStableId_ === undefined) {
+ this.eventsByStableId_ = {};
+ for (const event of this.getDescendantEvents()) {
+ this.eventsByStableId_[event.stableId] = event;
+ }
+ }
+ return this.eventsByStableId_[stableId];
+ },
+
+ getOrCreateHelper(constructor) {
+ if (!constructor.guid) {
+ throw new Error('Helper constructors must have GUIDs');
+ }
+
+ if (this.helpersByConstructorGUID_[constructor.guid] === undefined) {
+ if (this.doesHelperGUIDSupportThisModel_[constructor.guid] ===
+ undefined) {
+ this.doesHelperGUIDSupportThisModel_[constructor.guid] =
+ constructor.supportsModel(this);
+ }
+
+ if (!this.doesHelperGUIDSupportThisModel_[constructor.guid]) {
+ return undefined;
+ }
+
+ this.helpersByConstructorGUID_[constructor.guid] = new constructor(
+ this);
+ }
+ return this.helpersByConstructorGUID_[constructor.guid];
+ },
+
+ * childEvents() {
+ yield* this.globalMemoryDumps;
+ yield* this.instantEvents;
+ yield* this.flowEvents;
+ yield* this.alerts;
+ yield* this.samples;
+ },
+
+ * childEventContainers() {
+ yield this.userModel;
+ yield this.device;
+ yield this.kernel;
+ yield* Object.values(this.processes);
+ },
+
+ /**
+ * Some objects in the model can persist their state in ModelSettings.
+ *
+ * This iterates through them.
+ */
+ iterateAllPersistableObjects(callback) {
+ this.kernel.iterateAllPersistableObjects(callback);
+ for (const pid in this.processes) {
+ this.processes[pid].iterateAllPersistableObjects(callback);
+ }
+ },
+
+ updateBounds() {
+ this.bounds.reset();
+ const bounds = this.bounds;
+ for (const ec of this.childEventContainers()) {
+ ec.updateBounds();
+ bounds.addRange(ec.bounds);
+ }
+ for (const event of this.childEvents()) {
+ event.addBoundsToRange(bounds);
+ }
+ },
+
+ shiftWorldToZero() {
+ const shiftAmount = -this.bounds.min;
+ this.timestampShiftToZeroAmount_ = shiftAmount;
+ for (const ec of this.childEventContainers()) {
+ ec.shiftTimestampsForward(shiftAmount);
+ }
+
+ for (const event of this.childEvents()) {
+ event.start += shiftAmount;
+ }
+ this.updateBounds();
+ },
+
+ convertTimestampToModelTime(sourceClockDomainName, ts) {
+ if (sourceClockDomainName !== 'traceEventClock') {
+ throw new Error('Only traceEventClock is supported.');
+ }
+ return tr.b.Unit.timestampFromUs(ts) +
+ this.timestampShiftToZeroAmount_;
+ },
+
+ get numProcesses() {
+ let n = 0;
+ for (const p in this.processes) {
+ n++;
+ }
+ return n;
+ },
+
+ /**
+ * @return {Process} Gets a TimelineProcess for a specified pid. Returns
+ * undefined if the process doesn't exist.
+ */
+ getProcess(pid) {
+ return this.processes[pid];
+ },
+
+ /**
+ * @return {Process} Gets a TimelineProcess for a specified pid or
+ * creates one if it does not exist.
+ */
+ getOrCreateProcess(pid) {
+ if (!this.processes[pid]) {
+ this.processes[pid] = new Process(this, pid);
+ }
+ return this.processes[pid];
+ },
+
+ addStackFrame(stackFrame) {
+ if (this.stackFrames[stackFrame.id]) {
+ throw new Error('Stack frame already exists');
+ }
+ this.stackFrames[stackFrame.id] = stackFrame;
+ return stackFrame;
+ },
+
+ /**
+ * Generates the set of categories from the slices and counters.
+ */
+ updateCategories_() {
+ const categoriesDict = {};
+ this.userModel.addCategoriesToDict(categoriesDict);
+ this.device.addCategoriesToDict(categoriesDict);
+ this.kernel.addCategoriesToDict(categoriesDict);
+ for (const pid in this.processes) {
+ this.processes[pid].addCategoriesToDict(categoriesDict);
+ }
+
+ this.categories = [];
+ for (const category in categoriesDict) {
+ if (category !== '') {
+ this.categories.push(category);
+ }
+ }
+ },
+
+ getAllThreads() {
+ const threads = [];
+ for (const tid in this.kernel.threads) {
+ threads.push(process.threads[tid]);
+ }
+ for (const pid in this.processes) {
+ const process = this.processes[pid];
+ for (const tid in process.threads) {
+ threads.push(process.threads[tid]);
+ }
+ }
+ return threads;
+ },
+
+ /**
+ * @param {(!function(!tr.model.Process): boolean)=} opt_predicate Optional
+ * predicate for filtering the returned processes. If undefined, all
+ * process in the model will be returned.
+ * @return {!Array<!tr.model.Process>} An array of processes in the model.
+ */
+ getAllProcesses(opt_predicate) {
+ const processes = [];
+ for (const pid in this.processes) {
+ const process = this.processes[pid];
+ if (opt_predicate === undefined || opt_predicate(process)) {
+ processes.push(process);
+ }
+ }
+ return processes;
+ },
+
+ /**
+ * @return {Array} An array of all the counters in the model.
+ */
+ getAllCounters() {
+ const counters = [];
+ counters.push.apply(
+ counters, Object.values(this.device.counters || {}));
+ counters.push.apply(
+ counters, Object.values(this.kernel.counters || {}));
+ for (const pid in this.processes) {
+ const process = this.processes[pid];
+ for (const tid in process.counters) {
+ counters.push(process.counters[tid]);
+ }
+ }
+ return counters;
+ },
+
+ getAnnotationByGUID(guid) {
+ return this.annotationsByGuid_[guid];
+ },
+
+ addAnnotation(annotation) {
+ if (!annotation.guid) {
+ throw new Error('Annotation with undefined guid given');
+ }
+
+ this.annotationsByGuid_[annotation.guid] = annotation;
+ tr.b.dispatchSimpleEvent(this, 'annotationChange');
+ },
+
+ removeAnnotation(annotation) {
+ this.annotationsByGuid_[annotation.guid].onRemove();
+ delete this.annotationsByGuid_[annotation.guid];
+ tr.b.dispatchSimpleEvent(this, 'annotationChange');
+ },
+
+ getAllAnnotations() {
+ return Object.values(this.annotationsByGuid_);
+ },
+
+ addUserFriendlyCategoryDriver(ufcd) {
+ this.userFriendlyCategoryDrivers_.push(ufcd);
+ },
+
+ /**
+ * Gets the user friendly category string from an event.
+ *
+ * Returns undefined if none is known.
+ */
+ getUserFriendlyCategoryFromEvent(event) {
+ for (let i = 0; i < this.userFriendlyCategoryDrivers_.length; i++) {
+ const ufc = this.userFriendlyCategoryDrivers_[i].fromEvent(event);
+ if (ufc !== undefined) return ufc;
+ }
+ return undefined;
+ },
+
+ /**
+ * @param {String} The name of the thread to find.
+ * @return {Array} An array of all the matched threads.
+ */
+ findAllThreadsNamed(name) {
+ const namedThreads = [];
+ namedThreads.push.apply(
+ namedThreads,
+ this.kernel.findAllThreadsNamed(name));
+ for (const pid in this.processes) {
+ namedThreads.push.apply(
+ namedThreads,
+ this.processes[pid].findAllThreadsNamed(name));
+ }
+ return namedThreads;
+ },
+
+ get importOptions() {
+ return this.importOptions_;
+ },
+
+ set importOptions(options) {
+ this.importOptions_ = options;
+ },
+
+ /**
+ * Returns a time unit that is used to format values and determines the
+ * precision of the timestamp values.
+ */
+ get intrinsicTimeUnit() {
+ if (this.intrinsicTimeUnit_ === undefined) {
+ return tr.b.TimeDisplayModes.ms;
+ }
+ return this.intrinsicTimeUnit_;
+ },
+
+ set intrinsicTimeUnit(value) {
+ if (this.intrinsicTimeUnit_ === value) return;
+ if (this.intrinsicTimeUnit_ !== undefined) {
+ throw new Error('Intrinsic time unit already set');
+ }
+ this.intrinsicTimeUnit_ = value;
+ },
+
+ get isTimeHighResolution() {
+ return this.isTimeHighResolution_;
+ },
+
+ set isTimeHighResolution(value) {
+ this.isTimeHighResolution_ = value;
+ },
+
+ /**
+ * Returns a link to a trace data file that this model was imported from.
+ * This is NOT the URL of a site being traced, but instead an indicator of
+ * where the data is stored.
+ */
+ get canonicalUrl() {
+ return this.canonicalUrl_;
+ },
+
+ set canonicalUrl(value) {
+ if (this.canonicalUrl_ === value) return;
+ if (this.canonicalUrl_ !== undefined) {
+ throw new Error('canonicalUrl already set');
+ }
+ this.canonicalUrl_ = value;
+ },
+
+ /**
+ * Saves a warning that happened during import.
+ *
+ * Warnings are typically logged to the console, and optionally, the
+ * more critical ones are shown to the user.
+ *
+ * @param {Object} data The import warning data. Data must provide two
+ * accessors: type, message. The types are used to determine if we
+ * should output the message, we'll only output one message of each type.
+ * The message is the actual warning content.
+ */
+ importWarning(data) {
+ data.showToUser = !!data.showToUser;
+
+ this.importWarnings_.push(data);
+
+ // Only log each warning type once. We may want to add some kind of
+ // flag to allow reporting all importer warnings.
+ if (this.reportedImportWarnings_[data.type] === true) return;
+
+ this.reportedImportWarnings_[data.type] = true;
+ },
+
+ get hasImportWarnings() {
+ return (this.importWarnings_.length > 0);
+ },
+
+ get importWarnings() {
+ return this.importWarnings_;
+ },
+
+ get importWarningsThatShouldBeShownToUser() {
+ return this.importWarnings_.filter(function(warning) {
+ return warning.showToUser;
+ });
+ },
+
+ autoCloseOpenSlices() {
+ // Sort the samples.
+ this.samples.sort(function(x, y) {
+ return x.start - y.start;
+ });
+
+ this.updateBounds();
+ this.kernel.autoCloseOpenSlices();
+ for (const pid in this.processes) {
+ this.processes[pid].autoCloseOpenSlices();
+ }
+ },
+
+ createSubSlices() {
+ this.kernel.createSubSlices();
+ for (const pid in this.processes) {
+ this.processes[pid].createSubSlices();
+ }
+ },
+
+ preInitializeObjects() {
+ for (const pid in this.processes) {
+ this.processes[pid].preInitializeObjects();
+ }
+ },
+
+ initializeObjects() {
+ for (const pid in this.processes) {
+ this.processes[pid].initializeObjects();
+ }
+ },
+
+ pruneEmptyContainers() {
+ this.kernel.pruneEmptyContainers();
+ for (const pid in this.processes) {
+ this.processes[pid].pruneEmptyContainers();
+ }
+ },
+
+ mergeKernelWithUserland() {
+ for (const pid in this.processes) {
+ this.processes[pid].mergeKernelWithUserland();
+ }
+ },
+
+ computeWorldBounds(shiftWorldToZero) {
+ this.updateBounds();
+ this.updateCategories_();
+
+ if (shiftWorldToZero) {
+ this.shiftWorldToZero();
+ }
+ },
+
+ buildFlowEventIntervalTree() {
+ for (let i = 0; i < this.flowEvents.length; ++i) {
+ const flowEvent = this.flowEvents[i];
+ this.flowIntervalTree.insert(flowEvent);
+ }
+ this.flowIntervalTree.updateHighValues();
+ },
+
+ cleanupUndeletedObjects() {
+ for (const pid in this.processes) {
+ this.processes[pid].autoDeleteObjects(this.bounds.max);
+ }
+ },
+
+ sortMemoryDumps() {
+ this.globalMemoryDumps.sort(function(x, y) {
+ return x.start - y.start;
+ });
+
+ for (const pid in this.processes) {
+ this.processes[pid].sortMemoryDumps();
+ }
+ },
+
+ finalizeMemoryGraphs() {
+ this.globalMemoryDumps.forEach(function(dump) {
+ dump.finalizeGraph();
+ });
+ },
+
+ buildEventIndices() {
+ this.modelIndices = new tr.model.ModelIndices(this);
+ },
+
+ sortAlerts() {
+ this.alerts.sort(function(x, y) {
+ return x.start - y.start;
+ });
+ },
+
+ applyObjectRefPatchups() {
+ // Change all the fields pointing at id_refs to their real values.
+ const unresolved = [];
+ this.patchupsToApply_.forEach(function(patchup) {
+ if (patchup.pidRef in this.processes) {
+ const snapshot = this.processes[patchup.pidRef].objects.getSnapshotAt(
+ patchup.scopedId, patchup.ts);
+ if (snapshot) {
+ patchup.object[patchup.field] = snapshot;
+ snapshot.referencedAt(patchup.item, patchup.object, patchup.field);
+ return;
+ }
+ }
+ unresolved.push(patchup);
+ }, this);
+ this.patchupsToApply_ = unresolved;
+ },
+
+ replacePIDRefsInPatchups(oldPidRef, newPidRef) {
+ this.patchupsToApply_.forEach(function(patchup) {
+ if (patchup.pidRef === oldPidRef) {
+ patchup.pidRef = newPidRef;
+ }
+ });
+ },
+
+ /**
+ * Called by the model to join references between objects, after final model
+ * bounds have been computed.
+ */
+ joinRefs() {
+ this.joinObjectRefs_();
+ this.applyObjectRefPatchups();
+ },
+
+ joinObjectRefs_() {
+ for (const [pid, process] of Object.entries(this.processes)) {
+ this.joinObjectRefsForProcess_(pid, process);
+ }
+ },
+
+ joinObjectRefsForProcess_(pid, process) {
+ // Iterate the world, looking for id_refs
+ for (const thread of Object.values(process.threads)) {
+ thread.asyncSliceGroup.slices.forEach(function(item) {
+ this.searchItemForIDRefs_(pid, 'start', item);
+ }, this);
+ thread.sliceGroup.slices.forEach(function(item) {
+ this.searchItemForIDRefs_(pid, 'start', item);
+ }, this);
+ }
+ process.objects.iterObjectInstances(function(instance) {
+ instance.snapshots.forEach(function(item) {
+ this.searchItemForIDRefs_(pid, 'ts', item);
+ }, this);
+ }, this);
+ },
+
+ searchItemForIDRefs_(pid, itemTimestampField, item) {
+ if (!item.args && !item.contexts) return;
+ const patchupsToApply = this.patchupsToApply_;
+
+ function handleField(object, fieldName, fieldValue) {
+ if (!fieldValue || (!fieldValue.id_ref && !fieldValue.idRef)) {
+ return;
+ }
+
+ const scope = fieldValue.scope || tr.model.OBJECT_DEFAULT_SCOPE;
+ const idRef = fieldValue.id_ref || fieldValue.idRef;
+ const scopedId = new tr.model.ScopedId(scope, idRef);
+ const pidRef = fieldValue.pid_ref || fieldValue.pidRef || pid;
+ const ts = item[itemTimestampField];
+ // We have to delay the actual change to the new value until after all
+ // refs have been located. Otherwise, we could end up recursing in
+ // ways we definitely didn't intend.
+ patchupsToApply.push({
+ item,
+ object,
+ field: fieldName,
+ pidRef,
+ scopedId,
+ ts});
+ }
+ function iterObjectFieldsRecursively(object) {
+ if (!(object instanceof Object)) return;
+
+ if ((object instanceof tr.model.ObjectSnapshot) ||
+ (object instanceof Float32Array) ||
+ (object instanceof tr.b.math.Quad)) {
+ return;
+ }
+
+ if (object instanceof Array) {
+ for (let i = 0; i < object.length; i++) {
+ handleField(object, i, object[i]);
+ iterObjectFieldsRecursively(object[i]);
+ }
+ return;
+ }
+
+ for (const key in object) {
+ const value = object[key];
+ handleField(object, key, value);
+ iterObjectFieldsRecursively(value);
+ }
+ }
+
+ iterObjectFieldsRecursively(item.args);
+ iterObjectFieldsRecursively(item.contexts);
+ }
+ };
+
+ return {
+ Model,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_indices.html b/chromium/third_party/catapult/tracing/tracing/model/model_indices.html
new file mode 100644
index 00000000000..27f89dba0b0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_indices.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Event Index class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A Event Index maps an id to all the events that have that particular id
+ *
+ * @constructor
+ */
+ function ModelIndices(model) {
+ // For now the only indices we construct are for flowEvents
+ this.flowEventsById_ = {};
+ model.flowEvents.forEach(function(fe) {
+ if (fe.id !== undefined) {
+ if (!this.flowEventsById_.hasOwnProperty(fe.id)) {
+ this.flowEventsById_[fe.id] = [];
+ }
+ this.flowEventsById_[fe.id].push(fe);
+ }
+ }, this);
+ }
+
+ ModelIndices.prototype = {
+ addEventWithId(id, event) {
+ if (!this.flowEventsById_.hasOwnProperty(id)) {
+ this.flowEventsById_[id] = [];
+ }
+ this.flowEventsById_[id].push(event);
+ },
+
+ getFlowEventsWithId(id) {
+ if (!this.flowEventsById_.hasOwnProperty(id)) {
+ return [];
+ }
+ return this.flowEventsById_[id];
+ }
+ };
+
+ return {
+ ModelIndices,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html
new file mode 100644
index 00000000000..bc5d99a0a29
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+ const newModel = tr.c.TestUtils.newModel;
+
+ test('getCorrectModelIndices', function() {
+ const m = newModel(function(m) {
+ m.f1 = newFlowEventEx({
+ title: 'test1',
+ start: 0,
+ end: 10,
+ id: '0x100'
+ });
+
+ m.f2 = newFlowEventEx({
+ title: 'test2',
+ start: 0,
+ end: 10,
+ id: '0x100'
+ });
+
+ m.flowEvents.push(m.f1);
+ m.flowEvents.push(m.f2);
+ });
+
+ assert.isDefined(m.modelIndices);
+ const modelIndices = m.modelIndices;
+ assert.strictEqual(modelIndices.getFlowEventsWithId('0x100').length, 2);
+ assert.strictEqual(
+ modelIndices.getFlowEventsWithId('0x100')[0].id, '0x100');
+ assert.strictEqual(modelIndices.getFlowEventsWithId('0x101').length, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_settings.html b/chromium/third_party/catapult/tracing/tracing/model/model_settings.html
new file mode 100644
index 00000000000..c34040183d2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_settings.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/settings.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const Settings = tr.b.Settings;
+
+ /**
+ * A way to persist settings specific to parts of a trace model.
+ *
+ * This object should not be persisted because it builds up internal data
+ * structures that map model objects to settings keys. It should thus be
+ * created for the duration of whatever interaction(s) you're going to do with
+ * model settings, and then discarded.
+ *
+ * This system works on a notion of an object key: for an object's key, it
+ * considers all the other keys in the model. If it is unique, then the key is
+ * persisted to tr.b.Settings. However, if it is not unique, then the
+ * setting is stored on the object itself. Thus, objects with unique keys will
+ * be persisted across page reloads, whereas objects with nonunique keys will
+ * not.
+ */
+ function ModelSettings(model) {
+ this.model = model;
+ this.objectsByKey_ = [];
+ this.nonuniqueKeys_ = [];
+ this.buildObjectsByKeyMap_();
+ this.removeNonuniqueKeysFromSettings_();
+ this.ephemeralSettingsByGUID_ = {};
+ }
+
+ ModelSettings.prototype = {
+ buildObjectsByKeyMap_() {
+ const objects = [];
+ this.model.iterateAllPersistableObjects(function(o) {
+ objects.push(o);
+ });
+
+ const objectsByKey = {};
+ const NONUNIQUE_KEY = 'nonuniqueKey';
+ for (let i = 0; i < objects.length; i++) {
+ const object = objects[i];
+ const objectKey = object.getSettingsKey();
+ if (!objectKey) continue;
+ if (objectsByKey[objectKey] === undefined) {
+ objectsByKey[objectKey] = object;
+ continue;
+ }
+ objectsByKey[objectKey] = NONUNIQUE_KEY;
+ }
+
+ const nonuniqueKeys = {};
+ Object.keys(objectsByKey).forEach(function(objectKey) {
+ if (objectsByKey[objectKey] !== NONUNIQUE_KEY) {
+ return;
+ }
+ delete objectsByKey[objectKey];
+ nonuniqueKeys[objectKey] = true;
+ });
+
+ this.nonuniqueKeys = nonuniqueKeys;
+ this.objectsByKey_ = objectsByKey;
+ },
+
+ removeNonuniqueKeysFromSettings_() {
+ const settings = Settings.get('trace_model_settings', {});
+ let settingsChanged = false;
+ Object.keys(settings).forEach(function(objectKey) {
+ if (!this.nonuniqueKeys[objectKey]) {
+ return;
+ }
+ settingsChanged = true;
+ delete settings[objectKey];
+ }, this);
+ if (settingsChanged) {
+ Settings.set('trace_model_settings', settings);
+ }
+ },
+
+ hasUniqueSettingKey(object) {
+ const objectKey = object.getSettingsKey();
+ if (!objectKey) return false;
+ return this.objectsByKey_[objectKey] !== undefined;
+ },
+
+ getSettingFor(object, objectLevelKey, defaultValue) {
+ const objectKey = object.getSettingsKey();
+ if (!objectKey || !this.objectsByKey_[objectKey]) {
+ const settings = this.getEphemeralSettingsFor_(object);
+ const ephemeralValue = settings[objectLevelKey];
+ if (ephemeralValue !== undefined) {
+ return ephemeralValue;
+ }
+ return defaultValue;
+ }
+
+ const settings = Settings.get('trace_model_settings', {});
+ if (!settings[objectKey]) {
+ settings[objectKey] = {};
+ }
+ const value = settings[objectKey][objectLevelKey];
+ if (value !== undefined) {
+ return value;
+ }
+ return defaultValue;
+ },
+
+ setSettingFor(object, objectLevelKey, value) {
+ const objectKey = object.getSettingsKey();
+ if (!objectKey || !this.objectsByKey_[objectKey]) {
+ this.getEphemeralSettingsFor_(object)[objectLevelKey] = value;
+ return;
+ }
+
+ const settings = Settings.get('trace_model_settings', {});
+ if (!settings[objectKey]) {
+ settings[objectKey] = {};
+ }
+ if (settings[objectKey][objectLevelKey] === value) {
+ return;
+ }
+ settings[objectKey][objectLevelKey] = value;
+ Settings.set('trace_model_settings', settings);
+ },
+
+ getEphemeralSettingsFor_(object) {
+ if (object.guid === undefined) {
+ throw new Error('Only objects with GUIDs can be persisted');
+ }
+ if (this.ephemeralSettingsByGUID_[object.guid] === undefined) {
+ this.ephemeralSettingsByGUID_[object.guid] = {};
+ }
+ return this.ephemeralSettingsByGUID_[object.guid];
+ }
+ };
+
+ return {
+ ModelSettings,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html
new file mode 100644
index 00000000000..cec8898180c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/model_settings.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('process_name_uniqueness_0', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const settings = new tr.model.ModelSettings(model);
+ assert.isFalse(settings.hasUniqueSettingKey(p1));
+ });
+
+ test('process_name_uniqueness_1', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ p1.name = 'Browser';
+ const settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.hasUniqueSettingKey(p1));
+ });
+
+ test('process_name_uniqueness_2', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const p2 = model.getOrCreateProcess(2);
+ p1.name = 'Renderer';
+ p2.name = 'Renderer';
+ const settings = new tr.model.ModelSettings(model);
+ assert.isFalse(settings.hasUniqueSettingKey(p1));
+ assert.isFalse(settings.hasUniqueSettingKey(p2));
+ });
+
+ test('process_name_uniqueness_3', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const p2 = model.getOrCreateProcess(2);
+ p1.name = 'Renderer';
+ p1.labels.push('Google Search');
+ p2.name = 'Renderer';
+ const settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.hasUniqueSettingKey(p1));
+ assert.isTrue(settings.hasUniqueSettingKey(p2));
+ });
+
+ test('thread_name_uniqueness_0', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const p2 = model.getOrCreateProcess(2);
+ const t1 = p1.getOrCreateThread(1);
+ const t2 = p2.getOrCreateThread(2);
+ p1.name = 'Browser';
+ p2.name = 'Renderer';
+ t1.name = 'Main';
+ t2.name = 'Main';
+ const settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.hasUniqueSettingKey(t1));
+ assert.isTrue(settings.hasUniqueSettingKey(t2));
+ });
+
+ test('thread_name_uniqueness_1', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const p2 = model.getOrCreateProcess(2);
+ const t1 = p1.getOrCreateThread(1);
+ const t2 = p2.getOrCreateThread(2);
+ p1.name = 'Renderer';
+ p2.name = 'Renderer';
+ t1.name = 'Main';
+ t2.name = 'Main';
+ const settings = new tr.model.ModelSettings(model);
+ assert.isFalse(settings.hasUniqueSettingKey(t1));
+ assert.isFalse(settings.hasUniqueSettingKey(t2));
+ });
+
+ test('process_persistence_when_not_unique', function() {
+ let model = new tr.Model();
+ let p1 = model.getOrCreateProcess(1);
+ let settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true));
+
+ settings.setSettingFor(p1, 'true_by_default', false);
+ assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true));
+
+ // Now, clobber the model, and verify that it didn't persist.
+ model = new tr.Model();
+ p1 = model.getOrCreateProcess(1);
+ settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true));
+ });
+
+ test('process_persistence_when_not_unique_with_name', function() {
+ let model = new tr.Model();
+ let p1 = model.getOrCreateProcess(1);
+ p1.name = 'Browser';
+ let settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true));
+
+ settings.setSettingFor(p1, 'true_by_default', false);
+ assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true));
+
+ // Now, clobber the model, and verify that it persisted.
+ model = new tr.Model();
+ p1 = model.getOrCreateProcess(1);
+ p1.name = 'Browser';
+ settings = new tr.model.ModelSettings(model);
+ assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true));
+ });
+
+ test('thread_persistence_when_not_unique', function() {
+ let model = new tr.Model();
+ let p1 = model.getOrCreateProcess(1);
+ let p2 = model.getOrCreateProcess(2);
+ let t1 = p1.getOrCreateThread(1);
+ let t2 = p2.getOrCreateThread(2);
+ p1.name = 'Renderer';
+ p2.name = 'Renderer';
+ t1.name = 'Main';
+ t2.name = 'Main';
+ let settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true));
+
+ settings.setSettingFor(t1, 'true_by_default', false);
+ assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true));
+
+ // Now, clobber the model, and verify that it persisted.
+ model = new tr.Model();
+ p1 = model.getOrCreateProcess(1);
+ p2 = model.getOrCreateProcess(2);
+ t1 = p1.getOrCreateThread(1);
+ t2 = p2.getOrCreateThread(2);
+ p1.name = 'Renderer';
+ p2.name = 'Renderer';
+ t1.name = 'Main';
+ t2.name = 'Main';
+ settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true));
+ });
+
+ test('thread_persistence_when_unique', function() {
+ let model = new tr.Model();
+ let p1 = model.getOrCreateProcess(1);
+ let p2 = model.getOrCreateProcess(2);
+ let t1 = p1.getOrCreateThread(1);
+ let t2 = p2.getOrCreateThread(2);
+ p1.name = 'Browser';
+ p2.name = 'Renderer';
+ t1.name = 'Main';
+ t2.name = 'Main';
+ let settings = new tr.model.ModelSettings(model);
+ assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true));
+
+ settings.setSettingFor(t1, 'true_by_default', false);
+ assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true));
+
+ // Now, clobber the model, and verify that it persisted.
+ model = new tr.Model();
+ p1 = model.getOrCreateProcess(1);
+ p2 = model.getOrCreateProcess(2);
+ t1 = p1.getOrCreateThread(1);
+ t2 = p2.getOrCreateThread(2);
+ p1.name = 'Browser';
+ p2.name = 'Renderer';
+ t1.name = 'Main';
+ t2.name = 'Main';
+ settings = new tr.model.ModelSettings(model);
+ assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true));
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_stats.html b/chromium/third_party/catapult/tracing/tracing/model/model_stats.html
new file mode 100644
index 00000000000..41f7dd64602
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_stats.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * @constructor
+ */
+ function ModelStats() {
+ this.traceEventCountsByKey_ = new Map();
+ this.allTraceEventStats_ = [];
+
+ this.traceEventStatsInTimeIntervals_ = new Map();
+ this.allTraceEventStatsInTimeIntervals_ = [];
+
+ this.hasEventSizesinBytes_ = false;
+
+ this.traceImportDurationMs_ = undefined;
+ }
+
+ ModelStats.prototype = {
+ TIME_INTERVAL_SIZE_IN_MS: 100,
+
+ willProcessBasicTraceEvent(phase, category, title, ts,
+ opt_eventSizeinBytes) {
+ const key = phase + '/' + category + '/' + title;
+ let eventStats = this.traceEventCountsByKey_.get(key);
+ if (eventStats === undefined) {
+ eventStats = {
+ phase,
+ category,
+ title,
+ numEvents: 0,
+ totalEventSizeinBytes: 0
+ };
+ this.traceEventCountsByKey_.set(key, eventStats);
+ this.allTraceEventStats_.push(eventStats);
+ }
+ eventStats.numEvents++;
+
+ const timeIntervalKey = Math.floor(
+ tr.b.Unit.timestampFromUs(ts) / this.TIME_INTERVAL_SIZE_IN_MS);
+ let eventStatsByTimeInverval =
+ this.traceEventStatsInTimeIntervals_.get(timeIntervalKey);
+ if (eventStatsByTimeInverval === undefined) {
+ eventStatsByTimeInverval = {
+ timeInterval: timeIntervalKey,
+ numEvents: 0,
+ totalEventSizeinBytes: 0
+ };
+ this.traceEventStatsInTimeIntervals_.set(timeIntervalKey,
+ eventStatsByTimeInverval);
+ this.allTraceEventStatsInTimeIntervals_.push(eventStatsByTimeInverval);
+ }
+ eventStatsByTimeInverval.numEvents++;
+
+ if (opt_eventSizeinBytes !== undefined) {
+ this.hasEventSizesinBytes_ = true;
+ eventStats.totalEventSizeinBytes += opt_eventSizeinBytes;
+ eventStatsByTimeInverval.totalEventSizeinBytes += opt_eventSizeinBytes;
+ }
+ },
+
+ get allTraceEventStats() {
+ return this.allTraceEventStats_;
+ },
+
+ get allTraceEventStatsInTimeIntervals() {
+ return this.allTraceEventStatsInTimeIntervals_;
+ },
+
+ get hasEventSizesinBytes() {
+ return this.hasEventSizesinBytes_;
+ },
+
+ get traceImportDurationMs() {
+ return this.traceImportDurationMs_;
+ },
+
+ set traceImportDurationMs(traceImportDurationMs) {
+ this.traceImportDurationMs_ = traceImportDurationMs;
+ }
+ };
+
+ return {
+ ModelStats,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html
new file mode 100644
index 00000000000..a1108b1b57a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/model/model_stats.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ModelStats = tr.model.ModelStats;
+
+ test('getTraceEventStatsByCategory', function() {
+ const modelStats = new ModelStats();
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title3');
+
+ assert.strictEqual(modelStats.allTraceEventStats.length, 2);
+ assert.strictEqual(
+ modelStats.traceEventCountsByKey_.get('X/cat1/title1').numEvents,
+ 2);
+ assert.strictEqual(
+ modelStats.traceEventCountsByKey_.get('X/cat2/title3').numEvents,
+ 1);
+ });
+
+ test('getTraceEventStatsInTimeIntervals', function() {
+ const modelStats = new ModelStats();
+ const timeIntervalSizeInUs = modelStats.TIME_INTERVAL_SIZE_IN_MS * 1000;
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1', 1, 1);
+ modelStats.willProcessBasicTraceEvent(
+ 'X', 'cat1', 'title1', timeIntervalSizeInUs + 1, 2);
+ modelStats.willProcessBasicTraceEvent(
+ 'X', 'cat1', 'title1', 2 * timeIntervalSizeInUs + 1, 3);
+ modelStats.willProcessBasicTraceEvent(
+ 'X', 'cat2', 'title3', 2 * timeIntervalSizeInUs + 2, 4);
+
+ assert.strictEqual(modelStats.allTraceEventStatsInTimeIntervals.length, 3);
+ assert.strictEqual(
+ modelStats.traceEventStatsInTimeIntervals_.get(0).numEvents, 1);
+ assert.strictEqual(
+ modelStats.traceEventStatsInTimeIntervals_.get(1).numEvents, 1);
+ assert.strictEqual(
+ modelStats.traceEventStatsInTimeIntervals_.get(2).numEvents, 2);
+
+ assert.isTrue(modelStats.hasEventSizesinBytes);
+ assert.strictEqual(
+ modelStats.traceEventStatsInTimeIntervals_.get(0).totalEventSizeinBytes,
+ 1);
+ assert.strictEqual(
+ modelStats.traceEventStatsInTimeIntervals_.get(1).totalEventSizeinBytes,
+ 2);
+ assert.strictEqual(
+ modelStats.traceEventStatsInTimeIntervals_.get(2).totalEventSizeinBytes,
+ 7);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_test.html
new file mode 100644
index 00000000000..7613b421254
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/model_test.html
@@ -0,0 +1,343 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/annotation.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ThreadSlice = tr.model.ThreadSlice;
+ const TitleOrCategoryFilter = tr.c.TitleOrCategoryFilter;
+ const Frame = tr.model.Frame;
+
+ const createModelWithOneOfEverything = function() {
+ const m = new tr.Model();
+ const cpu = m.kernel.getOrCreateCpu(1);
+ cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3}));
+
+ const p = m.getOrCreateProcess(1);
+ const t = p.getOrCreateThread(1);
+ const slice = new ThreadSlice('', 'a', 0, 1, {}, 4);
+ t.sliceGroup.pushSlice(slice);
+ t.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSlice(0, 1, t, t));
+
+ const c = p.getOrCreateCounter('', 'ProcessCounter');
+ let aSeries = new tr.model.CounterSeries('a', 0);
+ let bSeries = new tr.model.CounterSeries('b', 0);
+ c.addSeries(aSeries);
+ c.addSeries(bSeries);
+
+ aSeries.addCounterSample(0, 5);
+ aSeries.addCounterSample(1, 6);
+ aSeries.addCounterSample(2, 5);
+ aSeries.addCounterSample(3, 7);
+
+ bSeries.addCounterSample(0, 10);
+ bSeries.addCounterSample(1, 15);
+ bSeries.addCounterSample(2, 12);
+ bSeries.addCounterSample(3, 16);
+
+ const c1 = cpu.getOrCreateCounter('', 'CpuCounter');
+ aSeries = new tr.model.CounterSeries('a', 0);
+ bSeries = new tr.model.CounterSeries('b', 0);
+ c1.addSeries(aSeries);
+ c1.addSeries(bSeries);
+
+ aSeries.addCounterSample(0, 5);
+ aSeries.addCounterSample(1, 6);
+ aSeries.addCounterSample(2, 5);
+ aSeries.addCounterSample(3, 7);
+
+ bSeries.addCounterSample(0, 10);
+ bSeries.addCounterSample(1, 15);
+ bSeries.addCounterSample(2, 12);
+ bSeries.addCounterSample(3, 16);
+
+ const frame1 = new Frame([slice], [{thread: t, start: 1, end: 5}]);
+ p.frames.push.apply(p.frames, frame1);
+
+ const gd = new tr.model.GlobalMemoryDump(m, 2);
+ const pd = new tr.model.ProcessMemoryDump(gd, p, 2);
+ gd.processMemoryDumps[1] = pd;
+ m.globalMemoryDumps.push(gd);
+ p.memoryDumps.push(pd);
+
+ m.updateBounds();
+
+ return m;
+ };
+
+ test('helper', function() {
+ function Helper(model) {
+ this.model = model;
+ }
+ Helper.guid = tr.b.GUID.allocateSimple();
+ Helper.supportsModel = function(model) {
+ return true;
+ };
+
+ const m = new tr.Model();
+ const h = m.getOrCreateHelper(Helper);
+ assert.isTrue(h instanceof Helper);
+ assert.isTrue(h === m.getOrCreateHelper(Helper));
+
+ function UnsupportedHelper(model) {
+ this.model = model;
+ }
+ UnsupportedHelper.guid = tr.b.GUID.allocateSimple();
+ UnsupportedHelper.supportsModel = function(model) {
+ return false;
+ };
+
+ assert.isUndefined(m.getOrCreateHelper(UnsupportedHelper));
+ // Try again to test doesHelperGUIDSupportThisModel_ .
+ assert.isUndefined(m.getOrCreateHelper(UnsupportedHelper));
+ });
+
+ test('modelBounds_EmptyModel', function() {
+ const m = new tr.Model();
+ m.updateBounds();
+ assert.isUndefined(m.bounds.min);
+ assert.isUndefined(m.bounds.max);
+ });
+
+ test('modelBounds_OneEmptyThread', function() {
+ const m = new tr.Model();
+ const t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ m.updateBounds();
+ assert.isUndefined(m.bounds.min);
+ assert.isUndefined(m.bounds.max);
+ });
+
+ test('modelBounds_OneThread', function() {
+ const m = new tr.Model();
+ const t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+ m.updateBounds();
+ assert.strictEqual(m.bounds.min, 1);
+ assert.strictEqual(m.bounds.max, 4);
+ });
+
+ test('modelBounds_OneThreadAndOneEmptyThread', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+ const t2 = m.getOrCreateProcess(1).getOrCreateThread(1);
+ m.updateBounds();
+ assert.strictEqual(m.bounds.min, 1);
+ assert.strictEqual(m.bounds.max, 4);
+ });
+
+ test('modelBounds_OneCpu', function() {
+ const m = new tr.Model();
+ const cpu = m.kernel.getOrCreateCpu(1);
+ cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3}));
+ m.updateBounds();
+ assert.strictEqual(m.bounds.min, 1);
+ assert.strictEqual(m.bounds.max, 4);
+ });
+
+ test('modelBounds_OneCpuOneThread', function() {
+ const m = new tr.Model();
+ const cpu = m.kernel.getOrCreateCpu(1);
+ cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3}));
+
+ const t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+
+ m.updateBounds();
+ assert.strictEqual(m.bounds.min, 1);
+ assert.strictEqual(m.bounds.max, 5);
+ });
+
+ test('modelBounds_GlobalMemoryDumps', function() {
+ const m = new tr.Model();
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 1));
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 3));
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 5));
+
+ m.updateBounds();
+ assert.strictEqual(m.bounds.min, 1);
+ assert.strictEqual(m.bounds.max, 5);
+ });
+
+ test('modelBounds_ProcessMemoryDumps', function() {
+ const m = new tr.Model();
+ const p = m.getOrCreateProcess(1);
+ const gd = new tr.model.GlobalMemoryDump(m, -1);
+ p.memoryDumps.push(new tr.model.ProcessMemoryDump(gd, m, 1));
+ p.memoryDumps.push(new tr.model.ProcessMemoryDump(gd, m, 3));
+ p.memoryDumps.push(new tr.model.ProcessMemoryDump(gd, m, 5));
+
+ m.updateBounds();
+ assert.strictEqual(m.bounds.min, 1);
+ assert.strictEqual(m.bounds.max, 5);
+ });
+
+
+ test('modelConvertsTimestampToModelTime', function() {
+ const m = new tr.Model();
+ const traceEvents = [
+ {ts: 1000, pid: 1, tid: 1, ph: 'B', cat: 'a', name: 'taskA', args: {}},
+ {ts: 2000, pid: 1, tid: 1, ph: 'E', cat: 'a', name: 'taskA', args: {}}
+ ];
+ const i = new tr.importer.Import(m);
+ i.importTraces([traceEvents]);
+ assert.strictEqual(
+ m.convertTimestampToModelTime('traceEventClock', 1000), 0);
+ assert.strictEqual(
+ m.convertTimestampToModelTime('traceEventClock', 2000), 1);
+ });
+
+ test('TitleOrCategoryFilter', function() {
+ const s0 = tr.c.TestUtils.newSliceEx({start: 1, duration: 3});
+ assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s0));
+ assert.isFalse(new TitleOrCategoryFilter('x').matchSlice(s0));
+
+ const s1 = tr.c.TestUtils.newSliceEx({title: 'ba', start: 1, duration: 3});
+ assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s1));
+ assert.isTrue(new TitleOrCategoryFilter('ba').matchSlice(s1));
+ assert.isFalse(new TitleOrCategoryFilter('x').matchSlice(s1));
+ });
+
+ test('model_findAllThreadsNamed', function() {
+ const m = new tr.Model();
+ const t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t.name = 'CrBrowserMain';
+
+ m.updateBounds();
+ let f = m.findAllThreadsNamed('CrBrowserMain');
+ assert.deepEqual([t], f);
+ f = m.findAllThreadsNamed('NoSuchThread');
+ assert.strictEqual(f.length, 0);
+ });
+
+ test('model_updateCategories', function() {
+ const m = new tr.Model();
+ const t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3));
+ t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3));
+ t.sliceGroup.pushSlice(new ThreadSlice('categoryB', 'a', 0, 1, {}, 3));
+ t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3));
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+ m.updateCategories_();
+ assert.deepEqual(['categoryA', 'categoryB'], m.categories);
+ });
+
+ test('getEventByStableId', function() {
+ const m = new tr.Model();
+ const p = m.getOrCreateProcess(0);
+ const t = p.getOrCreateThread(1);
+ const slice = tr.c.TestUtils.newSliceEx({start: 0, duration: 10});
+ t.sliceGroup.pushSlice(slice);
+ const ue = tr.c.TestUtils.newInteractionRecord(m, 0, 10);
+ m.userModel.expectations.push(ue);
+ const gie = tr.c.TestUtils.newInstantEvent({
+ title: 'gie',
+ start: 0,
+ colorId: 0
+ });
+ m.instantEvents.push(gie);
+
+ assert.strictEqual(slice, m.getEventByStableId(slice.stableId));
+ assert.strictEqual(ue, m.getEventByStableId(ue.stableId));
+ assert.strictEqual(gie, m.getEventByStableId(gie.stableId));
+ });
+
+ test('model_annotationAddRemove', function() {
+ const m = new tr.Model();
+ const a1 = new tr.model.Annotation();
+ const a2 = new tr.model.Annotation();
+
+ assert.strictEqual(m.getAllAnnotations().length, 0);
+ m.addAnnotation(a1);
+ assert.strictEqual(m.getAllAnnotations().length, 1);
+ m.addAnnotation(a2);
+ assert.strictEqual(m.getAllAnnotations().length, 2);
+
+ assert.strictEqual(m.getAnnotationByGUID(a1.guid), a1);
+ assert.strictEqual(m.getAnnotationByGUID(a2.guid), a2);
+
+ m.removeAnnotation(a1);
+ assert.isUndefined(m.getAnnotationByGUID(a1.guid));
+ assert.strictEqual(m.getAnnotationByGUID(a2.guid), a2);
+ assert.strictEqual(m.getAllAnnotations().length, 1);
+ });
+
+ test('model_intrinsicTimeUnit', function() {
+ const unit = tr.b.TimeDisplayModes;
+ const m = new tr.Model();
+
+ // by default it should be milliseconds
+ assert.strictEqual(m.intrinsicTimeUnit, unit.ms);
+
+ m.intrinsicTimeUnit = unit.ns;
+ assert.strictEqual(m.intrinsicTimeUnit, unit.ns);
+ // should be able to set to the same
+ m.intrinsicTimeUnit = unit.ns;
+ assert.strictEqual(m.intrinsicTimeUnit, unit.ns);
+ // should not be able to change it after fixing it
+ assert.throw(function() { m.intrinsicTimeUnit = unit.ms; });
+ assert.strictEqual(m.intrinsicTimeUnit, unit.ns);
+ });
+
+ test('model_getAllProcesses', function() {
+ const m = new tr.Model();
+ const p1 = m.getOrCreateProcess(1);
+ const p2 = m.getOrCreateProcess(2);
+ const p3 = m.getOrCreateProcess(3);
+ const p4 = m.getOrCreateProcess(4);
+ const p5 = m.getOrCreateProcess(5);
+
+ assert.sameMembers(m.getAllProcesses(), [p1, p2, p3, p4, p5]);
+ assert.sameMembers(m.getAllProcesses(p => true), [p1, p2, p3, p4, p5]);
+ assert.sameMembers(m.getAllProcesses(p => false), []);
+ assert.sameMembers(m.getAllProcesses(p => p.pid % 2 === 0), [p2, p4]);
+ });
+
+ test('model_joinRefs', function() {
+ function RefCountingSnapshot() {
+ tr.model.ObjectSnapshot.apply(this, arguments);
+ this.refCount = 0;
+ }
+
+ RefCountingSnapshot.prototype = {
+ __proto__: tr.model.ObjectSnapshot.prototype,
+
+ referencedAt() {
+ ++this.refCount;
+ }
+ };
+
+ const typeName = 'RefCountingSnapshot';
+ tr.model.ObjectSnapshot.subTypes.register(
+ RefCountingSnapshot,
+ {typeName});
+
+ const m = new tr.Model();
+ const p = m.getOrCreateProcess(1);
+ const s1 = p.objects.addSnapshot(new tr.model.ScopedId(typeName, '0x1'),
+ 'cat', typeName, 1000, {});
+ const s2 = p.objects.addSnapshot(new tr.model.ScopedId(typeName, '0x2'),
+ 'cat', typeName, 2000, {
+ myRef: {
+ scope: typeName,
+ id_ref: '0x1'
+ }
+ });
+ m.joinRefs();
+ assert.strictEqual(s1.refCount, 1);
+ assert.strictEqual(s2.refCount, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_collection.html b/chromium/third_party/catapult/tracing/tracing/model/object_collection.html
new file mode 100644
index 00000000000..5b4ed2325dc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/object_collection.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/model/time_to_object_instance_map.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ObjectCollection class.
+ */
+tr.exportTo('tr.model', function() {
+ const ObjectInstance = tr.model.ObjectInstance;
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * A collection of object instances and their snapshots, accessible by id and
+ * time, or by object name.
+ *
+ * @constructor
+ */
+ function ObjectCollection(parent) {
+ tr.model.EventContainer.call(this);
+ this.parent = parent;
+ // scope -> {id -> TimeToObjectInstanceMap}
+ this.instanceMapsByScopedId_ = {};
+ this.instancesByTypeName_ = {};
+ this.createObjectInstance_ = this.createObjectInstance_.bind(this);
+ }
+
+ ObjectCollection.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ * childEvents() {
+ for (const instance of this.getAllObjectInstances()) {
+ yield instance;
+ yield* instance.snapshots;
+ }
+ },
+
+ createObjectInstance_(
+ parent, scopedId, category, name, creationTs, opt_baseTypeName) {
+ const constructor = tr.model.ObjectInstance.subTypes.getConstructor(
+ category, name);
+ const instance = new constructor(
+ parent, scopedId, category, name, creationTs, opt_baseTypeName);
+ const typeName = instance.typeName;
+ let instancesOfTypeName = this.instancesByTypeName_[typeName];
+ if (!instancesOfTypeName) {
+ instancesOfTypeName = [];
+ this.instancesByTypeName_[typeName] = instancesOfTypeName;
+ }
+ instancesOfTypeName.push(instance);
+ return instance;
+ },
+
+ getOrCreateInstanceMap_(scopedId) {
+ let dict;
+ if (scopedId.scope in this.instanceMapsByScopedId_) {
+ dict = this.instanceMapsByScopedId_[scopedId.scope];
+ } else {
+ dict = {};
+ this.instanceMapsByScopedId_[scopedId.scope] = dict;
+ }
+ let instanceMap = dict[scopedId.id];
+ if (instanceMap) return instanceMap;
+ instanceMap = new tr.model.TimeToObjectInstanceMap(
+ this.createObjectInstance_, this.parent, scopedId);
+ dict[scopedId.id] = instanceMap;
+ return instanceMap;
+ },
+
+ idWasCreated(scopedId, category, name, ts) {
+ const instanceMap = this.getOrCreateInstanceMap_(scopedId);
+ return instanceMap.idWasCreated(category, name, ts);
+ },
+
+ addSnapshot(
+ scopedId, category, name, ts, args, opt_baseTypeName) {
+ const instanceMap = this.getOrCreateInstanceMap_(scopedId);
+ const snapshot = instanceMap.addSnapshot(
+ category, name, ts, args, opt_baseTypeName);
+ if (snapshot.objectInstance.category !== category) {
+ const msg = 'Added snapshot name=' + name + ' with cat=' + category +
+ ' impossible. It instance was created/snapshotted with cat=' +
+ snapshot.objectInstance.category + ' name=' +
+ snapshot.objectInstance.name;
+ throw new Error(msg);
+ }
+ if (opt_baseTypeName &&
+ snapshot.objectInstance.baseTypeName !== opt_baseTypeName) {
+ throw new Error('Could not add snapshot with baseTypeName=' +
+ opt_baseTypeName + '. It ' +
+ 'was previously created with name=' +
+ snapshot.objectInstance.baseTypeName);
+ }
+ if (snapshot.objectInstance.name !== name) {
+ throw new Error('Could not add snapshot with name=' + name + '. It ' +
+ 'was previously created with name=' +
+ snapshot.objectInstance.name);
+ }
+ return snapshot;
+ },
+
+ idWasDeleted(scopedId, category, name, ts) {
+ const instanceMap = this.getOrCreateInstanceMap_(scopedId);
+ const deletedInstance = instanceMap.idWasDeleted(category, name, ts);
+ if (!deletedInstance) return;
+
+ if (deletedInstance.category !== category) {
+ const msg = 'Deleting object ' + deletedInstance.name +
+ ' with a different category ' +
+ 'than when it was created. It previous had cat=' +
+ deletedInstance.category + ' but the delete command ' +
+ 'had cat=' + category;
+ throw new Error(msg);
+ }
+ if (deletedInstance.baseTypeName !== name) {
+ throw new Error('Deletion requested for name=' +
+ name + ' could not proceed: ' +
+ 'An existing object with baseTypeName=' +
+ deletedInstance.baseTypeName + ' existed.');
+ }
+ },
+
+ autoDeleteObjects(maxTimestamp) {
+ for (const imapById of Object.values(this.instanceMapsByScopedId_)) {
+ for (const i2imap of Object.values(imapById)) {
+ const lastInstance = i2imap.lastInstance;
+ if (lastInstance.deletionTs !== Number.MAX_VALUE) continue;
+ i2imap.idWasDeleted(
+ lastInstance.category, lastInstance.name, maxTimestamp);
+ // idWasDeleted will cause lastInstance.deletionTsWasExplicit to be
+ // set to true. Unset it here.
+ lastInstance.deletionTsWasExplicit = false;
+ }
+ }
+ },
+
+ getObjectInstanceAt(scopedId, ts) {
+ let instanceMap;
+ if (scopedId.scope in this.instanceMapsByScopedId_) {
+ instanceMap = this.instanceMapsByScopedId_[scopedId.scope][scopedId.id];
+ }
+ if (!instanceMap) return undefined;
+ return instanceMap.getInstanceAt(ts);
+ },
+
+ getSnapshotAt(scopedId, ts) {
+ const instance = this.getObjectInstanceAt(scopedId, ts);
+ if (!instance) return undefined;
+ return instance.getSnapshotAt(ts);
+ },
+
+ iterObjectInstances(iter, opt_this) {
+ opt_this = opt_this || this;
+ for (const imapById of Object.values(this.instanceMapsByScopedId_)) {
+ for (const i2imap of Object.values(imapById)) {
+ i2imap.instances.forEach(iter, opt_this);
+ }
+ }
+ },
+
+ getAllObjectInstances() {
+ const instances = [];
+ this.iterObjectInstances(function(i) { instances.push(i); });
+ return instances;
+ },
+
+ getAllInstancesNamed(name) {
+ return this.instancesByTypeName_[name];
+ },
+
+ getAllInstancesByTypeName() {
+ return this.instancesByTypeName_;
+ },
+
+ preInitializeAllObjects() {
+ this.iterObjectInstances(function(instance) {
+ instance.preInitialize();
+ });
+ },
+
+ initializeAllObjects() {
+ this.iterObjectInstances(function(instance) {
+ instance.initialize();
+ });
+ },
+
+ initializeInstances() {
+ this.iterObjectInstances(function(instance) {
+ instance.initialize();
+ });
+ },
+
+ updateBounds() {
+ this.bounds.reset();
+ this.iterObjectInstances(function(instance) {
+ instance.updateBounds();
+ this.bounds.addRange(instance.bounds);
+ }, this);
+ },
+
+ shiftTimestampsForward(amount) {
+ this.iterObjectInstances(function(instance) {
+ instance.shiftTimestampsForward(amount);
+ });
+ },
+
+ addCategoriesToDict(categoriesDict) {
+ this.iterObjectInstances(function(instance) {
+ categoriesDict[instance.category] = true;
+ });
+ }
+ };
+
+ return {
+ ObjectCollection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html b/chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html
new file mode 100644
index 00000000000..a04c23ff656
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/object_collection.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestObjectInstance = function(
+ parent, scopedId, category, name, creationTs) {
+ tr.model.ObjectInstance.call(
+ this, parent, scopedId, category, name, creationTs);
+ };
+
+ TestObjectInstance.prototype = {
+ __proto__: tr.model.ObjectInstance.prototype
+ };
+
+ test('objectInstanceSubtype', function() {
+ // Register that TestObjects are bound to TestObjectInstance.
+ tr.model.ObjectInstance.subTypes.register(
+ TestObjectInstance,
+ {typeName: 'TestObject'});
+
+ try {
+ const collection = new tr.model.ObjectCollection({ });
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasCreated(
+ scopedId, 'tr.e.cc', 'Frame', 10);
+ collection.idWasDeleted(
+ scopedId, 'tr.e.cc', 'Frame', 15);
+ collection.idWasCreated(
+ scopedId, 'skia', 'TestObject', 20);
+ collection.idWasDeleted(
+ scopedId, 'skia', 'TestObject', 25);
+
+ const testFrame = collection.getObjectInstanceAt(scopedId, 10);
+ assert.instanceOf(testFrame, tr.model.ObjectInstance);
+ assert.notInstanceOf(testFrame, TestObjectInstance);
+
+ const testObject = collection.getObjectInstanceAt(scopedId, 20);
+ assert.instanceOf(testObject, tr.model.ObjectInstance);
+ assert.instanceOf(testObject, TestObjectInstance);
+ } finally {
+ tr.model.ObjectInstance.subTypes.unregister(TestObjectInstance);
+ }
+ });
+
+ test('twoSnapshots', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasCreated(
+ scopedId, 'cat', 'Frame', 10);
+ collection.addSnapshot(
+ scopedId, 'cat', 'Frame', 10, {foo: 1});
+ collection.addSnapshot(
+ scopedId, 'cat', 'Frame', 20, {foo: 2});
+
+ collection.updateBounds();
+ assert.strictEqual(collection.bounds.min, 10);
+ assert.strictEqual(collection.bounds.max, 20);
+
+ const s0 = collection.getSnapshotAt(scopedId, 1);
+ assert.isUndefined(s0);
+
+ const s1 = collection.getSnapshotAt(scopedId, 10);
+ assert.strictEqual(s1.args.foo, 1);
+
+ const s2 = collection.getSnapshotAt(scopedId, 15);
+ assert.strictEqual(s2.args.foo, 1);
+ assert.strictEqual(s1, s2);
+
+ const s3 = collection.getSnapshotAt(scopedId, 20);
+ assert.strictEqual(s3.args.foo, 2);
+ assert.strictEqual(s1.object, s3.object);
+
+ const s4 = collection.getSnapshotAt(scopedId, 25);
+ assert.strictEqual(s4, s3);
+ });
+
+ test('twoObjectsSharingOneID', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasCreated(
+ scopedId, 'tr.e.cc', 'Frame', 10);
+ collection.idWasDeleted(
+ scopedId, 'tr.e.cc', 'Frame', 15);
+ collection.idWasCreated(
+ scopedId, 'skia', 'Picture', 20);
+ collection.idWasDeleted(
+ scopedId, 'skia', 'Picture', 25);
+
+ const frame = collection.getObjectInstanceAt(scopedId, 10);
+ assert.strictEqual(frame.category, 'tr.e.cc');
+ assert.strictEqual(frame.name, 'Frame');
+
+ const picture = collection.getObjectInstanceAt(scopedId, 20);
+ assert.strictEqual(picture.category, 'skia');
+ assert.strictEqual(picture.name, 'Picture');
+
+ const typeNames = Object.keys(collection.getAllInstancesByTypeName());
+ typeNames.sort();
+ assert.deepEqual(
+ ['Frame', 'Picture'],
+ typeNames);
+ assert.deepEqual(
+ [frame],
+ collection.getAllInstancesByTypeName().Frame);
+ assert.deepEqual(
+ [picture],
+ collection.getAllInstancesByTypeName().Picture);
+ });
+
+ test('createSnapDelete', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasCreated(
+ scopedId, 'cat', 'Frame', 10);
+ collection.addSnapshot(
+ scopedId, 'cat', 'Frame', 10, {foo: 1});
+ collection.idWasDeleted(
+ scopedId, 'cat', 'Frame', 15);
+
+ collection.updateBounds();
+ assert.strictEqual(collection.bounds.min, 10);
+ assert.strictEqual(collection.bounds.max, 15);
+
+ const s10 = collection.getSnapshotAt(scopedId, 10);
+ const i10 = s10.objectInstance;
+ assert.strictEqual(i10.creationTs, 10);
+ assert.strictEqual(i10.deletionTs, 15);
+ });
+
+ test('boundsOnUndeletedObject', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasCreated(
+ scopedId, 'cat', 'Frame', 10);
+ collection.addSnapshot(
+ scopedId, 'cat', 'Frame', 15, {foo: 1});
+
+ collection.updateBounds();
+ assert.strictEqual(10, collection.bounds.min);
+ assert.strictEqual(15, collection.bounds.max);
+ });
+
+ test('snapshotWithCustomBaseTypeThenDelete', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ const s10 = collection.addSnapshot(
+ scopedId, 'cat', 'cc::PictureLayerImpl', 10, {}, 'cc::LayerImpl');
+ collection.idWasDeleted(
+ scopedId, 'cat', 'cc::LayerImpl', 15);
+ collection.updateBounds();
+ assert.strictEqual(10, collection.bounds.min);
+ assert.strictEqual(15, collection.bounds.max);
+ assert.strictEqual(s10.objectInstance.name, 'cc::PictureLayerImpl');
+ assert.strictEqual(s10.objectInstance.baseTypeName, 'cc::LayerImpl');
+ });
+
+ test('newWithSnapshotThatChangesBaseType', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ const i10 = collection.idWasCreated(
+ scopedId, 'cat', 'cc::LayerImpl', 10);
+ const s15 = collection.addSnapshot(
+ scopedId, 'cat', 'cc::PictureLayerImpl', 15, {}, 'cc::LayerImpl');
+ collection.updateBounds();
+ assert.strictEqual(10, collection.bounds.min);
+ assert.strictEqual(15, collection.bounds.max);
+ assert.strictEqual(s15.objectInstance, i10);
+ assert.strictEqual(i10.name, 'cc::PictureLayerImpl');
+ assert.strictEqual(i10.baseTypeName, 'cc::LayerImpl');
+ });
+
+ test('deleteThenSnapshotWithCustomBase', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasDeleted(
+ scopedId, 'cat', 'cc::LayerImpl', 10);
+ const s15 = collection.addSnapshot(
+ scopedId, 'cat', 'cc::PictureLayerImpl', 15, {}, 'cc::LayerImpl');
+ collection.updateBounds();
+ assert.strictEqual(10, collection.bounds.min);
+ assert.strictEqual(15, collection.bounds.max);
+ assert.strictEqual(s15.objectInstance.name, 'cc::PictureLayerImpl');
+ });
+
+ test('autoDelete', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId = new tr.model.ScopedId('ptr', '0x1000');
+ collection.idWasCreated(
+ scopedId, 'cat', 'Frame', 10);
+ collection.addSnapshot(
+ scopedId, 'cat', 'Frame', 10, {foo: 1});
+ collection.autoDeleteObjects(15);
+
+ const s10 = collection.getSnapshotAt(scopedId, 10);
+ const i10 = s10.objectInstance;
+ assert.strictEqual(15, i10.deletionTs);
+ });
+
+ test('differentScopes', function() {
+ const collection = new tr.model.ObjectCollection({});
+ const scopedId1 = new tr.model.ScopedId('ptr', '0x1000');
+ const scopedId2 = new tr.model.ScopedId('cc', '0x1000');
+ collection.idWasCreated(
+ scopedId1, 'cat', 'ptr::object', 10);
+ collection.idWasDeleted(
+ scopedId1, 'cat', 'ptr::object', 15);
+ collection.idWasCreated(
+ scopedId2, 'cat', 'cc::object', 10);
+ collection.idWasDeleted(
+ scopedId2, 'cat', 'cc::object', 15);
+
+ let instance = collection.getObjectInstanceAt(scopedId1, 10);
+ assert.strictEqual(instance.name, 'ptr::object');
+
+ instance = collection.getObjectInstanceAt(scopedId2, 10);
+ assert.strictEqual(instance.name, 'cc::object');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_instance.html b/chromium/third_party/catapult/tracing/tracing/model/object_instance.html
new file mode 100644
index 00000000000..659ed22ad11
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/object_instance.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/object_snapshot.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ObjectSnapshot and ObjectHistory classes.
+ */
+tr.exportTo('tr.model', function() {
+ const ObjectSnapshot = tr.model.ObjectSnapshot;
+
+ /**
+ * An object with a specific id, whose state has been snapshotted several
+ * times.
+ *
+ * @constructor
+ */
+ function ObjectInstance(
+ parent, scopedId, category, name, creationTs, opt_baseTypeName) {
+ tr.model.Event.call(this);
+ this.parent = parent;
+ this.scopedId = scopedId;
+ this.category = category;
+ this.baseTypeName = opt_baseTypeName ? opt_baseTypeName : name;
+ this.name = name;
+ this.creationTs = creationTs;
+ this.creationTsWasExplicit = false;
+ this.deletionTs = Number.MAX_VALUE;
+ this.deletionTsWasExplicit = false;
+ this.colorId = 0;
+ this.bounds = new tr.b.math.Range();
+ this.snapshots = [];
+ this.hasImplicitSnapshots = false;
+ }
+
+ ObjectInstance.prototype = {
+ __proto__: tr.model.Event.prototype,
+
+ get typeName() {
+ return this.name;
+ },
+
+ addBoundsToRange(range) {
+ range.addRange(this.bounds);
+ },
+
+ addSnapshot(ts, args, opt_name, opt_baseTypeName) {
+ if (ts < this.creationTs) {
+ throw new Error('Snapshots must be >= instance.creationTs');
+ }
+ if (ts >= this.deletionTs) {
+ throw new Error('Snapshots cannot be added after ' +
+ 'an objects deletion timestamp.');
+ }
+
+ let lastSnapshot;
+ if (this.snapshots.length > 0) {
+ lastSnapshot = this.snapshots[this.snapshots.length - 1];
+ if (lastSnapshot.ts === ts) {
+ throw new Error('Snapshots already exists at this time!');
+ }
+ if (ts < lastSnapshot.ts) {
+ throw new Error(
+ 'Snapshots must be added in increasing timestamp order');
+ }
+ }
+
+ // Update baseTypeName if needed.
+ if (opt_name &&
+ (this.name !== opt_name)) {
+ if (!opt_baseTypeName) {
+ throw new Error('Must provide base type name for name update');
+ }
+ if (this.baseTypeName !== opt_baseTypeName) {
+ throw new Error('Cannot update type name: base types dont match');
+ }
+ this.name = opt_name;
+ }
+
+ const snapshotConstructor =
+ tr.model.ObjectSnapshot.subTypes.getConstructor(
+ this.category, this.name);
+ const snapshot = new snapshotConstructor(this, ts, args);
+ this.snapshots.push(snapshot);
+ return snapshot;
+ },
+
+ wasDeleted(ts) {
+ let lastSnapshot;
+ if (this.snapshots.length > 0) {
+ lastSnapshot = this.snapshots[this.snapshots.length - 1];
+ if (lastSnapshot.ts > ts) {
+ throw new Error(
+ 'Instance cannot be deleted at ts=' +
+ ts + '. A snapshot exists that is older.');
+ }
+ }
+ this.deletionTs = ts;
+ this.deletionTsWasExplicit = true;
+ },
+
+ /**
+ * See ObjectSnapshot constructor notes on object initialization.
+ */
+ preInitialize() {
+ for (let i = 0; i < this.snapshots.length; i++) {
+ this.snapshots[i].preInitialize();
+ }
+ },
+
+ /**
+ * See ObjectSnapshot constructor notes on object initialization.
+ */
+ initialize() {
+ for (let i = 0; i < this.snapshots.length; i++) {
+ this.snapshots[i].initialize();
+ }
+ },
+
+ isAliveAt(ts) {
+ if (ts < this.creationTs && this.creationTsWasExplicit) {
+ return false;
+ }
+ if (ts > this.deletionTs) {
+ return false;
+ }
+
+ return true;
+ },
+
+ getSnapshotAt(ts) {
+ if (ts < this.creationTs) {
+ if (this.creationTsWasExplicit) {
+ throw new Error('ts must be within lifetime of this instance');
+ }
+ return this.snapshots[0];
+ }
+ if (ts > this.deletionTs) {
+ throw new Error('ts must be within lifetime of this instance');
+ }
+
+ const snapshots = this.snapshots;
+ const i = tr.b.findIndexInSortedIntervals(
+ snapshots,
+ function(snapshot) { return snapshot.ts; },
+ function(snapshot, i) {
+ if (i === snapshots.length - 1) {
+ return snapshots[i].objectInstance.deletionTs;
+ }
+ return snapshots[i + 1].ts - snapshots[i].ts;
+ },
+ ts);
+ if (i < 0) {
+ // Note, this is a little bit sketchy: this lets early ts point at the
+ // first snapshot, even before it is taken. We do this because raster
+ // tasks usually post before their tile snapshots are dumped. This may
+ // be a good line of code to re-visit if we start seeing strange and
+ // confusing object references showing up in the traces.
+ return this.snapshots[0];
+ }
+ if (i >= this.snapshots.length) {
+ return this.snapshots[this.snapshots.length - 1];
+ }
+ return this.snapshots[i];
+ },
+
+ updateBounds() {
+ this.bounds.reset();
+ this.bounds.addValue(this.creationTs);
+ if (this.deletionTs !== Number.MAX_VALUE) {
+ this.bounds.addValue(this.deletionTs);
+ } else if (this.snapshots.length > 0) {
+ this.bounds.addValue(this.snapshots[this.snapshots.length - 1].ts);
+ }
+ },
+
+ shiftTimestampsForward(amount) {
+ this.creationTs += amount;
+ if (this.deletionTs !== Number.MAX_VALUE) {
+ this.deletionTs += amount;
+ }
+ this.snapshots.forEach(function(snapshot) {
+ snapshot.ts += amount;
+ });
+ },
+
+ get userFriendlyName() {
+ return this.typeName + ' object ' + this.scopedId;
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ ObjectInstance,
+ {
+ name: 'objectInstance',
+ pluralName: 'objectInstances'
+ });
+
+ return {
+ ObjectInstance,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html b/chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html
new file mode 100644
index 00000000000..703e98d37c0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('getSnapshotAtWithImplicitCreation', function() {
+ const instance = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'n', 10);
+ let s10 = instance.addSnapshot(10, 'a');
+ instance.addSnapshot(40, 'b');
+ instance.wasDeleted(60);
+
+ const s1 = instance.getSnapshotAt(1);
+ assert.strictEqual(s1, s10);
+
+ s10 = instance.getSnapshotAt(10);
+ assert.strictEqual(s10.args, 'a');
+ assert.strictEqual(instance.getSnapshotAt(15), s10);
+
+ const s40 = instance.getSnapshotAt(40);
+ assert.strictEqual(s40.args, 'b');
+ assert.strictEqual(instance.getSnapshotAt(50), s40);
+ assert.strictEqual(instance.getSnapshotAt(59.9), s40);
+ });
+
+ test('getSnapshotAtWithExplicitCreation', function() {
+ const instance = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'n', 10);
+ instance.creationTsWasExplicit = true;
+ instance.addSnapshot(10, 'a');
+ instance.wasDeleted(60);
+
+ assert.throws(function() {
+ instance.getSnapshotAt(1);
+ });
+
+ const s10 = instance.getSnapshotAt(10);
+ assert.strictEqual(s10.args, 'a');
+ assert.strictEqual(instance.getSnapshotAt(15), s10);
+ });
+
+ test('getSnapshotBeforeFirstSnapshot', function() {
+ const instance = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'n', 10);
+ const s15 = instance.addSnapshot(15, 'a');
+ instance.wasDeleted(40);
+
+ assert.strictEqual(instance.getSnapshotAt(10), s15);
+ });
+
+ test('getSnapshotAfterLastSnapshot', function() {
+ const instance = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'n', 10);
+ const s15 = instance.addSnapshot(15, 'a');
+ instance.wasDeleted(40);
+
+ assert.strictEqual(instance.getSnapshotAt(20), s15);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html
new file mode 100644
index 00000000000..09678f34a45
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/event.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * A snapshot of an object instance, at a given moment in time.
+ *
+ * Initialization of snapshots and instances is three phased:
+ *
+ * 1. Instances and snapshots are constructed. This happens during event
+ * importing. Little should be done here, because the object's data
+ * are still being used by the importer to reconstruct object references.
+ *
+ * 2. Instances and snapshtos are preinitialized. This happens after implicit
+ * objects have been found, but before any references have been found and
+ * switched to direct references. Thus, every snapshot stands on its own.
+ * This is a good time to do global field renaming and type conversion,
+ * e.g. recognizing domain-specific types and converting from C++ naming
+ * convention to JS.
+ *
+ * 3. Instances and snapshtos are initialized. At this point, {id_ref:
+ * '0x1000'} fields have been converted to snapshot references. This is a
+ * good time to generic initialization steps and argument verification.
+ *
+ * @constructor
+ */
+ function ObjectSnapshot(objectInstance, ts, args) {
+ tr.model.Event.call(this);
+ this.objectInstance = objectInstance;
+ this.ts = ts;
+ this.args = args;
+ }
+
+ ObjectSnapshot.prototype = {
+ __proto__: tr.model.Event.prototype,
+
+ /**
+ * See ObjectSnapshot constructor notes on object initialization.
+ */
+ preInitialize() {
+ },
+
+ /**
+ * See ObjectSnapshot constructor notes on object initialization.
+ */
+ initialize() {
+ },
+
+ /**
+ * Called when an object reference is resolved as this ObjectSnapshot.
+ * @param {Object} item The event (async slice, slice or object) containing
+ * the resolved reference.
+ * @param {Object} object The object directly containing the reference.
+ * @param {String} field The field name of the reference in |object|.
+ */
+ referencedAt(item, object, field) {
+ },
+
+ addBoundsToRange(range) {
+ range.addValue(this.ts);
+ },
+
+ get userFriendlyName() {
+ return 'Snapshot of ' + this.objectInstance.userFriendlyName + ' @ ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.ts);
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ ObjectSnapshot,
+ {
+ name: 'objectSnapshot',
+ pluralName: 'objectSnapshots'
+ });
+
+ return {
+ ObjectSnapshot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html
new file mode 100644
index 00000000000..c026851e5aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/model/object_snapshot.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('snapshotTypeRegistry', function() {
+ function MySnapshot() {
+ tr.model.ObjectSnapshot.apply(this, arguments);
+ this.myFoo = this.args.foo;
+ }
+
+ MySnapshot.prototype = {
+ __proto__: tr.model.ObjectSnapshot.prototype
+ };
+
+ const instance = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'MySnapshot', 10);
+ try {
+ tr.model.ObjectSnapshot.subTypes.register(
+ MySnapshot,
+ {typeName: 'MySnapshot'});
+ const snapshot = instance.addSnapshot(15, {foo: 'bar'});
+ assert.instanceOf(snapshot, MySnapshot);
+ assert.strictEqual(snapshot.myFoo, 'bar');
+ } finally {
+ tr.model.ObjectSnapshot.subTypes.unregister(MySnapshot);
+ }
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_sample.html b/chromium/third_party/catapult/tracing/tracing/model/power_sample.html
new file mode 100644
index 00000000000..b9816f76159
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/power_sample.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_registry.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const Event = tr.model.Event;
+ const EventRegistry = tr.model.EventRegistry;
+
+ /**
+ * A sample that contains a power measurement (in W).
+ *
+ * @constructor
+ * @extends {Event}
+ */
+ function PowerSample(series, start, powerInW) {
+ Event.call(this);
+
+ this.series_ = series;
+ this.start_ = parseFloat(start);
+ this.powerInW_ = parseFloat(powerInW);
+ }
+
+ PowerSample.prototype = {
+ __proto__: Event.prototype,
+
+ get series() {
+ return this.series_;
+ },
+
+ get start() {
+ return this.start_;
+ },
+
+ set start(value) {
+ this.start_ = value;
+ },
+
+ get powerInW() {
+ return this.powerInW_;
+ },
+
+ set powerInW(value) {
+ this.powerInW_ = value;
+ },
+
+ addBoundsToRange(range) {
+ range.addValue(this.start);
+ }
+ };
+
+ EventRegistry.register(
+ PowerSample,
+ {
+ name: 'powerSample',
+ pluralName: 'powerSamples'
+ });
+
+ return {
+ PowerSample,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html
new file mode 100644
index 00000000000..ebed90bd2b5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_sample.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const PowerSample = tr.model.PowerSample;
+
+ test('powerSample', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const sample1 = new PowerSample(series, 0.0, 1000.0);
+ const sample2 = new PowerSample(series, 1.0, 2000.0);
+
+ assert.strictEqual(sample1.series, series);
+ assert.strictEqual(sample1.start, 0.0);
+ assert.strictEqual(sample1.powerInW, 1000.0);
+
+ assert.strictEqual(sample2.series, series);
+ assert.strictEqual(sample2.start, 1.0);
+ assert.strictEqual(sample2.powerInW, 2000.0);
+ });
+
+ test('addBoundsToRange', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const sample1 = new PowerSample(series, 0.0, 1000.0);
+ const sample2 = new PowerSample(series, 1.0, 2000.0);
+
+ const range = new tr.b.math.Range();
+ sample1.addBoundsToRange(range);
+
+ assert.strictEqual(range.min, 0);
+ assert.strictEqual(range.max, 0);
+
+ sample2.addBoundsToRange(range);
+
+ assert.strictEqual(range.min, 0);
+ assert.strictEqual(range.max, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_series.html b/chromium/third_party/catapult/tracing/tracing/model/power_series.html
new file mode 100644
index 00000000000..c496b1e4810
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/power_series.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/power_sample.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const PowerSample = tr.model.PowerSample;
+
+ /**
+ * A container holding a time series of power samples.
+ *
+ * @constructor
+ * @extends {EventContainer}
+ */
+ function PowerSeries(device) {
+ tr.model.EventContainer.call(this);
+
+ this.device_ = device;
+ this.samples_ = [];
+ }
+
+ PowerSeries.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get device() {
+ return this.device_;
+ },
+
+ get samples() {
+ return this.samples_;
+ },
+
+ get stableId() {
+ return this.device_.stableId + '.PowerSeries';
+ },
+
+ /**
+ * Adds a power sample to the series and returns it.
+ *
+ * Note: Samples must be added in chronological order.
+ */
+ addPowerSample(ts, val) {
+ const sample = new PowerSample(this, ts, val);
+ this.samples_.push(sample);
+ return sample;
+ },
+
+ /**
+ * Returns the total energy (in Joules) consumed between the specified
+ * start and end timestamps (in milliseconds).
+ */
+ getEnergyConsumedInJ(start, end) {
+ const measurementRange = tr.b.math.Range.fromExplicitRange(start, end);
+
+ let energyConsumedInJ = 0;
+ let startIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, start) - 1;
+ const endIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, end);
+
+ if (startIndex < 0) {
+ startIndex = 0;
+ }
+
+ for (let i = startIndex; i < endIndex; i++) {
+ const sample = this.samples[i];
+ const nextSample = this.samples[i + 1];
+
+ const sampleRange = new tr.b.math.Range();
+ sampleRange.addValue(sample.start);
+ sampleRange.addValue(nextSample ? nextSample.start : sample.start);
+
+ const intersectionRangeInMs = measurementRange.findIntersection(
+ sampleRange);
+
+ const durationInS = tr.b.convertUnit(intersectionRangeInMs.duration,
+ tr.b.UnitPrefixScale.METRIC.MILLI,
+ tr.b.UnitPrefixScale.METRIC.NONE);
+
+ energyConsumedInJ += durationInS * sample.powerInW;
+ }
+
+ return energyConsumedInJ;
+ },
+
+ getSamplesWithinRange(start, end) {
+ const startIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, start);
+ const endIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, end);
+ return this.samples.slice(startIndex, endIndex);
+ },
+
+ shiftTimestampsForward(amount) {
+ for (let i = 0; i < this.samples_.length; ++i) {
+ this.samples_[i].start += amount;
+ }
+ },
+
+ updateBounds() {
+ this.bounds.reset();
+
+ if (this.samples_.length === 0) return;
+
+ this.bounds.addValue(this.samples_[0].start);
+ this.bounds.addValue(this.samples_[this.samples_.length - 1].start);
+ },
+
+ * childEvents() {
+ yield* this.samples_;
+ },
+ };
+
+ return {
+ PowerSeries,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_series_test.html b/chromium/third_party/catapult/tracing/tracing/model/power_series_test.html
new file mode 100644
index 00000000000..aa1e1c46d54
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/power_series_test.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/device.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ test('stableId', function() {
+ const device = { stableId: 'test' };
+ const series = new PowerSeries(device);
+
+ assert.strictEqual(series.stableId, 'test.PowerSeries');
+ });
+
+ test('device', function() {
+ const device = new tr.model.Device(new Model());
+ const series = new PowerSeries(device);
+
+ assert.strictEqual(series.device, device);
+ });
+
+ test('addPowerSample', function() {
+ const series = new PowerSeries(new Model().device);
+
+ assert.strictEqual(series.samples.length, 0);
+
+ const sample1 = series.addPowerSample(0, 1);
+ const sample2 = series.addPowerSample(1, 2);
+
+ assert.deepEqual(series.samples, [sample1, sample2]);
+ });
+
+ test('getEnergyConsumed_oneInterval', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+
+ assert.strictEqual(series.getEnergyConsumedInJ(0, 1000), 1);
+ });
+
+ test('getEnergyConsumed_twoIntervals', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 2);
+
+ assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 3);
+ });
+
+ test('getEnergyConsumed_oneSample', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(1000, 1);
+
+ assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 0);
+ });
+
+ test('getEnergyConsumed_samplesAfterStart', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(1000, 1);
+ series.addPowerSample(2000, 2);
+
+ assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 1);
+ });
+
+ test('getEnergyConsumed_extraSamplesBeforeStart', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 10);
+ series.addPowerSample(1000, 1);
+ series.addPowerSample(2000, 1);
+ series.addPowerSample(3000, 1);
+
+ assert.strictEqual(series.getEnergyConsumedInJ(2000, 4000), 1);
+ });
+
+ test('getEnergyConsumed_extraSamplesAfterEnd', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 1);
+ series.addPowerSample(2000, 1);
+ series.addPowerSample(3000, 10);
+
+ assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 2);
+ });
+
+ test('shiftTimestampsForward', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+
+ series.shiftTimestampsForward(2);
+
+ assert.strictEqual(series.samples[0].start, 2);
+ assert.strictEqual(series.samples[1].start, 3);
+
+ series.shiftTimestampsForward(-4);
+
+ assert.strictEqual(series.samples[0].start, -2);
+ assert.strictEqual(series.samples[1].start, -1);
+ });
+
+
+ test('updateBounds', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.updateBounds();
+
+ assert.strictEqual(series.bounds.min, 0);
+ assert.strictEqual(series.bounds.max, 1);
+
+ series.addPowerSample(4, 3);
+ series.updateBounds();
+
+ assert.strictEqual(series.bounds.min, 0);
+ assert.strictEqual(series.bounds.max, 4);
+ });
+
+ test('childEvents_empty', function() {
+ const series = new PowerSeries(new Model().device);
+ const eventsInSeries = [];
+ for (const event of series.childEvents()) {
+ eventsInSeries.push(event);
+ }
+ assert.deepEqual(eventsInSeries, []);
+ });
+
+ test('childEvents_nonempty', function() {
+ const series = new PowerSeries(new Model().device);
+ const sample1 = series.addPowerSample(0, 1);
+ const sample2 = series.addPowerSample(1, 2);
+ const eventsInSeries = [];
+ for (const event of series.childEvents()) {
+ eventsInSeries.push(event);
+ }
+ assert.deepEqual(eventsInSeries, [sample1, sample2]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/process.html b/chromium/third_party/catapult/tracing/tracing/model/process.html
new file mode 100644
index 00000000000..facaa3b1923
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/process.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/process_base.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Process class.
+ */
+tr.exportTo('tr.model', function() {
+ const ProcessBase = tr.model.ProcessBase;
+ const ProcessInstantEvent = tr.model.ProcessInstantEvent;
+ const Frame = tr.model.Frame;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+
+ /**
+ * The Process represents a single userland process in the
+ * trace.
+ * @constructor
+ */
+ function Process(model, pid) {
+ if (model === undefined) {
+ throw new Error('model must be provided');
+ }
+ if (pid === undefined) {
+ throw new Error('pid must be provided');
+ }
+ tr.model.ProcessBase.call(this, model);
+ this.pid = pid;
+ this.name = undefined;
+ this.labels = [];
+ this.uptime_seconds = 0;
+ this.instantEvents = [];
+ this.memoryDumps = [];
+ this.frames = [];
+ this.activities = [];
+ }
+
+ /**
+ * Comparison between processes that orders by pid.
+ */
+ Process.compare = function(x, y) {
+ let tmp = tr.model.ProcessBase.compare(x, y);
+ 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;
+
+ tmp = tr.b.compareArrays(x.labels, y.labels,
+ function(x, y) { return x.localeCompare(y); });
+ if (tmp) return tmp;
+
+ return x.pid - y.pid;
+ };
+
+ Process.prototype = {
+ __proto__: tr.model.ProcessBase.prototype,
+
+ get stableId() {
+ return this.pid;
+ },
+
+ compareTo(that) {
+ return Process.compare(this, that);
+ },
+
+ * childEvents() {
+ yield* ProcessBase.prototype.childEvents.call(this);
+ yield* this.instantEvents;
+ yield* this.frames;
+ yield* this.memoryDumps;
+ },
+
+ addLabelIfNeeded(labelName) {
+ for (let i = 0; i < this.labels.length; i++) {
+ if (this.labels[i] === labelName) return;
+ }
+ this.labels.push(labelName);
+ },
+
+ get userFriendlyName() {
+ let res;
+ if (this.name) {
+ res = this.name + ' (pid ' + this.pid + ')';
+ } else {
+ res = 'Process ' + this.pid;
+ }
+ if (this.labels.length) {
+ res += ': ' + this.labels.join(', ');
+ }
+ if (this.uptime_seconds) {
+ res += ', uptime:' + this.uptime_seconds + 's';
+ }
+ return res;
+ },
+
+ get userFriendlyDetails() {
+ if (this.name) {
+ return this.name + ' (pid ' + this.pid + ')';
+ }
+ return 'pid: ' + this.pid;
+ },
+
+ getSettingsKey() {
+ if (!this.name) return undefined;
+ if (!this.labels.length) return 'processes.' + this.name;
+ return 'processes.' + this.name + '.' + this.labels.join('.');
+ },
+
+ shiftTimestampsForward(amount) {
+ for (let i = 0; i < this.instantEvents.length; i++) {
+ this.instantEvents[i].start += amount;
+ }
+
+ for (let i = 0; i < this.frames.length; i++) {
+ this.frames[i].shiftTimestampsForward(amount);
+ }
+
+ for (let i = 0; i < this.memoryDumps.length; i++) {
+ this.memoryDumps[i].shiftTimestampsForward(amount);
+ }
+
+ for (let i = 0; i < this.activities.length; i++) {
+ this.activities[i].shiftTimestampsForward(amount);
+ }
+
+ tr.model.ProcessBase.prototype
+ .shiftTimestampsForward.apply(this, arguments);
+ },
+
+ updateBounds() {
+ tr.model.ProcessBase.prototype.updateBounds.apply(this);
+
+ for (let i = 0; i < this.frames.length; i++) {
+ this.frames[i].addBoundsToRange(this.bounds);
+ }
+
+ for (let i = 0; i < this.memoryDumps.length; i++) {
+ this.memoryDumps[i].addBoundsToRange(this.bounds);
+ }
+
+ for (let i = 0; i < this.activities.length; i++) {
+ this.activities[i].addBoundsToRange(this.bounds);
+ }
+ },
+
+ sortMemoryDumps() {
+ this.memoryDumps.sort(function(x, y) {
+ return x.start - y.start;
+ });
+ tr.model.ProcessMemoryDump.hookUpMostRecentVmRegionsLinks(
+ this.memoryDumps);
+ }
+ };
+
+ return {
+ Process,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_base.html b/chromium/third_party/catapult/tracing/tracing/model/process_base.html
new file mode 100644
index 00000000000..8ba031a764c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/process_base.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/object_collection.html">
+<link rel="import" href="/tracing/model/thread.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ProcessBase class.
+ */
+tr.exportTo('tr.model', function() {
+ const Thread = tr.model.Thread;
+ const Counter = tr.model.Counter;
+
+ /**
+ * The ProcessBase is a partial base class, upon which Kernel
+ * and Process are built.
+ *
+ * @constructor
+ * @extends {tr.model.EventContainer}
+ */
+ function ProcessBase(model) {
+ if (!model) {
+ throw new Error('Must provide a model');
+ }
+ tr.model.EventContainer.call(this);
+ this.model = model;
+ this.threads = {};
+ this.counters = {};
+ this.objects = new tr.model.ObjectCollection(this);
+ this.sortIndex = 0;
+ }
+
+ ProcessBase.compare = function(x, y) {
+ return x.sortIndex - y.sortIndex;
+ };
+
+ ProcessBase.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get stableId() {
+ throw new Error('Not implemented');
+ },
+
+ * childEventContainers() {
+ yield* Object.values(this.threads);
+ yield* Object.values(this.counters);
+ yield this.objects;
+ },
+
+ iterateAllPersistableObjects(cb) {
+ cb(this);
+ for (const tid in this.threads) {
+ this.threads[tid].iterateAllPersistableObjects(cb);
+ }
+ },
+
+ /**
+ * Gets the number of threads in this process.
+ */
+ get numThreads() {
+ let n = 0;
+ for (const p in this.threads) {
+ n++;
+ }
+ return n;
+ },
+
+ /**
+ * Shifts all the timestamps inside this process forward by the amount
+ * specified.
+ */
+ shiftTimestampsForward(amount) {
+ for (const child of this.childEventContainers()) {
+ child.shiftTimestampsForward(amount);
+ }
+ },
+
+ /**
+ * Closes any open slices.
+ */
+ autoCloseOpenSlices() {
+ for (const tid in this.threads) {
+ const thread = this.threads[tid];
+ thread.autoCloseOpenSlices();
+ }
+ },
+
+ autoDeleteObjects(maxTimestamp) {
+ this.objects.autoDeleteObjects(maxTimestamp);
+ },
+
+ /**
+ * Called by the model after finalizing imports,
+ * but before joining refs.
+ */
+ preInitializeObjects() {
+ this.objects.preInitializeAllObjects();
+ },
+
+ /**
+ * Called by the model after joining refs.
+ */
+ initializeObjects() {
+ this.objects.initializeAllObjects();
+ },
+
+ /**
+ * Merge slices from the kernel with those from userland for each thread.
+ */
+ mergeKernelWithUserland() {
+ for (const tid in this.threads) {
+ const thread = this.threads[tid];
+ thread.mergeKernelWithUserland();
+ }
+ },
+
+ updateBounds() {
+ this.bounds.reset();
+ for (const tid in this.threads) {
+ this.threads[tid].updateBounds();
+ this.bounds.addRange(this.threads[tid].bounds);
+ }
+ for (const id in this.counters) {
+ this.counters[id].updateBounds();
+ this.bounds.addRange(this.counters[id].bounds);
+ }
+ this.objects.updateBounds();
+ this.bounds.addRange(this.objects.bounds);
+ },
+
+ addCategoriesToDict(categoriesDict) {
+ for (const tid in this.threads) {
+ this.threads[tid].addCategoriesToDict(categoriesDict);
+ }
+ for (const id in this.counters) {
+ categoriesDict[this.counters[id].category] = true;
+ }
+ this.objects.addCategoriesToDict(categoriesDict);
+ },
+
+ findAllThreadsMatching(predicate, opt_this) {
+ const threads = [];
+ for (const tid in this.threads) {
+ const thread = this.threads[tid];
+ if (predicate.call(opt_this, thread)) {
+ threads.push(thread);
+ }
+ }
+ return threads;
+ },
+
+ /**
+ * @param {String} The name of the thread to find.
+ * @return {Array} An array of all the matched threads.
+ */
+ findAllThreadsNamed(name) {
+ const threads = this.findAllThreadsMatching(function(thread) {
+ if (!thread.name) return false;
+ return thread.name === name;
+ });
+ return threads;
+ },
+
+ findAtMostOneThreadNamed(name) {
+ const threads = this.findAllThreadsNamed(name);
+ if (threads.length === 0) return undefined;
+ if (threads.length > 1) {
+ throw new Error('Expected no more than one ' + name);
+ }
+ return threads[0];
+ },
+
+ /**
+ * Removes threads from the process that are fully empty.
+ */
+ pruneEmptyContainers() {
+ const threadsToKeep = {};
+ for (const tid in this.threads) {
+ const thread = this.threads[tid];
+ if (!thread.isEmpty) {
+ threadsToKeep[tid] = thread;
+ }
+ }
+ this.threads = threadsToKeep;
+ },
+
+ /**
+ * @return {TimelineThread} The thread identified by tid on this process,
+ * or undefined if it doesn't exist.
+ */
+ getThread(tid) {
+ return this.threads[tid];
+ },
+
+ /**
+ * @return {TimelineThread} The thread identified by tid on this process,
+ * creating it if it doesn't exist.
+ */
+ getOrCreateThread(tid) {
+ if (!this.threads[tid]) {
+ this.threads[tid] = new Thread(this, tid);
+ }
+ return this.threads[tid];
+ },
+
+ /**
+ * @return {Counter} The counter on this process with the given
+ * category/name combination, creating it if it doesn't exist.
+ */
+ getOrCreateCounter(cat, name) {
+ const id = cat + '.' + name;
+ if (!this.counters[id]) {
+ this.counters[id] = new Counter(this, id, cat, name);
+ }
+ return this.counters[id];
+ },
+
+ getSettingsKey() {
+ throw new Error('Not implemented');
+ },
+
+ createSubSlices() {
+ for (const tid in this.threads) {
+ this.threads[tid].createSubSlices();
+ }
+ }
+ };
+
+ return {
+ ProcessBase,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html
new file mode 100644
index 00000000000..1c9248e6eaf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html
@@ -0,0 +1,247 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ProcessMemoryDump class.
+ */
+tr.exportTo('tr.model', function() {
+ // Names of MemoryAllocatorDump(s) from which tracing overhead should be
+ // discounted.
+ const DISCOUNTED_ALLOCATOR_NAMES = ['winheap', 'malloc'];
+
+ // The path to where the tracing overhead dump should be added to the
+ // winheap/malloc allocator dump tree.
+ 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;
+ }
+
+ /**
+ * The ProcessMemoryDump represents a memory dump of a single process.
+ * @constructor
+ */
+ function ProcessMemoryDump(globalMemoryDump, process, start) {
+ tr.model.ContainerMemoryDump.call(this, start);
+ this.process = process;
+ this.globalMemoryDump = globalMemoryDump;
+
+ // Process memory totals (optional object) with the following fields (also
+ // optional):
+ // - residentBytes: Total resident bytes (number)
+ // - peakResidentBytes: Peak resident bytes (number)
+ // - arePeakResidentBytesResettable: Flag whether peak resident bytes are
+ // resettable (boolean)
+ // - privateFootprintBytes: Private footprint bytes (number)
+ // - platformSpecific: Map from OS-specific total names (string) to sizes
+ // (number)
+ this.totals = undefined;
+
+ this.vmRegions = undefined;
+
+ // Map from allocator names to heap dumps.
+ this.heapDumps = undefined;
+
+ this.tracingOverheadOwnershipSetUp_ = false;
+ this.tracingOverheadDiscountedFromVmRegions_ = false;
+ }
+
+ ProcessMemoryDump.prototype = {
+ __proto__: tr.model.ContainerMemoryDump.prototype,
+
+ 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) {
+ // Make sure that calling this method twice won't lead to
+ // 'double-discounting'.
+ if (this.tracingOverheadOwnershipSetUp_) return;
+
+ this.tracingOverheadOwnershipSetUp_ = true;
+
+ const tracingDump = this.getMemoryAllocatorDumpByFullName('tracing');
+ if (tracingDump === undefined || tracingDump.owns !== undefined) {
+ // The tracing dump either doesn't exist, or it already owns another
+ // dump.
+ return;
+ }
+
+ if (tracingDump.owns !== undefined) return;
+
+ // Add an ownership link from tracing to
+ // malloc/allocated_objects/tracing_overhead or
+ // winheap/allocated_objects/tracing_overhead.
+ const hasDiscountedFromAllocatorDumps = DISCOUNTED_ALLOCATOR_NAMES.some(
+ function(allocatorName) {
+ // First check if the allocator root exists.
+ const allocatorDump = this.getMemoryAllocatorDumpByFullName(
+ allocatorName);
+ if (allocatorDump === undefined) {
+ return false; // Allocator doesn't exist, try another one.
+ }
+
+ let nextPathIndex = 0;
+ let currentDump = allocatorDump;
+ let currentFullName = allocatorName;
+
+ // Descend from the root towards tracing_overhead as long as the
+ // dumps on the path exist.
+ for (; nextPathIndex < TRACING_OVERHEAD_PATH.length;
+ nextPathIndex++) {
+ const childFullName = currentFullName + '/' +
+ TRACING_OVERHEAD_PATH[nextPathIndex];
+ const childDump = this.getMemoryAllocatorDumpByFullName(
+ childFullName);
+ if (childDump === undefined) break;
+
+ currentDump = childDump;
+ currentFullName = childFullName;
+ }
+
+ // Create the missing descendant dumps on the path from the root
+ // towards tracing_overhead.
+ for (; nextPathIndex < TRACING_OVERHEAD_PATH.length;
+ nextPathIndex++) {
+ const childFullName = currentFullName + '/' +
+ TRACING_OVERHEAD_PATH[nextPathIndex];
+ const childDump = new tr.model.MemoryAllocatorDump(
+ currentDump.containerMemoryDump, childFullName);
+ childDump.parent = currentDump;
+ currentDump.children.push(childDump);
+
+ currentFullName = childFullName;
+ currentDump = childDump;
+ }
+
+ // Add the ownership link.
+ const ownershipLink =
+ new tr.model.MemoryAllocatorDumpLink(tracingDump, currentDump);
+ tracingDump.owns = ownershipLink;
+ currentDump.ownedBy.push(ownershipLink);
+ return true;
+ }, this);
+
+ // Force rebuilding the memory allocator dump index (if we've just added
+ // a new memory allocator dump).
+ if (hasDiscountedFromAllocatorDumps) {
+ this.forceRebuildingMemoryAllocatorDumpByFullNameIndex();
+ }
+ },
+
+ discountTracingOverheadFromVmRegions(opt_model) {
+ // Make sure that calling this method twice won't lead to
+ // 'double-discounting'.
+ if (this.tracingOverheadDiscountedFromVmRegions_) return;
+ this.tracingOverheadDiscountedFromVmRegions_ = true;
+
+ const tracingDump = this.getMemoryAllocatorDumpByFullName('tracing');
+ if (tracingDump === undefined) return;
+
+ const discountedSize =
+ getSizeNumericValue(tracingDump, SIZE_NUMERIC_NAME);
+ const discountedResidentSize =
+ getSizeNumericValue(tracingDump, RESIDENT_SIZE_NUMERIC_NAME);
+
+ if (discountedSize <= 0 && discountedResidentSize <= 0) return;
+
+ // Subtract the tracing size from the totals.
+ if (this.totals !== undefined) {
+ if (this.totals.residentBytes !== undefined) {
+ this.totals.residentBytes -= discountedResidentSize;
+ }
+ if (this.totals.peakResidentBytes !== undefined) {
+ this.totals.peakResidentBytes -= discountedResidentSize;
+ }
+ }
+
+ // Subtract the tracing size from VM regions. More precisely, subtract
+ // tracing resident_size from byte stats (private dirty and PSS) and
+ // tracing size from virtual size by injecting a fake VM region with
+ // negative values.
+ if (this.vmRegions !== undefined) {
+ const hasSizeInBytes = this.vmRegions.sizeInBytes !== undefined;
+ const hasPrivateDirtyResident =
+ this.vmRegions.byteStats.privateDirtyResident !== undefined;
+ const hasProportionalResident =
+ this.vmRegions.byteStats.proportionalResident !== undefined;
+
+ if ((hasSizeInBytes && discountedSize > 0) ||
+ ((hasPrivateDirtyResident || hasProportionalResident) &&
+ discountedResidentSize > 0)) {
+ const byteStats = {};
+ if (hasPrivateDirtyResident) {
+ byteStats.privateDirtyResident = -discountedResidentSize;
+ }
+ if (hasProportionalResident) {
+ byteStats.proportionalResident = -discountedResidentSize;
+ }
+ this.vmRegions.addRegion(tr.model.VMRegion.fromDict({
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: hasSizeInBytes ? -discountedSize : undefined,
+ byteStats
+ }));
+ }
+ }
+ }
+ };
+
+ ProcessMemoryDump.hookUpMostRecentVmRegionsLinks = function(processDumps) {
+ let mostRecentVmRegions = undefined;
+
+ processDumps.forEach(function(processDump) {
+ // Update the most recent VM regions from the current dump.
+ if (processDump.vmRegions !== undefined) {
+ mostRecentVmRegions = processDump.vmRegions;
+ }
+
+ // Set the most recent VM regions of the current dump.
+ processDump.mostRecentVmRegions = mostRecentVmRegions;
+ });
+ };
+
+ tr.model.EventRegistry.register(
+ ProcessMemoryDump,
+ {
+ name: 'processMemoryDump',
+ pluralName: 'processMemoryDumps'
+ });
+
+ return {
+ ProcessMemoryDump,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html
new file mode 100644
index 00000000000..3d507e879c9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html
@@ -0,0 +1,561 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const Scalar = tr.b.Scalar;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+ const checkDumpNumericsAndDiagnostics =
+ tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics;
+ const checkVMRegions = tr.model.MemoryDumpTestUtils.checkVMRegions;
+
+ function createClassificationNode(opt_sizeInBytes, opt_byteStats) {
+ const node = new VMRegionClassificationNode();
+ if (opt_sizeInBytes !== undefined || opt_byteStats !== undefined) {
+ node.addRegion(VMRegion.fromDict({
+ mappedFile: 'mock.so',
+ sizeInBytes: opt_sizeInBytes,
+ byteStats: opt_byteStats
+ }));
+ }
+ return node;
+ }
+
+ function createProcessMemoryDump(timestamp, model) {
+ const gmd = new GlobalMemoryDump(model, timestamp);
+ model.globalMemoryDumps.push(gmd);
+ const p = model.getOrCreateProcess(123);
+ const pmd = new ProcessMemoryDump(gmd, p, timestamp + 1);
+ gmd.processMemoryDumps[123] = pmd;
+ p.memoryDumps.push(pmd);
+ return pmd;
+ }
+
+ function createFinalizedProcessMemoryDump(timestamp, opt_createdCallback) {
+ return createFinalizedProcessMemoryDumps([timestamp], function(pmds) {
+ if (opt_createdCallback !== undefined) {
+ opt_createdCallback(pmds[0]);
+ }
+ })[0];
+ }
+
+ function createFinalizedProcessMemoryDumps(timestamps, createdCallback) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const pmds = timestamps.map(function(timestamp) {
+ return createProcessMemoryDump(timestamp, model);
+ });
+ createdCallback(pmds);
+ });
+ const pmds = model.getProcess(123).memoryDumps;
+ assert.lengthOf(pmds, timestamps.length);
+ return pmds;
+ }
+
+ test('processMemoryDumps', function() {
+ const pmd = createFinalizedProcessMemoryDump(42);
+ const pmds = pmd.processMemoryDumps;
+ assert.lengthOf(Object.keys(pmds), 1);
+ assert.strictEqual(pmds[123], pmd);
+ });
+
+ test('hookUpMostRecentVmRegionsLinks_emptyArray', function() {
+ const dumps = [];
+ ProcessMemoryDump.hookUpMostRecentVmRegionsLinks(dumps);
+ assert.lengthOf(dumps, 0);
+ });
+
+ test('hookUpMostRecentVmRegionsLinks_nonEmptyArray', function() {
+ const m = new tr.Model();
+
+ // A dump with no VM regions or allocator dumps.
+ const dump1 = createProcessMemoryDump(1, m);
+
+ // A dump with VM regions and malloc and Oilpan allocator dumps.
+ const dump2 = createProcessMemoryDump(2, m);
+ dump2.vmRegions = createClassificationNode();
+ dump2.memoryAllocatorDumps = [
+ newAllocatorDump(dump2, 'oilpan', {numerics: {
+ size: 1024,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7),
+ inner_size: 768
+ }}),
+ newAllocatorDump(dump2, 'v8', {numerics: {
+ size: 2048,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15),
+ inner_size: 1999
+ }})
+ ];
+
+ // A dump with malloc and V8 allocator dumps.
+ const dump3 = createProcessMemoryDump(3, m);
+ dump3.memoryAllocatorDumps = [
+ newAllocatorDump(dump3, 'malloc', {numerics: {
+ size: 1024,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7),
+ inner_size: 768
+ }}),
+ newAllocatorDump(dump3, 'v8', {numerics: {
+ size: 2048,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15),
+ inner_size: 1999
+ }})
+ ];
+
+ // A dump with VM regions.
+ const dump4 = createProcessMemoryDump(4, m);
+ dump4.vmRegions = createClassificationNode();
+
+ const dumps = [dump1, dump2, dump3, dump4];
+ ProcessMemoryDump.hookUpMostRecentVmRegionsLinks(dumps);
+
+ assert.lengthOf(dumps, 4);
+
+ assert.strictEqual(dumps[0], dump1);
+ assert.isUndefined(dump1.mostRecentVmRegions);
+
+ assert.strictEqual(dumps[1], dump2);
+ assert.strictEqual(dump2.mostRecentVmRegions, dump2.vmRegions);
+
+ assert.strictEqual(dumps[2], dump3);
+ assert.strictEqual(dump3.mostRecentVmRegions, dump2.vmRegions);
+
+ assert.strictEqual(dumps[3], dump4);
+ assert.strictEqual(dump4.mostRecentVmRegions, dump4.vmRegions);
+ });
+
+ test('checkDiscountTracingOverhead_undefinedFields', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.memoryAllocatorDumps = [
+ newAllocatorDump(pmd, 'v8', {numerics: {size: 2048}}),
+ newAllocatorDump(pmd, 'tracing', {numerics: {size: 1024}})
+ ];
+ });
+
+ assert.isUndefined(pmd.totals);
+ assert.isUndefined(pmd.vmRegions);
+
+ const v8Dump = pmd.getMemoryAllocatorDumpByFullName('v8');
+ checkDumpNumericsAndDiagnostics(v8Dump, {
+ size: 2048,
+ effective_size: 2048
+ }, {});
+
+ const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing');
+ checkDumpNumericsAndDiagnostics(tracingDump, {
+ size: 1024,
+ effective_size: 1024
+ }, {});
+ });
+
+ test('checkDiscountTracingOverhead_definedFields', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.totals = {residentBytes: 10240};
+ pmd.vmRegions = createClassificationNode(6000, {
+ privateDirtyResident: 4096,
+ proportionalResident: 5120,
+ swapped: 1536
+ });
+
+ const mallocDump = newAllocatorDump(pmd, 'malloc',
+ {numerics: {size: 3072}});
+ addChildDump(mallocDump, 'allocated_objects', {numerics: {size: 2560}});
+
+ const tracingDump = newAllocatorDump(
+ pmd, 'tracing', {numerics: {size: 1024, resident_size: 1000}});
+
+ pmd.memoryAllocatorDumps = [mallocDump, tracingDump];
+ });
+
+ assert.strictEqual(pmd.totals.residentBytes, 9240);
+ assert.isUndefined(pmd.totals.peakResidentBytes);
+
+ const vmRegions = pmd.vmRegions;
+ assert.strictEqual(vmRegions.sizeInBytes, 4976);
+ assert.deepEqual(vmRegions.byteStats, {
+ privateDirtyResident: 3096,
+ proportionalResident: 4120,
+ swapped: 1536
+ });
+
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ sizeInBytes: 6000,
+ byteStats: {
+ privateDirtyResident: 4096,
+ proportionalResident: 5120,
+ swapped: 1536
+ }
+ },
+ {
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: -1024,
+ byteStats: {
+ privateDirtyResident: -1000,
+ proportionalResident: -1000
+ }
+ }
+ ]);
+
+ const mallocDump = pmd.getMemoryAllocatorDumpByFullName('malloc');
+ checkDumpNumericsAndDiagnostics(mallocDump, {
+ size: 3072,
+ effective_size: 2048
+ }, {});
+ assert.lengthOf(
+ mallocDump.children, 2 /* 'allocated_objects' and '<unspecified>' */);
+
+ const allocatedObjectsDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'malloc/allocated_objects');
+ checkDumpNumericsAndDiagnostics(allocatedObjectsDump, {
+ size: 2560,
+ effective_size: 1536
+ }, {});
+ assert.lengthOf(
+ allocatedObjectsDump.children,
+ 2 /* 'tracing_overhead' and '<unspecified>' */);
+
+ const discountDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'malloc/allocated_objects/tracing_overhead');
+ assert.strictEqual(discountDump.parent, allocatedObjectsDump);
+ assert.include(allocatedObjectsDump.children, discountDump);
+ checkDumpNumericsAndDiagnostics(discountDump, {
+ size: 1024,
+ effective_size: 0
+ }, {});
+
+ const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing');
+ checkDumpNumericsAndDiagnostics(tracingDump, {
+ size: 1024,
+ effective_size: 1024,
+ resident_size: 1000
+ }, {});
+ assert.strictEqual(tracingDump.owns.target, discountDump);
+ });
+
+ test('checkDiscountTracingOverhead_winheap', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.memoryAllocatorDumps = [
+ newAllocatorDump(pmd, 'tracing', {numerics: {size: 2048}}),
+ newAllocatorDump(pmd, 'winheap', {numerics: {size: 5120}})
+ ];
+ });
+
+ assert.isUndefined(pmd.totals);
+ assert.isUndefined(pmd.vmRegions);
+
+ const winheapDump = pmd.getMemoryAllocatorDumpByFullName('winheap');
+ checkDumpNumericsAndDiagnostics(winheapDump, {
+ size: 5120,
+ effective_size: 3072
+ }, {});
+ assert.lengthOf(winheapDump.children,
+ 2 /* 'allocated_objects' and '<unspecified>' */);
+
+ const allocatedObjectsDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'winheap/allocated_objects');
+ checkDumpNumericsAndDiagnostics(allocatedObjectsDump, {
+ size: 2048,
+ effective_size: 0
+ }, {});
+ assert.lengthOf(
+ allocatedObjectsDump.children, 1 /* 'tracing_overhead' */);
+
+ const discountDump = pmd.getMemoryAllocatorDumpByFullName(
+ 'winheap/allocated_objects/tracing_overhead');
+ assert.strictEqual(discountDump.parent, allocatedObjectsDump);
+ assert.include(allocatedObjectsDump.children, discountDump);
+ checkDumpNumericsAndDiagnostics(discountDump, {
+ size: 2048,
+ effective_size: 0
+ }, {});
+
+ const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing');
+ checkDumpNumericsAndDiagnostics(tracingDump, {
+ size: 2048,
+ effective_size: 2048
+ }, {});
+ assert.strictEqual(tracingDump.owns.target, discountDump);
+ });
+
+ test('checkDiscountTracingOverhead_withMostRecentVmRegionsLinks', function() {
+ const pmds = createFinalizedProcessMemoryDumps([42, 90], function(pmds) {
+ pmds[0].totals = {residentBytes: 1000, peakResidentBytes: 2000};
+ pmds[0].vmRegions = createClassificationNode(6000, {
+ privateDirtyResident: 4096
+ });
+ pmds[0].memoryAllocatorDumps = [
+ newAllocatorDump(pmds[0], 'tracing',
+ {numerics: {size: 300, resident_size: 100}})
+ ];
+
+ pmds[1].totals = {peakResidentBytes: 3000};
+ pmds[1].memoryAllocatorDumps = [
+ newAllocatorDump(pmds[0], 'tracing', {numerics: {resident_size: 200}})
+ ];
+ });
+
+ // First PMD: Both total resident and private dirty resident size should be
+ // reduced by 100. Virtual size should be reduced by 300.
+ assert.strictEqual(pmds[0].totals.residentBytes, 900);
+ assert.strictEqual(pmds[0].totals.peakResidentBytes, 1900);
+ assert.strictEqual(pmds[0].vmRegions.sizeInBytes, 5700);
+ assert.deepEqual(pmds[0].vmRegions.byteStats, {
+ privateDirtyResident: 3996
+ });
+ checkVMRegions(pmds[0].vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ sizeInBytes: 6000,
+ byteStats: {
+ privateDirtyResident: 4096,
+ }
+ },
+ {
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: -300,
+ byteStats: {
+ privateDirtyResident: -100
+ }
+ }
+ ]);
+ assert.strictEqual(pmds[0].mostRecentVmRegions, pmds[0].vmRegions);
+
+ // Second PMD: Total resident size should be reduced by 200, whereas private
+ // dirty resident size should be reduced by 100 (because it comes from
+ // the VM regions in the first dump). Similarly, virtual size should be
+ // reduced by 300.
+ assert.isUndefined(pmds[1].totals.residentBytes);
+ assert.strictEqual(pmds[1].totals.peakResidentBytes, 2800);
+ assert.isUndefined(pmds[1].vmRegions);
+ assert.strictEqual(pmds[1].mostRecentVmRegions, pmds[0].vmRegions);
+ });
+
+ test('checkDiscountTracingOverhead_allDiscountedVmRegionFields', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.vmRegions = createClassificationNode(10000, {
+ privateDirtyResident: 4096,
+ proportionalResident: 8192,
+ swapped: 1536
+ });
+ pmd.memoryAllocatorDumps = [
+ newAllocatorDump(pmd, 'tracing',
+ {numerics: {size: 1000, resident_size: 1024}})
+ ];
+ });
+
+ const vmRegions = pmd.vmRegions;
+ assert.strictEqual(vmRegions.sizeInBytes, 9000);
+ assert.deepEqual(vmRegions.byteStats, {
+ privateDirtyResident: 3072,
+ proportionalResident: 7168,
+ swapped: 1536
+ });
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ sizeInBytes: 10000,
+ byteStats: {
+ privateDirtyResident: 4096,
+ proportionalResident: 8192,
+ swapped: 1536
+ }
+ },
+ {
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: -1000,
+ byteStats: {
+ privateDirtyResident: -1024,
+ proportionalResident: -1024
+ }
+ }
+ ]);
+ });
+
+ test('checkDiscountTracingOverhead_twoDiscountedVmRegionField', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.vmRegions = createClassificationNode(10000, {
+ privateDirtyResident: 4096,
+ swapped: 1536
+ });
+ pmd.memoryAllocatorDumps = [
+ newAllocatorDump(pmd, 'tracing',
+ {numerics: {size: 1000, resident_size: 1024}})
+ ];
+ });
+
+ const vmRegions = pmd.vmRegions;
+ assert.strictEqual(vmRegions.sizeInBytes, 9000);
+ assert.deepEqual(vmRegions.byteStats, {
+ privateDirtyResident: 3072,
+ swapped: 1536
+ });
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ sizeInBytes: 10000,
+ byteStats: {
+ privateDirtyResident: 4096,
+ swapped: 1536
+ }
+ },
+ {
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: -1000,
+ byteStats: {
+ privateDirtyResident: -1024
+ }
+ }
+ ]);
+ });
+
+ test('checkDiscountTracingOverhead_oneDiscountedVmRegionField', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.vmRegions = createClassificationNode(10000);
+ pmd.memoryAllocatorDumps = [
+ newAllocatorDump(pmd, 'tracing',
+ {numerics: {size: 1000, resident_size: 1024}})
+ ];
+ });
+
+ const vmRegions = pmd.vmRegions;
+ assert.strictEqual(vmRegions.sizeInBytes, 9000);
+ assert.deepEqual(vmRegions.byteStats, {});
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ sizeInBytes: 10000
+ },
+ {
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: -1000
+ }
+ ]);
+ });
+
+ test('checkDiscountTracingOverhead_noDiscountedVmRegionFields', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.vmRegions = createClassificationNode(undefined, {
+ swapped: 1536
+ });
+ pmd.memoryAllocatorDumps = [
+ newAllocatorDump(pmd, 'tracing',
+ {numerics: {size: 1000, resident_size: 1024}})
+ ];
+ });
+
+ const vmRegions = pmd.vmRegions;
+ assert.isUndefined(vmRegions.sizeInBytes);
+ assert.deepEqual(vmRegions.byteStats, {
+ swapped: 1536
+ });
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ byteStats: {
+ swapped: 1536
+ }
+ }
+ ]);
+ });
+
+ test('checkDiscountTracingOverhead_existingLink', function() {
+ const pmd = createFinalizedProcessMemoryDump(42, function(pmd) {
+ pmd.totals = {residentBytes: 10240};
+
+ pmd.vmRegions = createClassificationNode(6000, {
+ privateDirtyResident: 4096,
+ swapped: 1536,
+ proportionalResident: 5120
+ });
+
+ const mallocDump = newAllocatorDump(pmd, 'malloc',
+ {numerics: {size: 3072}});
+ const tracingDump = newAllocatorDump(pmd, 'tracing',
+ {numerics: {size: 1024, resident_size: 1000}});
+ const ownedDump = newAllocatorDump(pmd, 'owned');
+
+ // The code for discounting tracing overhead should *not* override an
+ // existing ownership.
+ addOwnershipLink(tracingDump, ownedDump);
+
+ pmd.memoryAllocatorDumps = [mallocDump, tracingDump, ownedDump];
+ });
+
+ assert.strictEqual(pmd.totals.residentBytes, 9240);
+ assert.isUndefined(pmd.totals.peakResidentBytes);
+
+ const vmRegions = pmd.vmRegions;
+ assert.strictEqual(vmRegions.sizeInBytes, 4976);
+ assert.deepEqual(vmRegions.byteStats, {
+ privateDirtyResident: 3096,
+ proportionalResident: 4120,
+ swapped: 1536
+ });
+ checkVMRegions(vmRegions, [
+ {
+ mappedFile: 'mock.so',
+ sizeInBytes: 6000,
+ byteStats: {
+ privateDirtyResident: 4096,
+ proportionalResident: 5120,
+ swapped: 1536
+ }
+ },
+ {
+ mappedFile: '[discounted tracing overhead]',
+ sizeInBytes: -1024,
+ byteStats: {
+ privateDirtyResident: -1000,
+ proportionalResident: -1000
+ }
+ }
+ ]);
+
+ const mallocDump = pmd.getMemoryAllocatorDumpByFullName('malloc');
+ checkDumpNumericsAndDiagnostics(mallocDump, {
+ size: 3072,
+ effective_size: 3072
+ }, {});
+ assert.lengthOf(mallocDump.children, 0);
+
+ const ownedDump = pmd.getMemoryAllocatorDumpByFullName('owned');
+ checkDumpNumericsAndDiagnostics(ownedDump, {
+ size: 1024,
+ effective_size: 0
+ }, {});
+ assert.lengthOf(ownedDump.children, 0);
+
+ const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing');
+ checkDumpNumericsAndDiagnostics(tracingDump, {
+ size: 1024,
+ effective_size: 1024,
+ resident_size: 1000
+ }, {});
+ assert.strictEqual(tracingDump.owns.target, ownedDump);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_test.html b/chromium/third_party/catapult/tracing/tracing/model/process_test.html
new file mode 100644
index 00000000000..9355971c099
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/process_test.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/frame.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/instant_event.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('getOrCreateCounter', function() {
+ const model = new tr.Model();
+ const process = new tr.model.Process(model, 7);
+ const ctrBar = process.getOrCreateCounter('foo', 'bar');
+ const ctrBar2 = process.getOrCreateCounter('foo', 'bar');
+ assert.strictEqual(ctrBar2, ctrBar);
+ });
+
+ test('shiftTimestampsForward', function() {
+ const model = new tr.Model();
+ const process = new tr.model.Process(model, 7);
+ const ctr = process.getOrCreateCounter('foo', 'bar');
+ const thread = process.getOrCreateThread(1);
+
+ const instantEvent = new tr.model.InstantEvent('cat', 'event1', 1, 100);
+ process.instantEvents.push(instantEvent);
+
+ const slice = new tr.model.ThreadSlice('', 'a', 0, 1, {}, 4);
+ const frame =
+ new tr.model.Frame([slice], [{thread, start: 100, end: 200}]);
+ process.frames.push(frame);
+
+ const memoryDump = new tr.model.GlobalMemoryDump(model, 100);
+ process.memoryDumps.push(memoryDump);
+
+ let shiftCount = 0;
+ thread.shiftTimestampsForward = function(ts) {
+ if (ts === 0.32) {
+ shiftCount++;
+ }
+ };
+ ctr.shiftTimestampsForward = function(ts) {
+ if (ts === 0.32) {
+ shiftCount++;
+ }
+ };
+
+ process.shiftTimestampsForward(0.32);
+ assert.strictEqual(shiftCount, 2);
+ assert.strictEqual(instantEvent.start, 100.32);
+ assert.strictEqual(frame.start, 100.32);
+ assert.strictEqual(frame.end, 200.32);
+ assert.strictEqual(memoryDump.start, 100.32);
+ });
+
+ test('compareOnPID', function() {
+ let model = new tr.Model();
+ const p1 = new tr.model.Process(model, 1);
+ p1.name = 'Renderer';
+
+ model = new tr.Model();
+ const p2 = new tr.model.Process(model, 2);
+ p2.name = 'Renderer';
+
+ assert.isBelow(p1.compareTo(p2), 0);
+ });
+
+ test('compareOnSortIndex', function() {
+ const model = new tr.Model();
+ const p1 = new tr.model.Process(model, 1);
+ p1.name = 'Renderer';
+ p1.sortIndex = 1;
+
+ const p2 = new tr.model.Process(model, 2);
+ p2.name = 'Renderer';
+
+ assert.isAbove(p1.compareTo(p2), 0);
+ });
+
+ test('compareOnName', function() {
+ const model = new tr.Model();
+ const p1 = new tr.model.Process(model, 1);
+ p1.name = 'Browser';
+
+ const p2 = new tr.model.Process(model, 2);
+ p2.name = 'Renderer';
+
+ assert.isBelow(p1.compareTo(p2), 0);
+ });
+
+ test('compareOnLabels', function() {
+ const model = new tr.Model();
+ const p1 = new tr.model.Process(model, 1);
+ p1.name = 'Renderer';
+ p1.labels = ['a'];
+
+ const p2 = new tr.model.Process(model, 2);
+ p2.name = 'Renderer';
+ p2.labels = ['b'];
+
+ assert.isBelow(p1.compareTo(p2), 0);
+ });
+
+ test('compareOnUptime', function() {
+ const model = new tr.Model();
+ const p1 = new tr.model.Process(model, 1);
+ p1.name = 'Renderer';
+ p1.uptime_seconds = 10;
+
+ const p2 = new tr.model.Process(model, 2);
+ p2.name = 'Renderer';
+ p2.uptime_seconds = 20;
+
+ assert.isBelow(p1.compareTo(p2), 0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/profile_node.html b/chromium/third_party/catapult/tracing/tracing/model/profile_node.html
new file mode 100644
index 00000000000..edb47b77852
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/profile_node.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ProfileNode class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A ProfileNode represents a node in the profile tree,
+ * it is essentially a frame in the stack when the sample gets recorded.
+ */
+ // TODO(lpy) Move V8 specific part out of ProfileNode.
+ function ProfileNode(id, title, parentNode) {
+ this.id_ = id;
+ this.title_ = title;
+ this.parentNode_ = parentNode;
+ this.colorId_ = -1;
+ // Cache the constructed call stack starting from this node to root.
+ this.userFriendlyStack_ = [];
+ }
+
+ ProfileNode.prototype = {
+ __proto__: Object.prototype,
+
+ get title() {
+ return this.title_;
+ },
+
+ get parentNode() {
+ return this.parentNode_;
+ },
+
+ set parentNode(value) {
+ this.parentNode_ = value;
+ },
+
+ get id() {
+ return this.id_;
+ },
+
+ get colorId() {
+ return this.colorId_;
+ },
+
+ set colorId(value) {
+ this.colorId_ = value;
+ },
+
+ get userFriendlyName() {
+ return this.title_;
+ },
+
+ get userFriendlyStack() {
+ if (this.userFriendlyStack_.length === 0) {
+ this.userFriendlyStack_ = [this.userFriendlyName];
+ if (this.parentNode_ !== undefined) {
+ this.userFriendlyStack_ =
+ this.userFriendlyStack_.concat(this.parentNode_.userFriendlyStack);
+ }
+ }
+ return this.userFriendlyStack_;
+ },
+
+ get sampleTitle() {
+ throw new Error('Not implemented.');
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ ProfileNode,
+ {
+ name: 'Node',
+ pluralName: 'Nodes'
+ }
+ );
+
+ return {
+ ProfileNode,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/profile_tree.html b/chromium/third_party/catapult/tracing/tracing/model/profile_tree.html
new file mode 100644
index 00000000000..57e73ed27b0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/profile_tree.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Sample class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A ProfileTree represents all call stack we collect in sampling
+ * in the form of a tree.
+ * By traversing from root to a leaf we get a call stack
+ * that belongs to some samples we collect.
+ */
+ function ProfileTree() {
+ this.startTime_ = undefined;
+ this.endTime_ = undefined;
+ this.tree_ = new Map();
+ this.pid_ = -1;
+ this.tid_ = -1;
+ }
+
+ ProfileTree.prototype = {
+ __proto__: Object.prototype,
+
+ get pid() {
+ return this.pid_;
+ },
+
+ set pid(value) {
+ this.pid_ = value;
+ },
+
+ get tid() {
+ return this.tid_;
+ },
+
+ set tid(value) {
+ this.tid_ = value;
+ },
+
+ get tree() {
+ return this.tree_;
+ },
+
+ get startTime() {
+ return this.startTime_;
+ },
+
+ set startTime(value) {
+ this.startTime_ = value;
+ this.endTime_ = value;
+ },
+
+ get endTime() {
+ return this.endTime_;
+ },
+
+ set endTime(value) {
+ this.endTime_ = value;
+ },
+
+ add(node) {
+ if (this.tree_.has(node.id)) {
+ throw new Error('Conflict id in the profile tree.');
+ }
+ this.tree_.set(node.id, node);
+ return node;
+ },
+
+ getNode(nodeId) {
+ return this.tree_.get(nodeId);
+ }
+ };
+
+ return {
+ ProfileTree,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html
new file mode 100644
index 00000000000..d61a944569b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const SelectableItem = tr.model.SelectableItem;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * A ProxySelectableItem is a selectable item which is not a model item itself
+ * but instead acts as a proxy for a model item.
+ *
+ * @constructor
+ * @extends {SelectableItem}
+ */
+ function ProxySelectableItem(modelItem) {
+ SelectableItem.call(this, modelItem);
+ }
+
+ ProxySelectableItem.prototype = {
+ __proto__: SelectableItem.prototype,
+
+ get selectionState() {
+ const modelItem = this.modelItem_;
+ if (modelItem === undefined) {
+ return SelectionState.NONE;
+ }
+ return modelItem.selectionState;
+ }
+ };
+
+ return {
+ ProxySelectableItem,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html
new file mode 100644
index 00000000000..c7dba2c88a8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProxySelectableItem = tr.model.ProxySelectableItem;
+ const SelectionState = tr.model.SelectionState;
+
+ test('checkSelectionState_undefinedModelItem', function() {
+ const proxyItem = new ProxySelectableItem(undefined);
+ assert.isFalse(proxyItem.selected);
+ assert.strictEqual(proxyItem.selectionState, SelectionState.NONE);
+ });
+
+ test('checkSelectionState_definedModelItem', function() {
+ // Start with a selected model event.
+ const modelItem = {selectionState: SelectionState.SELECTED};
+ const proxyItem = new ProxySelectableItem(modelItem);
+ assert.isTrue(proxyItem.selected);
+ assert.strictEqual(proxyItem.selectionState, SelectionState.SELECTED);
+
+ // Change the selection state of the model event to highlighted.
+ modelItem.selectionState = SelectionState.HIGHLIGHTED;
+ assert.isFalse(proxyItem.selected);
+ assert.strictEqual(proxyItem.selectionState, SelectionState.HIGHLIGHTED);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html b/chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html
new file mode 100644
index 00000000000..0ba5daf3ca5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/annotation.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/ui/annotations/rect_annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ function RectAnnotation(start, end) {
+ tr.model.Annotation.apply(this, arguments);
+
+ this.startLocation_ = start; // Location of top-left corner.
+ this.endLocation_ = end; // Location of bottom-right corner.
+ this.fillStyle = 'rgba(255, 180, 0, 0.3)';
+ }
+
+ RectAnnotation.fromDict = function(dict) {
+ const args = dict.args;
+ const startLoc =
+ new tr.model.Location(args.start.xWorld, args.start.yComponents);
+ const endLoc =
+ new tr.model.Location(args.end.xWorld, args.end.yComponents);
+ return new tr.model.RectAnnotation(startLoc, endLoc);
+ };
+
+ RectAnnotation.prototype = {
+ __proto__: tr.model.Annotation.prototype,
+
+ get startLocation() {
+ return this.startLocation_;
+ },
+
+ get endLocation() {
+ return this.endLocation_;
+ },
+
+ toDict() {
+ return {
+ typeName: 'rect',
+ args: {
+ start: this.startLocation.toDict(),
+ end: this.endLocation.toDict()
+ }
+ };
+ },
+
+ createView_(viewport) {
+ return new tr.ui.annotations.RectAnnotationView(viewport, this);
+ }
+ };
+
+ tr.model.Annotation.register(RectAnnotation, {typeName: 'rect'});
+
+ return {
+ RectAnnotation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html
new file mode 100644
index 00000000000..5ede6b97cff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_registry.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const Event = tr.model.Event;
+ const EventRegistry = tr.model.EventRegistry;
+
+ /**
+ * A sample containing data about what fraction of a resource
+ * (CPU or GPU) is being used at a given point in time.
+ */
+ class ResourceUsageSample extends Event {
+ /**
+ * @param {ResourceUsageSeries } series The ResourceUsageSeries that this
+ * sample will be a part of.
+ * @param {float} start Time of the sample.
+ * @param {float} usage Fraction of the resource (CPU or GPU) in use at the
+ * time of the sample.
+ */
+ constructor(series, start, usage) {
+ super();
+
+ this.series_ = series;
+ this.start_ = start;
+ this.usage_ = usage;
+ }
+
+ get series() {
+ return this.series_;
+ }
+
+ get start() {
+ return this.start_;
+ }
+
+ set start(value) {
+ this.start_ = value;
+ }
+
+ get usage() {
+ return this.usage_;
+ }
+
+ set usage(value) {
+ this.usage_ = value;
+ }
+
+ addBoundsToRange(range) {
+ range.addValue(this.start);
+ }
+ }
+
+ EventRegistry.register(
+ ResourceUsageSample,
+ {
+ name: 'resourceUsageSample',
+ pluralName: 'resourceUsageSamples'
+ });
+
+ return {
+ ResourceUsageSample,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html
new file mode 100644
index 00000000000..003ef6a327e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/resource_usage_sample.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ResourceUsageSample = tr.model.ResourceUsageSample;
+
+ test('usageSample', function() {
+ const series = new tr.model.ResourceUsageSeries(new tr.Model().device);
+
+ const sample1 = new ResourceUsageSample(series, 0.0, 0.11);
+ const sample2 = new ResourceUsageSample(series, 1.0, 0.22);
+
+ assert.strictEqual(sample1.series, series);
+ assert.strictEqual(sample1.start, 0.0);
+ assert.strictEqual(sample1.usage, 0.11);
+
+ assert.strictEqual(sample2.series, series);
+ assert.strictEqual(sample2.start, 1.0);
+ assert.strictEqual(sample2.usage, 0.22);
+ });
+
+ test('addBoundsToRange', function() {
+ const series = new tr.model.ResourceUsageSeries(new tr.Model().device);
+
+ const sample1 = new ResourceUsageSample(series, 0.0, 0.11);
+ const sample2 = new ResourceUsageSample(series, 1.0, 0.22);
+
+ const range = new tr.b.math.Range();
+ sample1.addBoundsToRange(range);
+
+ assert.strictEqual(range.min, 0);
+ assert.strictEqual(range.max, 0);
+
+ sample2.addBoundsToRange(range);
+
+ assert.strictEqual(range.min, 0);
+ assert.strictEqual(range.max, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html
new file mode 100644
index 00000000000..16f28c8d2a1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/resource_usage_sample.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const ResourceUsageSample = tr.model.ResourceUsageSample;
+
+ /**
+ * A container holding a time series of samples giving the
+ * fraction of usage of a resource (e.g. CPU or GPU) at each
+ * sample time.
+ */
+
+ class ResourceUsageSeries extends tr.model.EventContainer {
+ constructor(device) {
+ super();
+
+ this.device_ = device;
+ this.samples_ = [];
+ }
+
+ get device() {
+ return this.device_;
+ }
+
+ get samples() {
+ return this.samples_;
+ }
+
+ get stableId() {
+ return this.device_.stableId + '.ResourceUsageSeries';
+ }
+
+ /**
+ * Adds a usage sample to the series and returns it.
+ *
+ * Note: Samples must be added in chronological order.
+ */
+ addUsageSample(ts, val) {
+ const sample = new ResourceUsageSample(this, ts, val);
+ this.samples_.push(sample);
+ return sample;
+ }
+
+ /**
+ * Returns the total time consumed by a resource (e.g. CPU or GPU) between
+ * the specified start and end timestamps (in milliseconds).
+ */
+ computeResourceTimeConsumedInMs(start, end) {
+ const measurementRange = tr.b.math.Range.fromExplicitRange(start, end);
+
+ let resourceTimeInMs = 0;
+ let startIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, start) - 1;
+ const endIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, end);
+
+ if (startIndex < 0) startIndex = 0;
+
+ for (let i = startIndex; i < endIndex; i++) {
+ const sample = this.samples[i];
+ const nextSample = this.samples[i + 1];
+
+ const sampleRange = new tr.b.math.Range();
+ sampleRange.addValue(sample.start);
+ sampleRange.addValue(nextSample ? nextSample.start : sample.start);
+
+ const intersectionRangeInMs = measurementRange.findIntersection(
+ sampleRange);
+
+ resourceTimeInMs += intersectionRangeInMs.duration * sample.usage;
+ }
+
+ return resourceTimeInMs;
+ }
+
+ getSamplesWithinRange(start, end) {
+ const startIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, start);
+ const endIndex = tr.b.findLowIndexInSortedArray(
+ this.samples, x => x.start, end);
+ return this.samples.slice(startIndex, endIndex);
+ }
+
+ shiftTimestampsForward(amount) {
+ for (let i = 0; i < this.samples_.length; ++i) {
+ this.samples_[i].start += amount;
+ }
+ }
+
+ updateBounds() {
+ this.bounds.reset();
+
+ if (this.samples_.length === 0) return;
+
+ this.bounds.addValue(this.samples_[0].start);
+ this.bounds.addValue(this.samples_[this.samples_.length - 1].start);
+ }
+
+ * childEvents() {
+ yield* this.samples_;
+ }
+ }
+
+ return {
+ ResourceUsageSeries,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html
new file mode 100644
index 00000000000..80981e0ca26
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html
@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/model/device.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/resource_usage_series.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ResourceUsageSeries = tr.model.ResourceUsageSeries;
+
+ test('stableId', function() {
+ const device = { stableId: 'test' };
+ const series = new ResourceUsageSeries(device);
+
+ assert.strictEqual(series.stableId, 'test.ResourceUsageSeries');
+ });
+
+ test('device', function() {
+ const device = new tr.model.Device(new Model());
+ const series = new ResourceUsageSeries(device);
+
+ assert.strictEqual(series.device, device);
+ });
+
+ test('addUsageSample', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+
+ assert.strictEqual(series.samples.length, 0);
+
+ const sample1 = series.addUsageSample(0, 1);
+ const sample2 = series.addUsageSample(1, 2);
+
+ assert.deepEqual(series.samples, [sample1, sample2]);
+ });
+
+ test('getResourceTimeConsumed_oneInterval', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ series.addUsageSample(0, 1);
+ series.addUsageSample(1000, 2);
+
+ assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 1000), 1000);
+ });
+
+ test('getResourceTimeConsumed_twoIntervals', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ series.addUsageSample(0, 1);
+ series.addUsageSample(1000, 2);
+ series.addUsageSample(2000, 2);
+
+ assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 3000);
+ });
+
+ test('getResourceTimeConsumed_oneSample', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ series.addUsageSample(1000, 1);
+
+ assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 0);
+ });
+
+ test('getResourceTimeConsumed_samplesAfterStart', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ series.addUsageSample(1000, 1);
+ series.addUsageSample(2000, 2);
+
+ assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 1000);
+ });
+
+ test('getResourceTimeConsumed_extraSamplesBeforeStart', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ series.addUsageSample(0, 10);
+ series.addUsageSample(1000, 1);
+ series.addUsageSample(2000, 1);
+ series.addUsageSample(3000, 1);
+
+ assert.strictEqual(
+ series.computeResourceTimeConsumedInMs(2000, 4000), 1000);
+ });
+
+ test('getResourceTimeConsumed_extraSamplesAfterEnd', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ series.addUsageSample(0, 1);
+ series.addUsageSample(1000, 1);
+ series.addUsageSample(2000, 1);
+ series.addUsageSample(3000, 10);
+
+ assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 2000);
+ });
+
+ test('shiftTimestampsForward', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+
+ series.addUsageSample(0, 1);
+ series.addUsageSample(1, 2);
+
+ series.shiftTimestampsForward(2);
+
+ assert.strictEqual(series.samples[0].start, 2);
+ assert.strictEqual(series.samples[1].start, 3);
+
+ series.shiftTimestampsForward(-4);
+
+ assert.strictEqual(series.samples[0].start, -2);
+ assert.strictEqual(series.samples[1].start, -1);
+ });
+
+
+ test('updateBounds', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+
+ series.addUsageSample(0, 1);
+ series.addUsageSample(1, 2);
+ series.updateBounds();
+
+ assert.strictEqual(series.bounds.min, 0);
+ assert.strictEqual(series.bounds.max, 1);
+
+ series.addUsageSample(4, 3);
+ series.updateBounds();
+
+ assert.strictEqual(series.bounds.min, 0);
+ assert.strictEqual(series.bounds.max, 4);
+ });
+
+ test('childEvents_empty', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ const eventsInSeries = [];
+ for (const event of series.childEvents()) {
+ eventsInSeries.push(event);
+ }
+ assert.deepEqual(eventsInSeries, []);
+ });
+
+ test('childEvents_nonempty', function() {
+ const series = new ResourceUsageSeries(new Model().device);
+ const sample1 = series.addUsageSample(0, 1);
+ const sample2 = series.addUsageSample(1, 2);
+ const eventsInSeries = [];
+ for (const event of series.childEvents()) {
+ eventsInSeries.push(event);
+ }
+ assert.deepEqual(eventsInSeries, [sample1, sample2]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/sample.html b/chromium/third_party/catapult/tracing/tracing/model/sample.html
new file mode 100644
index 00000000000..59a34771d8b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/sample.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Sample class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A Sample represents a sample taken at an instant in time,
+ * plus its call stack and parameters associated with that sample.
+ *
+ * @constructor
+ */
+ function Sample(start, title, leafNode, thread, opt_cpu, opt_weight,
+ opt_args) {
+ tr.model.TimedEvent.call(this, start);
+
+ this.start_ = start;
+ this.title_ = title;
+ this.leafNode_ = leafNode;
+ this.thread_ = thread;
+ this.colorId_ = leafNode.colorId;
+
+ this.cpu_ = opt_cpu;
+ this.weight_ = opt_weight;
+ this.args = opt_args || {};
+ }
+
+ Sample.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ get title() {
+ return this.title_;
+ },
+
+ get colorId() {
+ return this.colorId_;
+ },
+
+ get thread() {
+ return this.thread_;
+ },
+
+ get leafNode() {
+ return this.leafNode_;
+ },
+
+ get userFriendlyName() {
+ return 'Sample at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.start);
+ },
+
+ get userFriendlyStack() {
+ return this.leafNode_.userFriendlyStack;
+ },
+
+ getNodesAsArray() {
+ const nodes = [];
+ let node = this.leafNode_;
+ while (node !== undefined) {
+ nodes.push(node);
+ node = node.parentNode;
+ }
+ return nodes;
+ },
+
+ get cpu() {
+ return this.cpu_;
+ },
+
+ get weight() {
+ return this.weight_;
+ },
+ };
+
+ tr.model.EventRegistry.register(
+ Sample,
+ {
+ name: 'Sample',
+ pluralName: 'Samples'
+ });
+
+ return {
+ Sample,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/sample_test.html
new file mode 100644
index 00000000000..f329f1096d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/sample_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Sample = tr.model.Sample;
+ const StackFrame = tr.model.StackFrame;
+ const Thread = tr.model.Thread;
+
+ test('sampleStackTrace', function() {
+ const thread = tr.c.TestUtils.newFakeThread();
+
+ const model = new tr.Model();
+ const node = tr.c.TestUtils.newProfileNodes(model, ['a', 'b', 'c']);
+
+ const s = new Sample(
+ 10, 'instructions_retired', node, thread, undefined, 10);
+ assert.deepEqual(s.userFriendlyStack, ['c', 'b', 'a']);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/scoped_id.html b/chromium/third_party/catapult/tracing/tracing/model/scoped_id.html
new file mode 100644
index 00000000000..b7b64a2e443
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/scoped_id.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/model/constants.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ function ScopedId(scope, id, pid) {
+ if (scope === undefined) {
+ throw new Error('Scope should be defined. Use \'' +
+ tr.model.OBJECT_DEFAULT_SCOPE +
+ '\' as the default scope.');
+ }
+ this.scope = scope;
+ this.id = id;
+ this.pid = pid;
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/selectable_item.html b/chromium/third_party/catapult/tracing/tracing/model/selectable_item.html
new file mode 100644
index 00000000000..3492dc00b9f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/selectable_item.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the SelectableItem class.
+ */
+tr.exportTo('tr.model', function() {
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * A SelectableItem is the abstract base class for any non-container data that
+ * has an associated model item in the trace model (possibly itself).
+ *
+ * Subclasses must provide a selectionState property (or getter).
+ *
+ * @constructor
+ */
+ function SelectableItem(modelItem) {
+ this.modelItem_ = modelItem;
+ }
+
+ SelectableItem.prototype = {
+ get modelItem() {
+ return this.modelItem_;
+ },
+
+ get selected() {
+ return this.selectionState === SelectionState.SELECTED;
+ },
+
+ addToSelection(selection) {
+ const modelItem = this.modelItem_;
+ if (!modelItem) return;
+ selection.push(modelItem);
+ },
+
+ addToTrackMap(eventToTrackMap, track) {
+ const modelItem = this.modelItem_;
+ if (!modelItem) return;
+ eventToTrackMap.addEvent(modelItem, track);
+ }
+ };
+
+ return {
+ SelectableItem,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html b/chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html
new file mode 100644
index 00000000000..120773852d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Event = tr.model.Event;
+ const EventToTrackMap = tr.ui.tracks.EventToTrackMap;
+ const SelectableItem = tr.model.SelectableItem;
+ const SelectionState = tr.model.SelectionState;
+
+ test('checkModelItem', function() {
+ const selectableItem1 = new SelectableItem(undefined);
+ assert.isUndefined(selectableItem1.modelItem);
+
+ const event = new Event();
+ const selectableItem2 = new SelectableItem(event);
+ assert.strictEqual(selectableItem2.modelItem, event);
+ });
+
+ test('checkSelected', function() {
+ const selectableItem = new SelectableItem(undefined);
+
+ selectableItem.selectionState = SelectionState.NONE;
+ assert.isFalse(selectableItem.selected);
+
+ selectableItem.selectionState = SelectionState.SELECTED;
+ assert.isTrue(selectableItem.selected);
+
+ selectableItem.selectionState = SelectionState.HIGHLIGHTED;
+ assert.isFalse(selectableItem.selected);
+ });
+
+ test('checkAddToSelection_undefinedModelItem', function() {
+ const selectableItem = new SelectableItem(undefined);
+ const selection = [];
+ selectableItem.addToSelection(selection);
+ assert.lengthOf(selection, 0);
+ });
+
+ test('checkAddToSelection_definedModelItem', function() {
+ const event = new Event();
+ const selectableItem = new SelectableItem(event);
+ const selection = [];
+ selectableItem.addToSelection(selection);
+ assert.lengthOf(selection, 1);
+ assert.strictEqual(selection[0], event);
+ });
+
+ test('checkAddToTrackMap_undefinedModelItem', function() {
+ const selectableItem = new SelectableItem(undefined);
+ const eventToTrackMap = new EventToTrackMap();
+ const track = {};
+ selectableItem.addToTrackMap(eventToTrackMap, track);
+ assert.lengthOf(Object.keys(eventToTrackMap), 0);
+ });
+
+ test('checkAddToTrackMap_definedModelItem', function() {
+ const event = new Event();
+ const selectableItem = new SelectableItem(event);
+ const eventToTrackMap = new EventToTrackMap();
+ const track = {};
+ selectableItem.addToTrackMap(eventToTrackMap, track);
+ assert.lengthOf(Object.keys(eventToTrackMap), 1);
+ assert.strictEqual(eventToTrackMap[event.guid], track);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/selection_state.html b/chromium/third_party/catapult/tracing/tracing/model/selection_state.html
new file mode 100644
index 00000000000..3e68d65a72b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/selection_state.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/color_scheme.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the SelectionState class.
+ */
+tr.exportTo('tr.model', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ /**
+ * Describes the level of visual highlighting to apply to an event when shown.
+ *
+ * color_scheme.html defines N variations off of a base color palette,
+ * one for each selection state, all concatenated into one flat array. To
+ * pick the final colorId for a given variations, the SelectionState is
+ * multiplied by the number of base colors.
+ *
+ * Thus, the values here must be kept in sync with color_scheme's palette
+ * layout.
+ */
+ const SelectionState = {
+ NONE: 0,
+
+ // Legacy names.
+ SELECTED: ColorScheme.properties.brightenedOffsets[0],
+ HIGHLIGHTED: ColorScheme.properties.brightenedOffsets[1],
+ DIMMED: ColorScheme.properties.dimmedOffsets[0],
+
+ // Modern names.
+ BRIGHTENED0: ColorScheme.properties.brightenedOffsets[0],
+ BRIGHTENED1: ColorScheme.properties.brightenedOffsets[1],
+ BRIGHTENED2: ColorScheme.properties.brightenedOffsets[2],
+
+ DIMMED0: ColorScheme.properties.dimmedOffsets[0],
+ DIMMED1: ColorScheme.properties.dimmedOffsets[1],
+ DIMMED2: ColorScheme.properties.dimmedOffsets[2]
+ };
+
+ const brighteningLevels = [
+ SelectionState.NONE,
+ SelectionState.BRIGHTENED0,
+ SelectionState.BRIGHTENED1,
+ SelectionState.BRIGHTENED2
+ ];
+ SelectionState.getFromBrighteningLevel = function(level) {
+ return brighteningLevels[level];
+ };
+
+ const dimmingLevels = [
+ SelectionState.DIMMED0,
+ SelectionState.DIMMED1,
+ SelectionState.DIMMED2
+ ];
+ SelectionState.getFromDimmingLevel = function(level) {
+ return dimmingLevels[level];
+ };
+
+ return {
+ SelectionState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice.html b/chromium/third_party/catapult/tracing/tracing/model/slice.html
new file mode 100644
index 00000000000..2ea422561e6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/slice.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Slice class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A Slice represents an interval of time plus parameters associated
+ * with that interval.
+ *
+ * @constructor
+ */
+ function Slice(category, title, colorId, start, args, opt_duration,
+ opt_cpuStart, opt_cpuDuration, opt_argsStripped,
+ opt_bindId) {
+ if (new.target) {
+ throw new Error('Can\'t instantiate pure virtual class Slice');
+ }
+ tr.model.TimedEvent.call(this, start);
+
+ this.category = category || '';
+ this.title = title;
+ this.colorId = colorId;
+ this.args = args;
+ this.startStackFrame = undefined;
+ this.endStackFrame = undefined;
+ this.didNotFinish = false;
+ this.inFlowEvents = [];
+ this.outFlowEvents = [];
+ this.subSlices = [];
+ this.selfTime = undefined;
+ this.cpuSelfTime = undefined;
+ this.important = false;
+ this.parentContainer = undefined;
+ this.argsStripped = false;
+
+ this.bind_id_ = opt_bindId;
+
+ // parentSlice and isTopLevel will be set by SliceGroup.
+ this.parentSlice = undefined;
+ this.isTopLevel = false;
+ // After SliceGroup processes Slices, isTopLevel should be equivalent to
+ // !parentSlice.
+
+ if (opt_duration !== undefined) {
+ this.duration = opt_duration;
+ }
+
+ if (opt_cpuStart !== undefined) {
+ this.cpuStart = opt_cpuStart;
+ }
+
+ if (opt_cpuDuration !== undefined) {
+ this.cpuDuration = opt_cpuDuration;
+ }
+
+ if (opt_argsStripped !== undefined) {
+ this.argsStripped = true;
+ }
+ }
+
+ Slice.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+
+ get analysisTypeName() {
+ return this.title;
+ },
+
+ get userFriendlyName() {
+ return 'Slice ' + this.title + ' at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.start);
+ },
+
+ get stableId() {
+ const parentSliceGroup = this.parentContainer.sliceGroup;
+ return parentSliceGroup.stableId + '.' +
+ parentSliceGroup.slices.indexOf(this);
+ },
+
+ get bindId() {
+ return this.bind_id_;
+ },
+
+ findDescendentSlice(targetTitle) {
+ if (!this.subSlices) {
+ return undefined;
+ }
+
+ for (let i = 0; i < this.subSlices.length; i++) {
+ if (this.subSlices[i].title === targetTitle) {
+ return this.subSlices[i];
+ }
+ const slice = this.subSlices[i].findDescendentSlice(targetTitle);
+ if (slice) return slice;
+ }
+ return undefined;
+ },
+
+ get mostTopLevelSlice() {
+ if (!this.parentSlice) return this;
+ return this.parentSlice.mostTopLevelSlice;
+ },
+
+ getProcess() {
+ const thread = this.parentContainer;
+ if (thread && thread.getProcess) {
+ return thread.getProcess();
+ }
+ return undefined;
+ },
+
+ get model() {
+ const process = this.getProcess();
+ if (process !== undefined) {
+ return this.getProcess().model;
+ }
+ return undefined;
+ },
+
+ /**
+ * Finds all topmost slices relative to this slice.
+ *
+ * Slices may have multiple direct descendants which satisfy
+ * |eventPredicate|, and in this case, all of them are topmost as long as
+ * this slice does not satisfy the predicate.
+ *
+ * For instance, suppose we are passing a predicate that checks whether
+ * events titles begin with 'C'.
+ * C1.findTopmostSlicesRelativeToThisSlice() returns C1 in this example:
+ * [ C1 ]
+ * [ C2 ]
+ *
+ * and D.findTopmostSlicesRelativeToThisSlice() returns C1 and C2 in this
+ * example:
+ * [ D ]
+ * [C1] [C2]
+ */
+ * findTopmostSlicesRelativeToThisSlice(eventPredicate) {
+ if (eventPredicate(this)) {
+ yield this;
+ return;
+ }
+ for (const s of this.subSlices) {
+ yield* s.findTopmostSlicesRelativeToThisSlice(eventPredicate);
+ }
+ },
+
+ /**
+ * Obtains all subsequent slices of this slice.
+ *
+ * Subsequent slices are slices that get executed after a particular
+ * slice, i.e., all the functions that are called after the current one.
+ *
+ * For instance, E.iterateAllSubsequentSlices() in the following example:
+ * [ A ]
+ * [ B][ D ][ G ]
+ * [C] [E][F] [H]
+ * will pass F, G, then H to the provided callback.
+ *
+ * The reason we need subsequent slices of a particular slice is that
+ * when there is flow event goes into, e.g., E, we only want to highlight
+ * E's subsequent slices to indicate the execution order.
+ *
+ * The idea to calculate the subsequent slices of slice E is to view
+ * the slice group as a tree where the top-level slice A is the root node.
+ * The preorder depth-first-search (DFS) order is naturally equivalent
+ * to the function call order. We just need to perform a DFS, and start
+ * recording the slices after we see the occurance of E.
+ */
+ iterateAllSubsequentSlices(callback, opt_this) {
+ const parentStack = [];
+ let started = false;
+
+ // get the root node and push it to the DFS stack
+ const topmostSlice = this.mostTopLevelSlice;
+ parentStack.push(topmostSlice);
+
+ // Using the stack to perform DFS
+ while (parentStack.length !== 0) {
+ const curSlice = parentStack.pop();
+
+ if (started) {
+ callback.call(opt_this, curSlice);
+ } else {
+ started = (curSlice.guid === this.guid);
+ }
+
+ for (let i = curSlice.subSlices.length - 1; i >= 0; i--) {
+ parentStack.push(curSlice.subSlices[i]);
+ }
+ }
+ },
+
+ get subsequentSlices() {
+ const res = [];
+
+ this.iterateAllSubsequentSlices(function(subseqSlice) {
+ res.push(subseqSlice);
+ });
+
+ return res;
+ },
+
+ /**
+ * Obtains the parents of a slice, from the most immediate to the root.
+ *
+ * For instance, E.enumerateAllAncestors() in the following example:
+ * [ A ]
+ * [ B][ D ][ G ]
+ * [C] [E][F] [H]
+ * will yield D, then A, in the order from the leaves to the root.
+ */
+ * enumerateAllAncestors() {
+ let curSlice = this.parentSlice;
+ while (curSlice) {
+ yield curSlice;
+ curSlice = curSlice.parentSlice;
+ }
+ },
+
+ get ancestorSlices() {
+ return Array.from(this.enumerateAllAncestors());
+ },
+
+ iterateEntireHierarchy(callback, opt_this) {
+ const mostTopLevelSlice = this.mostTopLevelSlice;
+ callback.call(opt_this, mostTopLevelSlice);
+ mostTopLevelSlice.iterateAllSubsequentSlices(callback, opt_this);
+ },
+
+ get entireHierarchy() {
+ const res = [];
+
+ this.iterateEntireHierarchy(function(slice) {
+ res.push(slice);
+ });
+
+ return res;
+ },
+
+ /**
+ * Returns this slice, and its ancestor and subsequent slices.
+ *
+ * For instance, E.ancestorAndSubsequentSlices in the following example:
+ * [ A ]
+ * [ B][ D ][ G ]
+ * [C] [E][F] [H]
+ * will return E, D, A, F, G, and H, where E is itself, D and A are
+ * E's ancestors, and F, G, and H are subsequent slices of E
+ */
+ get ancestorAndSubsequentSlices() {
+ const res = [];
+
+ res.push(this);
+
+ for (const aSlice of this.enumerateAllAncestors()) {
+ res.push(aSlice);
+ }
+
+ this.iterateAllSubsequentSlices(function(sSlice) {
+ res.push(sSlice);
+ });
+
+ return res;
+ },
+
+ * enumerateAllDescendents() {
+ for (const slice of this.subSlices) {
+ yield slice;
+ }
+ for (const slice of this.subSlices) {
+ yield* slice.enumerateAllDescendents();
+ }
+ },
+
+ get descendentSlices() {
+ const res = [];
+ for (const slice of this.enumerateAllDescendents()) {
+ res.push(slice);
+ }
+ return res;
+ }
+
+ };
+
+ return {
+ Slice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice_group.html b/chromium/third_party/catapult/tracing/tracing/model/slice_group.html
new file mode 100644
index 00000000000..82d24283cf4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/slice_group.html
@@ -0,0 +1,720 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the SliceGroup class.
+ */
+tr.exportTo('tr.model', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ function getSliceLo(s) {
+ return s.start;
+ }
+
+ function getSliceHi(s) {
+ return s.end;
+ }
+
+ /**
+ * A group of Slices, plus code to create them from B/E events, as
+ * well as arrange them into subRows.
+ *
+ * Do not mutate the slices array directly. Modify it only by
+ * SliceGroup mutation methods.
+ *
+ * @constructor
+ * @param {function(new:Slice, category, title, colorId, start, args)=}
+ * opt_sliceConstructor The constructor to use when creating slices.
+ * @extends {tr.model.EventContainer}
+ */
+ function SliceGroup(parentContainer, opt_sliceConstructor, opt_name) {
+ tr.model.EventContainer.call(this);
+
+ this.parentContainer_ = parentContainer;
+
+ const sliceConstructor = opt_sliceConstructor || ThreadSlice;
+ this.sliceConstructor = sliceConstructor;
+ this.sliceConstructorSubTypes = this.sliceConstructor.subTypes;
+ if (!this.sliceConstructorSubTypes) {
+ throw new Error('opt_sliceConstructor must have a subtype registry.');
+ }
+
+ this.openPartialSlices_ = [];
+
+ this.slices = [];
+ this.topLevelSlices = [];
+ this.haveTopLevelSlicesBeenBuilt = false;
+ this.name_ = opt_name;
+
+ if (this.model === undefined) {
+ throw new Error('SliceGroup must have model defined.');
+ }
+ }
+
+ SliceGroup.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get parentContainer() {
+ return this.parentContainer_;
+ },
+
+ get model() {
+ return this.parentContainer_.model;
+ },
+
+ get stableId() {
+ return this.parentContainer_.stableId + '.SliceGroup';
+ },
+
+ getSettingsKey() {
+ if (!this.name_) return undefined;
+ const parentKey = this.parentContainer_.getSettingsKey();
+ if (!parentKey) return undefined;
+ return parentKey + '.' + this.name;
+ },
+
+ /**
+ * @return {Number} The number of slices in this group.
+ */
+ get length() {
+ return this.slices.length;
+ },
+
+ /**
+ * Helper function that pushes the provided slice onto the slices array.
+ * @param {Slice} slice The slice to be added to the slices array.
+ */
+ pushSlice(slice) {
+ this.haveTopLevelSlicesBeenBuilt = false;
+ slice.parentContainer = this.parentContainer_;
+ this.slices.push(slice);
+ return slice;
+ },
+
+ /**
+ * Helper function that pushes the provided slices onto the slices array.
+ * @param {Array.<Slice>} slices An array of slices to be added.
+ */
+ pushSlices(slices) {
+ this.haveTopLevelSlicesBeenBuilt = false;
+ slices.forEach(function(slice) {
+ slice.parentContainer = this.parentContainer_;
+ this.slices.push(slice);
+ }, this);
+ },
+
+ /**
+ * Opens a new slice in the group's slices.
+ *
+ * Calls to beginSlice and
+ * endSlice must be made with non-monotonically-decreasing timestamps.
+ *
+ * @param {String} category Category name of the slice to add.
+ * @param {String} title Title of the slice to add.
+ * @param {Number} ts The timetsamp of the slice, in milliseconds.
+ * @param {Object.<string, Object>=} opt_args Arguments associated with
+ * the slice.
+ * @param {Number=} opt_colorId The color of the slice, defined by
+ * its palette id (see base/color_scheme.html).
+ */
+ beginSlice(category, title, ts, opt_args, opt_tts,
+ 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 : {}, null,
+ opt_tts, undefined,
+ opt_argsStripped, opt_bindId);
+ this.openPartialSlices_.push(slice);
+ slice.didNotFinish = true;
+ this.pushSlice(slice);
+
+ return slice;
+ },
+
+ isTimestampValidForBeginOrEnd(ts) {
+ if (!this.openPartialSlices_.length) return true;
+ const top = this.openPartialSlices_[this.openPartialSlices_.length - 1];
+ return ts >= top.start;
+ },
+
+ /**
+ * @return {Number} The number of beginSlices for which an endSlice has not
+ * been issued.
+ */
+ get openSliceCount() {
+ return this.openPartialSlices_.length;
+ },
+
+ get mostRecentlyOpenedPartialSlice() {
+ if (!this.openPartialSlices_.length) return undefined;
+ return this.openPartialSlices_[this.openPartialSlices_.length - 1];
+ },
+
+ /**
+ * Ends the last begun slice in this group and pushes it onto the slice
+ * array.
+ *
+ * @param {Number} ts Timestamp when the slice ended
+ * @param {Number=} opt_colorId The color of the slice, defined by
+ * its palette id (see base/color_scheme.html).
+ * @return {Slice} slice.
+ */
+ endSlice(ts, opt_tts, opt_colorId) {
+ if (!this.openSliceCount) {
+ throw new Error('endSlice called without an open slice');
+ }
+
+ const slice = this.openPartialSlices_[this.openSliceCount - 1];
+ this.openPartialSlices_.splice(this.openSliceCount - 1, 1);
+ if (ts < slice.start) {
+ throw new Error('Slice ' + slice.title +
+ ' end time is before its start.');
+ }
+
+ slice.duration = ts - slice.start;
+ slice.didNotFinish = false;
+ slice.colorId = opt_colorId || slice.colorId;
+
+ if (opt_tts && slice.cpuStart !== undefined) {
+ slice.cpuDuration = opt_tts - slice.cpuStart;
+ }
+
+ return slice;
+ },
+
+ /**
+ * Push a complete event as a Slice into the slice list.
+ * The timestamp can be in any order.
+ *
+ * @param {String} category Category name of the slice to add.
+ * @param {String} title Title of the slice to add.
+ * @param {Number} ts The timetsamp of the slice, in milliseconds.
+ * @param {Number} duration The duration of the slice, in milliseconds.
+ * @param {Object.<string, Object>=} opt_args Arguments associated with
+ * the slice.
+ * @param {Number=} opt_colorId The color of the slice, as defined by
+ * its palette id (see base/color_scheme.html).
+ */
+ 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;
+ },
+
+ /**
+ * Closes any open slices.
+ * @param {Number=} opt_maxTimestamp The end time to use for the closed
+ * slices. If not provided,
+ * the max timestamp for this slice is provided.
+ */
+ 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_ = [];
+ },
+
+ /**
+ * Shifts all the timestamps inside this group forward by the amount
+ * specified.
+ */
+ shiftTimestampsForward(amount) {
+ for (let sI = 0; sI < this.slices.length; sI++) {
+ const slice = this.slices[sI];
+ slice.start = (slice.start + amount);
+ }
+ },
+
+ /**
+ * Updates the bounds for this group based on the slices it contains.
+ */
+ 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() {
+ },
+
+ /**
+ * Provides a more efficient implementation than the default implementation
+ * in event_container.html, since child events are sorted.
+ */
+ * 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]);
+ }
+ }
+ 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];
+
+ // Now recurse on slice looking for subSlice of given ts.
+ while (true) {
+ i = tr.b.findIndexInSortedClosedIntervals(
+ curSlice.subSlices,
+ getSliceLo, getSliceHi,
+ ts);
+ if (i === -1 || i === curSlice.subSlices.length) {
+ return curSlice;
+ }
+ curSlice = curSlice.subSlices[i];
+ }
+ },
+
+ findNextSliceAfter(ts, refGuid) {
+ let i = tr.b.findLowIndexInSortedArray(this.slices, getSliceLo, ts);
+ if (i === this.slices.length) {
+ return undefined;
+ }
+ for (; i < this.slices.length; i++) {
+ const slice = this.slices[i];
+ if (slice.start > ts) return slice;
+ if (slice.guid <= refGuid) continue;
+ return slice;
+ }
+ return undefined;
+ },
+
+ /**
+ * This function assumes that if any slice has a cpu duration then
+ * then the group is considered to have cpu duration.
+ */
+ hasCpuDuration_() {
+ if (this.slices.some(function(slice) {
+ return slice.cpuDuration !== undefined;
+ })) return true;
+ return false;
+ },
+
+ /**
+ * Construct subSlices for this group.
+ * Populate the group topLevelSlices, parent slices get a subSlices[],
+ * a selfThreadTime and a selfTime, child slices get a parentSlice
+ * reference.
+ */
+ createSubSlices() {
+ this.haveTopLevelSlicesBeenBuilt = true;
+ this.createSubSlicesImpl_();
+ // If another source has cpu time, we can augment the cpuDuration of the
+ // slices in the group with that cpu time. This should be done only if
+ // the original source does not include cpuDuration.
+ if (!this.hasCpuDuration_() && this.parentContainer.timeSlices) {
+ this.addCpuTimeToSubslices_(this.parentContainer.timeSlices);
+ }
+ this.slices.forEach(function(slice) {
+ let selfTime = slice.duration;
+ for (let i = 0; i < slice.subSlices.length; i++) {
+ selfTime -= slice.subSlices[i].duration;
+ }
+ slice.selfTime = selfTime;
+
+ if (slice.cpuDuration === undefined) return;
+
+ let cpuSelfTime = slice.cpuDuration;
+ for (let i = 0; i < slice.subSlices.length; i++) {
+ if (slice.subSlices[i].cpuDuration !== undefined) {
+ cpuSelfTime -= slice.subSlices[i].cpuDuration;
+ }
+ }
+ slice.cpuSelfTime = cpuSelfTime;
+ });
+ },
+ createSubSlicesImpl_() {
+ const precisionUnit = this.model.intrinsicTimeUnit;
+
+
+ // Note that this doesn't check whether |child| should be added to
+ // |parent|'s descendant slices instead of |parent| directly.
+ function addSliceIfBounds(parent, child) {
+ if (parent.bounds(child, precisionUnit)) {
+ child.parentSlice = parent;
+ if (parent.subSlices === undefined) {
+ parent.subSlices = [];
+ }
+ parent.subSlices.push(child);
+ return true;
+ }
+ return false;
+ }
+
+ if (!this.slices.length) return;
+
+ const ops = [];
+ for (let i = 0; i < this.slices.length; i++) {
+ if (this.slices[i].subSlices) {
+ this.slices[i].subSlices.splice(0,
+ this.slices[i].subSlices.length);
+ }
+ ops.push(i);
+ }
+
+ const originalSlices = this.slices;
+ ops.sort(function(ix, iy) {
+ const x = originalSlices[ix];
+ const y = originalSlices[iy];
+ if (x.start !== y.start) {
+ return x.start - y.start;
+ }
+
+ // Elements get inserted into the slices array in order of when the
+ // slices start. Because slices must be properly nested, we break
+ // start-time ties by assuming that the elements appearing earlier
+ // in the slices array (and thus ending earlier) start earlier.
+ return ix - iy;
+ });
+
+ const slices = new Array(this.slices.length);
+ for (let i = 0; i < ops.length; i++) {
+ slices[i] = originalSlices[ops[i]];
+ }
+
+ // Actually build the subrows.
+ let rootSlice = slices[0];
+ this.topLevelSlices = [];
+ this.topLevelSlices.push(rootSlice);
+ rootSlice.isTopLevel = true;
+ for (let i = 1; i < slices.length; i++) {
+ const slice = slices[i];
+ while (rootSlice !== undefined &&
+ (!addSliceIfBounds(rootSlice, slice))) {
+ rootSlice = rootSlice.parentSlice;
+ }
+ if (rootSlice === undefined) {
+ this.topLevelSlices.push(slice);
+ slice.isTopLevel = true;
+ }
+ rootSlice = slice;
+ }
+
+ // Keep the slices in sorted form.
+ this.slices = slices;
+ },
+ addCpuTimeToSubslices_(timeSlices) {
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ let sliceIdx = 0;
+ timeSlices.forEach(function(timeSlice) {
+ if (timeSlice.schedulingState === SCHEDULING_STATE.RUNNING) {
+ while (sliceIdx < this.topLevelSlices.length) {
+ if (this.addCpuTimeToSubslice_(this.topLevelSlices[sliceIdx],
+ timeSlice)) {
+ // The current top-level slice and children are fully
+ // accounted for, proceed to next top-level slice.
+ sliceIdx++;
+ } else {
+ // The current top-level runs beyond the time slice, break out
+ // so we can potentially add more time slices to it
+ break;
+ }
+ }
+ }
+ }, this);
+ },
+ /* Add run-time of this timeSlice to the passed in slice
+ * and all of it's children (recursively).
+ * Returns whether the slice ends before or at the end of the
+ * time slice, signaling we are done with this slice.
+ */
+ addCpuTimeToSubslice_(slice, timeSlice) {
+ // Make sure they overlap
+ if (slice.start > timeSlice.end || slice.end < timeSlice.start) {
+ return slice.end <= timeSlice.end;
+ }
+
+ // Compute actual overlap
+ let duration = timeSlice.duration;
+ if (slice.start > timeSlice.start) {
+ duration -= slice.start - timeSlice.start;
+ }
+ if (timeSlice.end > slice.end) {
+ duration -= timeSlice.end - slice.end;
+ }
+
+ if (slice.cpuDuration) {
+ slice.cpuDuration += duration;
+ } else {
+ slice.cpuDuration = duration;
+ }
+
+ for (let i = 0; i < slice.subSlices.length; i++) {
+ this.addCpuTimeToSubslice_(slice.subSlices[i], timeSlice);
+ }
+
+ return slice.end <= timeSlice.end;
+ }
+ };
+
+ /**
+ * Merge two slice groups.
+ *
+ * If the two groups do not nest properly some of the slices of groupB will
+ * be split to accomodate the improper nesting. This is done to accomodate
+ * combined kernel and userland call stacks on Android. Because userland
+ * tracing is done by writing to the trace_marker file, the kernel calls
+ * that get invoked as part of that write may not be properly nested with
+ * the userland call trace. For example the following sequence may occur:
+ *
+ * kernel enter sys_write (the write to trace_marker)
+ * user enter some_function
+ * kernel exit sys_write
+ * ...
+ * kernel enter sys_write (the write to trace_marker)
+ * user exit some_function
+ * kernel exit sys_write
+ *
+ * This is handled by splitting the sys_write call into two slices as
+ * follows:
+ *
+ * | sys_write | some_function | sys_write (cont.) |
+ * | sys_write (cont.) | | sys_write |
+ *
+ * The colorId of both parts of the split slices are kept the same, and the
+ * " (cont.)" suffix is appended to the later parts of a split slice.
+ *
+ * The two input SliceGroups are not modified by this, and the merged
+ * SliceGroup will contain a copy of each of the input groups' slices (those
+ * copies may be split).
+ */
+ SliceGroup.merge = function(groupA, groupB) {
+ // This is implemented by traversing the two slice groups in reverse
+ // order. The slices in each group are sorted by ascending end-time, so
+ // we must do the traversal from back to front in order to maintain the
+ // sorting.
+ //
+ // We traverse the two groups simultaneously, merging as we go. At each
+ // iteration we choose the group from which to take the next slice based
+ // on which group's next slice has the greater end-time. During this
+ // traversal we maintain a stack of currently "open" slices for each input
+ // group. A slice is considered "open" from the time it gets reached in
+ // our input group traversal to the time we reach an slice in this
+ // traversal with an end-time before the start time of the "open" slice.
+ //
+ // Each time a slice from groupA is opened or closed (events corresponding
+ // to the end-time and start-time of the input slice, respectively) we
+ // split all of the currently open slices from groupB.
+
+ if (groupA.openPartialSlices_.length > 0) {
+ throw new Error('groupA has open partial slices');
+ }
+
+ if (groupB.openPartialSlices_.length > 0) {
+ throw new Error('groupB has open partial slices');
+ }
+
+ if (groupA.parentContainer !== groupB.parentContainer) {
+ throw new Error('Different parent threads. Cannot merge');
+ }
+
+ if (groupA.sliceConstructor !== groupB.sliceConstructor) {
+ throw new Error('Different slice constructors. Cannot merge');
+ }
+
+ const result = new SliceGroup(groupA.parentContainer,
+ groupA.sliceConstructor,
+ groupA.name_);
+
+ const slicesA = groupA.slices;
+ const slicesB = groupB.slices;
+ let idxA = 0;
+ let idxB = 0;
+ const openA = [];
+ const openB = [];
+
+ const splitOpenSlices = function(when) {
+ for (let i = 0; i < openB.length; i++) {
+ const oldSlice = openB[i];
+ const oldEnd = oldSlice.end;
+ if (when < oldSlice.start || oldEnd < when) {
+ throw new Error('slice should not be split');
+ }
+
+ const newSlice = result.copySlice(oldSlice);
+ newSlice.start = when;
+ newSlice.duration = oldEnd - when;
+ if (newSlice.title.indexOf(' (cont.)') === -1) {
+ newSlice.title += ' (cont.)';
+ }
+ oldSlice.duration = when - oldSlice.start;
+ openB[i] = newSlice;
+ result.pushSlice(newSlice);
+ }
+ };
+
+ const closeOpenSlices = function(upTo) {
+ while (openA.length > 0 || openB.length > 0) {
+ const nextA = openA[openA.length - 1];
+ const nextB = openB[openB.length - 1];
+ const endA = nextA && nextA.end;
+ const endB = nextB && nextB.end;
+
+ if ((endA === undefined || endA > upTo) &&
+ (endB === undefined || endB > upTo)) {
+ return;
+ }
+
+ if (endB === undefined || endA < endB) {
+ splitOpenSlices(endA);
+ openA.pop();
+ } else {
+ openB.pop();
+ }
+ }
+ };
+
+ while (idxA < slicesA.length || idxB < slicesB.length) {
+ const sA = slicesA[idxA];
+ const sB = slicesB[idxB];
+ let nextSlice;
+ let isFromB;
+
+ if (sA === undefined || (sB !== undefined && sA.start > sB.start)) {
+ nextSlice = result.copySlice(sB);
+ isFromB = true;
+ idxB++;
+ } else {
+ nextSlice = result.copySlice(sA);
+ isFromB = false;
+ idxA++;
+ }
+
+ closeOpenSlices(nextSlice.start);
+
+ result.pushSlice(nextSlice);
+
+ if (isFromB) {
+ openB.push(nextSlice);
+ } else {
+ splitOpenSlices(nextSlice.start);
+ openA.push(nextSlice);
+ }
+ }
+
+ closeOpenSlices();
+
+ return result;
+ };
+
+ return {
+ SliceGroup,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html b/chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html
new file mode 100644
index 00000000000..573456fba82
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html
@@ -0,0 +1,935 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Slice = tr.model.Slice;
+ const SliceGroup = tr.model.SliceGroup;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newThreadSlice = tr.c.TestUtils.newThreadSlice;
+ const newModel = tr.c.TestUtils.newModel;
+ const newFakeThread = tr.c.TestUtils.newFakeThread;
+
+ test('basicBeginEnd', function() {
+ const group = new SliceGroup(newFakeThread());
+ assert.strictEqual(group.openSliceCount, 0);
+ const sliceA = group.beginSlice('', 'a', 1, {a: 1});
+ assert.strictEqual(group.openSliceCount, 1);
+ assert.strictEqual(sliceA.title, 'a');
+ assert.strictEqual(sliceA.start, 1);
+ assert.strictEqual(sliceA.args.a, 1);
+
+ const sliceB = group.endSlice(3);
+ assert.strictEqual(sliceA, sliceB);
+ assert.strictEqual(sliceB.duration, 2);
+ });
+
+ test('subSlicesBuilderBasic', function() {
+ const group = new SliceGroup(newFakeThread());
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 2);
+ assert.deepEqual(group.topLevelSlices, [sA, sB]);
+ assert.strictEqual(group.findSliceAtTs(0), undefined);
+ assert.strictEqual(group.findSliceAtTs(1), sA);
+ assert.strictEqual(group.findSliceAtTs(3), sA);
+ assert.strictEqual(group.findSliceAtTs(3.1), sB);
+ assert.strictEqual(group.findSliceAtTs(4), sB);
+ assert.strictEqual(group.findSliceAtTs(5), undefined);
+ });
+
+ test('subSlicesBuilderBasic2', function() {
+ const group = new SliceGroup(newFakeThread());
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 1);
+ assert.deepEqual(group.topLevelSlices, [sA]);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.deepEqual(sA.subSlices, [sB]);
+ assert.strictEqual(sA.selfTime, 3);
+
+ assert.strictEqual(sA, sB.parentSlice);
+ assert.isTrue(sA.isTopLevel);
+ assert.isFalse(sB.isTopLevel);
+ });
+
+ test('subSlicesBuilderNestedExactly', function() {
+ const group = new SliceGroup(newFakeThread());
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 4}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 1);
+ assert.deepEqual(group.topLevelSlices, [sB]);
+
+ assert.strictEqual(sB.subSlices.length, 1);
+ assert.deepEqual(sB.subSlices, [sA]);
+ assert.strictEqual(sB.selfTime, 0);
+
+ assert.strictEqual(sB, sA.parentSlice);
+ assert.isTrue(sB.isTopLevel);
+ assert.isFalse(sA.isTopLevel);
+
+ assert.strictEqual(group.findSliceAtTs(0), undefined);
+ assert.strictEqual(group.findSliceAtTs(1), sA);
+ assert.strictEqual(group.findSliceAtTs(2), sA);
+ assert.strictEqual(group.findSliceAtTs(5), sA);
+ assert.strictEqual(group.findSliceAtTs(6), undefined);
+ });
+
+ test('subSlicesBuilderInstantEvents', function() {
+ const group = new SliceGroup(newFakeThread());
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 2);
+ assert.deepEqual(group.topLevelSlices, [sA, sB]);
+ assert.strictEqual(group.findSliceAtTs(1), sA);
+ assert.strictEqual(group.findSliceAtTs(1.5), undefined);
+ assert.strictEqual(group.findSliceAtTs(2), sB);
+ });
+
+ test('subSlicesBuilderTwoInstantEvents', function() {
+ const group = new SliceGroup(newFakeThread());
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 1);
+ assert.deepEqual(group.topLevelSlices, [sA]);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.deepEqual(sA.subSlices, [sB]);
+ assert.strictEqual(sA.selfTime, 0);
+
+ assert.strictEqual(sA, sB.parentSlice);
+ assert.isTrue(sA.isTopLevel);
+ assert.isFalse(sB.isTopLevel);
+ assert.strictEqual(group.findSliceAtTs(1), sB);
+ assert.strictEqual(group.findSliceAtTs(1.0001), undefined);
+ });
+
+ test('subSlicesBuilderOutOfOrderAddition', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // Pattern being tested:
+ // [ a ][ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 2);
+ assert.deepEqual(group.topLevelSlices, [sA, sB]);
+ assert.isTrue(sA.isTopLevel);
+ assert.isTrue(sB.isTopLevel);
+ assert.strictEqual(group.findSliceAtTs(3), sA);
+ });
+
+ test('subRowBuilderOutOfOrderAddition2', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 5}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 1);
+ assert.deepEqual(group.topLevelSlices, [sA]);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.deepEqual(sA.subSlices, [sB]);
+ assert.strictEqual(sA.selfTime, 4);
+
+ assert.strictEqual(sA, sB.parentSlice);
+ assert.isTrue(sA.isTopLevel);
+ assert.isFalse(sB.isTopLevel);
+ assert.strictEqual(group.findSliceAtTs(1.5), sA);
+ assert.strictEqual(group.findSliceAtTs(3), sB);
+ assert.strictEqual(group.findSliceAtTs(3.5), sB);
+ assert.strictEqual(group.findSliceAtTs(4), sB);
+ assert.strictEqual(group.findSliceAtTs(4.5), sA);
+ });
+
+ test('subSlicesBuilderOnNestedZeroLength', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b1 ] []<- b2 where b2.duration = 0 and b2.end === a.end.
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB1 = group.pushSlice(newSliceEx(
+ {title: 'b1', start: 1, duration: 2}));
+ const sB2 = group.pushSlice(newSliceEx(
+ {title: 'b2', start: 4, duration: 0}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 1);
+ assert.deepEqual(group.topLevelSlices, [sA]);
+
+ assert.strictEqual(sA.subSlices.length, 2);
+ assert.deepEqual(sA.subSlices, [sB1, sB2]);
+ assert.strictEqual(sA.selfTime, 1);
+
+ assert.strictEqual(sA, sB1.parentSlice);
+ assert.strictEqual(sA, sB2.parentSlice);
+ assert.strictEqual(group.findSliceAtTs(1), sB1);
+ assert.strictEqual(group.findSliceAtTs(4), sB2);
+ });
+
+ test('subSlicesBuilderOnGroup1', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // Pattern being tested:
+ // [ a ] [ c ]
+ // [ b ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx({title: 'c', start: 5, duration: 0}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 2);
+ assert.deepEqual(group.topLevelSlices, [sA, sC]);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.deepEqual(sA.subSlices, [sB]);
+ assert.strictEqual(sA.selfTime, 2);
+
+ assert.strictEqual(sA, sB.parentSlice);
+ assert.strictEqual(group.findSliceAtTs(1), sA);
+ assert.strictEqual(group.findSliceAtTs(2), sB);
+ assert.strictEqual(group.findSliceAtTs(3), sA);
+ assert.strictEqual(group.findSliceAtTs(4.5), undefined);
+ assert.strictEqual(group.findSliceAtTs(5), sC);
+ });
+
+ test('subSlicesBuilderOnGroup2', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // Pattern being tested:
+ // [ a ] [ d ]
+ // [ b ]
+ // [ c ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx(
+ {title: 'c', start: 1.75, duration: 0.5}));
+ const sD = group.pushSlice(newSliceEx(
+ {title: 'd', start: 5, duration: 0.25}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 2);
+ assert.deepEqual(group.topLevelSlices, [sA, sD]);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.deepEqual(sA.subSlices, [sB]);
+ assert.strictEqual(sA.selfTime, 2);
+
+ assert.strictEqual(sA, sB.parentSlice);
+ assert.strictEqual(sB.subSlices.length, 1);
+ assert.deepEqual(sB.subSlices, [sC]);
+ assert.strictEqual(sB.selfTime, 0.5);
+
+ assert.strictEqual(sB, sC.parentSlice);
+ assert.strictEqual(group.findSliceAtTs(2), sC);
+ });
+
+ test('findFirstSlice', function() {
+ const group = new SliceGroup(newFakeThread());
+ // Pattern being tested:
+ // [ a ] [ d ]
+ // [b] [ c ] where b is dur=0
+ const sC = group.pushSlice(newSliceEx({title: 'c', start: 2, end: 3}));
+ const sD = group.pushSlice(newSliceEx({title: 'd', start: 5, end: 6}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, end: 4}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, end: 1}));
+
+ assert.throws(group.findFirstSlice);
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.findFirstSlice(), sA);
+ });
+
+ test('findNextSliceAfterBasic', function() {
+ const group = new SliceGroup(newFakeThread());
+ // Pattern being tested:
+ // [ a ] [ d ]
+ // [b] [ c ] where b is dur=0
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, end: 4}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, end: 1}));
+ const sC = group.pushSlice(newSliceEx({title: 'c', start: 2, end: 3}));
+ const sD = group.pushSlice(newSliceEx({title: 'd', start: 5, end: 6}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.findNextSliceAfter(0, 0), sA);
+ assert.strictEqual(group.findNextSliceAfter(1, sA.guid), sB);
+ assert.strictEqual(group.findNextSliceAfter(1, sB.guid), sC);
+ assert.strictEqual(group.findNextSliceAfter(2, sC.guid), sD);
+ assert.strictEqual(group.findNextSliceAfter(6, 0), undefined);
+ });
+
+ test('subSlicesBuilderTolerateFPInaccuracy', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b ] where b.end contains a tiny FP calculation error.
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1, duration: 3.0000000001}));
+
+ group.createSubSlices();
+
+ assert.strictEqual(group.topLevelSlices.length, 1);
+ assert.deepEqual(group.topLevelSlices, [sA]);
+
+ assert.strictEqual(sA.subSlices.length, 1);
+ assert.deepEqual(sA.subSlices, [sB]);
+ assert.strictEqual(sA, sB.parentSlice);
+ assert.strictEqual(group.findSliceAtTs(1), sB);
+ assert.strictEqual(group.findSliceAtTs(3), sB);
+ });
+
+ test('basicMerge', function() {
+ function TestSlice(
+ cat, title, colorId, start, args, opt_duration,
+ opt_cpuStart, opt_cpuDuration) {
+ Slice.call(this, cat, title, colorId, start, args, opt_duration,
+ opt_cpuStart, opt_cpuDuration);
+ }
+ TestSlice.prototype = {
+ __proto__: Slice.prototype
+ };
+ TestSlice.subTypes = {
+ getConstructor(category, title) {
+ return TestSlice;
+ }
+ };
+
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread, TestSlice);
+ const b = new SliceGroup(thread, TestSlice);
+ a.beginSlice('', 'one', 1);
+ a.endSlice(2);
+ b.beginSlice('', 'two', 3);
+ b.endSlice(5);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 2);
+
+ assert.strictEqual(m.slices[0].title, 'one');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 1);
+
+ assert.strictEqual(m.slices[1].title, 'two');
+ assert.strictEqual(m.slices[1].start, 3);
+ assert.strictEqual(m.slices[1].duration, 2);
+
+ // ensure constructor merged correctly
+ assert.strictEqual(m.sliceConstructor, TestSlice);
+ });
+
+ test('nestedMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 1);
+ a.endSlice(4);
+ b.beginSlice('', 'two', 2);
+ b.endSlice(3);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 2);
+
+ assert.strictEqual(m.slices[0].title, 'one');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 3);
+
+ assert.strictEqual(m.slices[1].title, 'two');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 1);
+ });
+
+ test('startSplitMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 2);
+ a.endSlice(4);
+ b.beginSlice('', 'two', 1);
+ b.endSlice(3);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 3);
+
+ assert.strictEqual(m.slices[0].title, 'two');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 1);
+
+ assert.strictEqual(m.slices[1].title, 'one');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 2);
+
+ assert.strictEqual(m.slices[2].title, 'two (cont.)');
+ assert.strictEqual(m.slices[2].start, 2);
+ assert.strictEqual(m.slices[2].duration, 1);
+ });
+
+ test('startSplitTwoMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 3);
+ a.endSlice(6);
+ b.beginSlice('', 'two', 1);
+ b.beginSlice('', 'three', 2);
+ b.endSlice(4);
+ b.endSlice(5);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 5);
+
+ assert.strictEqual(m.slices[0].title, 'two');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 2);
+
+ assert.strictEqual(m.slices[1].title, 'three');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 1);
+
+ assert.strictEqual(m.slices[2].title, 'one');
+ assert.strictEqual(m.slices[2].start, 3);
+ assert.strictEqual(m.slices[2].duration, 3);
+
+ assert.strictEqual(m.slices[3].title, 'two (cont.)');
+ assert.strictEqual(m.slices[3].start, 3);
+ assert.strictEqual(m.slices[3].duration, 2);
+
+ assert.strictEqual(m.slices[4].title, 'three (cont.)');
+ assert.strictEqual(m.slices[4].start, 3);
+ assert.strictEqual(m.slices[4].duration, 1);
+ });
+
+ test('startSplitTwiceMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 2);
+ a.beginSlice('', 'two', 3);
+ a.endSlice(5);
+ a.endSlice(6);
+ b.beginSlice('', 'three', 1);
+ b.endSlice(4);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 5);
+
+ assert.strictEqual(m.slices[0].title, 'three');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 1);
+
+ assert.strictEqual(m.slices[1].title, 'one');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 4);
+
+ assert.strictEqual(m.slices[2].title, 'three (cont.)');
+ assert.strictEqual(m.slices[2].start, 2);
+ assert.strictEqual(m.slices[2].duration, 1);
+
+ assert.strictEqual(m.slices[3].title, 'two');
+ assert.strictEqual(m.slices[3].start, 3);
+ assert.strictEqual(m.slices[3].duration, 2);
+
+ assert.strictEqual(m.slices[4].title, 'three (cont.)');
+ assert.strictEqual(m.slices[4].start, 3);
+ assert.strictEqual(m.slices[4].duration, 1);
+ });
+
+ test('endSplitMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 1);
+ a.endSlice(3);
+ b.beginSlice('', 'two', 2);
+ b.endSlice(4);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 3);
+
+ assert.strictEqual(m.slices[0].title, 'one');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 2);
+
+ assert.strictEqual(m.slices[1].title, 'two');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 1);
+
+ assert.strictEqual(m.slices[2].title, 'two (cont.)');
+ assert.strictEqual(m.slices[2].start, 3);
+ assert.strictEqual(m.slices[2].duration, 1);
+ });
+
+ test('endSplitTwoMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 1);
+ a.endSlice(4);
+ b.beginSlice('', 'two', 2);
+ b.beginSlice('', 'three', 3);
+ b.endSlice(5);
+ b.endSlice(6);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 5);
+
+ assert.strictEqual(m.slices[0].title, 'one');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 3);
+
+ assert.strictEqual(m.slices[1].title, 'two');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 2);
+
+ assert.strictEqual(m.slices[2].title, 'three');
+ assert.strictEqual(m.slices[2].start, 3);
+ assert.strictEqual(m.slices[2].duration, 1);
+
+ assert.strictEqual(m.slices[3].title, 'two (cont.)');
+ assert.strictEqual(m.slices[3].start, 4);
+ assert.strictEqual(m.slices[3].duration, 2);
+
+ assert.strictEqual(m.slices[4].title, 'three (cont.)');
+ assert.strictEqual(m.slices[4].start, 4);
+ assert.strictEqual(m.slices[4].duration, 1);
+ });
+
+ test('endSplitTwiceMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 1);
+ a.beginSlice('', 'two', 2);
+ a.endSlice(4);
+ a.endSlice(5);
+ b.beginSlice('', 'three', 3);
+ b.endSlice(6);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 5);
+
+ assert.strictEqual(m.slices[0].title, 'one');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 4);
+
+ assert.strictEqual(m.slices[1].title, 'two');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 2);
+
+ assert.strictEqual(m.slices[2].title, 'three');
+ assert.strictEqual(m.slices[2].start, 3);
+ assert.strictEqual(m.slices[2].duration, 1);
+
+ assert.strictEqual(m.slices[3].title, 'three (cont.)');
+ assert.strictEqual(m.slices[3].start, 4);
+ assert.strictEqual(m.slices[3].duration, 1);
+
+ assert.strictEqual(m.slices[4].title, 'three (cont.)');
+ assert.strictEqual(m.slices[4].start, 5);
+ assert.strictEqual(m.slices[4].duration, 1);
+ });
+
+ // Input:
+ // A: | one | | two |
+ //
+ // B: | three |
+ //
+ // Output:
+ // | one | three | two |
+ // | three | | three |
+ test('splitTwiceMerge', function() {
+ const thread = newFakeThread();
+ const a = new SliceGroup(thread);
+ const b = new SliceGroup(thread);
+ a.beginSlice('', 'one', 1);
+ a.endSlice(3);
+ a.beginSlice('', 'two', 4);
+ a.endSlice(6);
+ b.beginSlice('', 'three', 2);
+ b.endSlice(5);
+
+ const m = SliceGroup.merge(a, b);
+ assert.strictEqual(m.slices.length, 5);
+
+ assert.strictEqual(m.slices[0].title, 'one');
+ assert.strictEqual(m.slices[0].start, 1);
+ assert.strictEqual(m.slices[0].duration, 2);
+
+ assert.strictEqual(m.slices[1].title, 'three');
+ assert.strictEqual(m.slices[1].start, 2);
+ assert.strictEqual(m.slices[1].duration, 1);
+
+ assert.strictEqual(m.slices[2].title, 'three (cont.)');
+ assert.strictEqual(m.slices[2].start, 3);
+ assert.strictEqual(m.slices[2].duration, 1);
+
+ assert.strictEqual(m.slices[3].title, 'two');
+ assert.strictEqual(m.slices[3].start, 4);
+ assert.strictEqual(m.slices[3].duration, 2);
+
+ assert.strictEqual(m.slices[4].title, 'three (cont.)');
+ assert.strictEqual(m.slices[4].start, 4);
+ assert.strictEqual(m.slices[4].duration, 1);
+ });
+
+ test('bounds', function() {
+ const group = new SliceGroup(newFakeThread());
+ group.updateBounds();
+ assert.isUndefined(group.bounds.min);
+ assert.isUndefined(group.bounds.max);
+
+ group.pushSlice(newSliceEx({start: 1, duration: 3}));
+ group.pushSlice(newSliceEx({start: 7, duration: 2}));
+ group.updateBounds();
+ assert.strictEqual(group.bounds.min, 1);
+ assert.strictEqual(group.bounds.max, 9);
+ });
+
+ test('boundsWithPartial', function() {
+ const group = new SliceGroup(newFakeThread());
+ group.beginSlice('', 'a', 7);
+ group.updateBounds();
+ assert.strictEqual(group.bounds.min, 7);
+ assert.strictEqual(group.bounds.max, 7);
+ });
+
+ test('boundsWithTwoPartials', function() {
+ const group = new SliceGroup(newFakeThread());
+ group.beginSlice('', 'a', 0);
+ group.beginSlice('', 'a', 1);
+ group.updateBounds();
+ assert.strictEqual(group.bounds.min, 0);
+ assert.strictEqual(group.bounds.max, 1);
+ });
+
+ test('boundsWithBothPartialAndRegular', function() {
+ const group = new SliceGroup(newFakeThread());
+ group.updateBounds();
+ assert.isUndefined(group.bounds.min);
+ assert.isUndefined(group.bounds.max);
+
+ group.pushSlice(newSliceEx({start: 1, duration: 3}));
+ group.beginSlice('', 'a', 7);
+ group.updateBounds();
+ assert.strictEqual(group.bounds.min, 1);
+ assert.strictEqual(group.bounds.max, 7);
+ });
+
+ test('autocloserBasic', function() {
+ const group = new SliceGroup(newFakeThread());
+ assert.strictEqual(0, group.openSliceCount);
+
+ group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0.5}));
+
+ group.beginSlice('', 'b', 2);
+ group.beginSlice('', 'c', 2.5);
+ group.endSlice(3);
+
+ group.autoCloseOpenSlices();
+ group.updateBounds();
+
+ assert.strictEqual(group.bounds.min, 1);
+ assert.strictEqual(group.bounds.max, 3);
+ assert.strictEqual(group.slices.length, 3);
+
+ assert.strictEqual(group.slices[0].title, 'a');
+ assert.isFalse(group.slices[0].didNotFinish);
+
+ assert.strictEqual(group.slices[1].title, 'b');
+ assert.isTrue(group.slices[1].didNotFinish);
+ assert.strictEqual(group.slices[1].duration, 1);
+
+ assert.strictEqual(group.slices[2].title, 'c');
+ assert.isFalse(group.slices[2].didNotFinish);
+ });
+
+ test('autocloserWithSubTasks', function() {
+ const group = new SliceGroup(newFakeThread());
+ assert.strictEqual(0, group.openSliceCount);
+
+ group.beginSlice('', 'a', 1);
+ group.beginSlice('', 'b1', 2);
+ group.endSlice(3);
+ group.beginSlice('', 'b2', 3);
+
+ group.autoCloseOpenSlices();
+ assert.strictEqual(group.slices.length, 3);
+
+ assert.strictEqual(group.slices[0].title, 'a');
+ assert.isTrue(group.slices[0].didNotFinish);
+ assert.strictEqual(group.slices[0].duration, 2);
+
+ assert.strictEqual(group.slices[1].title, 'b1');
+ assert.isFalse(group.slices[1].didNotFinish);
+ assert.strictEqual(group.slices[1].duration, 1);
+
+ assert.strictEqual(group.slices[2].title, 'b2');
+ assert.isTrue(group.slices[2].didNotFinish);
+ assert.strictEqual(group.slices[2].duration, 0);
+ });
+
+ test('autocloseCompleteSlice', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ group.pushCompleteSlice('', 'a', 1, undefined);
+ group.pushCompleteSlice('', 'b', 2, 3);
+
+ group.autoCloseOpenSlices();
+ assert.strictEqual(group.slices.length, 2);
+
+ assert.strictEqual(group.slices[0].title, 'a');
+ assert.isTrue(group.slices[0].didNotFinish);
+ assert.strictEqual(group.slices[0].duration, 4);
+
+ assert.strictEqual(group.slices[1].title, 'b');
+ assert.isFalse(group.slices[1].didNotFinish);
+ assert.strictEqual(group.slices[1].duration, 3);
+ });
+
+ test('sliceGroupStableId', function() {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new SliceGroup(thread);
+
+ assert.strictEqual(process.stableId, 123);
+ assert.strictEqual(thread.stableId, '123.456');
+ assert.strictEqual(group.stableId, '123.456.SliceGroup');
+ });
+
+ test('getSlicesOfName', function() {
+ const group = new SliceGroup(newFakeThread());
+ const expected = [];
+
+ for (let i = 0; i < 10; i++) {
+ const aSlice = newSliceEx({title: 'a', start: i, duration: i + 1});
+ group.pushSlice(aSlice);
+ group.pushSlice(newSliceEx({title: 'b', start: i + 1, duration: i + 2}));
+ expected.push(aSlice);
+ }
+
+ assert.deepEqual(group.getSlicesOfName('a'), expected);
+ });
+
+ test('iterSlicesInTimeRange', function() {
+ const group = new SliceGroup(newFakeThread());
+ const expected = [];
+
+ for (let i = 0; i < 10; i++) {
+ const slice = newSliceEx({title: 'a', start: i, duration: 1});
+ group.pushSlice(slice);
+ if (4 <= i && i <= 7) {
+ expected.push(slice);
+ }
+ }
+ group.createSubSlices();
+
+ const observed = [];
+ group.iterSlicesInTimeRange(function(slice) { observed.push(slice); },
+ 4.5, 7.5);
+ assert.deepEqual(observed, expected);
+ });
+
+ test('computeCpuDurationNoOverlap', function() {
+ const model = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const process = model.getOrCreateProcess(123);
+ const t = process.getOrCreateThread(456);
+ t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 10)];
+ const group = new SliceGroup(t);
+ group.pushSlice(newSliceEx({title: 'draw', start: 0, duration: 20}));
+ group.pushSlice(newSliceEx({title: 'render', start: 60, duration: 20}));
+ group.createSubSlices();
+ assert.strictEqual(group.slices[0].cpuDuration, 0);
+ assert.strictEqual(group.slices[1].cpuDuration, 0);
+ });
+
+ test('computeCpuDurationPartialOverlap', function() {
+ const model = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const process = model.getOrCreateProcess(123);
+ const t = process.getOrCreateThread(456);
+ t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 10)];
+ const group = new SliceGroup(t);
+ group.pushSlice(newSliceEx({title: 'draw', start: 10, duration: 30}));
+ group.pushSlice(newSliceEx({title: 'render', start: 50, duration: 20}));
+ group.createSubSlices();
+ assert.strictEqual(group.slices[0].cpuDuration, 20);
+ assert.strictEqual(group.slices[1].cpuDuration, 10);
+ });
+
+ test('computeCpuDurationFullOverlap', function() {
+ const model = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const process = model.getOrCreateProcess(123);
+ const t = process.getOrCreateThread(456);
+ t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 20)];
+ const group = new SliceGroup(t);
+ group.pushSlice(newSliceEx({title: 'draw', start: 20, duration: 30}));
+ group.pushSlice(newSliceEx({title: 'render', start: 50, duration: 20}));
+ group.createSubSlices();
+ assert.strictEqual(group.slices[0].cpuDuration, 20);
+ assert.strictEqual(group.slices[1].cpuDuration, 20);
+ });
+
+ test('computeCpuSelfDurationWithSubslices', function() {
+ const model = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const process = model.getOrCreateProcess(123);
+ const t = process.getOrCreateThread(456);
+ t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 20)];
+ const group = new SliceGroup(t);
+ group.pushSlice(newSliceEx({title: 'draw', start: 20, duration: 30}));
+ group.pushSlice(newSliceEx({title: 'render', start: 21, duration: 8}));
+ group.pushSlice(newSliceEx({title: 'flush', start: 29, duration: 11}));
+ group.createSubSlices();
+ assert.strictEqual(group.slices[0].cpuDuration, 20);
+ assert.strictEqual(group.slices[0].cpuSelfTime, 1);
+ assert.strictEqual(group.slices[1].cpuDuration, 8);
+ assert.strictEqual(group.slices[1].cpuSelfTime, 8);
+ assert.strictEqual(group.slices[2].cpuDuration, 11);
+ assert.strictEqual(group.slices[2].cpuSelfTime, 11);
+ });
+
+ test('computeCpuDurationSmallTimeslices', function() {
+ const model = new tr.Model();
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const process = model.getOrCreateProcess(123);
+ const t = process.getOrCreateThread(456);
+ t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 1),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 21, 1),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 22, 1),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 23, 1),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 24, 1),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 25, 1),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 26, 1),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 27, 1),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 28, 1),
+ newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 29, 1),
+ newThreadSlice(t, SCHEDULING_STATE.RUNNING, 30, 1)];
+ const group = new SliceGroup(t);
+ group.pushSlice(newSliceEx(
+ {title: 'draw', start: 20, duration: 11})); // 20,[22,24,26,28],30
+ group.pushSlice(newSliceEx(
+ {title: 'render', start: 22, duration: 8})); // 22,[24, 26, 28]
+ group.pushSlice(newSliceEx(
+ {title: 'flush', start: 24, duration: 6})); // 24, 26, 28
+ group.createSubSlices();
+ assert.strictEqual(group.slices[0].cpuDuration, 6);
+ assert.strictEqual(group.slices[0].cpuSelfTime, 2);
+ assert.strictEqual(group.slices[1].cpuDuration, 4);
+ assert.strictEqual(group.slices[1].cpuSelfTime, 1);
+ assert.strictEqual(group.slices[2].cpuDuration, 3);
+ assert.strictEqual(group.slices[2].cpuSelfTime, 3);
+ });
+
+ test('sliceParentContainerSetAtPush', function() {
+ const m = newModel(function(m) {
+ m.process = m.getOrCreateProcess(123);
+ m.thread = m.process.getOrCreateThread(456);
+ m.group = new SliceGroup(m.thread);
+
+ m.sA = m.group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+
+ m.group.createSubSlices();
+ });
+
+ assert.deepEqual(m.sA.parentContainer, m.thread);
+ });
+
+ test('getDescendantEventsInSortedRanges', function() {
+ // Create the following slices:
+ // 0 1 2 3 4 5 6 7 8 9 10
+ // <------------- 0 ------------->
+ // <- 2 -> <---- 3 ---->
+ // <- 1 ->
+ const group = new SliceGroup(newFakeThread());
+ group.pushSlice(newSliceEx({title: 's0', start: 0, end: 10}));
+ group.pushSlice(newSliceEx({title: 's1', start: 7, end: 9}));
+ group.pushSlice(newSliceEx({title: 's2', start: 3, end: 5}));
+ group.pushSlice(newSliceEx({title: 's3', start: 6, end: 10}));
+ group.createSubSlices();
+
+ // [0, 5] intersects s0 and s2.
+ const r1 = new tr.b.math.Range.fromExplicitRange(0, 5);
+ let slices = [...group.getDescendantEventsInSortedRanges([r1])];
+ assert.strictEqual(slices.length, 2);
+ assert.strictEqual(slices[0].title, 's0');
+ assert.strictEqual(slices[1].title, 's2');
+
+ // [10, 11] intersects s0 and s3.
+ const r2 = new tr.b.math.Range.fromExplicitRange(10, 11);
+ slices = [...group.getDescendantEventsInSortedRanges([r2])];
+ assert.strictEqual(slices.length, 2);
+ assert.strictEqual(slices[0].title, 's0');
+ assert.strictEqual(slices[1].title, 's3');
+
+ // [0, 5], [10, 11] intersects s0, s2, and s3.
+ slices = [...group.getDescendantEventsInSortedRanges([r1, r2])];
+ assert.strictEqual(slices.length, 3);
+ assert.strictEqual(slices[0].title, 's0');
+ assert.strictEqual(slices[1].title, 's2');
+ assert.strictEqual(slices[2].title, 's3');
+
+ // Ranges can be nested, too.
+ const r3 = new tr.b.math.Range.fromExplicitRange(1, 2);
+ slices = [...group.getDescendantEventsInSortedRanges([r1, r3])];
+ assert.strictEqual(slices.length, 2);
+ assert.strictEqual(slices[0].title, 's0');
+ assert.strictEqual(slices[1].title, 's2');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice_test.html b/chromium/third_party/catapult/tracing/tracing/model/slice_test.html
new file mode 100644
index 00000000000..a9d5b18ef7e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/slice_test.html
@@ -0,0 +1,239 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Slice = tr.model.Slice;
+ const SliceGroup = tr.model.SliceGroup;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFakeThread = tr.c.TestUtils.newFakeThread;
+
+ test('findDescendentSlice', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ assert.deepEqual(sB, sA.findDescendentSlice('sB'));
+ assert.deepEqual(sC, sA.findDescendentSlice('sC'));
+ assert.isUndefined(sA.findDescendentSlice('sD'));
+ });
+
+ test('findTopmostSlicesRelativeToThisSliceBaseCase', function() {
+ const PREDICATE = slice => slice.title.startsWith('sC');
+
+ const group = new SliceGroup(newFakeThread());
+
+ const sC1 = group.pushSlice(newSliceEx(
+ { title: 'sC1', start: 0.0, duration: 10.0 }));
+ const sC2 = group.pushSlice(newSliceEx(
+ { title: 'sC2', start: 0.0, duration: 4.0 }));
+
+ group.createSubSlices();
+
+ const foundSlices = [];
+ for (const slice of sC1.findTopmostSlicesRelativeToThisSlice(PREDICATE)) {
+ foundSlices.push(slice);
+ }
+
+ assert.deepEqual([sC1], foundSlices);
+ });
+
+ test('findTopmostSlicesRelativeToThisSliceRecursive', function() {
+ const PREDICATE = slice => slice.title.startsWith('sC');
+
+ const group = new SliceGroup(newFakeThread());
+
+ const sD = group.pushSlice(newSliceEx(
+ { title: 'sD', start: 0.0, duration: 10.0 }));
+ const sC1 = group.pushSlice(newSliceEx(
+ { title: 'sC1', start: 0.0, duration: 4.0 }));
+ const sC2 = group.pushSlice(newSliceEx(
+ { title: 'sC2', start: 6.0, duration: 3.0 }));
+
+ group.createSubSlices();
+
+ const foundSlices = [];
+ for (const slice of sD.findTopmostSlicesRelativeToThisSlice(PREDICATE)) {
+ foundSlices.push(slice);
+ }
+ assert.deepEqual([sC1, sC2], foundSlices);
+ });
+
+ test('enumerateAllDescendents', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ assert.deepEqual(sA.descendentSlices, [sB, sC]);
+ assert.deepEqual(sC.descendentSlices, []);
+ });
+
+ test('mostTopLevelSlice', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ assert.strictEqual(sA.mostTopLevelSlice, sA);
+ assert.strictEqual(sB.mostTopLevelSlice, sA);
+ assert.strictEqual(sC.mostTopLevelSlice, sA);
+ });
+
+ test('enumerateAllAncestors', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ // Note that we iterate ancestors from the leaves to the root
+ assert.deepEqual(sC.ancestorSlices, [sB, sA]);
+ assert.deepEqual(sA.ancestorSlices, []);
+ });
+
+ test('iterateAllSubsequentSlices', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // [ A ]
+ // [ B ][ D ][F]
+ // [C] [E]
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+ const sD = group.pushSlice(newSliceEx(
+ { title: 'sD', start: 5.0, duration: 2.0 }));
+ const sE = group.pushSlice(newSliceEx(
+ { title: 'sE', start: 5.0, duration: 1.0 }));
+ const sF = group.pushSlice(newSliceEx(
+ { title: 'sF', start: 8.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ assert.deepEqual(sA.subsequentSlices, [sB, sC, sD, sE, sF]);
+ assert.deepEqual(sD.subsequentSlices, [sE, sF]);
+ assert.deepEqual(sF.subsequentSlices, []);
+ });
+
+ test('ancestorAndSubsequentSlices', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // [ A ]
+ // [ B ][ D ][F]
+ // [C] [E]
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+ const sD = group.pushSlice(newSliceEx(
+ { title: 'sD', start: 5.0, duration: 2.0 }));
+ const sE = group.pushSlice(newSliceEx(
+ { title: 'sE', start: 5.0, duration: 1.0 }));
+ const sF = group.pushSlice(newSliceEx(
+ { title: 'sF', start: 8.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ assert.deepEqual(sD.ancestorAndSubsequentSlices, [sD, sA, sE, sF]);
+ });
+
+ test('entireHierarchy', function() {
+ const group = new SliceGroup(newFakeThread());
+
+ // [ A ]
+ // [ B ][ D ][F]
+ // [C] [E]
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 0.0, duration: 4.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 0.0, duration: 2.0 }));
+ const sD = group.pushSlice(newSliceEx(
+ { title: 'sD', start: 5.0, duration: 2.0 }));
+ const sE = group.pushSlice(newSliceEx(
+ { title: 'sE', start: 5.0, duration: 1.0 }));
+ const sF = group.pushSlice(newSliceEx(
+ { title: 'sF', start: 8.0, duration: 2.0 }));
+
+ group.createSubSlices();
+
+ assert.deepEqual(sD.entireHierarchy, [sA, sB, sC, sD, sE, sF]);
+ });
+
+ test('stableId', function() {
+ const thread = newFakeThread();
+ const group = thread.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx(
+ { title: 'sA', start: 0.0, duration: 10.0 }));
+ const sB = group.pushSlice(newSliceEx(
+ { title: 'sB', start: 10.0, duration: 20.0 }));
+ const sC = group.pushSlice(newSliceEx(
+ { title: 'sC', start: 20.0, duration: 30.0 }));
+
+ assert.strictEqual(group.stableId + '.0', sA.stableId);
+ assert.strictEqual(group.stableId + '.1', sB.stableId);
+ assert.strictEqual(group.stableId + '.2', sC.stableId);
+ });
+
+ test('cantInstantiateDirectly', function() {
+ assert.throws(function() {
+ new Slice('', 'Test', 0, 0, { });
+ });
+ });
+
+ test('canInstantiateSubclasses', function() {
+ function TestSlice() {
+ Slice.call(this, '', 'Test', 0, 0, { });
+ }
+ TestSlice.prototype = {
+ __proto__: Slice.prototype
+ };
+ assert.doesNotThrow(function() {
+ new TestSlice();
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html b/chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html
new file mode 100644
index 00000000000..37b975b0b6e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/source_info/source_info.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.source_info', function() {
+ function JSSourceInfo(file, line, column, isNative, scriptId, state) {
+ tr.model.source_info.SourceInfo.call(this, file, line, column);
+
+ this.isNative_ = isNative;
+ this.scriptId_ = scriptId;
+ this.state_ = state;
+ }
+
+ JSSourceInfo.prototype = {
+ __proto__: tr.model.source_info.SourceInfo.prototype,
+
+ get state() {
+ return this.state_;
+ },
+
+ get isNative() {
+ return this.isNative_;
+ },
+
+ get scriptId() {
+ return this.scriptId_;
+ },
+
+ toString() {
+ const str = this.isNative_ ? '[native v8] ' : '';
+ return str +
+ tr.model.source_info.SourceInfo.prototype.toString.call(this);
+ }
+ };
+
+ const JSSourceState = {
+ COMPILED: 'compiled',
+ OPTIMIZABLE: 'optimizable',
+ OPTIMIZED: 'optimized',
+ UNKNOWN: 'unknown',
+ };
+
+ return {
+ JSSourceInfo,
+ JSSourceState,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html
new file mode 100644
index 00000000000..a8cf526d6c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.source_info', function() {
+ function SourceInfo(file, opt_line, opt_column) {
+ this.file_ = file;
+ this.line_ = opt_line || -1;
+ this.column_ = opt_column || -1;
+ }
+
+ SourceInfo.prototype = {
+ get file() {
+ return this.file_;
+ },
+
+ get line() {
+ return this.line_;
+ },
+
+ get column() {
+ return this.column_;
+ },
+
+ get domain() {
+ if (!this.file_) return undefined;
+ const domain = this.file_.match(/(.*:\/\/[^:\/]*)/i);
+ return domain ? domain[1] : undefined;
+ },
+
+ toString() {
+ let str = '';
+
+ if (this.file_) {
+ str += this.file_;
+ }
+ if (this.line_ > 0) {
+ str += ':' + this.line_;
+ }
+ if (this.column_ > 0) {
+ str += ':' + this.column_;
+ }
+ return str;
+ }
+ };
+
+ return {
+ SourceInfo,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html
new file mode 100644
index 00000000000..269de596d10
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/source_info/source_info.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('domain', function() {
+ const urlDomains = {
+ 'http://www.google.com': 'http://www.google.com',
+ 'http://www.google.com/bla': 'http://www.google.com',
+ 'http://www.google.com:1234': 'http://www.google.com',
+ 'bad url': undefined
+ };
+ for (const url in urlDomains) {
+ const sourceInfo = new tr.model.source_info.SourceInfo(url);
+ assert.strictEqual(urlDomains[url], sourceInfo.domain);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/stack_frame.html b/chromium/third_party/catapult/tracing/tracing/model/stack_frame.html
new file mode 100644
index 00000000000..b417d20f94c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/stack_frame.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ function StackFrame(parentFrame, id, title, colorId, opt_sourceInfo) {
+ if (id === undefined) {
+ throw new Error('id must be given');
+ }
+ this.parentFrame_ = parentFrame;
+ this.id = id;
+ this.title_ = title;
+ this.colorId = colorId;
+ this.children = [];
+ this.sourceInfo_ = opt_sourceInfo;
+
+ if (this.parentFrame_) {
+ this.parentFrame_.addChild(this);
+ }
+ }
+
+ StackFrame.prototype = {
+ get parentFrame() {
+ return this.parentFrame_;
+ },
+
+ get title() {
+ if (this.sourceInfo_) {
+ const src = this.sourceInfo_.toString();
+ return this.title_ + (src === '' ? '' : ' ' + src);
+ }
+ return this.title_;
+ },
+
+ /**
+ * Attempts to find the domain of the origin of the script either from this
+ * stack trace or from its ancestors.
+ */
+ get domain() {
+ let result = 'unknown';
+ if (this.sourceInfo_ && this.sourceInfo_.domain) {
+ result = this.sourceInfo_.domain;
+ }
+ if (result === 'unknown' && this.parentFrame) {
+ result = this.parentFrame.domain;
+ }
+ return result;
+ },
+
+ get sourceInfo() {
+ return this.sourceInfo_;
+ },
+
+ set parentFrame(parentFrame) {
+ if (this.parentFrame_) {
+ Polymer.dom(this.parentFrame_).removeChild(this);
+ }
+ this.parentFrame_ = parentFrame;
+ if (this.parentFrame_) {
+ this.parentFrame_.addChild(this);
+ }
+ },
+
+ addChild(child) {
+ this.children.push(child);
+ },
+
+ removeChild(child) {
+ const i = this.children.indexOf(child.id);
+ if (i === -1) {
+ throw new Error('omg');
+ }
+ this.children.splice(i, 1);
+ },
+
+ removeAllChildren() {
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].parentFrame_ = undefined;
+ }
+ this.children.splice(0, this.children.length);
+ },
+
+ /**
+ * Returns stackFrames where the most specific frame is first.
+ */
+ get stackTrace() {
+ const stack = [this];
+ let cur = this.parentFrame;
+ while (cur) {
+ stack.push(cur);
+ cur = cur.parentFrame;
+ }
+ return stack;
+ },
+
+ getUserFriendlyStackTrace() {
+ return this.stackTrace.map(function(x) { return x.title; });
+ }
+ };
+
+ return {
+ StackFrame,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html b/chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html
new file mode 100644
index 00000000000..d0b971ecb78
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/source_info/source_info.html">
+<link rel="import" href="/tracing/model/stack_frame.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('domain', function() {
+ const stackFrame1 = new tr.model.StackFrame(undefined, 1, '1', 1);
+ assert.strictEqual('unknown', stackFrame1.domain);
+
+ const sourceInfo = new tr.model.source_info.SourceInfo(
+ 'http://www.google.com:1234');
+ const stackFrame2 = new tr.model.StackFrame(
+ stackFrame1, 2, '2', 2, sourceInfo);
+ assert.strictEqual('http://www.google.com', stackFrame2.domain);
+
+ const stackFrame3 = new tr.model.StackFrame(stackFrame2, 3, '3', 3);
+ assert.strictEqual('http://www.google.com', stackFrame3.domain);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread.html b/chromium/third_party/catapult/tracing/tracing/model/thread.html
new file mode 100644
index 00000000000..54826bd29db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/thread.html
@@ -0,0 +1,358 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/async_slice_group.html">
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Thread class.
+ */
+tr.exportTo('tr.model', function() {
+ const AsyncSlice = tr.model.AsyncSlice;
+ const AsyncSliceGroup = tr.model.AsyncSliceGroup;
+ const SliceGroup = tr.model.SliceGroup;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const ThreadTimeSlice = tr.model.ThreadTimeSlice;
+
+ /**
+ * A Thread stores all the trace events collected for a particular
+ * thread. We organize the synchronous slices on a thread by "subrows," where
+ * subrow 0 has all the root slices, subrow 1 those nested 1 deep, and so on.
+ * The asynchronous slices are stored in an AsyncSliceGroup object.
+ *
+ * The slices stored on a Thread should be instances of
+ * ThreadSlice.
+ *
+ * @constructor
+ * @extends {tr.model.EventContainer}
+ */
+ function Thread(parent, tid) {
+ if (!parent) {
+ throw new Error('Parent must be provided.');
+ }
+
+ tr.model.EventContainer.call(this);
+ this.parent = parent;
+ this.sortIndex = 0;
+ this.tid = tid;
+ this.name = undefined;
+ this.samples_ = undefined; // Set during createSubSlices
+
+ this.sliceGroup = new SliceGroup(this, ThreadSlice, 'slices');
+ this.timeSlices = undefined;
+ this.kernelSliceGroup = new SliceGroup(
+ this, ThreadSlice, 'kernel-slices');
+ this.asyncSliceGroup = new AsyncSliceGroup(this, 'async-slices');
+ }
+
+ Thread.prototype = {
+ __proto__: tr.model.EventContainer.prototype,
+
+ get model() {
+ return this.parent.model;
+ },
+
+ get stableId() {
+ return this.parent.stableId + '.' + this.tid;
+ },
+
+ compareTo(that) {
+ return Thread.compare(this, that);
+ },
+
+ * childEventContainers() {
+ if (this.sliceGroup.length) {
+ yield this.sliceGroup;
+ }
+ if (this.kernelSliceGroup.length) {
+ yield this.kernelSliceGroup;
+ }
+ if (this.asyncSliceGroup.length) {
+ yield this.asyncSliceGroup;
+ }
+ },
+
+ * childEvents() {
+ if (this.timeSlices) {
+ yield* this.timeSlices;
+ }
+ },
+
+ iterateAllPersistableObjects(cb) {
+ cb(this);
+ if (this.sliceGroup.length) {
+ cb(this.sliceGroup);
+ }
+ this.asyncSliceGroup.viewSubGroups.forEach(cb);
+ },
+
+ /**
+ * Shifts all the timestamps inside this thread forward by the amount
+ * specified.
+ */
+ shiftTimestampsForward(amount) {
+ this.sliceGroup.shiftTimestampsForward(amount);
+
+ if (this.timeSlices) {
+ for (let i = 0; i < this.timeSlices.length; i++) {
+ const slice = this.timeSlices[i];
+ slice.start += amount;
+ }
+ }
+
+ this.kernelSliceGroup.shiftTimestampsForward(amount);
+ this.asyncSliceGroup.shiftTimestampsForward(amount);
+ },
+
+ /**
+ * Determines whether this thread is empty. If true, it usually implies
+ * that it should be pruned from the model.
+ */
+ get isEmpty() {
+ if (this.sliceGroup.length) return false;
+ if (this.sliceGroup.openSliceCount) return false;
+ if (this.timeSlices && this.timeSlices.length) return false;
+ if (this.kernelSliceGroup.length) return false;
+ if (this.asyncSliceGroup.length) return false;
+ if (this.samples_.length) return false;
+ return true;
+ },
+
+ /**
+ * Updates the bounds based on the
+ * current objects associated with the thread.
+ */
+ updateBounds() {
+ this.bounds.reset();
+
+ this.sliceGroup.updateBounds();
+ this.bounds.addRange(this.sliceGroup.bounds);
+
+ this.kernelSliceGroup.updateBounds();
+ this.bounds.addRange(this.kernelSliceGroup.bounds);
+
+ this.asyncSliceGroup.updateBounds();
+ this.bounds.addRange(this.asyncSliceGroup.bounds);
+
+ if (this.timeSlices && this.timeSlices.length) {
+ this.bounds.addValue(this.timeSlices[0].start);
+ this.bounds.addValue(
+ this.timeSlices[this.timeSlices.length - 1].end);
+ }
+
+ if (this.samples_ && this.samples_.length) {
+ this.bounds.addValue(this.samples_[0].start);
+ this.bounds.addValue(
+ this.samples_[this.samples_.length - 1].end);
+ }
+ },
+
+ addCategoriesToDict(categoriesDict) {
+ for (let i = 0; i < this.sliceGroup.length; i++) {
+ categoriesDict[this.sliceGroup.slices[i].category] = true;
+ }
+ 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);
+ },
+
+ /**
+ * @return {String} A user-friendly name for this thread.
+ */
+ get userFriendlyName() {
+ return this.name || this.tid;
+ },
+
+ /**
+ * @return {String} User friendly details about this thread.
+ */
+ 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;
+ },
+
+ /*
+ * Returns the index of the slice in the timeSlices array, or undefined.
+ */
+ 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;
+ }
+ // We assume that if a slice doesn't lie entirely inside the range
+ // of interest, then |func| is evenly distributed inside of the
+ // slice.
+ sum += func(slice) * fractionOfSliceInsideRangeOfInterest;
+ });
+ return sum;
+ },
+
+ /**
+ * Returns the total cpu time consumed within |range| by this thread.
+ */
+ getCpuTimeForRange(range) {
+ return this.sumOverToplevelSlicesInRange(
+ range, slice => slice.cpuDuration || 0);
+ },
+
+ /**
+ * Returns the total number of top-level slices within |range| in this
+ * thread. If a slice overlaps with |range| and is not completely inside it,
+ * then we attribute the portion that is inside the range only. For example,
+ * |getNumToplevelSlicesForRange| will return 1 + 1/3 when we have:
+ *
+ * 01 02 03 04 05 06 07 08 09 10
+ * <---------- range ---------->
+ * <- slice #1 -> <- slice #2 ->
+ */
+ 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_;
+ },
+
+ /**
+ * Returns substring of this.name from beginning to the first numeric
+ * character or the character '/'.
+ *
+ * Example:
+ * ThreadName12 -> ThreadName
+ * ThreadName/34123 -> ThreadName
+ * ThreadName1/34123 -> ThreadName
+ */
+ get type() {
+ const re = /^[^0-9|\/]+/;
+ const matches = re.exec(this.name);
+ if (matches && matches[0]) return matches[0];
+
+ // If a thread is named 42GPU, let's not try to find its type.
+ // We should fix the thread name.
+ throw new Error('Could not determine thread type for thread name ' +
+ this.name);
+ }
+ };
+
+ /**
+ * Comparison between threads that orders first by parent.compareTo,
+ * then by names, then by tid.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_slice.html b/chromium/third_party/catapult/tracing/tracing/model/thread_slice.html
new file mode 100644
index 00000000000..f67d20d3e5d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/thread_slice.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Thread class.
+ */
+tr.exportTo('tr.model', function() {
+ const Slice = tr.model.Slice;
+
+ /**
+ * A ThreadSlice represents an interval of time on a thread resource
+ * with associated nesting slice information.
+ *
+ * ThreadSlices are typically associated with a specific trace event pair on a
+ * specific thread.
+ * For example,
+ * TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms
+ * TRACE_EVENT_END0() at time=0.3ms
+ * This results in a single slice from 0.1 with duration 0.2 on a
+ * specific thread.
+ *
+ * @constructor
+ */
+ function ThreadSlice(cat, title, colorId, start, args, opt_duration,
+ opt_cpuStart, opt_cpuDuration, opt_argsStripped,
+ opt_bindId) {
+ Slice.call(this, cat, title, colorId, start, args, opt_duration,
+ opt_cpuStart, opt_cpuDuration, opt_argsStripped, opt_bindId);
+ // Do not modify this directly.
+ // subSlices is configured by SliceGroup.rebuildSubRows_.
+ this.subSlices = [];
+ }
+
+ ThreadSlice.prototype = {
+ __proto__: Slice.prototype,
+
+ get overlappingSamples() {
+ const samples = new tr.model.EventSet();
+ if (!this.parentContainer || !this.parentContainer.samples) {
+ return samples;
+ }
+ this.parentContainer.samples.forEach(function(sample) {
+ if (this.start <= sample.start && sample.start <= this.end) {
+ samples.push(sample);
+ }
+ }, this);
+ return samples;
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ ThreadSlice,
+ {
+ name: 'slice',
+ pluralName: 'slices'
+ });
+
+ return {
+ ThreadSlice,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html b/chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html
new file mode 100644
index 00000000000..b69efb79ac4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ThreadSlice = tr.model.ThreadSlice;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFakeThread = tr.c.TestUtils.newFakeThread;
+
+ test('getOverlappingSamples', function() {
+ const model = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+ const node = tr.c.TestUtils.newProfileNodes(m, ['fake']);
+ m.samples.push(
+ new tr.model.Sample(1, 'a_1', node, m.t2),
+ new tr.model.Sample(2, 'a_2', node, m.t2),
+ new tr.model.Sample(3, 'a_3', node, m.t2),
+ new tr.model.Sample(5, 'b', node, m.t2)
+ );
+ });
+ const threadSlice = newSliceEx({title: 'a', start: 0, end: 4,
+ type: tr.model.ThreadSlice});
+ threadSlice.parentContainer = model;
+ const samplesIter = threadSlice.overlappingSamples[Symbol.iterator]();
+ assert.strictEqual(samplesIter.next().value.title, 'a_1');
+ assert.strictEqual(samplesIter.next().value.title, 'a_2');
+ assert.strictEqual(samplesIter.next().value.title, 'a_3');
+ assert.strictEqual(samplesIter.next().done, true);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_test.html b/chromium/third_party/catapult/tracing/tracing/model/thread_test.html
new file mode 100644
index 00000000000..4d2bccf0790
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/thread_test.html
@@ -0,0 +1,209 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ThreadSlice = tr.model.ThreadSlice;
+ const Process = tr.model.Process;
+ const Thread = tr.model.Thread;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newThreadSlice = tr.c.TestUtils.newThreadSlice;
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+
+ test('threadBounds_Empty', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ t.updateBounds();
+ assert.isUndefined(t.bounds.min);
+ assert.isUndefined(t.bounds.max);
+ });
+
+ test('threadBounds_SubRow', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+ t.updateBounds();
+ assert.strictEqual(t.bounds.min, 1);
+ assert.strictEqual(t.bounds.max, 4);
+ });
+
+ test('threadBounds_AsyncSliceGroup', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+ t.asyncSliceGroup.push(newAsyncSlice(0.1, 5, t, t));
+ t.updateBounds();
+ assert.strictEqual(t.bounds.min, 0.1);
+ assert.strictEqual(t.bounds.max, 5.1);
+ });
+
+ test('threadBounds_Cpu', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ t.timeSlices = [newSliceEx({title: 'x', start: 0, duration: 1})];
+ t.updateBounds();
+ assert.strictEqual(t.bounds.min, 0);
+ assert.strictEqual(t.bounds.max, 1);
+ });
+
+ test('shiftTimestampsForwardWithCpu', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 0, {}, 3));
+ t.asyncSliceGroup.push(newAsyncSlice(0, 5, t, t));
+ t.timeSlices = [newSliceEx({title: 'x', start: 0, duration: 1})];
+
+ let shiftCount = 0;
+ t.asyncSliceGroup.shiftTimestampsForward = function(ts) {
+ if (ts === 0.32) {
+ shiftCount++;
+ }
+ };
+
+ t.shiftTimestampsForward(0.32);
+
+ assert.strictEqual(shiftCount, 1);
+ assert.strictEqual(t.sliceGroup.slices[0].start, 0.32);
+ assert.strictEqual(t.timeSlices[0].start, 0.32);
+ });
+
+ test('shiftTimestampsForwardWithoutCpu', function() {
+ const model = new tr.Model();
+ const t = new Thread(new Process(model, 7), 1);
+ t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 0, {}, 3));
+ t.asyncSliceGroup.push(newAsyncSlice(0, 5, t, t));
+
+ let shiftCount = 0;
+ t.asyncSliceGroup.shiftTimestampsForward = function(ts) {
+ if (ts === 0.32) {
+ shiftCount++;
+ }
+ };
+
+ t.shiftTimestampsForward(0.32);
+
+ assert.strictEqual(shiftCount, 1);
+ assert.strictEqual(t.sliceGroup.slices[0].start, 0.32);
+ });
+
+ test('getSchedulingStatsForRange', function() {
+ let scheduledThread = undefined;
+ let unscheduledThread = undefined;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ unscheduledThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ unscheduledThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'work', start: 0, duration: 20}));
+
+ scheduledThread = model.getOrCreateProcess(2).getOrCreateThread(2);
+ scheduledThread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'work', start: 0, duration: 20}));
+ scheduledThread.timeSlices = [
+ newThreadSlice(scheduledThread, SCHEDULING_STATE.RUNNING, 0, 3),
+ newThreadSlice(scheduledThread, SCHEDULING_STATE.RUNNABLE, 3, 5),
+ newThreadSlice(scheduledThread, SCHEDULING_STATE.RUNNING, 8, 2),
+ newThreadSlice(scheduledThread, SCHEDULING_STATE.SLEEPING, 10, 10)
+ ];
+ });
+
+ // thread without scheduling states
+ let stats = unscheduledThread.getSchedulingStatsForRange(0, 20);
+ assert.deepEqual(stats, {});
+
+ // no scheduling info
+ stats = scheduledThread.getSchedulingStatsForRange(50, 100);
+ assert.deepEqual(stats, {});
+
+ // simple query
+ stats = scheduledThread.getSchedulingStatsForRange(0, 3);
+ let expected = {};
+ expected[SCHEDULING_STATE.RUNNING] = 3;
+ assert.deepEqual(stats, expected);
+
+ // aggregation
+ stats = scheduledThread.getSchedulingStatsForRange(0, 20);
+ expected = {};
+ expected[SCHEDULING_STATE.RUNNING] = 5;
+ expected[SCHEDULING_STATE.RUNNABLE] = 5;
+ expected[SCHEDULING_STATE.SLEEPING] = 10;
+ assert.deepEqual(stats, expected);
+ });
+
+ test('getCpuTimeForRange', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ const sliceSpecs = [
+ {wallTimeBounds: [100, 200], cpuStart: 120, cpuDuration: 50},
+ {wallTimeBounds: [300, 600], cpuStart: 350, cpuDuration: 150}
+ ];
+ for (const sliceSpec of sliceSpecs) {
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ isTopLevel: true,
+ start: sliceSpec.wallTimeBounds[0],
+ duration: sliceSpec.wallTimeBounds[1] - sliceSpec.wallTimeBounds[0],
+ cpuStart: sliceSpec.cpuStart,
+ cpuDuration: sliceSpec.cpuDuration,
+ }));
+ }
+ });
+
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(1);
+ const bounds = new tr.b.math.Range.fromExplicitRange(150, 400);
+ // 1/2 of first slice + 1/3 of second slice
+ const expectedCpuTime = 25 + 50;
+
+ // Should be essentially equal, but choosing a very small epsilon 1e-7
+ // to allow for floating point errors.
+ assert.closeTo(thread.getCpuTimeForRange(bounds), expectedCpuTime, 1e-7);
+ });
+
+ test('typeGetterReturnsCorrectType', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread1 = process.getOrCreateThread(1);
+ const thread2 = process.getOrCreateThread(2);
+ const thread3 = process.getOrCreateThread(3);
+ const thread4 = process.getOrCreateThread(4);
+
+ thread1.name = 'ThreadName12';
+ thread2.name = 'ThreadName/34123';
+ thread3.name = 'ThreadName1/34123';
+ thread4.name = 'ThreadName';
+
+ assert.strictEqual(thread1.type, 'ThreadName');
+ assert.strictEqual(thread2.type, 'ThreadName');
+ assert.strictEqual(thread3.type, 'ThreadName');
+ assert.strictEqual(thread4.type, 'ThreadName');
+ });
+ });
+
+ test('typeGetterThrowsIfThreadNameStartsWithNumberOrSlash', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread1 = process.getOrCreateThread(1);
+ const thread2 = process.getOrCreateThread(2);
+ const thread3 = process.getOrCreateThread(3);
+
+ thread1.name = '123';
+ thread2.name = '42GPU';
+ thread3.name = '/123';
+
+ assert.throws(() => thread1.type);
+ assert.throws(() => thread2.type);
+ assert.throws(() => thread3.type);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html b/chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html
new file mode 100644
index 00000000000..69437c76819
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/slice.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ const Slice = tr.model.Slice;
+
+
+ const SCHEDULING_STATE = {
+ DEBUG: 'Debug',
+ EXIT_DEAD: 'Exit Dead',
+ RUNNABLE: 'Runnable',
+ RUNNING: 'Running',
+ SLEEPING: 'Sleeping',
+ STOPPED: 'Stopped',
+ TASK_DEAD: 'Task Dead',
+ UNINTR_SLEEP: 'Uninterruptible Sleep',
+ UNINTR_SLEEP_WAKE_KILL: 'Uninterruptible Sleep | WakeKill',
+ UNINTR_SLEEP_WAKING: 'Uninterruptible Sleep | Waking',
+ UNINTR_SLEEP_IO: 'Uninterruptible Sleep - Block I/O',
+ UNINTR_SLEEP_WAKE_KILL_IO: 'Uninterruptible Sleep | WakeKill - Block I/O',
+ UNINTR_SLEEP_WAKING_IO: 'Uninterruptible Sleep | Waking - Block I/O',
+ UNKNOWN: 'UNKNOWN',
+ WAKE_KILL: 'Wakekill',
+ WAKING: 'Waking',
+ ZOMBIE: 'Zombie'
+ };
+
+ /**
+ * A ThreadTimeSlice is a slice of time on a specific thread where that thread
+ * was running on a specific CPU, or in a specific sleep state.
+ *
+ * As a thread switches moves through its life, it sometimes goes to sleep and
+ * can't run. Other times, its runnable but isn't actually assigned to a CPU.
+ * Finally, sometimes it gets put on a CPU to actually execute. Each of these
+ * states is represented by a ThreadTimeSlice:
+ *
+ * Sleeping or runnable: cpuOnWhichThreadWasRunning is undefined
+ * Running: cpuOnWhichThreadWasRunning is set.
+ *
+ * @constructor
+ */
+ function ThreadTimeSlice(thread, schedulingState, cat,
+ start, args, opt_duration) {
+ Slice.call(this, cat, schedulingState,
+ this.getColorForState_(schedulingState),
+ start, args, opt_duration);
+ this.thread = thread;
+ this.schedulingState = schedulingState;
+ this.cpuOnWhichThreadWasRunning = undefined;
+ }
+
+ ThreadTimeSlice.prototype = {
+ __proto__: Slice.prototype,
+
+ getColorForState_(state) {
+ const getColorIdForReservedName =
+ tr.b.ColorScheme.getColorIdForReservedName;
+
+ switch (state) {
+ case SCHEDULING_STATE.RUNNABLE:
+ return getColorIdForReservedName('thread_state_runnable');
+ case SCHEDULING_STATE.RUNNING:
+ return getColorIdForReservedName('thread_state_running');
+ case SCHEDULING_STATE.SLEEPING:
+ return getColorIdForReservedName('thread_state_sleeping');
+ case SCHEDULING_STATE.DEBUG:
+ case SCHEDULING_STATE.EXIT_DEAD:
+ case SCHEDULING_STATE.STOPPED:
+ case SCHEDULING_STATE.TASK_DEAD:
+ case SCHEDULING_STATE.UNINTR_SLEEP:
+ case SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL:
+ case SCHEDULING_STATE.UNINTR_SLEEP_WAKING:
+ case SCHEDULING_STATE.UNKNOWN:
+ case SCHEDULING_STATE.WAKE_KILL:
+ case SCHEDULING_STATE.WAKING:
+ case SCHEDULING_STATE.ZOMBIE:
+ return getColorIdForReservedName('thread_state_uninterruptible');
+ case SCHEDULING_STATE.UNINTR_SLEEP_IO:
+ case SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL_IO:
+ case SCHEDULING_STATE.UNINTR_SLEEP_WAKING_IO:
+ return getColorIdForReservedName('thread_state_iowait');
+ default:
+ return getColorIdForReservedName('thread_state_unknown');
+ }
+ },
+
+ get analysisTypeName() {
+ return 'tr.ui.analysis.ThreadTimeSlice';
+ },
+
+ getAssociatedCpuSlice() {
+ if (!this.cpuOnWhichThreadWasRunning) return undefined;
+ const cpuSlices = this.cpuOnWhichThreadWasRunning.slices;
+ for (let i = 0; i < cpuSlices.length; i++) {
+ const cpuSlice = cpuSlices[i];
+ if (cpuSlice.start !== this.start) continue;
+ if (cpuSlice.duration !== this.duration) continue;
+ return cpuSlice;
+ }
+ return undefined;
+ },
+
+ getCpuSliceThatTookCpu() {
+ if (this.cpuOnWhichThreadWasRunning) return undefined;
+ let curIndex = this.thread.indexOfTimeSlice(this);
+ let cpuSliceWhenLastRunning;
+ while (curIndex >= 0) {
+ const curSlice = this.thread.timeSlices[curIndex];
+ if (!curSlice.cpuOnWhichThreadWasRunning) {
+ curIndex--;
+ continue;
+ }
+ cpuSliceWhenLastRunning = curSlice.getAssociatedCpuSlice();
+ break;
+ }
+ if (!cpuSliceWhenLastRunning) return undefined;
+
+ const cpu = cpuSliceWhenLastRunning.cpu;
+ const indexOfSliceOnCpuWhenLastRunning =
+ cpu.indexOf(cpuSliceWhenLastRunning);
+ const nextRunningSlice = cpu.slices[indexOfSliceOnCpuWhenLastRunning + 1];
+ if (!nextRunningSlice) return undefined;
+ if (Math.abs(nextRunningSlice.start - cpuSliceWhenLastRunning.end) <
+ 0.00001) {
+ return nextRunningSlice;
+ }
+ return undefined;
+ }
+ };
+
+ tr.model.EventRegistry.register(
+ ThreadTimeSlice,
+ {
+ name: 'threadTimeSlice',
+ pluralName: 'threadTimeSlices'
+ });
+
+
+ return {
+ ThreadTimeSlice,
+ SCHEDULING_STATE,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html
new file mode 100644
index 00000000000..3c05b08d4a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the TimeToObjectInstanceMap class.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * Tracks all the instances associated with a given ID over its lifetime.
+ *
+ * A scoped id can be used multiple times throughout a trace, referring to
+ * different objects at different times. This data structure does the
+ * bookkeeping to figure out what ObjectInstance is referred to at a given
+ * timestamp.
+ *
+ * @constructor
+ */
+ function TimeToObjectInstanceMap(
+ createObjectInstanceFunction, parent, scopedId) {
+ this.createObjectInstanceFunction_ = createObjectInstanceFunction;
+ this.parent = parent;
+ this.scopedId = scopedId;
+ this.instances = [];
+ }
+
+ TimeToObjectInstanceMap.prototype = {
+ idWasCreated(category, name, ts) {
+ if (this.instances.length === 0) {
+ this.instances.push(this.createObjectInstanceFunction_(
+ this.parent, this.scopedId, category, name, ts));
+ this.instances[0].creationTsWasExplicit = true;
+ return this.instances[0];
+ }
+
+ let lastInstance = this.instances[this.instances.length - 1];
+ if (ts < lastInstance.deletionTs) {
+ throw new Error('Mutation of the TimeToObjectInstanceMap must be ' +
+ 'done in ascending timestamp order.');
+ }
+ lastInstance = this.createObjectInstanceFunction_(
+ this.parent, this.scopedId, category, name, ts);
+ lastInstance.creationTsWasExplicit = true;
+ this.instances.push(lastInstance);
+ return lastInstance;
+ },
+
+ addSnapshot(category, name, ts, args, opt_baseTypeName) {
+ if (this.instances.length === 0) {
+ this.instances.push(this.createObjectInstanceFunction_(
+ this.parent, this.scopedId, category, name, ts, opt_baseTypeName));
+ }
+
+ const i = tr.b.findIndexInSortedIntervals(
+ this.instances,
+ function(inst) { return inst.creationTs; },
+ function(inst) { return inst.deletionTs - inst.creationTs; },
+ ts);
+
+ let instance;
+ if (i < 0) {
+ instance = this.instances[0];
+ if (ts > instance.deletionTs ||
+ instance.creationTsWasExplicit) {
+ throw new Error(
+ 'At the provided timestamp, no instance was still alive');
+ }
+
+ if (instance.snapshots.length !== 0) {
+ throw new Error(
+ 'Cannot shift creationTs forward, ' +
+ 'snapshots have been added. First snap was at ts=' +
+ instance.snapshots[0].ts + ' and creationTs was ' +
+ instance.creationTs);
+ }
+ instance.creationTs = ts;
+ } else if (i >= this.instances.length) {
+ instance = this.instances[this.instances.length - 1];
+ if (ts >= instance.deletionTs) {
+ // The snap is added after our oldest and deleted instance. This means
+ // that this is a new implicit instance.
+ instance = this.createObjectInstanceFunction_(
+ this.parent, this.scopedId, category, name, ts, opt_baseTypeName);
+ this.instances.push(instance);
+ } else {
+ // If the ts is before the last objects deletion time, then the caller
+ // is trying to add a snapshot when there may have been an instance
+ // alive. In that case, try to move an instance's creationTs to
+ // include this ts, provided that it has an implicit creationTs.
+
+ // Search backward from the right for an instance that was definitely
+ // deleted before this ts. Any time an instance is found that has a
+ // moveable creationTs
+ let lastValidIndex;
+ for (let i = this.instances.length - 1; i >= 0; i--) {
+ const tmp = this.instances[i];
+ if (ts >= tmp.deletionTs) break;
+ if (tmp.creationTsWasExplicit === false &&
+ tmp.snapshots.length === 0) {
+ lastValidIndex = i;
+ }
+ }
+ if (lastValidIndex === undefined) {
+ throw new Error(
+ 'Cannot add snapshot. No instance was alive that was mutable.');
+ }
+ instance = this.instances[lastValidIndex];
+ instance.creationTs = ts;
+ }
+ } else {
+ instance = this.instances[i];
+ }
+
+ return instance.addSnapshot(ts, args, name, opt_baseTypeName);
+ },
+
+ get lastInstance() {
+ if (this.instances.length === 0) return undefined;
+ return this.instances[this.instances.length - 1];
+ },
+
+ idWasDeleted(category, name, ts) {
+ if (this.instances.length === 0) {
+ this.instances.push(this.createObjectInstanceFunction_(
+ this.parent, this.scopedId, category, name, ts));
+ }
+ let lastInstance = this.instances[this.instances.length - 1];
+ if (ts < lastInstance.creationTs) {
+ throw new Error('Cannot delete an id before it was created');
+ }
+ if (lastInstance.deletionTs === Number.MAX_VALUE) {
+ lastInstance.wasDeleted(ts);
+ return lastInstance;
+ }
+
+ if (ts < lastInstance.deletionTs) {
+ throw new Error('id was already deleted earlier.');
+ }
+
+ // A new instance was deleted with no snapshots in-between.
+ // Create an instance then kill it.
+ lastInstance = this.createObjectInstanceFunction_(
+ this.parent, this.scopedId, category, name, ts);
+ this.instances.push(lastInstance);
+ lastInstance.wasDeleted(ts);
+ return lastInstance;
+ },
+
+ getInstanceAt(ts) {
+ const i = tr.b.findIndexInSortedIntervals(
+ this.instances,
+ function(inst) { return inst.creationTs; },
+ function(inst) { return inst.deletionTs - inst.creationTs; },
+ ts);
+ if (i < 0) {
+ if (this.instances[0].creationTsWasExplicit) {
+ return undefined;
+ }
+ return this.instances[0];
+ } else if (i >= this.instances.length) {
+ return undefined;
+ }
+ return this.instances[i];
+ }
+ };
+
+ return {
+ TimeToObjectInstanceMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html
new file mode 100644
index 00000000000..70cd9ad0d5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/model/time_to_object_instance_map.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const createObjectInstance = function(
+ parent, id, category, name, creationTs) {
+ return new tr.model.ObjectInstance(
+ parent, id, category, name, creationTs);
+ };
+
+ test('timeToObjectInstanceMap', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ m.addSnapshot('cat', 'name', 10, 'a1');
+ m.addSnapshot('cat', 'name', 20, 'a2');
+ m.idWasDeleted('cat', 'name', 30);
+ m.addSnapshot('cat', 'name', 40, 'b');
+
+ assert.strictEqual(m.instances.length, 2);
+
+ const i0 = m.getInstanceAt(0);
+ const i10 = m.getInstanceAt(10);
+ assert.strictEqual(i0, i10);
+
+ assert.isDefined(i10);
+ assert.strictEqual(i10.snapshots.length, 2);
+ assert.strictEqual(i10.snapshots[0].args, 'a1');
+ assert.strictEqual(i10.snapshots[1].args, 'a2');
+
+ assert.strictEqual(i10.deletionTs, 30);
+
+ const i15 = m.getInstanceAt(15);
+ assert.strictEqual(i15, i10);
+
+ const i20 = m.getInstanceAt(20);
+ assert.strictEqual(i20, i10);
+
+ const i30 = m.getInstanceAt(30);
+ assert.isUndefined(i30);
+
+ const i35 = m.getInstanceAt(35);
+ assert.isUndefined(i35);
+
+ const i40 = m.getInstanceAt(40);
+ assert.isDefined(i40);
+ assert.notEqual(i40, i10);
+ assert.strictEqual(i40.snapshots.length, 1);
+ assert.strictEqual(i40.creationTs, 40);
+ assert.strictEqual(i40.deletionTs, Number.MAX_VALUE);
+
+ const i41 = m.getInstanceAt(41);
+ assert.strictEqual(i40, i41);
+ });
+
+ test('timeToObjectInstanceMapsBoundsLogic', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ m.addSnapshot('cat', 'name', 10, 'a1');
+ m.addSnapshot('cat', 'name', 20, 'a2');
+ m.idWasDeleted('cat', 'name', 30);
+ m.addSnapshot('cat', 'name', 40, 'b');
+ m.addSnapshot('cat', 'name', 41, 'b');
+
+ m.instances.forEach(function(i) { i.updateBounds(); });
+
+ const iA = m.getInstanceAt(10);
+ assert.strictEqual(iA.bounds.min, 10);
+ assert.strictEqual(iA.bounds.max, 30);
+
+ const iB = m.getInstanceAt(40);
+ assert.strictEqual(iB.bounds.min, 40);
+ assert.strictEqual(iB.bounds.max, 41);
+ });
+
+ test('earlySnapshot', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ const i10 = m.idWasCreated('cat', 'name', 10, 'a1');
+ m.idWasDeleted('cat', 'name', 20);
+
+ assert.throws(function() {
+ m.addSnapshot('cat', 'name', 5, 'a1');
+ });
+ assert.strictEqual(i10.creationTs, 10);
+ assert.strictEqual(i10.deletionTs, 20);
+ });
+
+ test('earlySnapshotWithImplicitCreate', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ const i10 = m.idWasDeleted('cat', 'name', 20);
+ m.addSnapshot('cat', 'name', 5, 'a1');
+ assert.strictEqual(i10.creationTs, 5);
+ assert.strictEqual(i10.deletionTs, 20);
+ });
+
+ test('getInstanceBeforeCreationImplicitCreate', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ const i10 = m.idWasCreated('cat', 'name', 10, 'a1');
+ m.idWasDeleted('cat', 'name', 20);
+ assert.isUndefined(m.getInstanceAt(5));
+ });
+
+ test('getInstanceBeforeCreationImplicitCreateWithSnapshot', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ const s5 = m.addSnapshot('cat', 'name', 5, 'a1');
+ const i10 = m.idWasDeleted('cat', 'name', 20);
+ assert.strictEqual(m.getInstanceAt(5), i10);
+ });
+
+ test('successiveDeletions', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ const i20 = m.idWasDeleted('cat', 'name', 20);
+ const i30 = m.idWasDeleted('cat', 'name', 30);
+ const i40 = m.idWasDeleted('cat', 'name', 40);
+ assert.strictEqual(i20.creationTs, 20);
+ assert.isFalse(i20.creationTsWasExplicit);
+ assert.strictEqual(i20.deletionTs, 20);
+ assert.isTrue(i20.deletionTsWasExplicit);
+
+ assert.strictEqual(i30.creationTs, 30);
+ assert.isFalse(i30.creationTsWasExplicit);
+ assert.strictEqual(i30.deletionTs, 30);
+ assert.isTrue(i30.deletionTsWasExplicit);
+
+
+ assert.strictEqual(i40.creationTs, 40);
+ assert.isFalse(i40.creationTsWasExplicit);
+ assert.strictEqual(i40.deletionTs, 40);
+ assert.isTrue(i40.deletionTsWasExplicit);
+ });
+
+ test('snapshotAfterDeletion', function() {
+ const m = new tr.model.TimeToObjectInstanceMap(
+ createObjectInstance, {}, 7);
+ const i10 = m.idWasCreated('cat', 'name', 10, 'a1');
+ m.idWasDeleted('cat', 'name', 20);
+
+ const s25 = m.addSnapshot('cat', 'name', 25, 'a1');
+ const i25 = s25.objectInstance;
+
+ assert.strictEqual(i10.creationTs, 10);
+ assert.strictEqual(i10.deletionTs, 20);
+ assert.notEqual(i25, i10);
+ assert.strictEqual(i25.creationTs, 25);
+ assert.strictEqual(i25.deletionTs, Number.MAX_VALUE);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/model/timed_event.html b/chromium/third_party/catapult/tracing/tracing/model/timed_event.html
new file mode 100644
index 00000000000..4916162a4e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/timed_event.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/model/event.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model', function() {
+ /**
+ * TimedEvent is a base type for any entity in the trace model with a specific
+ * start and duration.
+ *
+ * @constructor
+ */
+ function TimedEvent(start) {
+ tr.model.Event.call(this);
+ this.start = start;
+ this.duration = 0;
+ this.cpuStart = undefined;
+ this.cpuDuration = undefined;
+ // The set of contexts this event belongs to (order is unimportant). This
+ // array should never be modified.
+ this.contexts = Object.freeze([]);
+ }
+
+ TimedEvent.prototype = {
+ __proto__: tr.model.Event.prototype,
+
+ get end() {
+ return this.start + this.duration;
+ },
+
+ get boundsRange() {
+ return tr.b.math.Range.fromExplicitRange(this.start, this.end);
+ },
+
+ addBoundsToRange(range) {
+ range.addValue(this.start);
+ range.addValue(this.end);
+ },
+
+ // TODO(charliea): Can this be implemented in terms of Event.range()?
+ // Returns true if 'that' TimedEvent is fully contained within 'this' timed
+ // event.
+ bounds(that, opt_precisionUnit) {
+ if (opt_precisionUnit === undefined) {
+ opt_precisionUnit = tr.b.TimeDisplayModes.ms;
+ }
+
+ const startsBefore = opt_precisionUnit.roundedLess(
+ that.start, this.start);
+ const endsAfter = opt_precisionUnit.roundedLess(this.end, that.end);
+ return !startsBefore && !endsAfter;
+ }
+ };
+
+ return {
+ TimedEvent,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html b/chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html
new file mode 100644
index 00000000000..f635dcf677d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('bounds_startPrecision', function() {
+ const unit = tr.b.TimeDisplayModes;
+
+ const outer = new tr.model.TimedEvent(10.0001);
+ outer.duration = 0.9999;
+ const inner = new tr.model.TimedEvent(10.0000);
+ inner.duration = 1.0000;
+
+ assert.isTrue(outer.bounds(inner));
+ assert.isTrue(outer.bounds(inner, unit.ms));
+
+ assert.isFalse(outer.bounds(inner, unit.ns));
+ });
+
+ test('bounds_endPrecision', function() {
+ const unit = tr.b.TimeDisplayModes;
+
+ const outer = new tr.model.TimedEvent(10.0000);
+ outer.duration = 0.9999;
+ const inner = new tr.model.TimedEvent(10.0000);
+ inner.duration = 1.0000;
+
+ assert.isTrue(outer.bounds(inner));
+ assert.isTrue(outer.bounds(inner, unit.ms));
+
+ assert.isFalse(outer.bounds(inner, unit.ns));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html
new file mode 100644
index 00000000000..a3fced6de2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ function AnimationExpectation(
+ parentModel, initiatorTitle, start, duration) {
+ tr.model.um.UserExpectation.call(
+ this, parentModel, initiatorTitle, start, duration);
+ this.frameEvents_ = undefined;
+ }
+
+ AnimationExpectation.prototype = {
+ __proto__: tr.model.um.UserExpectation.prototype,
+ constructor: AnimationExpectation,
+
+ get frameEvents() {
+ if (this.frameEvents_) {
+ return this.frameEvents_;
+ }
+
+ this.frameEvents_ = new tr.model.EventSet();
+
+ this.associatedEvents.forEach(function(event) {
+ if (event.title === tr.model.helpers.IMPL_RENDERING_STATS) {
+ this.frameEvents_.push(event);
+ }
+ }, this);
+
+ return this.frameEvents_;
+ }
+ };
+
+ tr.model.um.UserExpectation.subTypes.register(AnimationExpectation, {
+ stageTitle: 'Animation',
+ colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_animation')
+ });
+
+ return {
+ AnimationExpectation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html
new file mode 100644
index 00000000000..2312d46b62a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ function IdleExpectation(parentModel, start, duration) {
+ const initiatorTitle = '';
+ tr.model.um.UserExpectation.call(
+ this, parentModel, initiatorTitle, start, duration);
+ }
+
+ IdleExpectation.prototype = {
+ __proto__: tr.model.um.UserExpectation.prototype,
+ constructor: IdleExpectation
+ };
+
+ tr.model.um.UserExpectation.subTypes.register(IdleExpectation, {
+ stageTitle: 'Idle',
+ colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_idle')
+ });
+
+ return {
+ IdleExpectation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html
new file mode 100644
index 00000000000..fb5298a46ed
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'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);
+
+ // |renderProcess| is the renderer process that contains the loading
+ // RenderFrame.
+ this.renderProcess = renderer;
+
+ // |renderMainThread| is the CrRendererMain thread in the |renderProcess|
+ // that contains the loading RenderFrame.
+ this.renderMainThread = undefined;
+
+ // |routingId| identifies the loading RenderFrame within the renderer
+ // process.
+ this.routingId = undefined;
+
+ // |parentRoutingId| identifies the RenderFrame that created and contains
+ // the loading RenderFrame.
+ this.parentRoutingId = undefined;
+
+ // |loadFinishedEvent|, if present, signals that this is a main frame.
+ this.loadFinishedEvent = undefined;
+
+ // Startup LoadExpectations do not have renderProcess, routingId, or
+ // parentRoutingId. Maybe RenderLoadExpectation should be a separate class?
+
+ // Navigation start event. The start of this event is the start time of
+ // load expectation.
+ this.navigationStart = navigationStart;
+
+ // First meaningful event corresponding to the navigation start event.
+ this.fmpEvent = fmpEvent;
+
+ // DomcontentLoadedEndEvent corresponding to the navigation start event.
+ this.domContentLoadedEndEvent = dclEndEvent;
+
+ // The computed firstCpuIdleTime. Please look at time_to_interactive.html
+ // for further details about this.
+ this.firstCpuIdleTime = cpuIdleTime;
+
+ // The time at which renderer is interactive. Please look at
+ // time_to_interactive.html for further details on how this is computed.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html
new file mode 100644
index 00000000000..83faacb27c3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ function ResponseExpectation(
+ parentModel, initiatorTitle, start, duration, opt_isAnimationBegin) {
+ tr.model.um.UserExpectation.call(
+ this, parentModel, initiatorTitle, start, duration);
+ this.isAnimationBegin = opt_isAnimationBegin || false;
+ }
+
+ ResponseExpectation.prototype = {
+ __proto__: tr.model.um.UserExpectation.prototype,
+ constructor: ResponseExpectation
+ };
+
+ tr.model.um.UserExpectation.subTypes.register(ResponseExpectation, {
+ stageTitle: 'Response',
+ colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_response')
+ });
+
+ return {
+ ResponseExpectation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html
new file mode 100644
index 00000000000..61da1e26363
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ /**
+ * Segment represents a range of time during which the set of active
+ * UserExpectations does not change. Segments are guaranteed to not overlap,
+ * whereas UserExpectations can overlap. After UserModelBuilder builds the
+ * UserExpectations in the model, it segments the timeline into
+ * non-overlapping Segments and adds the constituent UserExpectations to each
+ * Segment.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html
new file mode 100644
index 00000000000..809debc3fb3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ function StartupExpectation(parentModel, start, duration) {
+ tr.model.um.UserExpectation.call(
+ this, parentModel, '', start, duration);
+ }
+
+ StartupExpectation.prototype = {
+ __proto__: tr.model.um.UserExpectation.prototype,
+ constructor: StartupExpectation
+ };
+
+ tr.model.um.UserExpectation.subTypes.register(StartupExpectation, {
+ stageTitle: 'Startup',
+ colorId: tr.b.ColorScheme.getColorIdForReservedName('startup')
+ });
+
+ return {
+ StartupExpectation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html
new file mode 100644
index 00000000000..4bd48ffba58
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Stub version of UserExpectation for testing.
+ */
+tr.exportTo('tr.model.um', function() {
+ function StubExpectation(args) {
+ this.stageTitle_ = args.stageTitle || 'Idle';
+ this.initiatorTitle_ = args.initiatorTitle || '';
+
+ this.title_ = args.title;
+ if (!this.title_) {
+ const defaultTitle = [];
+ if (this.initiatorTitle_) {
+ defaultTitle.push(this.initiatorTitle_);
+ }
+ if (this.stageTitle_) {
+ defaultTitle.push(this.stageTitle_);
+ }
+ this.title_ = defaultTitle.join(' ') || 'title';
+ }
+
+ this.normalizedUserComfort_ = args.normalizedUserComfort || 0;
+ this.normalizedEfficiency_ = args.normalizedEfficiency || 0;
+
+ const sd = tr.c.TestUtils.getStartAndDurationFromDict(args);
+
+ tr.model.um.UserExpectation.call(
+ this, args.parentModel, this.initiatorTitle, sd.start, sd.duration);
+
+ // Must be set after base class call.
+ this.colorId_ = args.colorId || 0;
+
+ if (args.associatedEvents) {
+ args.associatedEvents.forEach(function(event) {
+ this.associatedEvents.push(event);
+ }, this);
+ }
+ }
+
+ StubExpectation.prototype = {
+ __proto__: tr.model.um.UserExpectation.prototype,
+
+ get colorId() {
+ return this.colorId_;
+ },
+
+ get title() {
+ return this.title_;
+ },
+
+ get stageTitle() {
+ return this.stageTitle_;
+ },
+
+ get initiatorTitle() {
+ return this.initiatorTitle_;
+ }
+ };
+
+ return {
+ StubExpectation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html
new file mode 100644
index 00000000000..04363da142a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/compound_event_selection_state.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/timed_event.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ const CompoundEventSelectionState = tr.model.CompoundEventSelectionState;
+
+ function UserExpectation(parentModel, initiatorType, start, duration) {
+ tr.model.TimedEvent.call(this, start);
+ this.associatedEvents = new tr.model.EventSet();
+ this.duration = duration;
+ this.initiatorType_ = initiatorType;
+ this.parentModel = parentModel;
+ this.typeInfo_ = undefined;
+
+ // sourceEvents are the ones that caused the UserModelBuilder to create this
+ // UserExpectation.
+ this.sourceEvents = new tr.model.EventSet();
+ }
+
+ // Strings used to name UEs.
+ const INITIATOR_TYPE = {
+ KEYBOARD: 'Keyboard',
+ MOUSE: 'Mouse',
+ MOUSE_WHEEL: 'MouseWheel',
+ TAP: 'Tap',
+ PINCH: 'Pinch',
+ FLING: 'Fling',
+ TOUCH: 'Touch',
+ SCROLL: 'Scroll',
+ CSS: 'CSS',
+ WEBGL: 'WebGL',
+ VIDEO: 'Video',
+ VR: 'VR',
+ };
+
+ UserExpectation.prototype = {
+ __proto__: tr.model.TimedEvent.prototype,
+
+ computeCompoundEvenSelectionState(selection) {
+ let cess = CompoundEventSelectionState.NOT_SELECTED;
+ if (selection.contains(this)) {
+ cess |= CompoundEventSelectionState.EVENT_SELECTED;
+ }
+
+ if (this.associatedEvents.intersectionIsEmpty(selection)) {
+ return cess;
+ }
+
+ const allContained = this.associatedEvents.every(function(event) {
+ return selection.contains(event);
+ });
+
+ if (allContained) {
+ cess |= CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED;
+ } else {
+ cess |= CompoundEventSelectionState.SOME_ASSOCIATED_EVENTS_SELECTED;
+ }
+ return cess;
+ },
+
+ // Returns samples which are overlapping with V8.Execute
+ get associatedSamples() {
+ const samples = new tr.model.EventSet();
+ this.associatedEvents.forEach(function(event) {
+ if (event instanceof tr.model.ThreadSlice) {
+ samples.addEventSet(event.overlappingSamples);
+ }
+ });
+ return samples;
+ },
+
+ get userFriendlyName() {
+ return this.title + ' User Expectation at ' +
+ tr.b.Unit.byName.timeStampInMs.format(this.start);
+ },
+
+ get stableId() {
+ return ('UserExpectation.' + this.guid);
+ },
+
+ get typeInfo() {
+ if (!this.typeInfo_) {
+ this.typeInfo_ = UserExpectation.subTypes.findTypeInfo(
+ this.constructor);
+ }
+
+ // If you set Subclass.prototype = {}, then you must explicitly specify
+ // constructor in that prototype object!
+ // http://javascript.info/tutorial/constructor
+
+ if (!this.typeInfo_) {
+ throw new Error('Unregistered UserExpectation');
+ }
+
+ return this.typeInfo_;
+ },
+
+ get colorId() {
+ return this.typeInfo.metadata.colorId;
+ },
+
+ get stageTitle() {
+ return this.typeInfo.metadata.stageTitle;
+ },
+
+ get initiatorType() {
+ return this.initiatorType_;
+ },
+
+ get title() {
+ if (!this.initiatorType) {
+ return this.stageTitle;
+ }
+
+ return this.initiatorType + ' ' + this.stageTitle;
+ },
+
+ /**
+ * Returns the sum of the number of CPU ms spent by this UserExpectation.
+ */
+ get totalCpuMs() {
+ let cpuMs = 0;
+ this.associatedEvents.forEach(function(event) {
+ if (event.cpuSelfTime) {
+ cpuMs += event.cpuSelfTime;
+ }
+ });
+ return cpuMs;
+ }
+ };
+
+ const subTypes = {};
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(subTypes, options);
+
+ subTypes.addEventListener('will-register', function(e) {
+ const metadata = e.typeInfo.metadata;
+
+ if (metadata.stageTitle === undefined) {
+ throw new Error('Registered UserExpectations must provide ' +
+ 'stageTitle');
+ }
+
+ if (metadata.colorId === undefined) {
+ throw new Error('Registered UserExpectations must provide ' +
+ 'colorId');
+ }
+ });
+
+ tr.model.EventRegistry.register(
+ UserExpectation,
+ {
+ name: 'userExpectation',
+ pluralName: 'userExpectations',
+ subTypes
+ });
+
+ return {
+ UserExpectation,
+ INITIATOR_TYPE,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html
new file mode 100644
index 00000000000..38b6e13a00c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_container.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.model.um', function() {
+ class UserModel extends tr.model.EventContainer {
+ constructor(parentModel) {
+ super();
+ this.parentModel_ = parentModel;
+ this.expectations_ = new tr.model.EventSet();
+ this.segments_ = [];
+ }
+
+ get stableId() {
+ return 'UserModel';
+ }
+
+ get parentModel() {
+ return this.parentModel_;
+ }
+
+ sortExpectations() {
+ this.expectations_.sortEvents((x, y) => (x.start - y.start));
+ }
+
+ get expectations() {
+ return this.expectations_;
+ }
+
+ shiftTimestampsForward(amount) {
+ }
+
+ addCategoriesToDict(categoriesDict) {
+ }
+
+ get segments() {
+ return this.segments_;
+ }
+
+ * childEvents() {
+ yield* this.expectations;
+ }
+
+ * childEventContainers() {
+ }
+
+ updateBounds() {
+ this.bounds.reset();
+ for (const expectation of this.expectations) {
+ expectation.addBoundsToRange(this.bounds);
+ }
+ }
+
+ /**
+ * Return a new array of new Segments by merging adjacent segments when
+ * |getKeyForSegment| returns identical keys.
+ * |getKeyForSegment| is called with each Segment and the index of that
+ * Segment.
+ *
+ * @param {!function(!tr.model.um.Segment, number):*} getKeyForSegment
+ * @return {!Array.<!tr.model.um.Segment>}
+ */
+ resegment(getKeyForSegment) {
+ const newSegments = [];
+ let prevKey = undefined;
+ let prevSegment = undefined;
+ for (let i = 0; i < this.segments.length; ++i) {
+ const segment = this.segments[i];
+ const key = getKeyForSegment(segment, i);
+ if (prevSegment !== undefined && key === prevKey) {
+ prevSegment.addSegment(segment);
+ } else {
+ prevSegment = segment.clone();
+ newSegments.push(prevSegment);
+ }
+ prevKey = key;
+ }
+ return newSegments;
+ }
+ }
+
+ return {
+ UserModel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html
new file mode 100644
index 00000000000..c2a09c5074e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/model/user_model/segment.html">
+<link rel="import" href="/tracing/model/user_model/user_model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('resegment', function() {
+ const userModel = new tr.model.um.UserModel(undefined);
+ userModel.segments.push(new tr.model.um.Segment(0, 1));
+ userModel.segments.push(new tr.model.um.Segment(1, 1));
+ userModel.segments.push(new tr.model.um.Segment(2, 1));
+ userModel.segments.push(new tr.model.um.Segment(3, 1));
+
+ userModel.segments[0].expectations.push('a');
+ userModel.segments[1].expectations.push('b');
+ userModel.segments[2].expectations.push('c');
+ userModel.segments[3].expectations.push('d');
+
+ const newSegments = userModel.resegment(
+ (segment, index) => Math.floor(index / 2));
+
+ assert.lengthOf(newSegments, 2);
+ assert.strictEqual(0, newSegments[0].start);
+ assert.strictEqual(2, newSegments[0].end);
+ assert.strictEqual(2, newSegments[1].start);
+ assert.strictEqual(4, newSegments[1].end);
+
+ assert.lengthOf(newSegments[0].expectations, 2);
+ assert.lengthOf(newSegments[1].expectations, 2);
+
+ assert.strictEqual('a', newSegments[0].expectations[0]);
+ assert.strictEqual('b', newSegments[0].expectations[1]);
+ assert.strictEqual('c', newSegments[1].expectations[0]);
+ assert.strictEqual('d', newSegments[1].expectations[1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/vm_region.html b/chromium/third_party/catapult/tracing/tracing/model/vm_region.html
new file mode 100644
index 00000000000..4b02f7c24cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/vm_region.html
@@ -0,0 +1,444 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides classes for representing and classifying VM regions.
+ *
+ * See https://goo.gl/5SSPv0 for more details.
+ */
+tr.exportTo('tr.model', function() {
+ /**
+ * A single virtual memory region (also called a memory map).
+ *
+ * @constructor
+ */
+ function VMRegion(startAddress, sizeInBytes, protectionFlags,
+ mappedFile, byteStats) {
+ this.startAddress = startAddress;
+ this.sizeInBytes = sizeInBytes;
+ this.protectionFlags = protectionFlags;
+ this.mappedFile = mappedFile || '';
+ this.byteStats = byteStats || {};
+ }
+
+ VMRegion.PROTECTION_FLAG_READ = 4;
+ VMRegion.PROTECTION_FLAG_WRITE = 2;
+ VMRegion.PROTECTION_FLAG_EXECUTE = 1;
+ VMRegion.PROTECTION_FLAG_MAYSHARE = 128;
+
+ VMRegion.prototype = {
+ get uniqueIdWithinProcess() {
+ // This value is assumed to be unique within a process.
+ return this.mappedFile + '#' + this.startAddress;
+ },
+
+ get protectionFlagsToString() {
+ if (this.protectionFlags === undefined) return undefined;
+ return (
+ (this.protectionFlags & VMRegion.PROTECTION_FLAG_READ ? 'r' : '-') +
+ (this.protectionFlags & VMRegion.PROTECTION_FLAG_WRITE ? 'w' : '-') +
+ (this.protectionFlags & VMRegion.PROTECTION_FLAG_EXECUTE ?
+ 'x' : '-') +
+ (this.protectionFlags & VMRegion.PROTECTION_FLAG_MAYSHARE ? 's' : 'p')
+ );
+ }
+ };
+
+ VMRegion.fromDict = function(dict) {
+ return new VMRegion(
+ dict.startAddress,
+ dict.sizeInBytes,
+ dict.protectionFlags,
+ dict.mappedFile,
+ dict.byteStats);
+ };
+
+ /**
+ * Node in a VM region classification tree.
+ *
+ * Note: Most users of this class should use the
+ * VMRegionClassificationNode.fromRegions static method instead of this
+ * constructor because it leads to better performance due to fewer memory
+ * allocations.
+ *
+ * @constructor
+ */
+ function VMRegionClassificationNode(opt_rule) {
+ this.rule_ = opt_rule || VMRegionClassificationNode.CLASSIFICATION_RULES;
+
+ // True iff this node or any of its descendant classification nodes has at
+ // least one classified VM region.
+ this.hasRegions = false;
+
+ // Total virtual size and byte stats of all regions matching this node's
+ // rule (including its sub-rules).
+ this.sizeInBytes = undefined;
+ this.byteStats = {};
+
+ // Array of child classification nodes if this is an intermediate node.
+ this.children_ = undefined;
+
+ // Array of VM regions. If this is an intermediate node, then the regions
+ // are cached for lazy tree construction (i.e. its child classification
+ // nodes yet have to be built).
+ this.regions_ = [];
+ }
+
+ /**
+ * Rules for classifying memory maps.
+ *
+ * These rules are derived from core/jni/android_os_Debug.cpp in Android.
+ */
+ VMRegionClassificationNode.CLASSIFICATION_RULES = {
+ name: 'Total',
+ children: [
+ {
+ name: 'Android',
+ file: /^\/dev\/ashmem(?!\/libc malloc)/,
+ children: [
+ {
+ name: 'Java runtime',
+ file: /^\/dev\/ashmem\/dalvik-/,
+ children: [
+ {
+ name: 'Spaces',
+ file: /\/dalvik-(alloc|main|large object|non moving|zygote) space/, // @suppress longLineCheck
+ children: [
+ {
+ name: 'Normal',
+ file: /\/dalvik-(alloc|main)/
+ },
+ {
+ name: 'Large',
+ file: /\/dalvik-large object/
+ },
+ {
+ name: 'Zygote',
+ file: /\/dalvik-zygote/
+ },
+ {
+ name: 'Non-moving',
+ file: /\/dalvik-non moving/
+ }
+ ]
+ },
+ {
+ name: 'Linear Alloc',
+ file: /\/dalvik-LinearAlloc/
+ },
+ {
+ name: 'Indirect Reference Table',
+ file: /\/dalvik-indirect.ref/
+ },
+ {
+ name: 'Cache',
+ file: /\/dalvik-jit-code-cache/
+ },
+ {
+ name: 'Accounting'
+ }
+ ]
+ },
+ {
+ name: 'Cursor',
+ file: /\/CursorWindow/
+ },
+ {
+ name: 'Ashmem'
+ }
+ ]
+ },
+ {
+ name: 'Native heap',
+ file: /^((\[heap\])|(\[anon:)|(\/dev\/ashmem\/libc malloc)|(\[discounted tracing overhead\])|$)/ // @suppress longLineCheck
+ },
+ {
+ name: 'Stack',
+ file: /^\[stack/
+ },
+ {
+ name: 'Files',
+ file: /\.((((jar)|(apk)|(ttf)|(odex)|(oat)|(art))$)|(dex)|(so))/,
+ children: [
+ {
+ name: 'so',
+ file: /\.so/
+ },
+ {
+ name: 'jar',
+ file: /\.jar$/
+ },
+ {
+ name: 'apk',
+ file: /\.apk$/
+ },
+ {
+ name: 'ttf',
+ file: /\.ttf$/
+ },
+ {
+ name: 'dex',
+ file: /\.((dex)|(odex$))/
+ },
+ {
+ name: 'oat',
+ file: /\.oat$/
+ },
+ {
+ name: 'art',
+ file: /\.art$/
+ }
+ ]
+ },
+ {
+ name: 'Devices',
+ file: /(^\/dev\/)|(anon_inode:dmabuf)/,
+ children: [
+ {
+ name: 'GPU',
+ file: /\/((nv)|(mali)|(kgsl))/
+ },
+ {
+ name: 'DMA',
+ file: /anon_inode:dmabuf/
+ }
+ ]
+ }
+ ]
+ };
+ VMRegionClassificationNode.OTHER_RULE = { name: 'Other' };
+
+ VMRegionClassificationNode.fromRegions = function(regions, opt_rules) {
+ const tree = new VMRegionClassificationNode(opt_rules);
+ tree.regions_ = regions;
+ for (let i = 0; i < regions.length; i++) {
+ tree.addStatsFromRegion_(regions[i]);
+ }
+ return tree;
+ };
+
+ VMRegionClassificationNode.prototype = {
+ get title() {
+ return this.rule_.name;
+ },
+
+ get children() {
+ if (this.isLeafNode) {
+ return undefined; // Leaf nodes don't have children (by definition).
+ }
+ if (this.children_ === undefined) {
+ this.buildTree_(); // Lazily classify VM regions.
+ }
+ return this.children_;
+ },
+
+ get regions() {
+ if (!this.isLeafNode) {
+ // Intermediate nodes only temporarily cache VM regions for lazy tree
+ // construction.
+ return undefined;
+ }
+ return this.regions_;
+ },
+
+ get allRegionsForTesting() {
+ if (this.regions_ !== undefined) {
+ if (this.children_ !== undefined) {
+ throw new Error('Internal error: a VM region classification node ' +
+ 'cannot have both regions and children');
+ }
+ // Leaf node (or caching internal node).
+ return this.regions_;
+ }
+
+ // Intermediate node.
+ let regions = [];
+ this.children_.forEach(function(childNode) {
+ regions = regions.concat(childNode.allRegionsForTesting);
+ });
+ return regions;
+ },
+
+ get isLeafNode() {
+ const children = this.rule_.children;
+ return children === undefined || children.length === 0;
+ },
+
+ addRegion(region) {
+ this.addRegionRecursively_(region, true /* addStatsToThisNode */);
+ },
+
+ someRegion(fn, opt_this) {
+ if (this.regions_ !== undefined) {
+ // Leaf node (or caching internal node).
+ return this.regions_.some(fn, opt_this);
+ }
+
+ // Intermediate node.
+ return this.children_.some(function(childNode) {
+ return childNode.someRegion(fn, opt_this);
+ });
+ },
+
+ addRegionRecursively_(region, addStatsToThisNode) {
+ if (addStatsToThisNode) {
+ this.addStatsFromRegion_(region);
+ }
+
+ if (this.regions_ !== undefined) {
+ if (this.children_ !== undefined) {
+ throw new Error('Internal error: a VM region classification node ' +
+ 'cannot have both regions and children');
+ }
+ // Leaf node or an intermediate node caching VM regions (add the
+ // region to this node and don't classify further).
+ this.regions_.push(region);
+ return;
+ }
+
+ // Non-leaf rule (classify region row further down the tree).
+ function regionRowMatchesChildNide(child) {
+ const fileRegExp = child.rule_.file;
+ if (fileRegExp === undefined) return true;
+ return fileRegExp.test(region.mappedFile);
+ }
+
+ let matchedChild = this.children_.find(regionRowMatchesChildNide);
+ if (matchedChild === undefined) {
+ // Region belongs to the 'Other' node (created lazily).
+ if (this.children_.length !== this.rule_.children.length) {
+ throw new Error('Internal error');
+ }
+ matchedChild = new VMRegionClassificationNode(
+ VMRegionClassificationNode.OTHER_RULE);
+ this.children_.push(matchedChild);
+ }
+
+ matchedChild.addRegionRecursively_(region, true);
+ },
+
+ buildTree_() {
+ const cachedRegions = this.regions_;
+ this.regions_ = undefined;
+
+ this.buildChildNodesRecursively_();
+ for (let i = 0; i < cachedRegions.length; i++) {
+ // Note that we don't add the VM region's stats to this node because
+ // they have already been added to it.
+ this.addRegionRecursively_(
+ cachedRegions[i], false /* addStatsToThisNode */);
+ }
+ },
+
+ buildChildNodesRecursively_() {
+ if (this.children_ !== undefined) {
+ throw new Error(
+ 'Internal error: Classification node already has children');
+ }
+ if (this.regions_ !== undefined && this.regions_.length !== 0) {
+ throw new Error(
+ 'Internal error: Classification node should have no regions');
+ }
+
+ if (this.isLeafNode) {
+ return; // Leaf node: Nothing to do.
+ }
+
+ // Intermediate node: Clear regions and build children recursively.
+ this.regions_ = undefined;
+ this.children_ = this.rule_.children.map(function(childRule) {
+ const child = new VMRegionClassificationNode(childRule);
+ child.buildChildNodesRecursively_();
+ return child;
+ });
+ },
+
+ addStatsFromRegion_(region) {
+ this.hasRegions = true;
+
+ // Aggregate virtual size.
+ const regionSizeInBytes = region.sizeInBytes;
+ if (regionSizeInBytes !== undefined) {
+ this.sizeInBytes = (this.sizeInBytes || 0) + regionSizeInBytes;
+ }
+
+ // Aggregate byte stats.
+ const thisByteStats = this.byteStats;
+ const regionByteStats = region.byteStats;
+ for (const byteStatName in regionByteStats) {
+ const regionByteStatValue = regionByteStats[byteStatName];
+ if (regionByteStatValue === undefined) continue;
+ thisByteStats[byteStatName] =
+ (thisByteStats[byteStatName] || 0) + regionByteStatValue;
+ }
+
+ // Aggregate java base.* stats.
+ if (region.mappedFile.includes('/base.odex') ||
+ region.mappedFile.includes('/base.vdex')) {
+ if (region.byteStats.proportionalResident !== undefined) {
+ thisByteStats.javaBasePss =
+ (thisByteStats.javaBasePss || 0) +
+ region.byteStats.proportionalResident;
+ }
+ if (region.byteStats.privateCleanResident !== undefined) {
+ thisByteStats.javaBaseCleanResident =
+ (thisByteStats.javaBaseCleanResident || 0) +
+ region.byteStats.privateCleanResident;
+ }
+ if (region.byteStats.sharedCleanResident !== undefined) {
+ thisByteStats.javaBaseCleanResident =
+ (thisByteStats.javaBaseCleanResident || 0) +
+ region.byteStats.sharedCleanResident;
+ }
+ }
+
+ // Aggregate native library stats.
+ const textProtectionFlags = (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE);
+ // On post-M devices, the native library is mapped from /base.apk. On M
+ // and earlier devices, it's mapped from /libchrome.so. In both cases only
+ // the regions that are readable and executable should be counted.
+ // TODO(mattcary): if both mappings are seen, something has gone wrong and
+ // some sort of error or fatal should be done. This should be tracked
+ // across regions, which means adding state to |this|.
+ if ((region.protectionFlags === textProtectionFlags) &&
+ (region.mappedFile.includes('/base.apk') ||
+ region.mappedFile.includes('/libchrome.so'))) {
+ if (regionSizeInBytes !== undefined) {
+ this.nativeLibrarySizeInBytes =
+ (this.nativeLibrarySizeInBytes || 0) + regionSizeInBytes;
+ }
+ if (region.byteStats.privateCleanResident !== undefined) {
+ thisByteStats.nativeLibraryPrivateCleanResident =
+ (thisByteStats.nativeLibraryPrivateCleanResident || 0) +
+ region.byteStats.privateCleanResident;
+ }
+ if (region.byteStats.sharedCleanResident !== undefined) {
+ thisByteStats.nativeLibrarySharedCleanResident =
+ (thisByteStats.nativeLibrarySharedCleanResident || 0) +
+ region.byteStats.sharedCleanResident;
+ }
+ if (region.byteStats.proportionalResident !== undefined) {
+ thisByteStats.nativeLibraryProportionalResident =
+ (thisByteStats.nativeLibraryProportionalResident || 0) +
+ region.byteStats.proportionalResident;
+ }
+ }
+ }
+ };
+
+ return {
+ VMRegion,
+ VMRegionClassificationNode,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html b/chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html
new file mode 100644
index 00000000000..93b8ad1647c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html
@@ -0,0 +1,1216 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const checkVMRegions = tr.model.MemoryDumpTestUtils.checkVMRegions;
+
+ function checkProtectionFlagsToString(protectionFlags, expectedString) {
+ const vmRegion = VMRegion.fromDict({
+ startAddress: 256,
+ sizeInBytes: 336,
+ protectionFlags,
+ mappedFile: '[stack:20310]',
+ byteStats: {
+ privateDirtyResident: 96,
+ swapped: 144,
+ proportionalResident: 158
+ }
+ });
+ assert.strictEqual(vmRegion.protectionFlagsToString, expectedString);
+ }
+
+ const TEST_RULES = {
+ name: 'Root',
+ children: [
+ {
+ name: 'Words',
+ file: /^[a-zA-Z]/,
+ children: [
+ {
+ name: 'A-D',
+ file: /^[a-dA-D]/
+ },
+ {
+ name: 'E-H',
+ file: /^[e-hE-H]/
+ }
+ ]
+ },
+ {
+ name: 'Digits',
+ file: /\d$/,
+ children: []
+ }
+ ]
+ };
+
+ // Constant representing the expectation that the children of a
+ // VMRegionClassificationNode have not been built yet.
+ const CHILDREN_NOT_BUILT_YET = {};
+
+ function checkTree(node, expectedStructure) {
+ assert.strictEqual(node.title, expectedStructure.title);
+ assert.strictEqual(node.hasRegions, expectedStructure.hasRegions);
+ assert.strictEqual(node.sizeInBytes, expectedStructure.sizeInBytes);
+ assert.deepEqual(node.byteStats, expectedStructure.byteStats || {});
+ assert.strictEqual(node.isLeafNode, expectedStructure.isLeafNode);
+
+ const actualRegions = node.regions;
+ const expectedRegions = expectedStructure.regions;
+ if (expectedRegions === undefined) {
+ assert.isUndefined(actualRegions);
+ } else {
+ assert.instanceOf(actualRegions, Array);
+ checkVMRegions(actualRegions, expectedRegions);
+ }
+
+ const expectedChildren = expectedStructure.children;
+ if (expectedChildren === CHILDREN_NOT_BUILT_YET) {
+ assert.isUndefined(node.children_);
+ } else if (expectedChildren === undefined) {
+ assert.isUndefined(node.children);
+ } else {
+ const actualChildrenMap = new Map();
+ node.children.forEach(function(childNode) {
+ actualChildrenMap.set(childNode.title, childNode);
+ });
+ const expectedChildrenMap = new Map();
+ expectedChildren.forEach(function(childNode) {
+ expectedChildrenMap.set(childNode.title, childNode);
+ });
+ assert.strictEqual(actualChildrenMap.size, expectedChildrenMap.size);
+ for (const title of expectedChildrenMap.keys()) {
+ checkTree(actualChildrenMap.get(title),
+ expectedChildrenMap.get(title));
+ }
+ }
+ }
+
+ function checkClassificationRules(mappedFile, expectedPath) {
+ const region = VMRegion.fromDict({
+ mappedFile,
+ sizeInBytes: 16,
+ byteStats: {
+ privateDirtyResident: 7
+ }
+ });
+ let node = VMRegionClassificationNode.fromRegions([region]);
+ for (const title of expectedPath) {
+ node = node.children.find(c => c.title === title);
+ }
+ assert.deepEqual(node.regions, [region]);
+ }
+
+ test('vmRegion_protectionFlagsToString', function() {
+ checkProtectionFlagsToString(undefined, undefined);
+ checkProtectionFlagsToString(0, '---p');
+ checkProtectionFlagsToString(VMRegion.PROTECTION_FLAG_READ, 'r--p');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_MAYSHARE,
+ 'r--s');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_EXECUTE,
+ 'r-xp');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE,
+ 'rw-p');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ 'rwxp');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE |
+ VMRegion.PROTECTION_FLAG_MAYSHARE,
+ 'rw-s');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_EXECUTE |
+ VMRegion.PROTECTION_FLAG_MAYSHARE,
+ 'r-xs');
+ checkProtectionFlagsToString(
+ VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE |
+ VMRegion.PROTECTION_FLAG_EXECUTE |
+ VMRegion.PROTECTION_FLAG_MAYSHARE,
+ 'rwxs');
+ });
+
+ // The add(After|Before)Build tests below check that the classification tree
+ // has the correct structure regardless of the ordering of adding regions and
+ // the lazy construction.
+
+ test('vmRegionClassificationNode_constructor_addAfterBuild', function() {
+ const rootNode = new VMRegionClassificationNode(TEST_RULES);
+
+ // Check the root node and verify that the full tree structure has *not*
+ // been constructed yet.
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: false,
+ isLeafNode: false,
+ children: CHILDREN_NOT_BUILT_YET
+ });
+
+ // Reading the children of the root node *should* trigger building the
+ // full tree.
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: false,
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: false,
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ }
+ ]
+ });
+
+ // Add VM regions to the tree *after* it has been fully built.
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: 'W2', // Root/Words/Other.
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }));
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '__42', // Root/Digits.
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }));
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32 + 33,
+ privateDirtyResident: 77,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'Other',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: 'W2',
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }
+ ]
+ }
+ ]
+ });
+ });
+
+ test('vmRegionClassificationNode_constructor_addBeforeBuild', function() {
+ const rootNode = new VMRegionClassificationNode(TEST_RULES);
+
+ // Add regions to the tree *before* it has been fully built. This should
+ // *not* trigger building the full tree (but the total sizeInBytes and
+ // byteStats should be updated accordingly).
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '__42', // Root/Digits.
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }));
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: 'W2', // Root/Words/Other.
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }));
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32 + 33,
+ privateDirtyResident: 77,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: CHILDREN_NOT_BUILT_YET
+ });
+
+ // Reading the children of the root node should trigger building the full
+ // tree.
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32 + 33,
+ privateDirtyResident: 77,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'Other',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: 'W2',
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }
+ ]
+ }
+ ]
+ });
+
+ // Add more VM regions *after* the tree has been fully built.
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '%invalid%', // Root/Other.
+ sizeInBytes: 123
+ }));
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '__43', // Root/Digits.
+ byteStats: {
+ swapped: 19
+ }
+ }));
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: 'free', // Root/Words/E-H.
+ sizeInBytes: undefined
+ }));
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 16 + 123,
+ byteStats: {
+ proportionalResident: 32 + 33,
+ privateDirtyResident: 77,
+ swapped: 64 + 19,
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: true,
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: 'free'
+ }
+ ]
+ },
+ {
+ title: 'Other',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: 'W2',
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77,
+ swapped: 19
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ },
+ {
+ mappedFile: '__43',
+ byteStats: {
+ swapped: 19
+ }
+ }
+ ]
+ },
+ {
+ title: 'Other',
+ hasRegions: true,
+ sizeInBytes: 123,
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '%invalid%',
+ sizeInBytes: 123
+ }
+ ]
+ }
+ ]
+ });
+ });
+
+ test('vmRegionClassificationNode_fromRegions_addAfterBuild', function() {
+ // Construct the root node from a list of regions. This should *not*
+ // trigger building the full tree (but the total sizeInBytes and byteStats
+ // should be updated accordingly).
+ const rootNode = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ mappedFile: '__42', // Root/Digits.
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '__43', // Root/Digits.
+ byteStats: {
+ swapped: 19
+ }
+ })
+ ], TEST_RULES);
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77,
+ swapped: 19
+ },
+ isLeafNode: false,
+ children: CHILDREN_NOT_BUILT_YET
+ });
+
+ // Reading the children of the root node should trigger building the full
+ // tree.
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77,
+ swapped: 19
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: false,
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77,
+ swapped: 19
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ },
+ {
+ mappedFile: '__43',
+ byteStats: {
+ swapped: 19
+ }
+ }
+ ]
+ }
+ ]
+ });
+
+ // Add more VM regions *after* the tree has been fully built.
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: 'W2', // Root/Words/Other.
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }));
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32 + 33,
+ privateDirtyResident: 77,
+ swapped: 19 + 64,
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'Other',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: 'W2',
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77,
+ swapped: 19
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ },
+ {
+ mappedFile: '__43',
+ byteStats: {
+ swapped: 19
+ }
+ }
+ ]
+ }
+ ]
+ });
+ });
+
+ test('vmRegionClassificationNode_fromRegions_addBeforeBuild', function() {
+ // Construct the root node from a list of regions and then add another
+ // region. This should *not* trigger building the full tree (but the total
+ // sizeInBytes and byteStats should be updated accordingly).
+ const rootNode = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ mappedFile: '__42', // Root/Digits.
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '__43', // Root/Digits.
+ byteStats: {
+ swapped: 19
+ }
+ })
+ ], TEST_RULES);
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '__42', // Root/Digits.
+ startAddress: 2048, // Necessary to distinguish from the first region.
+ sizeInBytes: 1000,
+ byteStats: {
+ privateDirtyResident: 500
+ }
+ }));
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 1000,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77 + 500,
+ swapped: 19
+ },
+ isLeafNode: false,
+ children: CHILDREN_NOT_BUILT_YET
+ });
+
+ // Reading the children of the root node should trigger building the full
+ // tree.
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 1000,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77 + 500,
+ swapped: 19
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: false,
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ sizeInBytes: 1000,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77 + 500,
+ swapped: 19
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ },
+ {
+ mappedFile: '__43',
+ byteStats: {
+ swapped: 19
+ }
+ },
+ {
+ mappedFile: '__42',
+ startAddress: 2048,
+ sizeInBytes: 1000,
+ byteStats: {
+ privateDirtyResident: 500
+ }
+ }
+ ]
+ }
+ ]
+ });
+
+ // Add more VM regions *after* the tree has been fully built.
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: 'W2', // Root/Words/Other.
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }));
+ checkTree(rootNode, {
+ title: 'Root',
+ hasRegions: true,
+ sizeInBytes: 1000 + 16,
+ byteStats: {
+ proportionalResident: 32 + 33,
+ privateDirtyResident: 500 + 77,
+ swapped: 19 + 64,
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'Words',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: false,
+ children: [
+ {
+ title: 'A-D',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'E-H',
+ hasRegions: false,
+ isLeafNode: true,
+ regions: []
+ },
+ {
+ title: 'Other',
+ hasRegions: true,
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: 'W2',
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'Digits',
+ hasRegions: true,
+ sizeInBytes: 1000,
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 500 + 77,
+ swapped: 19
+ },
+ isLeafNode: true,
+ regions: [
+ {
+ mappedFile: '__42',
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ },
+ {
+ mappedFile: '__43',
+ byteStats: {
+ swapped: 19
+ }
+ },
+ {
+ mappedFile: '__42',
+ startAddress: 2048,
+ sizeInBytes: 1000,
+ byteStats: {
+ privateDirtyResident: 500
+ }
+ }
+ ]
+ }
+ ]
+ });
+ });
+
+ test('vmRegionClassificationNode_someRegion', function() {
+ const rootNode = new VMRegionClassificationNode(TEST_RULES);
+
+ // There are no regions in the tree, so the method should always return
+ // false.
+ assert.isFalse(rootNode.someRegion(function(region) {
+ throw new Error('There are no regions in the tree!!!');
+ }));
+
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: 'W2', // Root/Words/Other.
+ sizeInBytes: 16,
+ byteStats: {
+ proportionalResident: 32,
+ swapped: 64
+ }
+ }));
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '__42', // Root/Digits.
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }));
+ rootNode.addRegion(VMRegion.fromDict({
+ mappedFile: '__43', // Root/Digits.
+ byteStats: {
+ proportionalResident: 33,
+ privateDirtyResident: 77
+ }
+ }));
+
+ function checkSomeRegion() {
+ // Find the order in which the regions are traversed and checked that all
+ // regions were visited.
+ const visitedRegionMappedFiles = [];
+ assert.isFalse(rootNode.someRegion(function(region) {
+ visitedRegionMappedFiles.push(region.mappedFile);
+ return false;
+ }));
+ assert.lengthOf(visitedRegionMappedFiles, 3);
+ assert.sameMembers(visitedRegionMappedFiles, ['W2', '__42', '__43']);
+
+ // Assuming the traversal order is deterministic, we check that once the
+ // callback returns true, no further regions are visited.
+ visitedRegionMappedFiles.forEach(
+ function(mappedFileToMatch, index) {
+ const visitedRegionMappedFiles2 = [];
+ assert.isTrue(rootNode.someRegion(function(region) {
+ this.files.push(region.mappedFile);
+ return region.mappedFile === mappedFileToMatch;
+ }, { files: visitedRegionMappedFiles2 } /* opt_this */));
+ assert.deepEqual(visitedRegionMappedFiles2,
+ visitedRegionMappedFiles.slice(0, index + 1));
+ });
+ }
+
+ // Before lazy construction (single node with a flat list of regions).
+ checkSomeRegion();
+ assert.isUndefined(rootNode.children_);
+
+ // After lazy construction (tree of nodes with lists of regions).
+ assert.isDefined(rootNode.children); // Force building the tree.
+ assert.isDefined(rootNode.children_);
+ checkSomeRegion();
+ });
+
+ test('vmRegionClassificationNode_libraryMemory', function() {
+ const regions = [
+ VMRegion.fromDict({
+ sizeInBytes: 20,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack:1234]',
+ byteStats: {
+ privateDirtyResident: 100,
+ proportionalResident: 124
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 500000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '/data/app/com.google.chrome/base.apk',
+ byteStats: {
+ privateCleanResident: 100000,
+ proportionalResident: 124000
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 1000,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/base.apk',
+ byteStats: {
+ privateCleanResident: 100,
+ proportionalResident: 124
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 300,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/base.apk',
+ byteStats: {
+ proportionalResident: 58,
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 400,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/base.apk',
+ byteStats: {
+ privateCleanResident: 76,
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 50,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/base.apk',
+ byteStats: {
+ privateCleanResident: 8,
+ proportionalResident: 24,
+ sharedCleanResident: 10,
+ }
+ })];
+ const node = VMRegionClassificationNode.fromRegions(regions);
+ assert.strictEqual(node.sizeInBytes, 20 + 500000 + 1000 + 300 + 400 + 50);
+ assert.strictEqual(node.nativeLibrarySizeInBytes,
+ 1000 + 300 + 400 + 50);
+
+ assert.deepEqual(node.byteStats, {
+ proportionalResident: 124 + 124000 + 124 + 58 + 24,
+ privateDirtyResident: 100,
+ privateCleanResident: 100000 + 100 + 76 + 8,
+ sharedCleanResident: 10,
+ nativeLibraryPrivateCleanResident: 100 + 76 + 8,
+ nativeLibrarySharedCleanResident: 10,
+ nativeLibraryProportionalResident: 124 + 58 + 24,
+ });
+ });
+
+ test('vmRegionClassificationNode_javaBaseMemory', function() {
+ const regions = [
+ VMRegion.fromDict({
+ sizeInBytes: 20,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack:1234]',
+ byteStats: {
+ privateDirtyResident: 100,
+ proportionalResident: 124
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 500000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '/data/app/com.google.chrome/oat/arm/base.vdex',
+ byteStats: {
+ privateCleanResident: 100000,
+ proportionalResident: 124000
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 1000,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/some/other.odex',
+ byteStats: {
+ privateCleanResident: 100,
+ proportionalResident: 124
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 300,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ),
+ mappedFile: '/data/app/com.google.chrome/oat/arm/base.odex',
+ byteStats: {
+ proportionalResident: 58,
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 400,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/base.vdex',
+ byteStats: {
+ privateCleanResident: 76,
+ privateDirtyResident: 17,
+ }
+ }),
+ VMRegion.fromDict({
+ sizeInBytes: 50,
+ protectionFlags: (VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE),
+ mappedFile: '/data/app/com.google.chrome/another/path/base.odex',
+ byteStats: {
+ privateCleanResident: 8,
+ proportionalResident: 24,
+ sharedCleanResident: 10,
+ }
+ })];
+ const node = VMRegionClassificationNode.fromRegions(regions);
+ assert.strictEqual(node.sizeInBytes, 20 + 500000 + 1000 + 300 + 400 + 50);
+
+ assert.deepEqual(node.byteStats, {
+ proportionalResident: 124 + 124000 + 124 + 58 + 24,
+ privateDirtyResident: 100 + 17,
+ privateCleanResident: 100000 + 100 + 76 + 8,
+ sharedCleanResident: 10,
+ javaBasePss: 124000 + 58 + 24,
+ javaBaseCleanResident: 100000 + 76 + 8 + 10,
+ });
+ });
+
+ test('classificationRules', function() {
+ checkClassificationRules('/dev/ashmem/dalvik-main space (deleted)',
+ ['Android', 'Java runtime', 'Spaces', 'Normal']);
+ checkClassificationRules('/dev/ashmem/dalvik-non moving space',
+ ['Android', 'Java runtime', 'Spaces', 'Non-moving']);
+ checkClassificationRules('/dev/ashmem/dalvik-zygote space (deleted)',
+ ['Android', 'Java runtime', 'Spaces', 'Zygote']);
+ checkClassificationRules('/dev/ashmem/dalvik-allocation stack (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules(
+ '/dev/ashmem/dalvik-allocspace main rosalloc space 1 live-bitmap 2',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules(
+ '/dev/ashmem/dalvik-allocspace non moving space live-bitmap 4',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules('/dev/ashmem/dalvik-allocspace zygote / ' +
+ 'non moving space live-bitmap 0 (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules('/dev/ashmem/dalvik-card table (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules('/dev/ashmem/dalvik-large live objects (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules('/dev/ashmem/dalvik-live stack (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules(
+ '/dev/ashmem/dalvik-mark sweep sweep array free buffer (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules('/dev/ashmem/dalvik-rosalloc page map (deleted)',
+ ['Android', 'Java runtime', 'Accounting']);
+ checkClassificationRules('/dev/ashmem/dalvik-indirect ref table (deleted)',
+ ['Android', 'Java runtime', 'Indirect Reference Table']);
+ checkClassificationRules('/dev/ashmem/dalvik-LinearAlloc (deleted)',
+ ['Android', 'Java runtime', 'Linear Alloc']);
+ checkClassificationRules('/dev/ashmem/dalvik-jit-code-cache (deleted)',
+ ['Android', 'Java runtime', 'Cache']);
+ checkClassificationRules('/dev/ashmem/CursorWindow (deleted)',
+ ['Android', 'Cursor']);
+ checkClassificationRules('/dev/ashmem (deleted)', ['Android', 'Ashmem']);
+ checkClassificationRules('/dev/ashmem/GFXStats-10082',
+ ['Android', 'Ashmem']);
+
+ checkClassificationRules('[stack:23164]', ['Stack']);
+ checkClassificationRules('[stack]', ['Stack']);
+
+ checkClassificationRules('[discounted tracing overhead]', ['Native heap']);
+ checkClassificationRules('', ['Native heap']);
+ checkClassificationRules('[heap]', ['Native heap']);
+ checkClassificationRules('[anon:libc_malloc]', ['Native heap']);
+ checkClassificationRules('[anon:thread signal stack]', ['Native heap']);
+ checkClassificationRules('/dev/ashmem/libc malloc (deleted)',
+ ['Native heap']);
+
+ checkClassificationRules('/usr/lib/nvidia-340/libGL.so.331.79',
+ ['Files', 'so']);
+ checkClassificationRules('/usr/lib/x86_64-linux-gnu/libibus-1.0.so.5.0.505',
+ ['Files', 'so']);
+ checkClassificationRules('/data/data/com.google.android.apps.chrome/' +
+ 'app_chrome/RELRO:libchrome.so (deleted)', ['Files', 'so']);
+ checkClassificationRules(
+ '/usr/share/fonts/truetype/msttcorefonts/Times_New_Roman.ttf',
+ ['Files', 'ttf']);
+ checkClassificationRules(
+ '/data/app/com.google.android.apps.chrome-2/base.apk',
+ ['Files', 'apk']);
+ checkClassificationRules(
+ '/data/app/com.google.android.apps.chrome-2/lib/arm/libchrome.so',
+ ['Files', 'so']);
+ checkClassificationRules(
+ '/data/app/com.google.android.apps.chrome-2/oat/arm/base.odex',
+ ['Files', 'dex']);
+ checkClassificationRules(
+ '/data/dalvik-cache/arm/system@framework@boot.art', ['Files', 'art']);
+ checkClassificationRules(
+ '/data/dalvik-cache/arm/system@framework@boot.oat', ['Files', 'oat']);
+
+ checkClassificationRules('/dev/nvidia0', ['Devices', 'GPU']);
+ checkClassificationRules('/dev/kgsl-3d0', ['Devices', 'GPU']);
+ checkClassificationRules('anon_inode:dmabuf', ['Devices', 'DMA']);
+ checkClassificationRules('/dev/binder', ['Devices', 'Other']);
+
+ checkClassificationRules('/src/out/Release/chrome', ['Other']);
+ checkClassificationRules('/tmp/gluY4SVp (deleted)', ['Other']);
+ checkClassificationRules('/src/out/Release/resources.pak', ['Other']);
+ checkClassificationRules('[vdso]', ['Other']);
+ checkClassificationRules('[vsyscall]', ['Other']);
+ checkClassificationRules('[vectors]', ['Other']);
+ checkClassificationRules('[vvar]', ['Other']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html b/chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html
new file mode 100644
index 00000000000..7eb79b1d78e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/annotation.html">
+<link rel="import" href="/tracing/ui/annotations/x_marker_annotation_view.html">
+
+<script>
+'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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/__init__.py b/chromium/third_party/catapult/tracing/tracing/mre/__init__.py
new file mode 100644
index 00000000000..50b23dff631
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/__init__.py
@@ -0,0 +1,3 @@
+# 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.
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/cloud_storage.py b/chromium/third_party/catapult/tracing/tracing/mre/cloud_storage.py
new file mode 100644
index 00000000000..877ddaa0645
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/cloud_storage.py
@@ -0,0 +1,71 @@
+# 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 subprocess
+import sys
+
+_GSUTIL_PATH = os.path.abspath(
+ os.path.join(
+ os.path.dirname(__file__),
+ '..', '..', 'third_party', 'gsutil', 'gsutil'))
+
+
+class CloudStorageError(Exception):
+
+ @staticmethod
+ def _GetConfigInstructions():
+ command = _GSUTIL_PATH
+ return ('To configure your credentials:\n'
+ ' 1. Run "%s config" and follow its instructions.\n'
+ ' 2. If you have a @google.com account, use that account.\n'
+ ' 3. For the project-id, just enter 0.' % command)
+
+
+class PermissionError(CloudStorageError):
+
+ def __init__(self):
+ super(PermissionError, self).__init__(
+ 'Attempted to access a file from Cloud Storage but you don\'t '
+ 'have permission. ' + self._GetConfigInstructions())
+
+
+class CredentialsError(CloudStorageError):
+
+ def __init__(self):
+ super(CredentialsError, self).__init__(
+ 'Attempted to access a file from Cloud Storage but you have no '
+ 'configured credentials. ' + self._GetConfigInstructions())
+
+
+class NotFoundError(CloudStorageError):
+ pass
+
+
+class ServerError(CloudStorageError):
+ pass
+
+
+def Copy(src, dst):
+ # TODO(simonhatch): switch to use py_utils.cloud_storage.
+ args = [sys.executable, _GSUTIL_PATH, 'cp', src, dst]
+ gsutil = subprocess.Popen(args, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ _, stderr = gsutil.communicate()
+
+ if gsutil.returncode:
+ if stderr.startswith((
+ 'You are attempting to access protected data with no configured',
+ 'Failure: No handler was ready to authenticate.')):
+ raise CredentialsError()
+ if ('status=403' in stderr or 'status 403' in stderr or
+ '403 Forbidden' in stderr):
+ raise PermissionError()
+ if (stderr.startswith('InvalidUriError') or 'No such object' in stderr or
+ 'No URLs matched' in stderr or
+ 'One or more URLs matched no' in stderr):
+ raise NotFoundError(stderr)
+ if '500 Internal Server Error' in stderr:
+ raise ServerError(stderr)
+ raise CloudStorageError(stderr)
+ return gsutil.returncode
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/corpus_driver.py b/chromium/third_party/catapult/tracing/tracing/mre/corpus_driver.py
new file mode 100644
index 00000000000..28bb8a208b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/corpus_driver.py
@@ -0,0 +1,9 @@
+# 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.
+
+
+class CorpusDriver(object):
+
+ def GetTraceHandles(self):
+ raise NotImplementedError()
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/corpus_driver_cmdline.py b/chromium/third_party/catapult/tracing/tracing/mre/corpus_driver_cmdline.py
new file mode 100644
index 00000000000..06d70ba8f68
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/corpus_driver_cmdline.py
@@ -0,0 +1,23 @@
+# 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.
+from tracing.mre import local_directory_corpus_driver
+
+
+_CORPUS_DRIVERS = {
+ 'local-directory': {
+ 'description': 'Use traces from a local directory.',
+ 'class': local_directory_corpus_driver.LocalDirectoryCorpusDriver
+ }
+}
+_CORPUS_DRIVER_DEFAULT = 'local-directory'
+
+
+def GetCorpusDriver(parser, args):
+ # With parse_known_args, optional arguments aren't guaranteed to be there so
+ # we need to check if it's there, and use the default otherwise.
+ corpus = _CORPUS_DRIVER_DEFAULT
+
+ cls = _CORPUS_DRIVERS[corpus]['class']
+ init_args = cls.CheckAndCreateInitArguments(parser, args)
+ return cls(**init_args)
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/failure.html b/chromium/third_party/catapult/tracing/tracing/mre/failure.html
new file mode 100644
index 00000000000..7b12cf775da
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/failure.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ function Failure(job, functionHandleString, traceCanonicalUrl,
+ failureTypeName, description, stack) {
+ this.job = job;
+ this.functionHandleString = functionHandleString;
+ this.traceCanonicalUrl = traceCanonicalUrl;
+ this.failureTypeName = failureTypeName;
+ this.description = description;
+ this.stack = stack;
+ }
+
+ Failure.prototype = {
+ asDict() {
+ // TODO(eakuefner): Serialize job once reduction is implemented.
+ return {
+ function_handle_string: this.functionHandleString,
+ trace_canonical_url: this.traceCanonicalUrl,
+ type: this.failureTypeName,
+ description: this.description,
+ stack: this.stack
+ };
+ }
+ };
+
+ Failure.fromDict = function(failureDict) {
+ return new Failure(undefined, failureDict.function_handle_string,
+ failureDict.trace_canonical_url, failureDict.type,
+ failureDict.description, failureDict.stack);
+ };
+
+ return {
+ Failure,
+ };
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/failure.py b/chromium/third_party/catapult/tracing/tracing/mre/failure.py
new file mode 100644
index 00000000000..8c0713e1d8b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/failure.py
@@ -0,0 +1,53 @@
+# 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 tracing.mre import job as job_module
+
+
+class Failure(object):
+
+ def __init__(self, job, function_handle_string, trace_canonical_url,
+ failure_type_name, description, stack):
+ assert isinstance(job, job_module.Job)
+
+ self.job = job
+ self.function_handle_string = function_handle_string
+ self.trace_canonical_url = trace_canonical_url
+ self.failure_type_name = failure_type_name
+ self.description = description
+ self.stack = stack
+
+ def __str__(self):
+ return (
+ 'Failure for job %s with function handle %s and trace handle %s:\n'
+ 'of type %s wtih description %s. Stack:\n\n%s' % (
+ self.job.guid, self.function_handle_string,
+ self.trace_canonical_url, self.failure_type_name,
+ self.description, self.stack))
+
+ def AsDict(self):
+ return {
+ 'job_guid': str(self.job.guid),
+ 'function_handle_string': self.function_handle_string,
+ 'trace_canonical_url': self.trace_canonical_url,
+ 'type': self.failure_type_name,
+ 'description': self.description,
+ 'stack': self.stack
+ }
+
+ @staticmethod
+ def FromDict(failure_dict, job, failure_names_to_constructors=None):
+ if failure_names_to_constructors is None:
+ failure_names_to_constructors = {}
+ failure_type_name = failure_dict['type']
+ if failure_type_name in failure_names_to_constructors:
+ cls = failure_names_to_constructors[failure_type_name]
+ else:
+ cls = Failure
+
+ return cls(job,
+ failure_dict['function_handle_string'],
+ failure_dict['trace_canonical_url'],
+ failure_type_name, failure_dict['description'],
+ failure_dict['stack'])
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/failure_test.html b/chromium/third_party/catapult/tracing/tracing/mre/failure_test.html
new file mode 100644
index 00000000000..c20b67a45f5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/failure_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/mre/failure.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('failureAsDictTest', function() {
+ const failure = new tr.mre.Failure(undefined, 'foo.html:Foo',
+ 'file://foo.html', 'err', 'desc', 'stack');
+
+ assert.deepEqual(failure.asDict(), {
+ function_handle_string: 'foo.html:Foo',
+ trace_canonical_url: 'file://foo.html',
+ type: 'err',
+ description: 'desc',
+ stack: 'stack'
+ });
+ });
+
+ test('failureFromDictTest', function() {
+ const failureDict = {
+ function_handle_string: 'foo.html:Foo',
+ trace_canonical_url: 'file://foo.html',
+ type: 'err',
+ description: 'desc',
+ stack: 'stack'
+ };
+
+ const failure = tr.mre.Failure.fromDict(failureDict);
+
+ assert.strictEqual(failure.functionHandleString, 'foo.html:Foo');
+ assert.strictEqual(failure.traceCanonicalUrl, 'file://foo.html');
+ assert.strictEqual(failure.failureTypeName, 'err');
+ assert.strictEqual(failure.description, 'desc');
+ assert.strictEqual(failure.stack, 'stack');
+ });
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/failure_unittest.py b/chromium/third_party/catapult/tracing/tracing/mre/failure_unittest.py
new file mode 100644
index 00000000000..1140b17aa66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/failure_unittest.py
@@ -0,0 +1,56 @@
+# 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.
+
+import unittest
+
+from tracing.mre import function_handle
+from tracing.mre import failure as failure_module
+from tracing.mre import job as job_module
+
+
+def _SingleFileFunctionHandle(filename, function_name, guid):
+ return function_handle.FunctionHandle(
+ modules_to_load=[function_handle.ModuleToLoad(filename=filename)],
+ function_name=function_name, guid=guid)
+
+
+class FailureTests(unittest.TestCase):
+
+ def testAsDict(self):
+ map_function_handle = _SingleFileFunctionHandle('foo.html', 'Foo', '2')
+ job = job_module.Job(map_function_handle, '1')
+ failure = failure_module.Failure(job, 'foo.html:Foo',
+ 'file://foo.html',
+ 'err', 'desc', 'stack')
+
+ self.assertEquals(failure.AsDict(), {
+ 'job_guid': '1',
+ 'function_handle_string': 'foo.html:Foo',
+ 'trace_canonical_url': 'file://foo.html',
+ 'type': 'err',
+ 'description': 'desc',
+ 'stack': 'stack'
+ })
+
+ def testFromDict(self):
+ map_function_handle = _SingleFileFunctionHandle('foo.html', 'Foo', '2')
+ job = job_module.Job(map_function_handle, '1')
+
+ failure_dict = {
+ 'job_guid': '1',
+ 'function_handle_string': 'foo.html:Foo',
+ 'trace_canonical_url': 'file://foo.html',
+ 'type': 'err',
+ 'description': 'desc',
+ 'stack': 'stack'
+ }
+
+ failure = failure_module.Failure.FromDict(failure_dict, job)
+
+ self.assertEquals(failure.job.guid, '1')
+ self.assertEquals(failure.function_handle_string, 'foo.html:Foo')
+ self.assertEquals(failure.trace_canonical_url, 'file://foo.html')
+ self.assertEquals(failure.failure_type_name, 'err')
+ self.assertEquals(failure.description, 'desc')
+ self.assertEquals(failure.stack, 'stack')
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/file_handle.html b/chromium/third_party/catapult/tracing/tracing/mre/file_handle.html
new file mode 100644
index 00000000000..7d3ad6f6d5c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/file_handle.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/extras/full_config.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ function FileHandle(canonicalUrl) {
+ this.canonicalUrl_ = canonicalUrl;
+ }
+
+ FileHandle.prototype = {
+ get canonicalUrl() { return this.canonicalUrl_; },
+
+ asDict() {
+ const d = {
+ canonical_url: this.canonicalUrl_
+ };
+
+ this.asDictInto_(d);
+ if (d.type === undefined) {
+ throw new Error('asDictInto_ must set type field');
+ }
+ },
+
+ load() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ FileHandle.fromDict = function(handleDict) {
+ if (handleDict.type === 'url') {
+ return URLFileHandle.fromDict(handleDict);
+ }
+ if (handleDict.type === 'in-memory') {
+ return InMemoryFileHandle.fromDict(handleDict);
+ }
+
+ throw new Error('Not implemented: fromDict for ' + handleDict.type);
+ };
+
+
+ function URLFileHandle(canonicalUrl, urlToLoad) {
+ // TODO(eakuefner): assert startswith file://
+ FileHandle.call(this, canonicalUrl);
+ this.urlToLoad = urlToLoad;
+ this.loadAsTraceStream_ = false;
+ }
+
+ URLFileHandle.prototype = {
+ __proto__: FileHandle.prototype,
+
+ asDictInto_(handleDict) {
+ handleDict.urlToLoad = this.urlToLoad;
+ handleDict.type = 'url';
+ },
+
+ load() {
+ try {
+ return tr.b.getSync(this.urlToLoad, this.loadAsTraceStream_);
+ } catch (ex) {
+ const err = new Error('Could not open ' + this.urlToLoad);
+ err.name = 'FileLoadingError';
+ throw err;
+ }
+ }
+ };
+
+
+ URLFileHandle.fromDict = function(handleDict) {
+ return new URLFileHandle(handleDict.canonical_url, handleDict.url_to_load);
+ };
+
+ function InMemoryFileHandle(fileData, canonicalUrl) {
+ FileHandle.call(this, canonicalUrl);
+ this.fileData = fileData;
+ }
+
+ InMemoryFileHandle.prototype = {
+ __proto__: FileHandle.prototype,
+
+ asDictInto_(handleDict) {
+ handleDict.data = this.fileData;
+ handleDict.type = 'in-memory';
+ },
+
+ load() {
+ return this.fileData;
+ }
+ };
+
+ InMemoryFileHandle.fromDict = function(handleDict) {
+ return new InMemoryFileHandle(
+ handleDict.data, handleDict.canonical_url);
+ };
+
+ return {
+ FileHandle,
+ URLFileHandle,
+ InMemoryFileHandle,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/file_handle.py b/chromium/third_party/catapult/tracing/tracing/mre/file_handle.py
new file mode 100644
index 00000000000..e89b2f348f0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/file_handle.py
@@ -0,0 +1,100 @@
+# 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.
+
+import contextlib
+import os
+import tempfile
+
+from tracing.mre import cloud_storage
+
+
+class FilePreparationError(Exception):
+ """Raised if something goes wrong while preparing a file for processing."""
+
+
+class FileHandle(object):
+ def __init__(self, canonical_url):
+ self._canonical_url = canonical_url
+
+ @property
+ def canonical_url(self):
+ return self._canonical_url
+
+ @contextlib.contextmanager
+ def PrepareFileForProcessing(self):
+ """Ensure that the URL to the file will be acessible during processing.
+
+ This function must do any pre-work to ensure that mappers will
+ be able to read from the URL contained in the file handle.
+
+ Raises:
+ FilePreparationError: If something went wrong while preparing the file.
+ """
+ yield self._WillProcess()
+ self._DidProcess()
+
+ def _WillProcess(self):
+ raise NotImplementedError()
+
+ def _DidProcess(self):
+ raise NotImplementedError()
+
+
+class URLFileHandle(FileHandle):
+ def __init__(self, canonical_url, url_to_load):
+ super(URLFileHandle, self).__init__(canonical_url)
+
+ self._url_to_load = url_to_load
+
+ def AsDict(self):
+ return {
+ 'type': 'url',
+ 'canonical_url': self._canonical_url,
+ 'url_to_load': self._url_to_load
+ }
+
+ def _WillProcess(self):
+ return self
+
+ def _DidProcess(self):
+ pass
+
+
+class GCSFileHandle(FileHandle):
+ def __init__(self, canonical_url, cache_directory):
+ super(GCSFileHandle, self).__init__(canonical_url)
+ file_name = canonical_url.split('/')[-1]
+ self.cache_file = os.path.join(
+ cache_directory, file_name + '.gz')
+
+ def _WillProcess(self):
+ if not os.path.exists(self.cache_file):
+ try:
+ cloud_storage.Copy(self.canonical_url, self.cache_file)
+ except cloud_storage.CloudStorageError:
+ return None
+ return URLFileHandle(self.canonical_url, 'file://' + self.cache_file)
+
+ def _DidProcess(self):
+ pass
+
+
+class InMemoryFileHandle(FileHandle):
+ def __init__(self, canonical_url, data):
+ super(InMemoryFileHandle, self).__init__(canonical_url)
+
+ self.data = data
+ self._temp_file_path = None
+
+ def _WillProcess(self):
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temp_file.write(self.data)
+ temp_file.close()
+ self._temp_file_path = temp_file.name
+
+ return URLFileHandle(self.canonical_url, 'file://' + self._temp_file_path)
+
+ def _DidProcess(self):
+ os.remove(self._temp_file_path)
+ self._temp_file_path = None
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/function_handle.html b/chromium/third_party/catapult/tracing/tracing/mre/function_handle.html
new file mode 100644
index 00000000000..8a2a5fc1dad
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/function_handle.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ const FunctionRegistry = {
+ allFunctions_: [],
+ allFunctionsByName_: {},
+ get allFunctions() { return this.allFunctions_; },
+ get allFunctionsByName() { return this.allFunctionsByName_; }
+ };
+
+ FunctionRegistry.getFunction = function(name) {
+ return this.allFunctionsByName_[name];
+ };
+
+ FunctionRegistry.register = function(func) {
+ if (func.name === '') {
+ throw new Error('Registered functions must not be anonymous');
+ }
+ if (this.allFunctionsByName[func.name] !== undefined) {
+ throw new Error('Function named ' + func.name + 'is already registered.');
+ }
+ this.allFunctionsByName[func.name] = func;
+ this.allFunctions.push(func);
+ };
+
+ function ModuleToLoad(href, filename) {
+ if ((href !== undefined) ? (filename !== undefined) :
+ (filename === undefined)) {
+ throw new Error('ModuleToLoad must specify exactly one of href or ' +
+ 'filename');
+ }
+ this.href = href;
+ this.filename = filename;
+ }
+
+ ModuleToLoad.prototype = {
+ asDict() {
+ if (this.href !== undefined) {
+ return {'href': this.href};
+ }
+ return {'filename': this.filename};
+ },
+
+ toString() {
+ if (this.href !== undefined) {
+ return 'ModuleToLoad(href="' + this.href + '")';
+ }
+ return 'ModuleToLoad(filename="' + this.filename + '")';
+ }
+ };
+
+ ModuleToLoad.fromDict = function(moduleDict) {
+ return new ModuleToLoad(moduleDict.href, moduleDict.filename);
+ };
+
+ function FunctionHandle(modulesToLoad, functionName, opt_options) {
+ if (!(modulesToLoad instanceof Array)) {
+ throw new Error('modulesToLoad in FunctionHandle must be an array');
+ }
+ if (typeof(functionName) !== 'string') {
+ throw new Error('functionName in FunctionHandle must be a string');
+ }
+ this.modulesToLoad = modulesToLoad;
+ this.functionName = functionName;
+ this.options_ = opt_options;
+ }
+
+ FunctionHandle.prototype = {
+ get options() {
+ return this.options_;
+ },
+
+ asDict() {
+ return {
+ 'modules_to_load': this.modulesToLoad.map(
+ function(m) {return m.asDict();}),
+ 'function_name': this.functionName,
+ 'options': this.options_
+ };
+ },
+
+ asUserFriendlyString() {
+ const parts = this.modulesToLoad.map(mtl => mtl.filename);
+ parts.push(this.functionName);
+ parts.push(JSON.stringify(this.options_));
+ return parts.join(',');
+ },
+
+ hasHrefs() {
+ for (const module in this.modulesToLoad) {
+ if (this.modulesToLoad[module].href !== undefined) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ load() {
+ if (this.hasHrefs()) {
+ const err = new Error(
+ 'FunctionHandle named ' + this.functionName +
+ ' specifies hrefs, which cannot be loaded.');
+ err.name = 'FunctionLoadingError';
+ throw err;
+ }
+
+ for (const module in this.modulesToLoad) {
+ const filename = this.modulesToLoad[module].filename;
+ try {
+ HTMLImportsLoader.loadHTMLFile(filename);
+ } catch (err) {
+ err.name = 'FunctionLoadingError';
+ throw err;
+ }
+ }
+
+ const func = FunctionRegistry.getFunction(this.functionName);
+ if (func === undefined) {
+ const err = new Error(
+ 'No registered function named ' + this.functionName);
+ err.name = 'FunctionNotDefinedError';
+ throw err;
+ }
+
+ return func;
+ },
+
+ toString() {
+ const modulesToLoadStr = this.modulesToLoad.map(function(module) {
+ return module.toString();
+ });
+ return 'FunctionHandle(modulesToLoad=[' + modulesToLoadStr + '], ' +
+ 'functionName="' + this.functionName + '", options="' +
+ JSON.stringify(this.options_) + '")';
+ }
+ };
+
+ FunctionHandle.loadFromFilename_ = function(filename) {
+ try {
+ const numFunctionsBefore = FunctionRegistry.allFunctions.length;
+ HTMLImportsLoader.loadHTMLFile(filename);
+ } catch (err) {
+ err.name = 'FunctionLoadingError';
+ throw err;
+ }
+
+ // Verify a new function was registered.
+ const numFunctionsNow = FunctionRegistry.allFunctions.length;
+ if (numFunctionsNow !== (numFunctionsBefore + 1)) {
+ const err = new Error(
+ filename + ' didn\'t call FunctionRegistry.register');
+ err.name = 'FunctionNotDefinedError';
+ throw err;
+ }
+
+ return FunctionRegistry.allFunctions[numFunctionsNow - 1];
+ };
+
+ FunctionHandle.fromDict = function(handleDict) {
+ const options = handleDict.options;
+ let modulesToLoad;
+ if (handleDict.modules_to_load !== undefined) {
+ modulesToLoad = handleDict.modules_to_load.map(function(module) {
+ return ModuleToLoad.fromDict(module);
+ });
+ }
+ return new FunctionHandle(modulesToLoad, handleDict.function_name, options);
+ };
+
+ return {
+ FunctionHandle,
+ ModuleToLoad,
+ FunctionRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/function_handle.py b/chromium/third_party/catapult/tracing/tracing/mre/function_handle.py
new file mode 100644
index 00000000000..1efa5d647a9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/function_handle.py
@@ -0,0 +1,139 @@
+# 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 uuid
+
+
+class AbspathInvalidError(Exception):
+ """Raised if an abspath cannot be sanitized based on an app's source paths."""
+
+
+class UserFriendlyStringInvalidError(Exception):
+ """Raised if a user friendly string cannot be parsed."""
+
+
+class ModuleToLoad(object):
+
+ def __init__(self, href=None, filename=None):
+ if bool(href) == bool(filename):
+ raise Exception('ModuleToLoad must specify exactly one of href and '
+ 'filename.')
+ self.href = href
+ self.filename = filename
+
+ def __repr__(self):
+ if self.href:
+ return 'ModuleToLoad(href="%s")' % self.href
+ return 'ModuleToLoad(filename="%s")' % self.filename
+
+ def AsDict(self):
+ if self.href:
+ return {'href': self.href}
+ return {'filename': self.filename}
+
+ @staticmethod
+ def FromDict(module_dict):
+ return ModuleToLoad(module_dict.get('href'), module_dict.get('filename'))
+
+
+class FunctionHandle(object):
+
+ def __init__(self, modules_to_load=None, function_name=None,
+ options=None, guid=uuid.uuid4()):
+ self.modules_to_load = modules_to_load
+ self.function_name = function_name
+ self.options = options
+ self._guid = guid
+
+ def __repr__(self):
+ return 'FunctionHandle(modules_to_load=[%s], function_name="%s")' % (
+ ', '.join([str(module) for module in self.modules_to_load]),
+ self.function_name)
+
+ @property
+ def guid(self):
+ return self._guid
+
+ @property
+ def has_hrefs(self):
+ return any(module.href for module in self.modules_to_load)
+
+ def AsDict(self):
+ handle_dict = {
+ 'function_name': self.function_name
+ }
+
+ if self.modules_to_load is not None:
+ handle_dict['modules_to_load'] = [module.AsDict() for module in
+ self.modules_to_load]
+ if self.options is not None:
+ handle_dict['options'] = self.options
+
+ return handle_dict
+
+ def ConvertHrefsToAbsFilenames(self, app):
+ """Converts hrefs to absolute filenames in the context of |app|.
+
+ In an app-serving context, functions must only reside in files which the app
+ is serving, in order to prevent directory traversal attacks. In addition, we
+ rely on paths being absolute when actually executing functions.
+
+ Args:
+ app: A dev server instance requesting abspath conversion.
+
+ Returns:
+ A new FunctionHandle instance with no hrefs.
+
+ Raises:
+ AbspathInvalidError: If there is no source path with which a given abspath
+ shares a common prefix.
+ """
+ new_modules_to_load = []
+ for module in self.modules_to_load:
+ if module.href:
+ abspath = app.GetAbsFilenameForHref(module.href)
+ else:
+ assert os.path.abspath(module.filename) == module.filename
+ abspath = module.filename
+
+ if not abspath:
+ raise AbspathInvalidError('Filename %s invalid', abspath)
+
+ new_modules_to_load.append(ModuleToLoad(filename=abspath))
+
+ return FunctionHandle(modules_to_load=new_modules_to_load,
+ function_name=self.function_name)
+
+ @staticmethod
+ def FromDict(handle_dict):
+ if handle_dict.get('modules_to_load') is not None:
+ modules_to_load = [ModuleToLoad.FromDict(module_dict) for module_dict in
+ handle_dict['modules_to_load']]
+ else:
+ modules_to_load = []
+ options = handle_dict.get('options')
+ return FunctionHandle(modules_to_load=modules_to_load,
+ function_name=handle_dict['function_name'],
+ options=options)
+
+ def AsUserFriendlyString(self, app):
+ parts = [module.filename for module in
+ self.ConvertHrefsToAbsFilenames(app).modules_to_load]
+ parts.append(self.function_name)
+
+ return ':'.join(parts)
+
+ @staticmethod
+ def FromUserFriendlyString(user_str):
+ parts = user_str.split(':')
+ if len(parts) < 2:
+ raise UserFriendlyStringInvalidError(
+ 'Tried to deserialize string with less than two parts: ' + user_str)
+
+ modules_to_load = [ModuleToLoad(filename=name) for name in parts[:-1]]
+
+ return FunctionHandle(modules_to_load=modules_to_load,
+ function_name=parts[-1])
+
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/function_handle_test.html b/chromium/third_party/catapult/tracing/tracing/mre/function_handle_test.html
new file mode 100644
index 00000000000..ab83edae429
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/function_handle_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/mre/function_handle.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('moduleToLoadExactlyOneHrefOrFilename', function() {
+ assert.throws(function() {tr.mre.ModuleToLoad('foo', 'foo');});
+ assert.throws(tr.mre.ModuleToLoad);
+ });
+
+ test('moduleToLoadAsDictTest', function() {
+ const mtl0 = new tr.mre.ModuleToLoad('/foo');
+ const mtl1 = new tr.mre.ModuleToLoad(undefined, 'foo.html');
+
+ assert.deepEqual(mtl0.asDict(), {'href': '/foo'});
+ assert.deepEqual(mtl1.asDict(), {'filename': 'foo.html'});
+ });
+
+ test('moduleToLoadFromDictTest', function() {
+ const moduleDict0 = {
+ href: '/foo'
+ };
+ const moduleDict1 = {
+ filename: 'foo.html'
+ };
+
+ const mtl0 = tr.mre.ModuleToLoad.fromDict(moduleDict0);
+ const mtl1 = tr.mre.ModuleToLoad.fromDict(moduleDict1);
+
+ assert.strictEqual(mtl0.href, '/foo');
+ assert.isUndefined(mtl0.filename);
+ assert.strictEqual(mtl1.filename, 'foo.html');
+ assert.isUndefined(mtl1.href);
+ });
+
+ test('moduleToLoadToStringTest', function() {
+ const mtl0 = new tr.mre.ModuleToLoad('/foo');
+ const mtl1 = new tr.mre.ModuleToLoad(undefined, 'foo.html');
+
+ assert.strictEqual(
+ mtl0.toString(),
+ 'ModuleToLoad(href="/foo")');
+ assert.strictEqual(
+ mtl1.toString(),
+ 'ModuleToLoad(filename="foo.html")');
+ });
+
+ test('modulesToLoadMustBeArrayTest', function() {
+ assert.throws(tr.mre.FunctionHandle);
+ });
+
+ test('functionNameMustBeStringTest', function() {
+ assert.throws(function() {tr.mre.FunctionHandle([], 3);});
+ });
+
+ test('asDictTest', function() {
+ const module = new tr.mre.ModuleToLoad('/foo');
+ const handle = new tr.mre.FunctionHandle([module], 'Bar', {'a': 'b'});
+
+ assert.deepEqual(handle.asDict(), {
+ modules_to_load: [{href: '/foo'}],
+ function_name: 'Bar',
+ options: {'a': 'b'}
+ });
+ });
+
+ test('fromDictTest', function() {
+ const handleDict = {
+ modules_to_load: [{href: '/foo'}],
+ function_name: 'Bar'
+ };
+
+ const handle = tr.mre.FunctionHandle.fromDict(handleDict);
+
+ assert.strictEqual(handle.modulesToLoad.length, 1);
+ assert.strictEqual(handle.modulesToLoad[0].href, '/foo');
+ assert.strictEqual(handle.functionName, 'Bar');
+ });
+
+ test('hasHrefsTest', function() {
+ const module0 = new tr.mre.ModuleToLoad('/foo');
+ const handle0 = new tr.mre.FunctionHandle([module0], 'Bar');
+ const module1 = new tr.mre.ModuleToLoad(undefined, 'foo.html');
+ const handle1 = new tr.mre.FunctionHandle([module1], 'Bar');
+
+ assert.isTrue(handle0.hasHrefs());
+ assert.isFalse(handle1.hasHrefs());
+ });
+
+ test('loadFailsWithHrefs', function() {
+ const module = new tr.mre.ModuleToLoad('/foo');
+ const handle = new tr.mre.FunctionHandle([module], 'railMapFunction');
+
+ assert.throws(handle.load);
+ });
+
+ test('loadFailsUnregistered', function() {
+ const handle = new tr.mre.FunctionHandle([], 'Bar');
+
+ assert.throws(handle.load);
+ });
+
+ test('toStringTest', function() {
+ const module = new tr.mre.ModuleToLoad('/foo');
+ const handle = new tr.mre.FunctionHandle([module], 'Bar', {'a': 'b'});
+
+ assert.strictEqual(
+ handle.toString(),
+ 'FunctionHandle(modulesToLoad=[ModuleToLoad(href="/foo")], ' +
+ 'functionName="Bar", options="{"a":"b"}")');
+ });
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/function_handle_unittest.py b/chromium/third_party/catapult/tracing/tracing/mre/function_handle_unittest.py
new file mode 100644
index 00000000000..d3bf71dfe66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/function_handle_unittest.py
@@ -0,0 +1,86 @@
+# 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 unittest
+
+from tracing.mre import function_handle
+
+
+class ModuleToLoadTests(unittest.TestCase):
+
+ def testExactlyOneHrefOrFilename(self):
+ with self.assertRaises(Exception):
+ function_handle.ModuleToLoad()
+
+ with self.assertRaises(Exception):
+ function_handle.ModuleToLoad('foo', 'foo')
+
+ def testRepr(self):
+ mtl0 = function_handle.ModuleToLoad(href='/foo')
+ mtl1 = function_handle.ModuleToLoad(filename='foo.html')
+
+ self.assertEquals(str(mtl0), 'ModuleToLoad(href="/foo")')
+ self.assertEquals(str(mtl1), 'ModuleToLoad(filename="foo.html")')
+
+ def testAsDict(self):
+ mtl0 = function_handle.ModuleToLoad(href='/foo')
+ mtl1 = function_handle.ModuleToLoad(filename='foo.html')
+
+ self.assertEquals(mtl0.AsDict(), {
+ 'href': '/foo'
+ })
+
+ self.assertEquals(mtl1.AsDict(), {
+ 'filename': 'foo.html'
+ })
+
+ def testFromDict(self):
+ module_dict0 = {
+ 'href': '/foo'
+ }
+
+ module_dict1 = {
+ 'filename': 'foo.html'
+ }
+
+ mtl0 = function_handle.ModuleToLoad.FromDict(module_dict0)
+ mtl1 = function_handle.ModuleToLoad.FromDict(module_dict1)
+
+ self.assertEquals(mtl0.href, '/foo')
+ self.assertIsNone(mtl0.filename)
+ self.assertEquals(mtl1.filename, 'foo.html')
+ self.assertIsNone(mtl1.href)
+
+
+class FunctionHandleTests(unittest.TestCase):
+
+ def testRepr(self):
+ module = function_handle.ModuleToLoad(href='/foo')
+ handle = function_handle.FunctionHandle([module], 'Bar')
+
+ self.assertEquals(
+ str(handle),
+ 'FunctionHandle(modules_to_load=[ModuleToLoad(href="/foo")], '
+ 'function_name="Bar")')
+
+ def testAsDict(self):
+ module = function_handle.ModuleToLoad(href='/foo')
+ handle = function_handle.FunctionHandle([module], 'Bar')
+
+ self.assertEquals(
+ handle.AsDict(), {
+ 'modules_to_load': [{'href': '/foo'}],
+ 'function_name': 'Bar'
+ })
+
+ def testFromDict(self):
+ handle_dict = {
+ 'modules_to_load': [{'href': '/foo'}],
+ 'function_name': 'Bar'
+ }
+
+ handle = function_handle.FunctionHandle.FromDict(handle_dict)
+ self.assertEquals(len(handle.modules_to_load), 1)
+ self.assertEquals(handle.modules_to_load[0].href, '/foo')
+ self.assertEquals(handle.function_name, 'Bar')
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/gtest_progress_reporter.py b/chromium/third_party/catapult/tracing/tracing/mre/gtest_progress_reporter.py
new file mode 100644
index 00000000000..2557b08e809
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/gtest_progress_reporter.py
@@ -0,0 +1,91 @@
+# Copyright 2014 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 print_function
+
+import time
+import sys
+
+from tracing.mre import progress_reporter
+
+
+class GTestRunReporter(progress_reporter.RunReporter):
+
+ def __init__(self, canonical_url, output_stream, timestamp):
+ super(GTestRunReporter, self).__init__(canonical_url)
+ self._output_stream = output_stream
+ self._timestamp = timestamp
+
+ def _GetMs(self):
+ assert self._timestamp is not None, 'Did not call WillRun.'
+ return (time.time() - self._timestamp) * 1000
+
+ def DidAddFailure(self, failure):
+ super(GTestRunReporter, self).DidAddFailure(failure)
+ print(failure.stack.encode('utf-8'), file=self._output_stream)
+ self._output_stream.flush()
+
+ def DidRun(self, run_failed):
+ super(GTestRunReporter, self).DidRun(run_failed)
+ if run_failed:
+ print('[ FAILED ] %s (%0.f ms)' % (self.canonical_url, self._GetMs()),
+ file=self._output_stream)
+ else:
+ print('[ OK ] %s (%0.f ms)' % (self.canonical_url, self._GetMs()),
+ file=self._output_stream)
+ self._output_stream.flush()
+
+
+class GTestProgressReporter(progress_reporter.ProgressReporter):
+ """A progress reporter that outputs the progress report in gtest style.
+
+ Be careful each print should only handle one string. Otherwise, the output
+ might be interrupted by Chrome logging, and the output interpretation might
+ be incorrect. For example:
+ print("[ OK ]", testname, file=self._output_stream)
+ should be written as
+ print("[ OK ] %s" % testname, file=self._output_stream)
+ """
+
+ def __init__(self, output_stream=sys.stdout):
+ super(GTestProgressReporter, self).__init__()
+ self._output_stream = output_stream
+
+ def WillRun(self, canonical_url):
+ super(GTestProgressReporter, self).WillRun(canonical_url)
+ print('[ RUN ] %s' % canonical_url.encode('utf-8'),
+ file=self._output_stream)
+ self._output_stream.flush()
+ return GTestRunReporter(canonical_url, self._output_stream, time.time())
+
+ def DidFinishAllRuns(self, result_list):
+ super(GTestProgressReporter, self).DidFinishAllRuns(result_list)
+ successful_runs = 0
+ failed_canonical_urls = []
+ failed_runs = 0
+ for run in result_list:
+ if len(run.failures) != 0:
+ failed_runs += 1
+ for f in run.failures:
+ failed_canonical_urls.append(f.trace_canonical_url)
+ else:
+ successful_runs += 1
+
+ unit = 'test' if successful_runs == 1 else 'tests'
+ print('[ PASSED ] %d %s.' % (successful_runs, unit),
+ file=self._output_stream)
+ if len(failed_canonical_urls) > 0:
+ unit = 'test' if len(failed_canonical_urls) == 1 else 'tests'
+ print('[ FAILED ] %d %s, listed below:' % (failed_runs, unit),
+ file=self._output_stream)
+ for failed_canonical_url in failed_canonical_urls:
+ print('[ FAILED ] %s' % failed_canonical_url.encode('utf-8'),
+ file=self._output_stream)
+ print(file=self._output_stream)
+ count = len(failed_canonical_urls)
+ unit = 'TEST' if count == 1 else 'TESTS'
+ print('%d FAILED %s' % (count, unit), file=self._output_stream)
+ print(file=self._output_stream)
+
+ self._output_stream.flush()
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/job.html b/chromium/third_party/catapult/tracing/tracing/mre/job.html
new file mode 100644
index 00000000000..5424ef751f0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/job.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/mre/function_handle.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ function Job(mapFunctionHandle, opt_guid) {
+ this.mapFunctionHandle_ = mapFunctionHandle;
+ if (opt_guid === undefined) {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ } else {
+ this.guid_ = opt_guid;
+ }
+ }
+
+ Job.prototype = {
+ get mapFunctionHandle() { return this.mapFunctionHandle_; },
+ get guid() { return this.guid_; },
+
+ asDict() {
+ return {
+ map_function_handle: this.mapFunctionHandle_.asDict(),
+ guid: this.guid_.toString()
+ };
+ }
+ };
+
+ Job.fromDict = function(jobDict) {
+ let mapFunctionHandle = null;
+ if (jobDict.map_function_handle !== null) {
+ mapFunctionHandle = tr.mre.FunctionHandle.fromDict(
+ jobDict.map_function_handle);
+ }
+
+ return new Job(mapFunctionHandle, jobDict.guid);
+ };
+
+ return {
+ Job,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/job.py b/chromium/third_party/catapult/tracing/tracing/mre/job.py
new file mode 100644
index 00000000000..d8493c3b651
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/job.py
@@ -0,0 +1,39 @@
+# 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.
+import uuid
+
+from tracing.mre import function_handle
+
+
+class Job(object):
+
+ def __init__(self, map_function_handle, guid=None):
+ if guid is None:
+ guid = uuid.uuid4()
+
+ assert map_function_handle is not None
+
+ self._map_function_handle = map_function_handle
+ self._guid = guid
+
+ @property
+ def guid(self):
+ return self._guid
+
+ @property
+ def map_function_handle(self):
+ return self._map_function_handle
+
+ def AsDict(self):
+ values_dict = {
+ 'map_function_handle': self._map_function_handle.AsDict(),
+ 'guid': str(self._guid)
+ }
+ return values_dict
+
+ @staticmethod
+ def FromDict(job_dict):
+ return Job(
+ function_handle.FunctionHandle.FromDict(
+ job_dict['map_function_handle']))
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/job_unittest.py b/chromium/third_party/catapult/tracing/tracing/mre/job_unittest.py
new file mode 100644
index 00000000000..625dff2a15e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/job_unittest.py
@@ -0,0 +1,15 @@
+# Copyright 2017 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 tracing.mre import job
+
+
+class JobTest(unittest.TestCase):
+
+ def testNoDuplicateUUID(self):
+ job0 = job.Job('not none')
+ job1 = job.Job('not none')
+ self.assertNotEqual(job0.guid, job1.guid)
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/json_output_formatter.py b/chromium/third_party/catapult/tracing/tracing/mre/json_output_formatter.py
new file mode 100644
index 00000000000..aafc8838de3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/json_output_formatter.py
@@ -0,0 +1,20 @@
+# 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 json
+
+from tracing.mre import output_formatter
+
+
+class JSONOutputFormatter(output_formatter.OutputFormatter):
+
+ def __init__(self, output_file):
+ # TODO(nduca): Resolve output_file here vs output_stream in base class.
+ super(JSONOutputFormatter, self).__init__(output_file)
+ self.output_file = output_file
+
+ def Format(self, result_list):
+ d = [result.AsDict() for result in result_list]
+ json.dump(d, self.output_file, indent=2)
+ if hasattr(self.output_file, 'flush'):
+ self.output_file.flush()
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/local_directory_corpus_driver.py b/chromium/third_party/catapult/tracing/tracing/mre/local_directory_corpus_driver.py
new file mode 100644
index 00000000000..a235f916baf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/local_directory_corpus_driver.py
@@ -0,0 +1,69 @@
+# 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
+
+from tracing.mre import corpus_driver
+from tracing.mre import file_handle
+
+
+def _GetFilesIn(basedir):
+ data_files = []
+ for dirpath, dirnames, filenames in os.walk(basedir, followlinks=True):
+ new_dirnames = [d for d in dirnames if not d.startswith('.')]
+ del dirnames[:]
+ dirnames += new_dirnames
+ for f in filenames:
+ if f.startswith('.'):
+ continue
+ if f == 'README.md':
+ continue
+ full_f = os.path.join(dirpath, f)
+ rel_f = os.path.relpath(full_f, basedir)
+ data_files.append(rel_f)
+
+ data_files.sort()
+ return data_files
+
+
+def _DefaultUrlResover(abspath):
+ return 'file:///%s' % abspath
+
+
+class LocalDirectoryCorpusDriver(corpus_driver.CorpusDriver):
+
+ def __init__(self, trace_directory, url_resolver=_DefaultUrlResover):
+ self.directory = trace_directory
+ self.url_resolver = url_resolver
+
+ @staticmethod
+ def CheckAndCreateInitArguments(parser, args):
+ del args # Unused by LocalDirectoryCorpusDriver.
+ trace_dir = os.getcwd()
+ if not os.path.exists(trace_dir):
+ parser.error('Trace directory does not exist')
+ return None
+ return {'trace_directory': trace_dir}
+
+ @staticmethod
+ def AddArguments(parser):
+ parser.add_argument(
+ '--trace_directory',
+ help='Local directory containing traces to process.')
+
+ def GetTraceHandles(self):
+ trace_handles = []
+
+ files = _GetFilesIn(self.directory)
+ for rel_filename in files:
+ filename = os.path.join(self.directory, rel_filename)
+
+ url = self.url_resolver(filename)
+ if url is None:
+ url = _DefaultUrlResover(filename)
+
+ th = file_handle.URLFileHandle(url, 'file://' + filename)
+ trace_handles.append(th)
+
+ return trace_handles
+
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_runner.py b/chromium/third_party/catapult/tracing/tracing/mre/map_runner.py
new file mode 100644
index 00000000000..5b3deca8ab8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_runner.py
@@ -0,0 +1,105 @@
+# 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.
+import multiprocessing
+import sys
+
+from tracing.mre import map_single_trace
+from tracing.mre import threaded_work_queue
+from tracing.mre import gtest_progress_reporter
+
+AUTO_JOB_COUNT = -1
+
+
+class MapError(Exception):
+
+ def __init__(self, *args):
+ super(MapError, self).__init__(*args)
+ self.canonical_url = None
+
+
+class MapRunner(object):
+ def __init__(self, trace_handles, job,
+ stop_on_error=False, progress_reporter=None,
+ jobs=AUTO_JOB_COUNT,
+ output_formatters=None,
+ extra_import_options=None):
+ self._job = job
+ self._stop_on_error = stop_on_error
+ self._failed_canonical_url_to_dump = None
+ if progress_reporter is None:
+ self._progress_reporter = gtest_progress_reporter.GTestProgressReporter(
+ sys.stdout)
+ else:
+ self._progress_reporter = progress_reporter
+ self._output_formatters = output_formatters or []
+ self._extra_import_options = extra_import_options
+
+ self._trace_handles = trace_handles
+ self._num_traces_merged_into_results = 0
+ self._map_results = None
+ self._map_results_file = None
+
+ if jobs == AUTO_JOB_COUNT:
+ jobs = multiprocessing.cpu_count()
+ self._wq = threaded_work_queue.ThreadedWorkQueue(num_threads=jobs)
+
+ def _ProcessOneTrace(self, trace_handle):
+ canonical_url = trace_handle.canonical_url
+ run_reporter = self._progress_reporter.WillRun(canonical_url)
+ result = map_single_trace.MapSingleTrace(
+ trace_handle,
+ self._job,
+ extra_import_options=self._extra_import_options)
+
+ had_failure = len(result.failures) > 0
+
+ for f in result.failures:
+ run_reporter.DidAddFailure(f)
+ run_reporter.DidRun(had_failure)
+
+ self._wq.PostMainThreadTask(
+ self._MergeResultIntoMaster, result, trace_handle)
+
+ def _MergeResultIntoMaster(self, result, trace_handle):
+ self._map_results[trace_handle.canonical_url] = result
+
+ had_failure = len(result.failures) > 0
+ if self._stop_on_error and had_failure:
+ err = MapError("Mapping error")
+ self._AbortMappingDueStopOnError(err)
+ raise err
+
+ self._num_traces_merged_into_results += 1
+ if self._num_traces_merged_into_results == len(self._trace_handles):
+ self._wq.PostMainThreadTask(self._AllMappingDone)
+
+ def _AbortMappingDueStopOnError(self, err):
+ self._wq.Stop(err)
+
+ def _AllMappingDone(self):
+ self._wq.Stop()
+
+ def RunMapper(self):
+ self._map_results = {}
+
+ if not self._trace_handles:
+ err = MapError("No trace handles specified.")
+ raise err
+
+ if self._job.map_function_handle:
+ for trace_handle in self._trace_handles:
+ self._wq.PostAnyThreadTask(self._ProcessOneTrace, trace_handle)
+
+ self._wq.Run()
+
+ return self._map_results
+
+ def Run(self):
+ results_by_trace = self.RunMapper()
+ results = results_by_trace.values()
+
+ for of in self._output_formatters:
+ of.Format(results)
+
+ return results
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.html b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.html
new file mode 100644
index 00000000000..104ac919e20
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/extras/full_config.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/mre/failure.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ const Failure = tr.mre.Failure;
+
+ function runAndConvertErrorsToFailures(result, job,
+ traceHandle, cb, opt_this) {
+ try {
+ cb.call(opt_this);
+ } catch (e) {
+ const err = tr.b.normalizeException(e);
+ // TODO(eakuefner): Set job once reduction is implemented.
+ result.addFailure(new Failure(
+ job, job.mapFunctionHandle.asUserFriendlyString(),
+ traceHandle.canonicalUrl, err.typeName, err.message, err.stack));
+ }
+ }
+
+ function mapSingleTrace(result, model, options, mapFunction) {
+ // Map the function.
+ const numPairsBeforeMapping = Object.keys(result.pairs).length;
+ const numFailuresBeforeMapping = result.failures.length;
+ try {
+ mapFunction(result, model, options);
+ } catch (ex) {
+ ex.name = 'MapFunctionError';
+ throw ex;
+ }
+
+ const addedPairs = (Object.keys(result.pairs).length >
+ numPairsBeforeMapping);
+ const addedFailures = result.failures.length > numFailuresBeforeMapping;
+ if (!(addedPairs || addedFailures)) {
+ const err = new Error('Mapper did not add any results!');
+ err.name = 'NoResultsAddedError';
+ throw err;
+ }
+ }
+
+ return {
+ mapSingleTrace,
+ runAndConvertErrorsToFailures
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.py b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.py
new file mode 100644
index 00000000000..83b7177984a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace.py
@@ -0,0 +1,196 @@
+# 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 json
+import os
+import re
+import sys
+import tempfile
+import platform
+
+import tracing_project
+import vinn
+
+from tracing.mre import failure
+from tracing.mre import file_handle
+from tracing.mre import function_handle
+from tracing.mre import mre_result
+from tracing.mre import job as job_module
+
+_MAP_SINGLE_TRACE_CMDLINE_PATH = os.path.join(
+ tracing_project.TracingProject.tracing_src_path, 'mre',
+ 'map_single_trace_cmdline.html')
+
+class TemporaryMapScript(object):
+ def __init__(self, js):
+ tempfile_kwargs = {'mode': 'w+', 'delete': False}
+ if sys.version_info >= (3,):
+ tempfile_kwargs['encoding'] = 'utf-8'
+ temp_file = tempfile.NamedTemporaryFile(**tempfile_kwargs)
+
+ temp_file.write("""
+<!DOCTYPE html>
+<script>
+%s
+</script>
+""" % js)
+ temp_file.close()
+ self._filename = temp_file.name
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args, **kwargs):
+ os.remove(self._filename)
+ self._filename = None
+
+ @property
+ def filename(self):
+ return self._filename
+
+
+class FunctionLoadingFailure(failure.Failure):
+ pass
+
+class FunctionNotDefinedFailure(failure.Failure):
+ pass
+
+class MapFunctionFailure(failure.Failure):
+ pass
+
+class FileLoadingFailure(failure.Failure):
+ pass
+
+class TraceImportFailure(failure.Failure):
+ pass
+
+class NoResultsAddedFailure(failure.Failure):
+ pass
+
+class InternalMapError(Exception):
+ pass
+
+_FAILURE_NAME_TO_FAILURE_CONSTRUCTOR = {
+ 'FileLoadingError': FileLoadingFailure,
+ 'FunctionLoadingError': FunctionLoadingFailure,
+ 'FunctionNotDefinedError': FunctionNotDefinedFailure,
+ 'TraceImportError': TraceImportFailure,
+ 'MapFunctionError': MapFunctionFailure,
+ 'NoResultsAddedError': NoResultsAddedFailure
+}
+
+
+def MapSingleTrace(trace_handle,
+ job,
+ extra_import_options=None):
+ assert (isinstance(extra_import_options, (type(None), dict))), (
+ 'extra_import_options should be a dict or None.')
+ project = tracing_project.TracingProject()
+
+ all_source_paths = list(project.source_paths)
+ all_source_paths.append(project.trace_processor_root_path)
+
+ result = mre_result.MreResult()
+
+ with trace_handle.PrepareFileForProcessing() as prepared_trace_handle:
+ js_args = [
+ json.dumps(prepared_trace_handle.AsDict()),
+ json.dumps(job.AsDict()),
+ ]
+ if extra_import_options:
+ js_args.append(json.dumps(extra_import_options))
+
+ # Use 8gb heap space to make sure we don't OOM'ed on big trace, but not
+ # on ARM devices since we use 32-bit d8 binary.
+ if platform.machine() == 'armv7l' or platform.machine() == 'aarch64':
+ v8_args = None
+ else:
+ v8_args = ['--max-old-space-size=8192']
+
+ res = vinn.RunFile(
+ _MAP_SINGLE_TRACE_CMDLINE_PATH,
+ source_paths=all_source_paths,
+ js_args=js_args,
+ v8_args=v8_args)
+
+ stdout = res.stdout
+ if not isinstance(stdout, str):
+ stdout = stdout.decode('utf-8', errors='replace')
+
+ if res.returncode != 0:
+ sys.stderr.write(stdout)
+ result.AddFailure(failure.Failure(
+ job,
+ trace_handle.canonical_url,
+ 'Error', 'vinn runtime error while mapping trace.',
+ 'vinn runtime error while mapping trace.', 'Unknown stack'))
+ return result
+
+ for line in stdout.split('\n'):
+ m = re.match('^MRE_RESULT: (.+)', line, re.DOTALL)
+ if m:
+ found_dict = json.loads(m.group(1))
+ failures = [
+ failure.Failure.FromDict(f, job, _FAILURE_NAME_TO_FAILURE_CONSTRUCTOR)
+ for f in found_dict['failures']]
+
+ for f in failures:
+ result.AddFailure(f)
+
+ for k, v in found_dict['pairs'].items():
+ result.AddPair(k, v)
+
+ else:
+ if len(line) > 0:
+ sys.stderr.write(line)
+ sys.stderr.write('\n')
+
+ if not (len(result.pairs) or len(result.failures)):
+ raise InternalMapError('Internal error: No results were produced!')
+
+ return result
+
+
+def ExecuteTraceMappingCode(trace_file_path, process_trace_func_code,
+ extra_import_options=None,
+ trace_canonical_url=None):
+ """Execute |process_trace_func_code| on the input |trace_file_path|.
+
+ process_trace_func_code must contain a function named 'process_trace' with
+ signature as follows:
+
+ function processTrace(results, model) {
+ // call results.addPair(<key>, <value>) to add data to results object.
+ }
+
+ Whereas results is an instance of tr.mre.MreResult, and model is an instance
+ of tr.model.Model which was resulted from parsing the input trace.
+
+ Returns:
+ This function returns the dictionay that represents data collected in
+ |results|.
+
+ Raises:
+ RuntimeError if there is any error with execute trace mapping code.
+ """
+
+ with TemporaryMapScript("""
+ //# sourceURL=processTrace
+ %s;
+ tr.mre.FunctionRegistry.register(processTrace);
+ """ % process_trace_func_code) as map_script:
+ handle = function_handle.FunctionHandle(
+ [function_handle.ModuleToLoad(filename=map_script.filename)],
+ function_name='processTrace')
+ mapping_job = job_module.Job(handle)
+ trace_file_path = os.path.abspath(trace_file_path)
+ if not trace_canonical_url:
+ trace_canonical_url = 'file://%s' % trace_file_path
+ trace_handle = file_handle.URLFileHandle(
+ trace_file_path, trace_canonical_url)
+ results = MapSingleTrace(trace_handle, mapping_job, extra_import_options)
+ if results.failures:
+ raise RuntimeError(
+ 'Failures mapping trace:\n%s' %
+ ('\n'.join(str(f) for f in results.failures)))
+ return results.pairs
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_cmdline.html b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_cmdline.html
new file mode 100644
index 00000000000..b79f9ec67c7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_cmdline.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/mre/failure.html">
+<link rel="import" href="/tracing/mre/file_handle.html">
+<link rel="import" href="/tracing/mre/function_handle.html">
+<link rel="import" href="/tracing/mre/job.html">
+<link rel="import" href="/tracing/mre/map_single_trace.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+tr.exportTo('tr.mre', function() {
+ const Failure = tr.mre.Failure;
+
+ function createModelFromTraceData(traceData,
+ canonicalUrl,
+ opt_extraImportOptions) {
+ const model = new tr.Model();
+ try {
+ const importOptions = new tr.importer.ImportOptions();
+ importOptions.pruneEmptyContainers = false;
+ importOptions.showImportWarnings = false;
+ if (opt_extraImportOptions !== undefined) {
+ for (const property in opt_extraImportOptions) {
+ if (opt_extraImportOptions.hasOwnProperty(property)) {
+ importOptions[property] = opt_extraImportOptions[property];
+ }
+ }
+ }
+
+ const i = new tr.importer.Import(model, importOptions);
+ i.importTraces([traceData]);
+ } catch (ex) {
+ ex.name = 'TraceImportError';
+ throw ex;
+ }
+
+ model.canonicalUrl = canonicalUrl;
+
+ return model;
+ }
+
+ function mapSingleTraceWithResult(options) {
+ const result = new tr.mre.MreResult();
+
+ tr.mre.runAndConvertErrorsToFailures(
+ result, options.job, options.traceHandle,
+ function() {
+ const mapFunction = options.job.mapFunctionHandle.load();
+ const traceData = options.traceHandle.load();
+ const model = createModelFromTraceData(
+ traceData, options.traceHandle.canonicalUrl,
+ options.extraImportOptions);
+ const opt_options = options.job.mapFunctionHandle.options;
+ tr.mre.mapSingleTrace(result, model, opt_options, mapFunction);
+ });
+ return result;
+ }
+
+ function mapSingleTraceMain(args) {
+ if (args.length !== 2 && args.length !== 3) {
+ throw new Error('Must provide two or three arguments.');
+ }
+
+ const options = {
+ traceHandle: tr.mre.FileHandle.fromDict(JSON.parse(args[0])),
+ job: tr.mre.Job.fromDict(JSON.parse(args[1])),
+ extraImportOptions: args.length === 3 ? JSON.parse(args[2]) : undefined
+ };
+
+ const result = mapSingleTraceWithResult(options);
+
+ console.log('MRE_RESULT: ' + JSON.stringify(result.asDict()));
+ return 0;
+ }
+
+ return {
+ mapSingleTraceMain,
+ mapSingleTraceWithResult
+ };
+});
+
+if (tr.isHeadless) {
+ quit(tr.mre.mapSingleTraceMain(sys.argv.slice(1)));
+}
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_unittest.py b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_unittest.py
new file mode 100644
index 00000000000..fb6b5bff071
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_single_trace_unittest.py
@@ -0,0 +1,235 @@
+# 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 json
+import os
+import unittest
+
+from tracing.mre import function_handle
+from tracing.mre import map_single_trace
+from tracing.mre import file_handle
+from tracing.mre import failure
+from tracing.mre import job as job_module
+
+
+def _Handle(filename):
+ module = function_handle.ModuleToLoad(filename=filename)
+ map_handle = function_handle.FunctionHandle(
+ modules_to_load=[module], function_name='MyMapFunction')
+ return job_module.Job(map_handle, None)
+
+
+class MapSingleTraceTests(unittest.TestCase):
+
+ def testPassingMapScript(self):
+ events = [
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'a', 'cat': 'c',
+ 'ts': 0, 'dur': 10, 'args': {}},
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'b', 'cat': 'c',
+ 'ts': 3, 'dur': 5, 'args': {}}
+ ]
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', json.dumps(events).encode('utf-8'))
+
+ with map_single_trace.TemporaryMapScript("""
+ tr.mre.FunctionRegistry.register(
+ function MyMapFunction(result, model) {
+ var canonicalUrl = model.canonicalUrl;
+ result.addPair('result', {
+ numProcesses: model.getAllProcesses().length
+ });
+ });
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertFalse(result.failures)
+ r = result.pairs['result']
+ self.assertEquals(r['numProcesses'], 1)
+
+
+ def testProcessingGiantTrace(self):
+ # Populate a string trace of 2 million events.
+ trace_events = ['[']
+ for i in range(2000000):
+ trace_events.append(
+ '{"pid": 1, "tid": %i, "ph": "X", "name": "a", "cat": "c",'
+ '"ts": %i, "dur": 1, "args": {}},' % (i % 5, 2 * i))
+ trace_events.append('{}]')
+ trace_data = ''.join(trace_events)
+ if not isinstance(trace_data, bytes):
+ trace_data = trace_data.encode('utf-8')
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', trace_data)
+
+ with map_single_trace.TemporaryMapScript("""
+ tr.mre.FunctionRegistry.register(
+ function MyMapFunction(result, model) {
+ var canonicalUrl = model.canonicalUrl;
+ var numEvents = 0;
+ for (var e of model.getProcess(1).getDescendantEvents()) {
+ numEvents++;
+ }
+ result.addPair('result', {
+ numEvents: numEvents
+ });
+ });
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertFalse(result.failures,
+ msg='\n'.join(str(f) for f in result.failures))
+ r = result.pairs['result']
+ self.assertEquals(r['numEvents'], 2000000)
+
+
+
+ def testTraceDidntImport(self):
+ trace_string = b'This is intentionally not a trace-formatted string.'
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', trace_string)
+
+ with map_single_trace.TemporaryMapScript("""
+ tr.mre.FunctionRegistry.register(
+ function MyMapFunction(results, model) {
+ });
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertEquals(len(result.failures), 1)
+ self.assertEquals(len(result.pairs), 0)
+ f = result.failures[0]
+ self.assertIsInstance(f, map_single_trace.TraceImportFailure)
+
+ def testMapFunctionThatThrows(self):
+ events = [
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'a', 'cat': 'c',
+ 'ts': 0, 'dur': 10, 'args': {}},
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'b', 'cat': 'c',
+ 'ts': 3, 'dur': 5, 'args': {}}
+ ]
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', json.dumps(events).encode('utf-8'))
+
+ with map_single_trace.TemporaryMapScript("""
+ tr.mre.FunctionRegistry.register(
+ function MyMapFunction(results, model) {
+ throw new Error('Expected error');
+ });
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertEquals(len(result.failures), 1)
+ self.assertEquals(len(result.pairs), 0)
+ f = result.failures[0]
+ self.assertIsInstance(f, map_single_trace.MapFunctionFailure)
+
+ def testMapperWithLoadError(self):
+ events = [
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'a', 'cat': 'c',
+ 'ts': 0, 'dur': 10, 'args': {}},
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'b', 'cat': 'c',
+ 'ts': 3, 'dur': 5, 'args': {}}
+ ]
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', json.dumps(events).encode('utf-8'))
+
+ with map_single_trace.TemporaryMapScript("""
+ throw new Error('Expected load error');
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertEquals(len(result.failures), 1)
+ self.assertEquals(len(result.pairs), 0)
+ f = result.failures[0]
+ self.assertIsInstance(f, map_single_trace.FunctionLoadingFailure)
+
+ def testMapperWithUnexpectedError(self):
+ events = [
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'a', 'cat': 'c',
+ 'ts': 0, 'dur': 10, 'args': {}},
+ ]
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', json.dumps(events).encode('utf-8'))
+
+ with map_single_trace.TemporaryMapScript("""
+ quit(100);
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertEquals(len(result.failures), 1)
+ self.assertEquals(len(result.pairs), 0)
+ f = result.failures[0]
+ self.assertIsInstance(f, failure.Failure)
+
+ def testNoMapper(self):
+ events = [
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'a', 'cat': 'c',
+ 'ts': 0, 'dur': 10, 'args': {}},
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'b', 'cat': 'c',
+ 'ts': 3, 'dur': 5, 'args': {}}
+ ]
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', json.dumps(events).encode('utf-8'))
+
+ with map_single_trace.TemporaryMapScript("""
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertEquals(len(result.failures), 1)
+ self.assertEquals(len(result.pairs), 0)
+ f = result.failures[0]
+ self.assertIsInstance(f, map_single_trace.FunctionNotDefinedFailure)
+
+ def testMapperDoesntAddValues(self):
+ events = [
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'a', 'cat': 'c',
+ 'ts': 0, 'dur': 10, 'args': {}},
+ {'pid': 1, 'tid': 2, 'ph': 'X', 'name': 'b', 'cat': 'c',
+ 'ts': 3, 'dur': 5, 'args': {}}
+ ]
+ trace_handle = file_handle.InMemoryFileHandle(
+ '/a.json', json.dumps(events).encode('utf-8'))
+
+ with map_single_trace.TemporaryMapScript("""
+ tr.mre.FunctionRegistry.register(
+ function MyMapFunction(results, model) {
+ });
+ """) as map_script:
+ result = map_single_trace.MapSingleTrace(trace_handle,
+ _Handle(map_script.filename))
+
+ self.assertEquals(len(result.failures), 1)
+ self.assertEquals(len(result.pairs), 0)
+ f = result.failures[0]
+ self.assertIsInstance(f, map_single_trace.NoResultsAddedFailure)
+
+ def testExecuteTraceMappingCode(self):
+ test_trace_path = os.path.join(os.path.dirname(__file__), 'test_trace.json')
+ results = map_single_trace.ExecuteTraceMappingCode(
+ test_trace_path,
+ """
+ function processTrace(results, model) {
+ var canonicalUrl = model.canonicalUrl;
+ results.addPair('numProcesses', model.getAllProcesses().length);
+ };
+ """)
+ self.assertEquals(results['numProcesses'], 2)
+
+ def testExecuteTraceMappingCodeWithError(self):
+ test_trace_path = os.path.join(os.path.dirname(__file__), 'test_trace.json')
+ with self.assertRaises(RuntimeError):
+ map_single_trace.ExecuteTraceMappingCode(
+ test_trace_path,
+ """
+ function processTrace(results, model) {
+ throw new Error('Expected error');
+ };
+ """)
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_traces.py b/chromium/third_party/catapult/tracing/tracing/mre/map_traces.py
new file mode 100644
index 00000000000..749e7acab39
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_traces.py
@@ -0,0 +1,67 @@
+# 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 argparse
+import sys
+
+from tracing.mre import corpus_driver_cmdline
+from tracing.mre import map_runner
+from tracing.mre import function_handle
+from tracing.mre import job as job_module
+from tracing.mre import json_output_formatter
+
+
+def Main(argv):
+ parser = argparse.ArgumentParser(
+ description='Bulk trace processing')
+ parser.add_argument('--map_function_handle')
+ parser.add_argument('-j', '--jobs', type=int,
+ default=map_runner.AUTO_JOB_COUNT)
+ parser.add_argument('-o', '--output-file')
+ parser.add_argument('-s', '--stop-on-error',
+ action='store_true')
+
+ if len(sys.argv) == 1:
+ parser.print_help()
+ sys.exit(0)
+
+ args = parser.parse_args(argv[1:])
+
+ corpus_driver = corpus_driver_cmdline.GetCorpusDriver(parser, args)
+
+ if args.output_file:
+ ofile = open(args.output_file, 'w')
+ else:
+ ofile = sys.stdout
+
+ output_formatter = json_output_formatter.JSONOutputFormatter(ofile)
+
+ try:
+ map_handle = None
+ if args.map_function_handle:
+ map_handle = function_handle.FunctionHandle.FromUserFriendlyString(
+ args.map_function_handle)
+ job = job_module.Job(map_handle)
+ except function_handle.UserFriendlyStringInvalidError:
+ error_lines = [
+ 'The map_traces command-line API has changed! You must now specify the',
+ 'filenames to load and the map function name, separated by :. For '
+ 'example, a mapper in',
+ 'foo.html called Foo would be written as foo.html:Foo .'
+ ]
+ parser.error('\n'.join(error_lines))
+
+ try:
+ trace_handles = corpus_driver.GetTraceHandles()
+ runner = map_runner.MapRunner(trace_handles, job,
+ stop_on_error=args.stop_on_error,
+ jobs=args.jobs,
+ output_formatters=[output_formatter])
+ results = runner.Run()
+ if not any(result.failures for result in results):
+ return 0
+ else:
+ return 255
+ finally:
+ if ofile != sys.stdout:
+ ofile.close()
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/map_traces_handler.py b/chromium/third_party/catapult/tracing/tracing/mre/map_traces_handler.py
new file mode 100644
index 00000000000..ea9b0b6d533
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/map_traces_handler.py
@@ -0,0 +1,14 @@
+# 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 webapp2
+
+
+def MapTrace(trace_corpus_driver): # pylint: disable=unused-argument
+ pass
+
+
+class MapTracesHandler(webapp2.RequestHandler):
+
+ def post(self, *args, **kwargs): # pylint: disable=unused-argument
+ pass
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/mre_result.html b/chromium/third_party/catapult/tracing/tracing/mre/mre_result.html
new file mode 100644
index 00000000000..3251bb051ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/mre_result.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/mre/failure.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ class MreResult {
+ constructor(failures, pairs) {
+ if (failures === undefined) {
+ failures = [];
+ }
+ if (pairs === undefined) {
+ pairs = {};
+ }
+ this.failures = failures;
+ this.pairs = pairs;
+ }
+
+ addFailure(failure) {
+ this.failures.push(failure);
+ }
+
+ addPair(key, value) {
+ if (key in this.pairs) {
+ throw new Error('Key ' + key + ' already exists in result.');
+ }
+ this.pairs[key] = value;
+ }
+
+ asDict() {
+ const d = {
+ pairs: this.pairs
+ };
+
+ if (this.failures) {
+ d.failures = this.failures.map(function(f) {return f.asDict();});
+ }
+
+ return d;
+ }
+
+ hadFailures() {
+ return this.failures.length > 0;
+ }
+
+ static fromDict(resultDict) {
+ const failures = (resultDict.failures !== undefined) ?
+ resultDict.failures.map(tr.mre.Failure.fromDict) : undefined;
+ const pairs = resultDict.pairs;
+ return new MreResult(failures, pairs);
+ }
+ }
+
+ return {
+ MreResult,
+ };
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/mre_result.py b/chromium/third_party/catapult/tracing/tracing/mre/mre_result.py
new file mode 100644
index 00000000000..f3d128e23a8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/mre_result.py
@@ -0,0 +1,48 @@
+# 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 tracing.mre import failure as failure_module
+
+class DuplicateKeyError(Exception):
+ """Raised if an attempt is made to set a key more than once."""
+
+
+class MreResult(object):
+
+ def __init__(self, failures=None, pairs=None):
+ if failures is None:
+ failures = []
+ if pairs is None:
+ pairs = {}
+ self._failures = failures
+ self._pairs = pairs
+
+ @property
+ def failures(self):
+ return self._failures
+
+ @property
+ def pairs(self):
+ return self._pairs
+
+ def AsDict(self):
+ d = {
+ 'pairs': self._pairs
+ }
+
+ if self.failures:
+ d['failures'] = [failure.AsDict() for failure in self._failures]
+
+ return d
+
+ def AddFailure(self, failure):
+ if not isinstance(failure, failure_module.Failure):
+ raise ValueError('Attempted to add %s as Failure', failure)
+
+ self._failures.append(failure)
+
+ def AddPair(self, key, value):
+ if key in self._pairs:
+ raise DuplicateKeyError('Key ' + key + 'already exists in result.')
+ self._pairs[key] = value
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/mre_result_test.html b/chromium/third_party/catapult/tracing/tracing/mre/mre_result_test.html
new file mode 100644
index 00000000000..5b8946990ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/mre_result_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/mre/failure.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('mreResultAsDictTest', function() {
+ const result = new tr.mre.MreResult();
+
+ const failure = new tr.mre.Failure('1', '2', '3', 'err', 'desc', 'stack');
+ result.addFailure(failure);
+
+ result.addPair('foo', 'bar');
+
+ const resultDict = result.asDict();
+
+ assert.deepEqual(resultDict.failures, [failure.asDict()]);
+ assert.deepEqual(resultDict.pairs, {foo: 'bar'});
+ });
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/mre_result_unittest.py b/chromium/third_party/catapult/tracing/tracing/mre/mre_result_unittest.py
new file mode 100644
index 00000000000..2bdb5a8f19a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/mre_result_unittest.py
@@ -0,0 +1,46 @@
+# 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.
+
+import unittest
+
+from tracing.mre import function_handle
+from tracing.mre import map_single_trace
+from tracing.mre import failure as failure_module
+from tracing.mre import job as job_module
+from tracing.mre import mre_result
+
+
+class MreResultTests(unittest.TestCase):
+
+ def testAsDict(self):
+ result = mre_result.MreResult()
+
+ with map_single_trace.TemporaryMapScript("""
+ tr.mre.FunctionRegistry.register(
+ function MyMapFunction(result, model) {
+ var canonicalUrl = model.canonicalUrl;
+ result.addPair('result', {
+ numProcesses: model.getAllProcesses().length
+ });
+ });
+ """) as map_script:
+
+ module = function_handle.ModuleToLoad(filename=map_script.filename)
+ map_handle = function_handle.FunctionHandle(
+ modules_to_load=[module], function_name='MyMapFunction')
+ job = job_module.Job(map_handle, None)
+ failure = failure_module.Failure(job, '2', '3', 'err', 'desc', 'stack')
+ result.AddFailure(failure)
+
+ result.AddPair('foo', 'bar')
+
+ result_dict = result.AsDict()
+
+ self.assertEquals(result_dict['failures'], [failure.AsDict()])
+ self.assertEquals(result_dict['pairs'], {'foo': 'bar'})
+
+ def testAddingNonFailure(self):
+ result = mre_result.MreResult()
+ with self.assertRaises(ValueError):
+ result.AddFailure('foo')
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/output_formatter.py b/chromium/third_party/catapult/tracing/tracing/mre/output_formatter.py
new file mode 100644
index 00000000000..5a4be13acce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/output_formatter.py
@@ -0,0 +1,19 @@
+# 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.
+
+# Derived from telemetry OutputFormatter. Should stay close in architecture
+# to telemetry OutputFormatter.
+
+
+class OutputFormatter(object):
+
+ def __init__(self, output_stream):
+ self._output_stream = output_stream
+
+ def Format(self, results):
+ raise NotImplementedError()
+
+ @property
+ def output_stream(self):
+ return self._output_stream
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/progress_reporter.py b/chromium/third_party/catapult/tracing/tracing/mre/progress_reporter.py
new file mode 100644
index 00000000000..b33d744da13
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/progress_reporter.py
@@ -0,0 +1,27 @@
+# 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.
+
+
+class RunReporter(object):
+
+ def __init__(self, canonical_url):
+ self.canonical_url = canonical_url
+
+ def DidAddFailure(self, failure):
+ pass
+
+ def DidRun(self, run_failed):
+ pass
+
+
+# Derived from telemetry ProgressReporter. Should stay close in architecture
+# to telemetry ProgressReporter.
+class ProgressReporter(object):
+
+ def WillRun(self, canonical_url):
+ return RunReporter(canonical_url)
+
+ # TODO(eakuefner): Implement reduction, make this not take a result list.
+ def DidFinishAllRuns(self, result_list):
+ pass
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results.html b/chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results.html
new file mode 100644
index 00000000000..9e64041f3e3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/mre/failure.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ function reduceMapResults(jobResults, key, mapResults, reduceFunction) {
+ try {
+ const result = reduceFunction(key, mapResults);
+ jobResults.addPair(key, result);
+ } catch (ex) {
+ ex.name = 'ReduceFunctionError';
+ throw ex;
+ }
+ }
+
+ return {
+ reduceMapResults
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results_cmdline.html b/chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results_cmdline.html
new file mode 100644
index 00000000000..ac6e578b6c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/reduce_map_results_cmdline.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/mre/file_handle.html">
+<link rel="import" href="/tracing/mre/job.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+<link rel="import" href="/tracing/mre/reduce_map_results.html">
+<link rel="import" href="/tracing/mre/run_and_convert_errors_to_failures.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+tr.exportTo('tr.mre', function() {
+ function jsonReplacer(key, value) {
+ if (value instanceof tr.v.Histogram) {
+ return value.asDict();
+ }
+ return value;
+ }
+
+ function reduceMapResultsMain(args) {
+ if (args.length !== 3) {
+ throw new Error('Must provide three arguments');
+ }
+
+ const options = {
+ key: args[0],
+ fileHandle: tr.mre.FileHandle.fromDict(JSON.parse(args[1])),
+ job: tr.mre.Job.fromDict(JSON.parse(args[2]))
+ };
+
+ const mapResultsLoaded = options.fileHandle.load();
+ const mapResults = JSON.parse(mapResultsLoaded);
+
+ const jobResults = new tr.mre.MreResult();
+
+ tr.mre.runAndConvertErrorsToFailures(
+ jobResults, options.job, options.job.reduceFunctionHandle,
+ undefined,
+ function() {
+ const reduceFunction = options.job.reduceFunctionHandle.load();
+ tr.mre.reduceMapResults(jobResults, options.key, mapResults.pairs,
+ reduceFunction);
+ });
+
+ if (Object.keys(jobResults.pairs).length !== 0) {
+ console.log('JOB_RESULTS: ' + JSON.stringify(jobResults.pairs,
+ jsonReplacer));
+ }
+ jobResults.failures.forEach(function(failure) {
+ console.log('JOB_FAILURE: ' + JSON.stringify(failure.asDict()));
+ });
+ return 0;
+ }
+
+ return {
+ reduceMapResultsMain
+ };
+});
+
+
+if (tr.isHeadless) {
+ quit(tr.mre.reduceMapResultsMain(sys.argv.slice(1)));
+}
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/run_and_convert_errors_to_failures.html b/chromium/third_party/catapult/tracing/tracing/mre/run_and_convert_errors_to_failures.html
new file mode 100644
index 00000000000..70ca7aed12d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/run_and_convert_errors_to_failures.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/mre/failure.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.mre', function() {
+ function runAndConvertErrorsToFailures(results, job, functionHandle,
+ traceHandle, cb, opt_this) {
+ try {
+ cb.call(opt_this);
+ } catch (e) {
+ const err = tr.b.normalizeException(e);
+ results.addFailure(new tr.mre.Failure(
+ job, functionHandle, traceHandle, err.typeName,
+ err.message, err.stack));
+ }
+ }
+
+ return {
+ runAndConvertErrorsToFailures
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/test_trace.json b/chromium/third_party/catapult/tracing/tracing/mre/test_trace.json
new file mode 100644
index 00000000000..c44efce2579
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/test_trace.json
@@ -0,0 +1,6 @@
+[
+ {"pid": 1, "tid": 2, "ph": "X", "name": "a", "cat": "c",
+ "ts": 0, "dur": 10, "args": {}},
+ {"pid": 2, "tid": 2, "ph": "X", "name": "b", "cat": "c",
+ "ts": 3, "dur": 5, "args": {}}
+]
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue.py b/chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue.py
new file mode 100644
index 00000000000..ccd0e6b7985
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue.py
@@ -0,0 +1,124 @@
+# 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.
+import threading
+import traceback
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+class ThreadedWorkQueue(object):
+
+ def __init__(self, num_threads):
+ self._num_threads = num_threads
+
+ self._main_thread_tasks = None
+ self._any_thread_tasks = None
+
+ self._running = False
+ self._stop = False
+ self._stop_result = None
+
+ self.Reset()
+
+ @property
+ def is_running(self):
+ return self._running
+
+ def Run(self):
+ if self.is_running:
+ raise Exception('Already running')
+
+ self._running = True
+ self._stop = False
+ self._stop_result = None
+
+ if self._num_threads == 1:
+ self._RunSingleThreaded()
+ else:
+ self._RunMultiThreaded()
+
+ self._main_thread_tasks = queue.Queue()
+ self._any_thread_tasks = queue.Queue()
+
+ r = self._stop_result
+ self._stop_result = None
+ self._running = False
+
+ return r
+
+ def Stop(self, stop_result=None):
+ if not self.is_running:
+ raise Exception('Not running')
+
+ if self._stop:
+ return False
+ self._stop_result = stop_result
+ self._stop = True
+ return True
+
+ def Reset(self):
+ assert not self.is_running
+ self._main_thread_tasks = queue.Queue()
+ self._any_thread_tasks = queue.Queue()
+
+ def PostMainThreadTask(self, cb, *args, **kwargs):
+ def RunTask():
+ cb(*args, **kwargs)
+ self._main_thread_tasks.put(RunTask)
+
+ def PostAnyThreadTask(self, cb, *args, **kwargs):
+ def RunTask():
+ cb(*args, **kwargs)
+ self._any_thread_tasks.put(RunTask)
+
+ def _TryToRunOneTask(self, task_queue, block=False):
+ if block:
+ try:
+ task = task_queue.get(True, 0.1)
+ except queue.Empty:
+ return
+ else:
+ if task_queue.empty():
+ return
+ task = task_queue.get()
+
+ try:
+ task()
+ except KeyboardInterrupt as ex:
+ raise ex
+ except Exception: # pylint: disable=broad-except
+ traceback.print_exc()
+ finally:
+ task_queue.task_done()
+
+ def _RunSingleThreaded(self):
+ while True:
+ if self._stop:
+ break
+ self._TryToRunOneTask(self._any_thread_tasks)
+ self._TryToRunOneTask(self._main_thread_tasks)
+
+ def _RunMultiThreaded(self):
+ threads = []
+ for _ in range(self._num_threads):
+ t = threading.Thread(target=self._ThreadMain)
+ t.setDaemon(True)
+ t.start()
+ threads.append(t)
+
+ while True:
+ if self._stop:
+ break
+ self._TryToRunOneTask(self._main_thread_tasks)
+
+ for t in threads:
+ t.join()
+
+ def _ThreadMain(self):
+ while True:
+ if self._stop:
+ break
+ self._TryToRunOneTask(self._any_thread_tasks, block=True)
diff --git a/chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue_unittest.py b/chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue_unittest.py
new file mode 100644
index 00000000000..6e93744540a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/mre/threaded_work_queue_unittest.py
@@ -0,0 +1,33 @@
+# 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 unittest
+
+from tracing.mre import threaded_work_queue
+
+
+class ThreadedWorkQueueTests(unittest.TestCase):
+
+ def testSingleThreaded(self):
+ wq = threaded_work_queue.ThreadedWorkQueue(num_threads=1)
+ self._RunSimpleDecrementingTest(wq)
+
+ def testMultiThreaded(self):
+ wq = threaded_work_queue.ThreadedWorkQueue(num_threads=4)
+ self._RunSimpleDecrementingTest(wq)
+
+ def _RunSimpleDecrementingTest(self, wq):
+
+ remaining = [10]
+
+ def Decrement():
+ remaining[0] -= 1
+ if remaining[0]:
+ wq.PostMainThreadTask(Done)
+
+ def Done():
+ wq.Stop(314)
+
+ wq.PostAnyThreadTask(Decrement)
+ res = wq.Run()
+ self.assertEquals(res, 314)
diff --git a/chromium/third_party/catapult/tracing/tracing/tests.html b/chromium/third_party/catapult/tracing/tracing/tests.html
new file mode 100644
index 00000000000..beb5ab1d75a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/tests.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<!--
+Copyright (c) 2014 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.
+-->
+<head>
+ <title>Trace-Viewer Tests: loading...</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+ <link rel="shortcut icon" href="data:image/x-icon;base64,"
+ type="image/x-icon">
+
+ <script src="/components/webcomponentsjs/HTMLImports.js"></script>
+ <link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+ <link rel="import" href="/tracing/base/unittest/interactive_test_runner.html">
+ <style>
+ html, body {
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ margin: 0px;
+ }
+ </style>
+</head>
+<body>
+ <script>
+ 'use strict';
+ window.addEventListener('load', function() {
+ HTMLImports.whenReady(function loadAndRunTests(e) {
+ tr.b.unittest.loadAndRunTests({
+ title: 'All Trace-Viewer Tests',
+ getAllSuiteRelPathsAsync() {
+ return tr.b.getAsync('/tracing/tests').then(function(json) {
+ return JSON.parse(json).test_relpaths;
+ }).catch(function(e) {
+ throw e;
+ });
+ },
+ testLinks: [
+ {linkPath: '/tracing_examples/skia_debugger.html',
+ title: 'Skia Debugger'},
+ {linkPath: '/tracing_examples/trace_viewer.html',
+ title: 'Trace File Viewer'}
+ ]
+ });
+ });
+ });
+ </script>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/tracing/trace2html.html b/chromium/third_party/catapult/tracing/tracing/trace2html.html
new file mode 100644
index 00000000000..640f042bc3d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/trace2html.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/ui/base/base.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+<script>
+'use strict';
+
+let g_timelineViewEl;
+
+(function() {
+ const styleEl = document.createElement('style');
+ const lines = [
+ 'html, body {',
+ ' box-sizing: border-box;',
+ ' overflow: hidden;',
+ ' margin: 0px;',
+ ' padding: 0;',
+ ' width: 100%;',
+ ' height: 100%;',
+ '}',
+ 'tr-ui-timeline-view {',
+ ' width: 100%;',
+ ' height: 100%;',
+ '}',
+ 'tr-ui-timeline-view:focus {',
+ ' outline: none;',
+ '}'
+ ];
+ Polymer.dom(styleEl).textContent = lines.join('\n');
+ Polymer.dom(document.head).appendChild(styleEl);
+})();
+
+document.addEventListener('DOMContentLoaded', function() {
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ g_timelineViewEl = document.createElement('tr-ui-timeline-view');
+ Polymer.dom(g_timelineViewEl).appendChild(container);
+
+ Polymer.dom(document.body).appendChild(g_timelineViewEl);
+
+ const traces = [];
+ const viewerDataScripts = Polymer.dom(document).querySelectorAll(
+ '#viewer-data');
+ for (let i = 0; i < viewerDataScripts.length; i++) {
+ let text = Polymer.dom(viewerDataScripts[i]).textContent;
+ // Trim leading newlines off the text. They happen during writing.
+ while (text[0] === '\n') {
+ text = text.substring(1);
+ }
+ traces.push(tr.b.Base64.atob(text));
+ }
+
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m);
+ const p = i.importTracesWithProgressDialog(traces);
+ p.then(
+ function() {
+ g_timelineViewEl.model = m;
+ g_timelineViewEl.updateDocumentFavicon();
+ g_timelineViewEl.globalMode = true;
+ g_timelineViewEl.viewTitle = document.title;
+ },
+ function(err) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = tr.b.normalizeException(err).message;
+ overlay.title = 'Import error';
+ overlay.visible = true;
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/trace_data/__init__.py b/chromium/third_party/catapult/tracing/tracing/trace_data/__init__.py
new file mode 100644
index 00000000000..bdb1f26b0b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/trace_data/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2017 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.
+
diff --git a/chromium/third_party/catapult/tracing/tracing/trace_data/trace_data.py b/chromium/third_party/catapult/tracing/tracing/trace_data/trace_data.py
new file mode 100644
index 00000000000..b0ad325e8de
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/trace_data/trace_data.py
@@ -0,0 +1,346 @@
+# Copyright 2014 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 copy
+import json
+import logging
+import os
+import shutil
+import subprocess
+import tempfile
+import time
+
+
+try:
+ StringTypes = basestring
+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
+
+
+class TraceDataPart(object):
+ """TraceData can have a variety of events.
+
+ These are called "parts" and are accessed by the following fixed field names.
+ """
+ def __init__(self, raw_field_name):
+ self._raw_field_name = raw_field_name
+
+ def __repr__(self):
+ return 'TraceDataPart("%s")' % self._raw_field_name
+
+ @property
+ def raw_field_name(self):
+ return self._raw_field_name
+
+ def __eq__(self, other):
+ return self.raw_field_name == other.raw_field_name
+
+ def __hash__(self):
+ return hash(self.raw_field_name)
+
+
+ANDROID_PROCESS_DATA_PART = TraceDataPart('androidProcessDump')
+ATRACE_PART = TraceDataPart('systemTraceEvents')
+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')
+
+ALL_TRACE_PARTS = {ANDROID_PROCESS_DATA_PART,
+ ATRACE_PART,
+ 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
+
+
+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.
+ """
+ if isinstance(trace, TraceFileHandle):
+ return trace.file_path
+ with tempfile.NamedTemporaryFile(mode='w', dir=dir_path, delete=False) as fp:
+ if isinstance(trace, StringTypes):
+ 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 HasTracesFor(self, part):
+ return _HasTraceFor(part, self._raw_data)
+
+ 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
+
+ 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.values():
+ 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.items():
+ 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, StringTypes)
+ 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
+
+
+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.
+ """
+ def __init__(self):
+ self._raw_data = {}
+
+ 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
+
+ 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, StringTypes) or
+ isinstance(trace, dict) or
+ isinstance(trace, list))
+
+ if self._raw_data is None:
+ raise Exception('Already called AsData() on this builder.')
+
+ self._raw_data.setdefault(part.raw_field_name, [])
+ self._raw_data[part.raw_field_name].append(trace)
+
+ def HasTracesFor(self, part):
+ return _HasTraceFor(part, self._raw_data)
+
+
+def CreateTraceDataFromRawData(raw_data):
+ """Convenient method for creating a TraceData object from |raw_data|.
+ This is mostly used 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.
+
+ """
+ raw_data = copy.deepcopy(raw_data)
+ if isinstance(raw_data, StringTypes):
+ json_data = json.loads(raw_data)
+ else:
+ json_data = raw_data
+
+ 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()
diff --git a/chromium/third_party/catapult/tracing/tracing/trace_data/trace_data_unittest.py b/chromium/third_party/catapult/tracing/tracing/trace_data/trace_data_unittest.py
new file mode 100644
index 00000000000..4917efc5019
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/trace_data/trace_data_unittest.py
@@ -0,0 +1,98 @@
+# Copyright 2014 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 datetime
+import os
+import shutil
+import tempfile
+import unittest
+
+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'}])
+ 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))
+
+
+class TraceDataBuilderTest(unittest.TestCase):
+ def testBasicChrome(self):
+ builder = trace_data.TraceDataBuilder()
+ builder.AddTraceFor(trace_data.CHROME_TRACE_PART,
+ {'traceEvents': [1, 2, 3]})
+
+ d = builder.AsData()
+ self.assertTrue(d.HasTracesFor(trace_data.CHROME_TRACE_PART))
+
+ self.assertRaises(Exception, builder.AsData)
+
+ def testSetTraceFor(self):
+ telemetry_trace = {
+ 'traceEvents': [1, 2, 3],
+ 'metadata': {
+ 'field1': 'value1'
+ }
+ }
+
+ builder = trace_data.TraceDataBuilder()
+ builder.AddTraceFor(trace_data.TELEMETRY_PART, telemetry_trace)
+ d = builder.AsData()
+
+ self.assertEqual(d.GetTracesFor(trace_data.TELEMETRY_PART),
+ [telemetry_trace])
+
+ def testSetTraceForRaisesWithInvalidPart(self):
+ builder = trace_data.TraceDataBuilder()
+
+ self.assertRaises(AssertionError,
+ lambda: builder.AddTraceFor('not_a_trace_part', {}))
+
+ def testSetTraceForRaisesWithInvalidTrace(self):
+ builder = trace_data.TraceDataBuilder()
+
+ self.assertRaises(
+ AssertionError,
+ lambda: builder.AddTraceFor(trace_data.TELEMETRY_PART,
+ datetime.time.min))
+
+ def testSetTraceForRaisesAfterAsData(self):
+ builder = trace_data.TraceDataBuilder()
+ builder.AsData()
+
+ self.assertRaises(
+ Exception,
+ lambda: builder.AddTraceFor(trace_data.TELEMETRY_PART, {}))
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html
new file mode 100644
index 00000000000..b44741aace1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-a-alert-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-alert-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ this.$.table.tableColumns = [
+ {
+ title: 'Label',
+ value(row) { return row.name; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) { return row.value; }
+ }
+ ];
+ this.$.table.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ getRowsForSingleAlert_(alert) {
+ const rows = [];
+
+ // Arguments
+ for (const argName in alert.args) {
+ const argView =
+ document.createElement('tr-ui-a-generic-object-view');
+ argView.object = alert.args[argName];
+ rows.push({ name: argName, value: argView });
+ }
+
+ // Associated events
+ if (alert.associatedEvents.length) {
+ alert.associatedEvents.forEach(function(event, i) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(
+ new tr.model.EventSet(event), event.title);
+
+ let valueString = '';
+ if (event instanceof tr.model.TimedEvent) {
+ valueString = 'took ' + event.duration.toFixed(2) + 'ms';
+ }
+
+ rows.push({
+ name: linkEl,
+ value: valueString
+ });
+ });
+ }
+
+ // Description
+ const descriptionEl = tr.ui.b.createDiv({
+ textContent: alert.info.description,
+ maxWidth: '300px'
+ });
+ rows.push({
+ name: 'Description',
+ value: descriptionEl
+ });
+
+ // Additional Reading Links
+ if (alert.info.docLinks) {
+ alert.info.docLinks.forEach(function(linkObject) {
+ const linkEl = document.createElement('a');
+ linkEl.target = '_blank';
+ linkEl.href = linkObject.href;
+ Polymer.dom(linkEl).textContent = Polymer.dom(linkObject).textContent;
+ rows.push({
+ name: linkObject.label,
+ value: linkEl
+ });
+ });
+ }
+ return rows;
+ },
+
+ getRowsForAlerts_(alerts) {
+ if (alerts.length === 1) {
+ const rows = [{
+ name: 'Alert',
+ value: tr.b.getOnlyElement(alerts).title
+ }];
+ const detailRows = this.getRowsForSingleAlert_(tr.b.getOnlyElement(
+ alerts));
+ rows.push.apply(rows, detailRows);
+ return rows;
+ }
+ return alerts.map(function(alert) {
+ return {
+ name: 'Alert',
+ value: alert.title,
+ isExpanded: alerts.size < 10, // This is somewhat arbitrary for now.
+ subRows: this.getRowsForSingleAlert_(alert)
+ };
+ }, this);
+ },
+
+ updateContents_() {
+ if (this.currentSelection_ === undefined) {
+ this.$.table.rows = [];
+ this.$.table.rebuild();
+ return;
+ }
+
+ const alerts = this.currentSelection_;
+ this.$.table.tableRows = this.getRowsForAlerts_(alerts);
+ this.$.table.rebuild();
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ const result = new tr.model.EventSet();
+ for (const event of this.currentSelection_) {
+ result.addEventSet(event.associatedEvents);
+ }
+ return result;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-alert-sub-view',
+ tr.model.Alert,
+ {
+ multi: false,
+ title: 'Alert',
+ });
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-alert-sub-view',
+ tr.model.Alert,
+ {
+ multi: true,
+ title: 'Alerts',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html
new file mode 100644
index 00000000000..574cf5f0b86
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('instantiate', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+
+ const alertInfo = new tr.model.EventInfo(
+ 'alertInfo', 'Critical alert',
+ [{
+ label: 'Project Page',
+ textContent: 'Trace-Viewer Github Project',
+ href: 'https://github.com/google/trace-viewer/'
+ }]);
+
+ const alert = new tr.model.Alert(alertInfo, 5, [slice]);
+ assert.strictEqual(1, alert.associatedEvents.length);
+
+ const subView = document.createElement('tr-ui-a-alert-sub-view');
+ subView.selection = new tr.model.EventSet(alert);
+ assert.isTrue(
+ subView.relatedEventsToHighlight.equals(alert.associatedEvents));
+ this.addHTMLOutput(subView);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ subView, 'tr-ui-b-table');
+
+ const rows = table.tableRows;
+ const columns = table.tableColumns;
+ assert.lengthOf(rows, 4);
+ assert.lengthOf(columns, 2);
+ });
+
+ test('instantiate_twoAlertsWithRelatedEvents', function() {
+ const slice1 = newSliceEx({title: 'b', start: 0, duration: 0.002});
+ const slice2 = newSliceEx({title: 'b', start: 1, duration: 0.002});
+
+ const alertInfo1 = new tr.model.EventInfo(
+ 'alertInfo1', 'Critical alert',
+ [{
+ label: 'Project Page',
+ textContent: 'Trace-Viewer Github Project',
+ href: 'https://github.com/google/trace-viewer/'
+ }]);
+
+ const alertInfo2 = new tr.model.EventInfo(
+ 'alertInfo2', 'Critical alert',
+ [{
+ label: 'Google Homepage',
+ textContent: 'Google Search Page',
+ href: 'http://www.google.com'
+ }]);
+
+ const alert1 = new tr.model.Alert(alertInfo1, 5, [slice1]);
+ const alert2 = new tr.model.Alert(alertInfo2, 5, [slice2]);
+
+ const subView = document.createElement('tr-ui-a-alert-sub-view');
+ subView.selection = new tr.model.EventSet([alert1, alert2]);
+ assert.isTrue(subView.relatedEventsToHighlight.equals(
+ new tr.model.EventSet([
+ tr.b.getOnlyElement(alert1.associatedEvents),
+ tr.b.getOnlyElement(alert2.associatedEvents)
+ ])));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html
new file mode 100644
index 00000000000..8d996afeeb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<dom-module id='tr-ui-a-analysis-link'>
+ <template>
+ <style>
+ :host {
+ display: inline;
+ cursor: pointer;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+ a {
+ text-decoration: underline;
+ }
+ </style>
+ <a href="{{href}}" on-click="onClicked_" on-mouseenter="onMouseEnter_" on-mouseleave="onMouseLeave_"><slot></slot></a>
+
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-analysis-link',
+
+ properties: {
+ href: {
+ type: String
+ }
+ },
+
+ listeners: {
+ 'click': 'onClicked_',
+ 'mouseenter': 'onMouseEnter_',
+ 'mouseleave': 'onMouseLeave_'
+ },
+
+ ready() {
+ this.selection_ = undefined;
+ },
+
+ attached() {
+ // Save an instance of the controller since it's going to be used in
+ // detached() where it can no longer be obtained.
+ this.controller_ =
+ tr.c.BrushingStateController.getControllerForElement(this);
+ },
+
+ detached() {
+ // Reset highlights.
+ this.clearHighlight_();
+ this.controller_ = undefined;
+ },
+
+ set color(c) {
+ this.style.color = c;
+ },
+
+ /**
+ * @return {*|function():*}
+ */
+ get selection() {
+ return this.selection_;
+ },
+
+ /**
+ * |selection| can be anything except a function, or else a function that
+ * can return anything.
+ *
+ * In the context of trace_viewer, |selection| is typically an EventSet,
+ * whose events will be highlighted by trace_viewer when this link is
+ * clicked or mouse-entered.
+ *
+ * If |selection| is not a function, then it will be dispatched to this
+ * link's embedder via a RequestSelectionChangeEvent when this link is
+ * clicked or mouse-entered.
+ *
+ * If |selection| is a function, then it will be called when this link is
+ * clicked or mouse-entered, and its result will be dispatched to this
+ * link's embedder via a RequestSelectionChangeEvent.
+ *
+ * @param {*|function():*} selection
+ */
+ set selection(selection) {
+ this.selection_ = selection;
+ Polymer.dom(this).textContent = selection.userFriendlyName;
+ },
+
+ setSelectionAndContent(selection, opt_textContent) {
+ this.selection_ = selection;
+ if (opt_textContent) {
+ Polymer.dom(this).textContent = opt_textContent;
+ }
+ },
+
+ /**
+ * If |selection| is a function, call it and return the result.
+ * Otherwise return |selection| directly.
+ *
+ * @return {*}
+ */
+ getCurrentSelection_() {
+ // Gets the current selection, invoking the selection function if needed.
+ if (typeof this.selection_ === 'function') {
+ return this.selection_();
+ }
+ return this.selection_;
+ },
+
+ setHighlight_(opt_eventSet) {
+ if (this.controller_) {
+ this.controller_.changeAnalysisLinkHoveredEvents(opt_eventSet);
+ }
+ },
+
+ clearHighlight_(opt_eventSet) {
+ this.setHighlight_();
+ },
+
+ onClicked_(clickEvent) {
+ if (!this.selection_) return;
+
+ clickEvent.stopPropagation();
+
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = this.getCurrentSelection_();
+ this.dispatchEvent(event);
+ },
+
+ onMouseEnter_() {
+ this.setHighlight_(this.getCurrentSelection_());
+ },
+
+ onMouseLeave_() {
+ this.clearHighlight_();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html
new file mode 100644
index 00000000000..e8caed8f601
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('testBasic', function() {
+ const link = document.createElement('tr-ui-a-analysis-link');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ link.selection = new tr.model.EventSet(s10);
+ this.addHTMLOutput(link);
+
+ let didRSC = false;
+ link.addEventListener('requestSelectionChange', function(e) {
+ didRSC = true;
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(s10)));
+ });
+ link.click();
+ assert.isTrue(didRSC);
+ });
+
+ test('testGeneratorVersion', function() {
+ const link = document.createElement('tr-ui-a-analysis-link');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ function selectionGenerator() {
+ return new tr.model.EventSet(s10);
+ }
+ selectionGenerator.userFriendlyName = 'hello world';
+ link.selection = selectionGenerator;
+ this.addHTMLOutput(link);
+
+ let didRSC = false;
+ link.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(s10)));
+ didRSC = true;
+ });
+ link.click();
+ assert.isTrue(didRSC);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html
new file mode 100644
index 00000000000..8bd967c8c75
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html
@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+<!--
+Copyright 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<!--
+@fileoverview Polymer element for various analysis sub-views.
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const AnalysisSubView = {
+ set tabLabel(label) {
+ Polymer.dom(this).setAttribute('tab-label', label);
+ },
+
+ get tabLabel() {
+ return this.getAttribute('tab-label');
+ },
+
+ get requiresTallView() {
+ return false;
+ },
+
+ get relatedEventsToHighlight() {
+ return undefined;
+ },
+
+ /**
+ * Each element extending this one must implement
+ * a 'selection' property.
+ */
+ set selection(selection) {
+ throw new Error('Not implemented!');
+ },
+
+ get selection() {
+ throw new Error('Not implemented!');
+ }
+ };
+
+ // Basic registry.
+ const allTypeInfosByEventProto = new Map();
+ let onlyRootTypeInfosByEventProto = undefined;
+ let eventProtoToRootTypeInfoMap = undefined;
+
+ function AnalysisSubViewTypeInfo(eventConstructor, options) {
+ if (options.multi === undefined) {
+ throw new Error('missing field: multi');
+ }
+ if (options.title === undefined) {
+ throw new Error('missing field: title');
+ }
+ this.eventConstructor = eventConstructor;
+
+ this.singleTagName = undefined;
+ this.singleTitle = undefined;
+
+ this.multiTagName = undefined;
+ this.multiTitle = undefined;
+
+ // This is computed by rebuildRootSubViewTypeInfos, so don't muck with it!
+ this.childrenTypeInfos_ = undefined;
+ }
+
+ AnalysisSubViewTypeInfo.prototype = {
+ get childrenTypeInfos() {
+ return this.childrenTypeInfos_;
+ },
+
+ resetchildrenTypeInfos() {
+ this.childrenTypeInfos_ = [];
+ }
+ };
+
+ AnalysisSubView.register = function(tagName, eventConstructor, options) {
+ let typeInfo = allTypeInfosByEventProto.get(eventConstructor.prototype);
+ if (typeInfo === undefined) {
+ typeInfo = new AnalysisSubViewTypeInfo(eventConstructor, options);
+ allTypeInfosByEventProto.set(typeInfo.eventConstructor.prototype,
+ typeInfo);
+
+ onlyRootTypeInfosByEventProto = undefined;
+ }
+
+ if (!options.multi) {
+ if (typeInfo.singleTagName !== undefined) {
+ throw new Error('SingleTagName already set');
+ }
+ typeInfo.singleTagName = tagName;
+ typeInfo.singleTitle = options.title;
+ } else {
+ if (typeInfo.multiTagName !== undefined) {
+ throw new Error('MultiTagName already set');
+ }
+ typeInfo.multiTagName = tagName;
+ typeInfo.multiTitle = options.title;
+ }
+ return typeInfo;
+ };
+
+ function rebuildRootSubViewTypeInfos() {
+ onlyRootTypeInfosByEventProto = new Map();
+ allTypeInfosByEventProto.forEach(function(typeInfo) {
+ typeInfo.resetchildrenTypeInfos();
+ });
+
+ // Find all root typeInfos.
+ allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) {
+ const eventPrototype = typeInfo.eventConstructor.prototype;
+
+ let lastEventProto = eventPrototype;
+ let curEventProto = eventPrototype.__proto__;
+ while (true) {
+ if (!allTypeInfosByEventProto.has(curEventProto)) {
+ const rootTypeInfo = allTypeInfosByEventProto.get(lastEventProto);
+ const rootEventProto = lastEventProto;
+
+ const isNew = onlyRootTypeInfosByEventProto.has(rootEventProto);
+ onlyRootTypeInfosByEventProto.set(rootEventProto,
+ rootTypeInfo);
+ break;
+ }
+
+ lastEventProto = curEventProto;
+ curEventProto = curEventProto.__proto__;
+ }
+ });
+
+ // Build the childrenTypeInfos array.
+ allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) {
+ const eventPrototype = typeInfo.eventConstructor.prototype;
+ const parentEventProto = eventPrototype.__proto__;
+ const parentTypeInfo = allTypeInfosByEventProto.get(parentEventProto);
+ if (!parentTypeInfo) return;
+ parentTypeInfo.childrenTypeInfos.push(typeInfo);
+ });
+
+ // Build the eventProto to rootTypeInfo map.
+ eventProtoToRootTypeInfoMap = new Map();
+ allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) {
+ const eventPrototype = typeInfo.eventConstructor.prototype;
+
+ let curEventProto = eventPrototype;
+ while (true) {
+ if (onlyRootTypeInfosByEventProto.has(curEventProto)) {
+ const rootTypeInfo = onlyRootTypeInfosByEventProto.get(
+ curEventProto);
+ eventProtoToRootTypeInfoMap.set(eventPrototype,
+ rootTypeInfo);
+ break;
+ }
+ curEventProto = curEventProto.__proto__;
+ }
+ });
+ }
+
+ function findLowestTypeInfoForEvents(thisTypeInfo, events) {
+ if (events.length === 0) return thisTypeInfo;
+ const event0 = tr.b.getFirstElement(events);
+
+ let candidateSubTypeInfo;
+ for (let i = 0; i < thisTypeInfo.childrenTypeInfos.length; i++) {
+ const childTypeInfo = thisTypeInfo.childrenTypeInfos[i];
+ if (event0 instanceof childTypeInfo.eventConstructor) {
+ candidateSubTypeInfo = childTypeInfo;
+ break;
+ }
+ }
+ if (!candidateSubTypeInfo) return thisTypeInfo;
+
+ // Validate that all the other events are instances of the candidate type.
+ let allMatch = true;
+ for (const event of events) {
+ if (event instanceof candidateSubTypeInfo.eventConstructor) continue;
+ allMatch = false;
+ break;
+ }
+
+ if (!allMatch) {
+ return thisTypeInfo;
+ }
+
+ return findLowestTypeInfoForEvents(candidateSubTypeInfo, events);
+ }
+
+ const primaryEventProtoToTypeInfoMap = new Map();
+ function getRootTypeInfoForEvent(event) {
+ const curProto = event.__proto__;
+ const typeInfo = primaryEventProtoToTypeInfoMap.get(curProto);
+ if (typeInfo) return typeInfo;
+ return getRootTypeInfoForEventSlow(event);
+ }
+
+ function getRootTypeInfoForEventSlow(event) {
+ let typeInfo;
+ let curProto = event.__proto__;
+ while (true) {
+ if (curProto === Object.prototype) {
+ throw new Error('No view registered for ' + event.toString());
+ }
+ typeInfo = onlyRootTypeInfosByEventProto.get(curProto);
+ if (typeInfo) {
+ primaryEventProtoToTypeInfoMap.set(event.__proto__, typeInfo);
+ return typeInfo;
+ }
+ curProto = curProto.__proto__;
+ }
+ }
+
+ AnalysisSubView.getEventsOrganizedByTypeInfo = function(selection) {
+ if (onlyRootTypeInfosByEventProto === undefined) {
+ rebuildRootSubViewTypeInfos();
+ }
+
+ // Base grouping.
+ const eventsByRootTypeInfo = tr.b.groupIntoMap(
+ selection,
+ function(event) {
+ return getRootTypeInfoForEvent(event);
+ },
+ this, tr.model.EventSet);
+
+ // Now, try to lower the typeinfo to the most specific type that still
+ // encompasses the event group.
+ //
+ // For instance, if we have 3 ThreadSlices, and all three are V8 slices,
+ // then we can convert this to use the V8Slices's typeinfos. But, if one
+ // of those slices was not a V8Slice, then we must still use
+ // ThreadSlice.
+ //
+ // The reason for this is for the confusion that might arise from the
+ // alternative. Suppose you click on a set of mixed slices, we want to show
+ // you the most correct information, and let you navigate to . If we instead
+ // showed you a V8 slices tab, and a Slices tab, we present the user with an
+ // ambiguity: is the V8 slice also in the Slices tab? Or is it not? Better,
+ // we think, to just only ever show an event in one place at a time, and
+ // avoid the possible confusion.
+ const eventsByLowestTypeInfo = new Map();
+ eventsByRootTypeInfo.forEach(function(events, typeInfo) {
+ const lowestTypeInfo = findLowestTypeInfoForEvents(typeInfo, events);
+ eventsByLowestTypeInfo.set(lowestTypeInfo, events);
+ });
+
+ return eventsByLowestTypeInfo;
+ };
+
+ return {
+ AnalysisSubView,
+ AnalysisSubViewTypeInfo,
+ };
+});
+
+// Dummy element for testing
+Polymer({
+ is: 'tr-ui-a-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView]
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html
new file mode 100644
index 00000000000..0f3e85ea4ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('subViewThrowsNotImplementedErrors', function() {
+ const subView = document.createElement('tr-ui-a-sub-view');
+
+ assert.throw(function() {
+ subView.selection = new tr.model.EventSet();
+ }, 'Not implemented!');
+
+ assert.throw(function() {
+ const viewSelection = subView.selection;
+ }, 'Not implemented!');
+
+ subView.tabLabel = 'Tab Label';
+ assert.strictEqual(subView.getAttribute('tab-label'), 'Tab Label');
+ assert.strictEqual(subView.tabLabel, 'Tab Label');
+
+ subView.tabLabel = 'New Label';
+ assert.strictEqual(subView.getAttribute('tab-label'), 'New Label');
+ assert.strictEqual(subView.tabLabel, 'New Label');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html
new file mode 100644
index 00000000000..edc14edca11
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/container_memory_dump_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/counter_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_async_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_cpu_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_flow_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_frame_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/multi_instant_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_object_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_thread_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/multi_thread_time_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/multi_user_expectation_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_async_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_cpu_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_flow_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_frame_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_instant_event_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_object_instance_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_object_snapshot_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_power_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_sample_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_thread_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_thread_time_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_user_expectation_sub_view.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+
+<!--
+@fileoverview A component used to display an analysis of a selection,
+using custom elements specialized for different event types.
+-->
+<dom-module id='tr-ui-a-analysis-view'>
+ <template>
+ <style>
+ :host {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ height: 275px;
+ overflow: auto;
+ }
+
+ :host(.tall-mode) {
+ height: 525px;
+ }
+ </style>
+ <slot></slot>
+ </template>
+</dom-module>
+<script>
+'use strict';
+(function() {
+ const EventRegistry = tr.model.EventRegistry;
+
+ /** Returns the label that goes next to the list of tabs. */
+ function getTabStripLabel(numEvents) {
+ if (numEvents === 0) {
+ return 'Nothing selected. Tap stuff.';
+ } else if (numEvents === 1) {
+ return '1 item selected.';
+ }
+ return numEvents + ' items selected.';
+ }
+
+ function createSubView(subViewTypeInfo, selection) {
+ let tagName;
+ if (selection.length === 1) {
+ tagName = subViewTypeInfo.singleTagName;
+ } else {
+ tagName = subViewTypeInfo.multiTagName;
+ }
+
+ if (tagName === undefined) {
+ throw new Error('No view registered for ' +
+ subViewTypeInfo.eventConstructor.name);
+ }
+ const subView = document.createElement(tagName);
+
+ let title;
+ if (selection.length === 1) {
+ title = subViewTypeInfo.singleTitle;
+ } else {
+ title = subViewTypeInfo.multiTitle;
+ }
+ title += ' (' + selection.length + ')';
+ subView.tabLabel = title;
+
+ subView.selection = selection;
+ return subView;
+ }
+
+ Polymer({
+ is: 'tr-ui-a-analysis-view',
+
+ ready() {
+ this.brushingStateController_ = undefined;
+ this.lastSelection_ = undefined;
+ this.tabView_ = document.createElement('tr-ui-b-tab-view');
+ this.tabView_.addEventListener(
+ 'selected-tab-change', this.onSelectedSubViewChanged_.bind(this));
+
+ Polymer.dom(this).appendChild(this.tabView_);
+ },
+
+ set tallMode(value) {
+ Polymer.dom(this).classList.toggle('tall-mode', value);
+ },
+
+ get tallMode() {
+ return Polymer.dom(this).classList.contains('tall-mode');
+ },
+
+ get tabView() {
+ return this.tabView_;
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ set brushingStateController(brushingStateController) {
+ if (this.brushingStateController_) {
+ this.brushingStateController_.removeEventListener(
+ 'change', this.onSelectionChanged_.bind(this));
+ }
+
+ this.brushingStateController_ = brushingStateController;
+ if (this.brushingStateController) {
+ this.brushingStateController_.addEventListener(
+ 'change', this.onSelectionChanged_.bind(this));
+ }
+
+ // The new brushing controller may have a different selection than the
+ // last one, so we have to refresh the subview.
+ this.onSelectionChanged_();
+ },
+
+ get selection() {
+ return this.brushingStateController_.selection;
+ },
+
+ onSelectionChanged_(e) {
+ if (this.lastSelection_ && this.selection.equals(this.lastSelection_)) {
+ return;
+ }
+ this.lastSelection_ = this.selection;
+
+ this.tallMode = false;
+
+ this.tabView_.label = getTabStripLabel(this.selection.length);
+ const eventsByBaseTypeName =
+ this.selection.getEventsOrganizedByBaseType(true);
+
+ const ASV = tr.ui.analysis.AnalysisSubView;
+ const eventsByTagName = ASV.getEventsOrganizedByTypeInfo(this.selection);
+ const newSubViews = [];
+ eventsByTagName.forEach(function(events, typeInfo) {
+ newSubViews.push(createSubView(typeInfo, events));
+ });
+
+ this.tabView_.resetSubViews(newSubViews);
+ },
+
+ onSelectedSubViewChanged_() {
+ const selectedSubView = this.tabView_.selectedSubView;
+
+ if (!selectedSubView) {
+ this.tallMode = false;
+ this.maybeChangeRelatedEvents_(undefined);
+ return;
+ }
+
+ this.tallMode = selectedSubView.requiresTallView;
+ this.maybeChangeRelatedEvents_(selectedSubView.relatedEventsToHighlight);
+ },
+
+ /** Changes the highlighted related events if possible. */
+ maybeChangeRelatedEvents_(events) {
+ if (this.brushingStateController) {
+ this.brushingStateController.changeAnalysisViewRelatedEvents(events);
+ }
+ }
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html
new file mode 100644
index 00000000000..fa7b51256a6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/counter_sample.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const BrushingStateController = tr.c.BrushingStateController;
+ const Model = tr.Model;
+ const Counter = tr.model.Counter;
+ const CounterSeries = tr.model.CounterSeries;
+ const CounterSample = tr.model.CounterSample;
+ const newThreadSlice = tr.c.TestUtils.newThreadSlice;
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const StubExpectation = tr.model.um.StubExpectation;
+
+ function assertEventSet(actualEventSet, expectedEvents) {
+ const expectedEventSet = new EventSet(expectedEvents);
+ assert.isTrue(actualEventSet.equals(expectedEventSet),
+ 'EventSet objects are not equal');
+ }
+
+ function checkTab(tab, expectedTagName, expectedSelectionEvents) {
+ assert.strictEqual(tab.tagName, expectedTagName.toUpperCase());
+ assertEventSet(tab.selection, expectedSelectionEvents);
+ }
+
+ test('selectedTabChange', function() {
+ // Set up the model.
+ const model = new Model();
+ const process = model.getOrCreateProcess(1);
+
+ const counter = process.getOrCreateCounter('universe', 'planets');
+ const series = counter.addSeries(new CounterSeries('x', 0));
+ const sample1 = series.addCounterSample(0, 100);
+ const sample2 = series.addCounterSample(1, 90);
+ const sample3 = series.addCounterSample(2, 80);
+
+ const thread = process.getOrCreateThread(2);
+ const slice1 = newThreadSlice(thread, SCHEDULING_STATE.RUNNING, 0, 1);
+ const slice2 = newThreadSlice(thread, SCHEDULING_STATE.SLEEPING, 1, 2.718);
+ thread.timeSlices = [slice1, slice2];
+
+ const record1 = new StubExpectation(
+ {parentModel: model, initiatorTitle: 'r1', start: 200, duration: 300});
+ record1.associatedEvents.push(sample1);
+ record1.associatedEvents.push(slice1);
+ const record2 = new StubExpectation(
+ {parentModel: model, initiatorTitle: 'r2', start: 600, duration: 100});
+ record2.associatedEvents.push(sample2);
+ record2.associatedEvents.push(sample3);
+ record2.associatedEvents.push(slice1);
+
+ // Set up the analysis views and brushing state controller.
+ const analysisView = document.createElement('tr-ui-a-analysis-view');
+ this.addHTMLOutput(analysisView);
+ const tabView = analysisView.tabView;
+ const controller = new BrushingStateController(undefined);
+ analysisView.brushingStateController = controller;
+
+ function checkSelectedTab(expectedSelectedTab, expectedRelatedEvents) {
+ assert.strictEqual(tabView.selectedSubView, expectedSelectedTab);
+ assertEventSet(controller.currentBrushingState.analysisViewRelatedEvents,
+ expectedRelatedEvents);
+ }
+
+ // 1. Empty selection (implicit).
+ assert.lengthOf(tabView.tabs, 0);
+ checkSelectedTab(undefined, []);
+
+ // 2. Event selection: two samples and one thread slice.
+ controller.changeSelectionFromRequestSelectionChangeEvent(
+ new EventSet([sample1, slice1, sample2]));
+ assert.lengthOf(tabView.tabs, 2);
+ const sampleTab2 = tabView.tabs[0];
+ checkTab(sampleTab2,
+ 'tr-ui-a-counter-sample-sub-view',
+ [sample1, sample2]);
+ const singleThreadSliceTab2 = tabView.tabs[1];
+ checkTab(singleThreadSliceTab2,
+ 'tr-ui-a-single-thread-time-slice-sub-view',
+ [slice1]);
+ // First tab should be selected.
+ checkSelectedTab(sampleTab2, []);
+
+ // 3. Tab selection: single thread slice tab.
+ tabView.selectedSubView = singleThreadSliceTab2;
+ checkSelectedTab(singleThreadSliceTab2, []);
+
+ // 4. Event selection: one sample, two thread slices, and one
+ // user expectation.
+ controller.changeSelectionFromRequestSelectionChangeEvent(
+ new EventSet([slice1, slice2, sample3, record1]));
+ assert.lengthOf(tabView.tabs, 3);
+ const sampleTab4 = tabView.tabs[1];
+ checkTab(sampleTab4,
+ 'tr-ui-a-counter-sample-sub-view',
+ [sample3]);
+ const singleRecordTab4 = tabView.tabs[2];
+ checkTab(singleRecordTab4,
+ 'tr-ui-a-single-user-expectation-sub-view',
+ [record1]);
+ const multiThreadSliceTab4 = tabView.tabs[0];
+ checkTab(multiThreadSliceTab4,
+ 'tr-ui-a-multi-thread-time-slice-sub-view',
+ [slice1, slice2]);
+ // Remember selected tab (even though the tab was destroyed).
+ checkSelectedTab(multiThreadSliceTab4, []);
+
+ // 5. Tab selection: single user expectation tab.
+ tabView.selectedSubView = singleRecordTab4;
+ checkSelectedTab(singleRecordTab4, [sample1, slice1]);
+
+ // 6. Event selection: one user expectation.
+ controller.changeSelectionFromRequestSelectionChangeEvent(
+ new EventSet([record2]));
+ assert.lengthOf(tabView.tabs, 1);
+ const singleRecordTab6 = tabView.tabs[0];
+ checkTab(singleRecordTab6,
+ 'tr-ui-a-single-user-expectation-sub-view',
+ [record2]);
+ // Remember selected tab.
+ checkSelectedTab(singleRecordTab6, [sample2, sample3, slice1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html
new file mode 100644
index 00000000000..cc0e9155358
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_header_pane.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-container-memory-dump-sub-view'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <div id="content"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ Polymer({
+ is: 'tr-ui-a-container-memory-dump-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ set selection(selection) {
+ if (selection === undefined) {
+ this.currentSelection_ = undefined;
+ this.dumpsByContainerName_ = undefined;
+ this.updateContents_();
+ return;
+ }
+
+ // Check that the selection contains only container memory dumps.
+ selection.forEach(function(event) {
+ if (!(event instanceof tr.model.ContainerMemoryDump)) {
+ throw new Error(
+ 'Memory dump sub-view only supports container memory dumps');
+ }
+ });
+ this.currentSelection_ = selection;
+
+ // Group the selected memory dumps by container name and sort them
+ // chronologically.
+ this.dumpsByContainerName_ = tr.b.groupIntoMap(
+ this.currentSelection_.toArray(), dump => dump.containerName);
+ for (const dumps of this.dumpsByContainerName_.values()) {
+ dumps.sort((a, b) => a.start - b.start);
+ }
+
+ this.updateContents_();
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ get requiresTallView() {
+ return true;
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.content).textContent = '';
+
+ if (this.dumpsByContainerName_ === undefined) return;
+
+ const containerNames = Array.from(this.dumpsByContainerName_.keys());
+ if (containerNames.length === 0) return;
+
+ if (containerNames.length > 1) {
+ this.buildViewForMultipleContainerNames_();
+ } else {
+ this.buildViewForSingleContainerName_();
+ }
+ },
+
+ buildViewForSingleContainerName_() {
+ const containerMemoryDumps = tr.b.getFirstElement(
+ this.dumpsByContainerName_.values());
+ const dumpView = this.ownerDocument.createElement(
+ 'tr-ui-a-stacked-pane-view');
+ Polymer.dom(this.$.content).appendChild(dumpView);
+ dumpView.setPaneBuilder(function() {
+ const headerPane = document.createElement(
+ 'tr-ui-a-memory-dump-header-pane');
+ headerPane.containerMemoryDumps = containerMemoryDumps;
+ return headerPane;
+ });
+ },
+
+ buildViewForMultipleContainerNames_() {
+ // TODO(petrcermak): Provide a more sophisticated view for this case.
+ const ownerDocument = this.ownerDocument;
+
+ const rows = [];
+ for (const [containerName, dumps] of this.dumpsByContainerName_) {
+ rows.push({
+ containerName,
+ subRows: dumps,
+ isExpanded: true,
+ });
+ }
+ rows.sort(function(a, b) {
+ return a.containerName.localeCompare(b.containerName);
+ });
+
+ const columns = [
+ {
+ title: 'Dump',
+
+ value(row) {
+ if (row.subRows === undefined) {
+ return this.singleDumpValue_(row);
+ }
+ return this.groupedDumpValue_(row);
+ },
+
+ singleDumpValue_(row) {
+ const linkEl = ownerDocument.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(new tr.model.EventSet([row]));
+ Polymer.dom(linkEl).appendChild(tr.v.ui.createScalarSpan(
+ row.start, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument
+ }));
+ return linkEl;
+ },
+
+ groupedDumpValue_(row) {
+ const linkEl = ownerDocument.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(new tr.model.EventSet(row.subRows));
+ Polymer.dom(linkEl).appendChild(tr.ui.b.createSpan({
+ ownerDocument,
+ textContent: row.subRows.length + ' memory dump' +
+ (row.subRows.length === 1 ? '' : 's') + ' in '
+ }));
+ Polymer.dom(linkEl).appendChild(tr.ui.b.createSpan({
+ ownerDocument,
+ textContent: row.containerName,
+ bold: true
+ }));
+ return linkEl;
+ }
+ }
+ ];
+
+ const table = this.ownerDocument.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.showHeader = false;
+ table.rebuild();
+ Polymer.dom(this.$.content).appendChild(table);
+ }
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.GlobalMemoryDump,
+ {
+ multi: false,
+ title: 'Global Memory Dump',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.GlobalMemoryDump,
+ {
+ multi: true,
+ title: 'Global Memory Dumps',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.ProcessMemoryDump,
+ {
+ multi: false,
+ title: 'Process Memory Dump',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.ProcessMemoryDump,
+ {
+ multi: true,
+ title: 'Process Memory Dumps',
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html
new file mode 100644
index 00000000000..974837f545e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import"
+ href="/tracing/ui/analysis/container_memory_dump_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const extractVmRegions = tr.ui.analysis.extractVmRegions;
+ const extractMemoryAllocatorDumps =
+ tr.ui.analysis.extractMemoryAllocatorDumps;
+ const extractHeapDumps = tr.ui.analysis.extractHeapDumps;
+
+ function createViewWithSelection(selection, opt_parentElement) {
+ const viewEl = document.createElement(
+ 'tr-ui-a-container-memory-dump-sub-view');
+ if (opt_parentElement) {
+ Polymer.dom(opt_parentElement).appendChild(viewEl);
+ }
+ if (selection === undefined) {
+ viewEl.selection = undefined;
+ } else {
+ // Rotate the list of selected dumps to check that the sub-view sorts
+ // them properly.
+ const length = selection.length;
+ viewEl.selection = new tr.model.EventSet(
+ selection.slice(length / 2, length).concat(
+ selection.slice(0, length / 2)));
+ }
+ return viewEl;
+ }
+
+ function createAndCheckContainerMemoryDumpView(
+ test, containerMemoryDumps, detailsCheckCallback, opt_parentElement) {
+ const viewEl =
+ createViewWithSelection(containerMemoryDumps, opt_parentElement);
+ if (!opt_parentElement) {
+ test.addHTMLOutput(viewEl);
+ }
+
+ // The view should contain a stacked pane view with memory dump header and
+ // overview panes.
+ const stackedPaneViewEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-stacked-pane-view');
+ const headerPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-header-pane');
+ const overviewPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-overview-pane');
+
+ // Check that the header pane and overview pane are correctly set up.
+ const processMemoryDumps = containerMemoryDumps.map(
+ containerDump => containerDump.processMemoryDumps);
+ assert.deepEqual(
+ Array.from(headerPaneEl.containerMemoryDumps), containerMemoryDumps);
+ assert.deepEqual(overviewPaneEl.processMemoryDumps, processMemoryDumps);
+ assert.strictEqual(
+ overviewPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+
+ // Get the overview pane table to drive the details pane checks.
+ const overviewTableEl = tr.ui.b.findDeepElementMatching(
+ overviewPaneEl, 'tr-ui-b-table');
+
+ function checkVmRegionsPane(pid) {
+ const detailsPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ if (pid === undefined) {
+ assert.isUndefined(detailsPaneEl);
+ } else {
+ assert.deepEqual(Array.from(detailsPaneEl.vmRegions),
+ extractVmRegions(processMemoryDumps, pid));
+ assert.strictEqual(
+ detailsPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+ }
+ }
+
+ function checkAllocatorPane(pid, allocatorName, withHeapDetailsPane) {
+ const allocatorDetailsPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-allocator-details-pane');
+ if (pid === undefined) {
+ assert.isUndefined(allocatorDetailsPaneEl);
+ assert.isUndefined(allocatorName); // Test sanity check.
+ assert.isUndefined(withHeapDetailsPane); // Test sanity check.
+ return;
+ }
+
+ assert.deepEqual(
+ Array.from(allocatorDetailsPaneEl.memoryAllocatorDumps),
+ extractMemoryAllocatorDumps(processMemoryDumps, pid, allocatorName));
+ assert.strictEqual(
+ allocatorDetailsPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+
+ const heapDetailsPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-heap-details-pane');
+ if (!withHeapDetailsPane) {
+ assert.isUndefined(heapDetailsPaneEl);
+ return;
+ }
+
+ assert.deepEqual(Array.from(heapDetailsPaneEl.heapDumps),
+ extractHeapDumps(processMemoryDumps, pid, allocatorName));
+ assert.strictEqual(
+ heapDetailsPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+ }
+
+ detailsCheckCallback(
+ overviewTableEl, checkVmRegionsPane, checkAllocatorPane);
+ }
+
+ test('instantiate_empty', function() {
+ // All these views should be completely empty.
+ const unsetViewEl = document.createElement(
+ 'tr-ui-a-container-memory-dump-sub-view');
+ this.addHTMLOutput(unsetViewEl);
+ assert.strictEqual(unsetViewEl.getBoundingClientRect().width, 0);
+ assert.strictEqual(unsetViewEl.getBoundingClientRect().height, 0);
+
+ const undefinedViewEl = createViewWithSelection(undefined);
+ this.addHTMLOutput(undefinedViewEl);
+ assert.strictEqual(undefinedViewEl.getBoundingClientRect().width, 0);
+ assert.strictEqual(undefinedViewEl.getBoundingClientRect().height, 0);
+
+ const emptyViewEl = createViewWithSelection([]);
+ this.addHTMLOutput(emptyViewEl);
+ assert.strictEqual(emptyViewEl.getBoundingClientRect().width, 0);
+ assert.strictEqual(emptyViewEl.getBoundingClientRect().height, 0);
+ });
+
+ test('instantiate_singleGlobalMemoryDump', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ [tr.ui.analysis.createSingleTestGlobalMemoryDump()],
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Total resident of Process 1.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 1;
+ checkVmRegionsPane(1 /* PID */);
+ checkAllocatorPane(undefined);
+
+ // PSS of process 4.
+ overviewTableEl.selectedColumnIndex = 3;
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[2];
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Malloc of process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[1];
+ overviewTableEl.selectedColumnIndex = 10;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'malloc',
+ false /* no heap details pane */);
+ });
+ });
+
+ test('instantiate_multipleGlobalMemoryDumps', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps(),
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Blink of Process 1.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 8;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Peak total resident of Process 4.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[3];
+ overviewTableEl.selectedColumnIndex = 2;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // V8 of Process 3.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[2];
+ overviewTableEl.selectedColumnIndex = 12;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(3 /* PID */, 'v8', true /* heap details pane */);
+ });
+ });
+
+ test('instantiate_singleProcessMemoryDump', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()],
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Tracing of Process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 13;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'tracing',
+ false /* no heap details pane */);
+
+ // Blink of Process 2.
+ overviewTableEl.selectedColumnIndex = 8;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'blink',
+ false /* no heap details pane */);
+
+ // Total resident of Process 2.
+ overviewTableEl.selectedColumnIndex = 1;
+ checkVmRegionsPane(2 /* PID */);
+ checkAllocatorPane(undefined);
+ });
+ });
+
+ test('instantiate_multipleProcessMemoryDumps', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ tr.ui.analysis.createMultipleTestProcessMemoryDumps(),
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Tracing of Process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 13;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'tracing',
+ false /* no heap details pane */);
+
+ // V8 of Process 2.
+ overviewTableEl.selectedColumnIndex = 12;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'v8',
+ false /* no heap details pane */);
+
+ // PSS of Process 2.
+ overviewTableEl.selectedColumnIndex = 3;
+ checkVmRegionsPane(2 /* PID */);
+ checkAllocatorPane(undefined);
+ });
+ });
+
+ test('memory', function() {
+ const containerEl = document.createElement('div');
+ containerEl.brushingStateController =
+ new tr.c.BrushingStateController(undefined);
+
+ // Create the first container memory view.
+ createAndCheckContainerMemoryDumpView(this,
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()],
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Select V8 of Process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 12;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'v8',
+ false /* no heap details pane */);
+ }, containerEl);
+
+ // Destroy the first container memory view.
+ Polymer.dom(containerEl).textContent = '';
+
+ // Create the second container memory view.
+ createAndCheckContainerMemoryDumpView(this,
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps(),
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // V8 of Process 2 should still be selected (even though the selection
+ // changed).
+ assert.strictEqual(
+ overviewTableEl.selectedTableRow, overviewTableEl.tableRows[1]);
+ assert.strictEqual(overviewTableEl.selectedColumnIndex, 12);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'v8',
+ false /* no heap details pane */);
+ }, containerEl);
+ });
+
+ test('instantiate_differentProcessMemoryDumps', function() {
+ const globalMemoryDumps =
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps();
+ // 2 dumps in Process 1, 3 dumps in Process 2, and 1 dump in Process 4
+ // (intentionally shuffled to check sorting).
+ const differentProcessDumps = [
+ globalMemoryDumps[1].processMemoryDumps[2],
+ globalMemoryDumps[0].processMemoryDumps[1],
+ globalMemoryDumps[0].processMemoryDumps[2],
+ globalMemoryDumps[1].processMemoryDumps[4],
+ globalMemoryDumps[1].processMemoryDumps[1],
+ globalMemoryDumps[2].processMemoryDumps[2]
+ ];
+
+ const viewEl = createViewWithSelection(differentProcessDumps);
+ this.addHTMLOutput(viewEl);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-table');
+ assert.lengthOf(tableEl.tableRows, 3);
+ assert.lengthOf(tableEl.tableColumns, 1);
+ const rows = tableEl.tableRows;
+ const col = tableEl.tableColumns[0];
+
+ assert.strictEqual(Polymer.dom(col.value(rows[0])).textContent,
+ '2 memory dumps in Process 1');
+ assert.strictEqual(Polymer.dom(col.value(rows[1])).textContent,
+ '3 memory dumps in Process 2');
+ assert.strictEqual(Polymer.dom(col.value(rows[2])).textContent,
+ '1 memory dump in Process 4');
+
+ // Check that the analysis link is associated with the right dumps.
+ assert.isTrue(col.value(rows[1]).selection.equals(new tr.model.EventSet([
+ globalMemoryDumps[0].processMemoryDumps[2],
+ globalMemoryDumps[1].processMemoryDumps[2],
+ globalMemoryDumps[2].processMemoryDumps[2]
+ ])));
+
+ assert.lengthOf(rows[1].subRows, 3);
+ const subRow = rows[1].subRows[0];
+
+ // Check the timestamp.
+ assert.strictEqual(col.value(subRow).children[0].value, 42);
+
+ // Check that the analysis link is associated with the right dump.
+ assert.isTrue(col.value(subRow).selection.equals(
+ new tr.model.EventSet(globalMemoryDumps[0].processMemoryDumps[2])));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html
new file mode 100644
index 00000000000..a9275b19d0c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-counter-sample-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id='table'></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+(function() {
+ const COUNTER_SAMPLE_TABLE_COLUMNS = [
+ {
+ title: 'Counter',
+ width: '150px',
+ value(row) { return row.counter; }
+ },
+ {
+ title: 'Series',
+ width: '150px',
+ value(row) { return row.series; }
+ },
+ {
+ title: 'Time',
+ width: '150px',
+ value(row) { return row.start; }
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) { return row.value; }
+ }
+ ];
+
+ Polymer({
+ is: 'tr-ui-a-counter-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ this.$.table.tableColumns = COUNTER_SAMPLE_TABLE_COLUMNS;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.$.table.tableRows =
+ this.selection ? this.getRows_(this.selection.toArray()) : [];
+ this.$.table.rebuild();
+ },
+
+ /**
+ * Returns the table rows for the specified samples.
+ *
+ * We print each counter/series combination the first time that it
+ * appears. For subsequent samples in each series, we omit the counter
+ * and series name. This makes it easy to scan to find the next series.
+ *
+ * Each series can be collapsed. In the expanded state, all samples
+ * are shown. In the collapsed state, only the first sample is displayed.
+ */
+ getRows_(samples) {
+ const samplesByCounter = tr.b.groupIntoMap(
+ samples, sample => sample.series.counter.guid);
+
+ const rows = [];
+ for (const counterSamples of samplesByCounter.values()) {
+ const samplesBySeries = tr.b.groupIntoMap(
+ counterSamples, sample => sample.series.guid);
+
+ for (const seriesSamples of samplesBySeries.values()) {
+ const seriesRows = this.getRowsForSamples_(seriesSamples);
+ seriesRows[0].counter = seriesSamples[0].series.counter.name;
+ seriesRows[0].series = seriesSamples[0].series.name;
+
+ if (seriesRows.length > 1) {
+ seriesRows[0].subRows = seriesRows.slice(1);
+ seriesRows[0].isExpanded = true;
+ }
+
+ rows.push(seriesRows[0]);
+ }
+ }
+
+ return rows;
+ },
+
+ getRowsForSamples_(samples) {
+ return samples.map(function(sample) {
+ return {
+ start: sample.timestamp,
+ value: sample.value
+ };
+ });
+ }
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-counter-sample-sub-view',
+ tr.model.CounterSample,
+ {
+ multi: false,
+ title: 'Counter Sample',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-counter-sample-sub-view',
+ tr.model.CounterSample,
+ {
+ multi: true,
+ title: 'Counter Samples',
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html
new file mode 100644
index 00000000000..9d7fa370313
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/counter_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Counter = tr.model.Counter;
+ const CounterSeries = tr.model.CounterSeries;
+ const EventSet = tr.model.EventSet;
+
+ test('instantiate_undefinedSelection', function() {
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(undefined);
+
+ assert.lengthOf(analysisEl.$.table.tableRows, 0);
+ });
+
+ test('instantiate_oneCounterOneSeries', function() {
+ const series = new CounterSeries('series1', 0);
+ series.addCounterSample(0, 0);
+ series.addCounterSample(1, 10);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(series.samples);
+ this.addHTMLOutput(analysisEl);
+
+ // The first sample should be listed as a collapsible header row for the
+ // series.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ assert.isTrue(rows[0].isExpanded);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 0);
+ assert.strictEqual(rows[0].value, 0);
+
+ // The second sample should be listed as a subrow of the first.
+ const subRows = rows[0].subRows;
+ assert.lengthOf(subRows, 1);
+ assert.isUndefined(subRows[0].counter);
+ assert.isUndefined(subRows[0].series);
+ assert.strictEqual(subRows[0].start, 1);
+ assert.strictEqual(subRows[0].value, 10);
+ });
+
+ test('instantiate_singleSampleDoesntHaveSubrows', function() {
+ const series = new CounterSeries('series1', 0);
+ series.addCounterSample(0, 0);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(series.samples);
+ this.addHTMLOutput(analysisEl);
+
+ // The first sample should be listed as a collapsible header row for the
+ // series.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 0);
+ assert.strictEqual(rows[0].value, 0);
+ assert.isUndefined(rows[0].subRows);
+ });
+
+ test('instantiate_oneCounterTwoSeries', function() {
+ const series1 = new CounterSeries('series1', 0);
+ series1.addCounterSample(1, 10);
+ series1.addCounterSample(2, 20);
+
+ const series2 = new CounterSeries('series2', 0);
+ series2.addCounterSample(3, 30);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series1);
+ counter.addSeries(series2);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection =
+ new EventSet(series1.samples.concat(series2.samples));
+ this.addHTMLOutput(analysisEl);
+
+ // The first samples should be listed as collapsible header rows for the
+ // series.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 1);
+ assert.strictEqual(rows[0].value, 10);
+
+ assert.strictEqual(rows[1].counter, 'ctr1');
+ assert.strictEqual(rows[1].series, 'series2');
+ assert.strictEqual(rows[1].start, 3);
+ assert.strictEqual(rows[1].value, 30);
+
+ // The subsequent samples should be listed as subrows of the first.
+ const subRows1 = rows[0].subRows;
+ assert.lengthOf(subRows1, 1);
+ assert.isUndefined(subRows1[0].counter);
+ assert.isUndefined(subRows1[0].series);
+ assert.strictEqual(subRows1[0].start, 2);
+ assert.strictEqual(subRows1[0].value, 20);
+
+ assert.isUndefined(rows[1].subRows);
+ });
+
+ test('instantiate_twoCountersTwoSeries', function() {
+ const series1 = new CounterSeries('series1', 0);
+ series1.addCounterSample(1, 10);
+
+ const series2 = new CounterSeries('series2', 0);
+ series2.addCounterSample(2, 20);
+
+ const counter1 = new Counter(null, 0, 'cat', 'ctr1');
+ const counter2 = new Counter(null, 0, 'cat', 'ctr2');
+ counter1.addSeries(series1);
+ counter2.addSeries(series2);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection =
+ new EventSet(series1.samples.concat(series2.samples));
+ this.addHTMLOutput(analysisEl);
+
+ // Each sample should be a header row with no subrows.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 1);
+ assert.strictEqual(rows[0].value, 10);
+ assert.isUndefined(rows[0].subRows);
+
+ assert.strictEqual(rows[1].counter, 'ctr2');
+ assert.strictEqual(rows[1].series, 'series2');
+ assert.strictEqual(rows[1].start, 2);
+ assert.strictEqual(rows[1].value, 20);
+ assert.isUndefined(rows[1].subRows);
+ });
+
+ test('instantiate_contentsClearedEachSelection', function() {
+ const series = new CounterSeries('series1', 0);
+ series.addCounterSample(0, 0);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(series.samples);
+ analysisEl.selection = new EventSet(series.samples);
+ this.addHTMLOutput(analysisEl);
+
+ assert.lengthOf(analysisEl.$.table.tableRows, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html
new file mode 100644
index 00000000000..1773a09f32f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'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);
+ }
+ }
+ return selection;
+ },
+
+ get outFlowEvents() {
+ const selection = new tr.model.EventSet();
+ for (const guid in this.eventsByGUID_) {
+ const fs = this.eventsByGUID_[guid];
+ if (fs.state === FLOW_OUT) {
+ selection.push(fs.event);
+ }
+ }
+ return selection;
+ },
+
+ get internalFlowEvents() {
+ const selection = new tr.model.EventSet();
+ for (const guid in this.eventsByGUID_) {
+ const fs = this.eventsByGUID_[guid];
+ if (fs.state === FLOW_IN_OUT) {
+ selection.push(fs.event);
+ }
+ }
+ return selection;
+ }
+ };
+
+ return {
+ FlowClassifier,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html
new file mode 100644
index 00000000000..ba68f671b57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/flow_classifier.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ test('basic', function() {
+ const a = newFlowEventEx({
+ title: 'a', start: 0, end: 10 });
+ const b = newFlowEventEx({
+ title: 'b', start: 10, end: 20 });
+ const c = newFlowEventEx({
+ title: 'c', start: 20, end: 25 });
+ const d = newFlowEventEx({
+ title: 'd', start: 30, end: 35 });
+
+ const fc = new tr.ui.analysis.FlowClassifier();
+ fc.addInFlow(a);
+
+ fc.addInFlow(b);
+ fc.addOutFlow(b);
+
+ fc.addInFlow(c);
+ fc.addOutFlow(c);
+
+ fc.addOutFlow(d);
+
+ function asSortedArray(selection) {
+ const events = Array.from(selection);
+ events.sort(function(a, b) {
+ return a.guid - b.guid;
+ });
+ return events;
+ }
+
+ assert.deepEqual(Array.from(fc.inFlowEvents), [a]);
+ assert.deepEqual(Array.from(fc.outFlowEvents), [d]);
+ assert.deepEqual(Array.from(fc.internalFlowEvents), [b, c]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html
new file mode 100644
index 00000000000..bc3f4fcead8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+
+<!--
+@fileoverview A line chart showing milliseconds since the start of the frame on
+the x-axis and power consumption on the y-axis. Each frame is shown as a
+separate line in the chart. Vertical sync events are used as the start of each
+frame.
+
+This chart aims to help users understand the shape of the power consumption
+curve over the course of a frame or set of frames.
+-->
+<dom-module id='tr-ui-a-frame-power-usage-chart'>
+ <template>
+ <div id="content"></div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+const EventSet = tr.model.EventSet;
+
+const CHART_TITLE = 'Power (W) by ms since vertical sync';
+
+Polymer({
+ is: 'tr-ui-a-frame-power-usage-chart',
+
+ ready() {
+ this.chart_ = undefined;
+ this.samples_ = new EventSet();
+ this.vSyncTimestamps_ = [];
+ },
+
+ attached() {
+ if (this.samples_) this.updateContents_();
+ },
+
+ get chart() {
+ return this.chart_;
+ },
+
+ get samples() {
+ return this.samples_;
+ },
+
+ get vSyncTimestamps() {
+ return this.vSyncTimestamps_;
+ },
+
+ /**
+ * Sets the data that powers the chart. Vsync timestamps must be in
+ * chronological order.
+ */
+ setData(samples, vSyncTimestamps) {
+ this.samples_ = (samples === undefined) ? new EventSet() : samples;
+ this.vSyncTimestamps_ =
+ (vSyncTimestamps === undefined) ? [] : vSyncTimestamps;
+ if (this.isAttached) this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearChart_();
+
+ const data = this.getDataForLineChart_();
+
+ if (data.length === 0) return;
+
+ this.chart_ = new tr.ui.b.LineChart();
+ Polymer.dom(this.$.content).appendChild(this.chart_);
+ this.chart_.chartTitle = CHART_TITLE;
+ this.chart_.data = data;
+ },
+
+ clearChart_() {
+ const content = this.$.content;
+ while (Polymer.dom(content).firstChild) {
+ Polymer.dom(content).removeChild(Polymer.dom(content).firstChild);
+ }
+
+ this.chart_ = undefined;
+ },
+
+ // TODO(charliea): Limit the ms since vsync to the median frame length. The
+ // vertical syncs are not 100% regular and highlighting any sample that's
+ // in one of these 'vertical sync lulls' makes the x-axis have a much larger
+ // scale than it should, effectively squishing the other samples into the
+ // left side of the chart.
+ /**
+ * Returns an array of data points for the chart. Each element in the array
+ * is of the form { x: <ms since vsync>, f<frame#>: <power in mW> }.
+ */
+ getDataForLineChart_() {
+ const sortedSamples = this.sortSamplesByTimestampAscending_(this.samples);
+ const vSyncTimestamps = this.vSyncTimestamps.slice();
+
+ let lastVSyncTimestamp = undefined;
+ const points = [];
+
+ // For each power sample, find and record the frame number that it belongs
+ // to as well as the amount of time elapsed since that frame began.
+ let frameNumber = 0;
+ sortedSamples.forEach(function(sample) {
+ while (vSyncTimestamps.length > 0 && vSyncTimestamps[0] <= sample.start) {
+ lastVSyncTimestamp = vSyncTimestamps.shift();
+ frameNumber++;
+ }
+
+ // If no vertical sync occurred before the power sample, don't use the
+ // power sample.
+ if (lastVSyncTimestamp === undefined) return;
+
+ const point = { x: sample.start - lastVSyncTimestamp };
+ point['f' + frameNumber] = sample.powerInW;
+ points.push(point);
+ });
+
+ return points;
+ },
+
+ sortSamplesByTimestampAscending_(samples) {
+ return samples.toArray().sort(function(smpl1, smpl2) {
+ return smpl1.start - smpl2.start;
+ });
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html
new file mode 100644
index 00000000000..caf4601f33c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_sample.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function instantiateManyFrames() {
+ const model = new tr.Model();
+ const numFrames = 200;
+ const samplesPerFrame = 200;
+
+ // Set up the test data.
+ const series = new tr.model.PowerSeries(model.device);
+ const vsyncTimestamps = [];
+ for (let i = 0; i < numFrames; i++) {
+ vsyncTimestamps.push(i * samplesPerFrame);
+ for (let j = 0; j < samplesPerFrame; j++) {
+ series.addPowerSample(vsyncTimestamps[i] + j, j);
+ }
+ }
+ const samples = series.samples;
+
+ // Display the chart.
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(samples), vsyncTimestamps);
+ this.addHTMLOutput(chart);
+ }
+
+ timedPerfTest('frame_power_usage_chart', instantiateManyFrames, {
+ iterations: 1
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html
new file mode 100644
index 00000000000..04ba9388852
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_sample.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_noSamples', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(undefined, [0]);
+
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_noVSyncs', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), []);
+
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_noSamplesOrVSyncs', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(undefined, []);
+
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_oneFrame', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_twoFrames', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0, 4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0, f2: 2 },
+ { x: 1, f2: 3 },
+ { x: 2, f2: 4 },
+ { x: 3, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_twoFramesDifferentXValues', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ // Power samples taken at 0, 1, 2, and 3s after frame start.
+ const vSyncTimestamps = [0, 4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ // Power samples taken at 0.5, 1.5, 2.5, and 3.5s after frame start.
+ series.addPowerSample(4.5, 2);
+ series.addPowerSample(5.5, 3);
+ series.addPowerSample(6.5, 4);
+ series.addPowerSample(7.5, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0.5, f2: 2 },
+ { x: 1.5, f2: 3 },
+ { x: 2.5, f2: 4 },
+ { x: 3.5, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_samplesBeforeFirstVSync', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 2 },
+ { x: 1, f1: 3 },
+ { x: 2, f1: 4 },
+ { x: 3, f1: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_allSamplesBeforeFirstVSync', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ const expectedChartData = [
+ { x: 0, f1: 2 },
+ { x: 1, f1: 3 },
+ { x: 2, f1: 4 },
+ { x: 3, f1: 3 }
+ ];
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_vSyncsAfterLastPowerSample', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0, 4, 8, 12];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0, f2: 2 },
+ { x: 1, f2: 3 },
+ { x: 2, f2: 4 },
+ { x: 3, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_onlyVSyncAfterLastPowerSample', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [8];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ assert.isUndefined(chart.chart);
+ });
+
+
+ test('instantiate_samplesNotInChronologicalOrder', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0, 4];
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0, f2: 2 },
+ { x: 1, f2: 3 },
+ { x: 2, f2: 4 },
+ { x: 3, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html
new file mode 100644
index 00000000000..e0c33df1231
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html
@@ -0,0 +1,347 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/model/object_snapshot.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-generic-object-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ font-family: monospace;
+ }
+ </style>
+ <div id="content">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+function isTable(object) {
+ if (!(object instanceof Array) ||
+ (object.length < 2)) return false;
+ for (const colName in object[0]) {
+ if (typeof colName !== 'string') return false;
+ }
+ for (let i = 0; i < object.length; ++i) {
+ if (!(object[i] instanceof Object)) return false;
+ for (const colName in object[i]) {
+ if (i && (object[0][colName] === undefined)) return false;
+ const cellType = typeof object[i][colName];
+ if (cellType !== 'string' && cellType !== 'number') return false;
+ }
+ if (i) {
+ for (const colName in object[0]) {
+ if (object[i][colName] === undefined) return false;
+ }
+ }
+ }
+ return true;
+}
+
+Polymer({
+ is: 'tr-ui-a-generic-object-view',
+
+ ready() {
+ this.object_ = undefined;
+ },
+
+ get object() {
+ return this.object_;
+ },
+
+ set object(object) {
+ this.object_ = object;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.content).textContent = '';
+ this.appendElementsForType_('', this.object_, 0, 0, 5, '');
+ },
+
+ appendElementsForType_(
+ label, object, indent, depth, maxDepth, suffix) {
+ if (depth > maxDepth) {
+ this.appendSimpleText_(
+ label, indent, '<recursion limit reached>', suffix);
+ return;
+ }
+
+ if (object === undefined) {
+ this.appendSimpleText_(label, indent, 'undefined', suffix);
+ return;
+ }
+
+ if (object === null) {
+ this.appendSimpleText_(label, indent, 'null', suffix);
+ return;
+ }
+
+ if (!(object instanceof Object)) {
+ const type = typeof object;
+ if (type !== 'string') {
+ return this.appendSimpleText_(label, indent, object, suffix);
+ }
+ let objectReplaced = false;
+ if ((object[0] === '{' && object[object.length - 1] === '}') ||
+ (object[0] === '[' && object[object.length - 1] === ']')) {
+ try {
+ object = JSON.parse(object);
+ objectReplaced = true;
+ } catch (e) {
+ }
+ }
+ if (!objectReplaced) {
+ if (object.includes('\n')) {
+ const lines = object.split('\n');
+ lines.forEach(function(line, i) {
+ let text;
+ let ioff;
+ let ll;
+ let ss;
+ if (i === 0) {
+ text = '"' + line;
+ ioff = 0;
+ ll = label;
+ ss = '';
+ } else if (i < lines.length - 1) {
+ text = line;
+ ioff = 1;
+ ll = '';
+ ss = '';
+ } else {
+ text = line + '"';
+ ioff = 1;
+ ll = '';
+ ss = suffix;
+ }
+
+ const el = this.appendSimpleText_(
+ ll, indent + ioff * label.length + ioff, text, ss);
+ el.style.whiteSpace = 'pre';
+ return el;
+ }, this);
+ return;
+ }
+ if (tr.b.isUrl(object)) {
+ const link = document.createElement('a');
+ link.href = object;
+ link.textContent = object;
+ this.appendElementWithLabel_(label, indent, link, suffix);
+ return;
+ }
+ this.appendSimpleText_(
+ label, indent, '"' + object + '"', suffix);
+ return;
+ }
+ }
+
+ if (object instanceof tr.model.ObjectSnapshot) {
+ const link = document.createElement('tr-ui-a-analysis-link');
+ link.selection = new tr.model.EventSet(object);
+ this.appendElementWithLabel_(label, indent, link, suffix);
+ return;
+ }
+
+ if (object instanceof tr.model.ObjectInstance) {
+ const link = document.createElement('tr-ui-a-analysis-link');
+ link.selection = new tr.model.EventSet(object);
+ this.appendElementWithLabel_(label, indent, link, suffix);
+ return;
+ }
+
+ if (object instanceof tr.b.math.Rect) {
+ this.appendSimpleText_(label, indent, object.toString(), suffix);
+ return;
+ }
+
+ if (object instanceof tr.b.Scalar) {
+ const el = this.ownerDocument.createElement('tr-v-ui-scalar-span');
+ el.value = object;
+ el.inline = true;
+ this.appendElementWithLabel_(label, indent, el, suffix);
+ return;
+ }
+
+ if (object instanceof Array) {
+ this.appendElementsForArray_(
+ label, object, indent, depth, maxDepth, suffix);
+ return;
+ }
+
+ this.appendElementsForObject_(
+ label, object, indent, depth, maxDepth, suffix);
+ },
+
+ appendElementsForArray_(
+ label, object, indent, depth, maxDepth, suffix) {
+ if (object.length === 0) {
+ this.appendSimpleText_(label, indent, '[]', suffix);
+ return;
+ }
+
+ if (isTable(object)) {
+ const table = document.createElement('tr-ui-b-table');
+ const columns = [];
+ for (const colName of Object.keys(object[0])) {
+ let allStrings = true;
+ let allNumbers = true;
+ for (let i = 0; i < object.length; ++i) {
+ if (typeof(object[i][colName]) !== 'string') {
+ allStrings = false;
+ }
+
+ if (typeof(object[i][colName]) !== 'number') {
+ allNumbers = false;
+ }
+
+ if (!allStrings && !allNumbers) break;
+ }
+
+ const column = {title: colName};
+ column.value = function(row) {
+ return row[colName];
+ };
+
+ if (allStrings) {
+ column.cmp = function(x, y) {
+ return x[colName].localeCompare(y[colName]);
+ };
+ } else if (allNumbers) {
+ column.cmp = function(x, y) {
+ return x[colName] - y[colName];
+ };
+ }
+ columns.push(column);
+ }
+ table.tableColumns = columns;
+ table.tableRows = object;
+ this.appendElementWithLabel_(label, indent, table, suffix);
+ table.rebuild();
+ return;
+ }
+
+ this.appendElementsForType_(
+ label + '[',
+ object[0],
+ indent, depth + 1, maxDepth,
+ object.length > 1 ? ',' : ']' + suffix);
+ for (let i = 1; i < object.length; i++) {
+ this.appendElementsForType_(
+ '',
+ object[i],
+ indent + label.length + 1, depth + 1, maxDepth,
+ i < object.length - 1 ? ',' : ']' + suffix);
+ }
+ return;
+ },
+
+ appendElementsForObject_(
+ label, object, indent, depth, maxDepth, suffix) {
+ const keys = Object.keys(object);
+ if (keys.length === 0) {
+ this.appendSimpleText_(label, indent, '{}', suffix);
+ return;
+ }
+
+ this.appendElementsForType_(
+ label + '{' + keys[0] + ': ',
+ object[keys[0]],
+ indent, depth, maxDepth,
+ keys.length > 1 ? ',' : '}' + suffix);
+ for (let i = 1; i < keys.length; i++) {
+ this.appendElementsForType_(
+ keys[i] + ': ',
+ object[keys[i]],
+ indent + label.length + 1, depth + 1, maxDepth,
+ i < keys.length - 1 ? ',' : '}' + suffix);
+ }
+ },
+
+ appendElementWithLabel_(label, indent, dataElement, suffix) {
+ const row = document.createElement('div');
+
+ const indentSpan = document.createElement('span');
+ indentSpan.style.whiteSpace = 'pre';
+ for (let i = 0; i < indent; i++) {
+ Polymer.dom(indentSpan).textContent += ' ';
+ }
+ Polymer.dom(row).appendChild(indentSpan);
+
+ const labelSpan = document.createElement('span');
+ Polymer.dom(labelSpan).textContent = label;
+ Polymer.dom(row).appendChild(labelSpan);
+
+ Polymer.dom(row).appendChild(dataElement);
+ const suffixSpan = document.createElement('span');
+ Polymer.dom(suffixSpan).textContent = suffix;
+ Polymer.dom(row).appendChild(suffixSpan);
+
+ row.dataElement = dataElement;
+ Polymer.dom(this.$.content).appendChild(row);
+ },
+
+ appendSimpleText_(label, indent, text, suffix) {
+ const el = this.ownerDocument.createElement('span');
+ Polymer.dom(el).textContent = text;
+ this.appendElementWithLabel_(label, indent, el, suffix);
+ return el;
+ }
+});
+</script>
+
+<dom-module id='tr-ui-a-generic-object-view-with-label'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-generic-object-view-with-label',
+
+ ready() {
+ this.labelEl_ = document.createElement('div');
+ this.genericObjectView_ =
+ document.createElement('tr-ui-a-generic-object-view');
+ Polymer.dom(this.root).appendChild(this.labelEl_);
+ Polymer.dom(this.root).appendChild(this.genericObjectView_);
+ },
+
+ get label() {
+ return Polymer.dom(this.labelEl_).textContent;
+ },
+
+ set label(label) {
+ Polymer.dom(this.labelEl_).textContent = label;
+ },
+
+ get object() {
+ return this.genericObjectView_.object;
+ },
+
+ set object(object) {
+ this.genericObjectView_.object = object;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html
new file mode 100644
index 00000000000..2e7812e2730
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('undefinedValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = undefined;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, 'undefined');
+ });
+
+ test('nullValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = null;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, 'null');
+ });
+
+ test('stringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = 'string value';
+ assert.strictEqual(
+ Polymer.dom(view.$.content).textContent, '"string value"');
+ });
+
+ test('multiLineStringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = 'i am a\n string value\ni have\n various indents';
+ this.addHTMLOutput(view);
+ const c = view.$.content;
+ });
+
+ test('multiLineStringValueInsideObject', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = {key: 'i am a\n string value\ni have\n various indents',
+ value: 'simple'};
+ this.addHTMLOutput(view);
+ const c = view.$.content;
+ });
+
+ test('jsonObjectStringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = '{"x": 1}';
+ assert.strictEqual(view.$.content.children.length, 1);
+ assert.strictEqual(view.$.content.children[0].children.length, 4);
+ });
+
+ test('jsonArrayStringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = '[1,2,3]';
+ assert.strictEqual(view.$.content.children.length, 3);
+ });
+
+ test('booleanValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = false;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, 'false');
+ });
+
+ test('numberValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = 3.14159;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, '3.14159');
+ });
+
+ test('objectSnapshotValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ view.object = s10;
+ this.addHTMLOutput(view);
+ assert.strictEqual(view.$.content.children[0].dataElement.tagName,
+ 'TR-UI-A-ANALYSIS-LINK');
+ });
+
+ test('objectInstanceValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ view.object = i10;
+ assert.strictEqual(view.$.content.children[0].dataElement.tagName,
+ 'TR-UI-A-ANALYSIS-LINK');
+ });
+
+ test('instantiate_emptyArrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_twoValueArrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [1, 2];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_twoValueBArrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [1, {x: 1}];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_arrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [1, 2, 'three'];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_arrayWithSimpleObjectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [{simple: 'object'}];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_arrayWithComplexObjectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [{col0: 'object', col1: 0},
+ {col2: 'Object', col3: 1}];
+ this.addHTMLOutput(view);
+ assert.strictEqual(undefined, tr.ui.b.findDeepElementMatching(
+ view.$.content, 'table'));
+ });
+
+ test('instantiate_arrayWithDeepObjectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [{key: {deep: 'object values make isTable() return false'}}];
+ this.addHTMLOutput(view);
+ assert.strictEqual(undefined, tr.ui.b.findDeepElementMatching(
+ view.$.content, 'table'));
+ });
+
+ test('jsonTableValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [
+ {col0: 'object', col1: 0, col2: 'foo'},
+ {col0: 'Object', col1: 1, col2: 42}
+ ];
+ this.addHTMLOutput(view);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-ui-b-table');
+ assert.strictEqual('col0', table.tableColumns[0].title);
+ assert.strictEqual('col1', table.tableColumns[1].title);
+ assert.strictEqual(
+ 'object', table.tableColumns[0].value(table.tableRows[0]));
+ assert.strictEqual(
+ 'Object', table.tableColumns[0].value(table.tableRows[1]));
+ assert.strictEqual(0, table.tableColumns[1].value(table.tableRows[0]));
+ assert.strictEqual(1, table.tableColumns[1].value(table.tableRows[1]));
+ assert.isDefined(table.tableColumns[0].cmp);
+ assert.isDefined(table.tableColumns[1].cmp);
+ assert.isUndefined(table.tableColumns[2].cmp);
+ });
+
+ test('instantiate_objectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = {
+ 'entry_one': 'entry_one_value',
+ 'entry_two': 2,
+ 'entry_three': [3, 4, 5]
+ };
+ this.addHTMLOutput(view);
+ });
+
+ test('timeDurationValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object =
+ new tr.b.Scalar(tr.b.Unit.byName.timeDurationInMs, 3);
+ this.addHTMLOutput(view);
+ assert.isDefined(tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-v-ui-scalar-span'));
+ });
+
+ test('timeStampValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = new tr.b.Scalar(tr.b.Unit.byName.timeStampInMs, 3);
+ this.addHTMLOutput(view);
+ assert.isDefined(tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-v-ui-scalar-span'));
+ });
+
+ test('scalarValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object =
+ new tr.b.Scalar(tr.b.Unit.byName.normalizedPercentage, .3);
+ this.addHTMLOutput(view);
+ const m = tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-v-ui-scalar-span');
+ assert.isDefined(m);
+ assert.strictEqual(m.value, .3);
+ assert.strictEqual(m.unit, tr.b.Unit.byName.normalizedPercentage);
+ });
+
+ test('httpLink', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ const url = 'https://google.com/chrome';
+ view.object = {a: url};
+ this.addHTMLOutput(view);
+ const a = tr.ui.b.findDeepElementMatching(view.$.content, 'a');
+ assert.isDefined(a);
+ assert.strictEqual(url, a.href);
+ assert.strictEqual(url, a.textContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html
new file mode 100644
index 00000000000..88b3c3ccc7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html
@@ -0,0 +1,893 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+
+<dom-module id='tr-ui-a-memory-dump-allocator-details-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #label {
+ flex: 0 0 auto;
+ padding: 8px;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #table {
+ display: none; /* Hide until memory allocator dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <div id="label">Component details</div>
+ <div id="contents">
+ <div id="info_text">No memory allocator dump selected</div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ // Link to docs.
+ const URL_TO_SIZE_VS_EFFECTIVE_SIZE = 'https://chromium.googlesource.com/chromium/src/+/master/docs/memory-infra/README.md#effective_size-vs_size';
+
+ // Constant representing the context in suballocation rows.
+ const SUBALLOCATION_CONTEXT = true;
+
+ // Size numeric info types.
+ 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;
+
+ // Unicode symbols used for memory cell info icons and messages.
+ const LEFTWARDS_OPEN_HEADED_ARROW = String.fromCharCode(0x21FD);
+ const RIGHTWARDS_OPEN_HEADED_ARROW = String.fromCharCode(0x21FE);
+ const EN_DASH = String.fromCharCode(0x2013);
+ const CIRCLED_LATIN_SMALL_LETTER_I = String.fromCharCode(0x24D8);
+
+ /** @constructor */
+ function AllocatorDumpNameColumn() {
+ tr.ui.analysis.TitleColumn.call(this, 'Component');
+ }
+
+ AllocatorDumpNameColumn.prototype = {
+ __proto__: tr.ui.analysis.TitleColumn.prototype,
+
+ formatTitle(row) {
+ if (!row.suballocation) {
+ return row.title;
+ }
+ return tr.ui.b.createSpan({
+ textContent: row.title,
+ italic: true,
+ tooltip: row.fullNames === undefined ?
+ undefined : row.fullNames.join(', ')
+ });
+ }
+ };
+
+ /**
+ * Retrieve the entry associated with a given name from a map and increment
+ * its count.
+ *
+ * If there is no entry associated with the name, a new entry is created, the
+ * creation callback is called, the entry's count is incremented (from 0 to
+ * 1) and the newly created entry is returned.
+ */
+ function getAndUpdateEntry(map, name, createdCallback) {
+ let entry = map.get(name);
+ if (entry === undefined) {
+ entry = {count: 0};
+ createdCallback(entry);
+ map.set(name, entry);
+ }
+ entry.count++;
+ return entry;
+ }
+
+ /**
+ * Helper class for building size and effective size column info messages.
+ *
+ * @constructor
+ */
+ function SizeInfoMessageBuilder() {
+ this.parts_ = [];
+ this.indent_ = 0;
+ }
+
+ SizeInfoMessageBuilder.prototype = {
+ append(/* arguments */) {
+ this.parts_.push.apply(
+ this.parts_, Array.prototype.slice.apply(arguments));
+ },
+
+ /**
+ * Append the entries of a map to the message according to the following
+ * rules:
+ *
+ * 1. If the map is empty, append emptyText to the message (if provided).
+ * Examples:
+ *
+ * emptyText=undefined
+ * Hello, World! ====================> Hello, World!
+ *
+ * emptyText='empty'
+ * The bottle is ====================> The bottle is empty
+ *
+ * 2. If the map contains a single entry, append a space and call
+ * itemCallback on the entry (which is in turn expected to append a
+ * message for the entry). Example:
+ *
+ * Please do not ====================> Please do not [item-message]
+ *
+ * 3. If the map contains multiple entries, append them as a list
+ * with itemCallback called on each entry. If hasPluralSuffix is true,
+ * 's' will be appended to the message before the list. Examples:
+ *
+ * hasPluralSuffix=false
+ * I need to buy ====================> I need to buy:
+ * - [item1-message]
+ * - [item2-message]
+ * [...]
+ * - [itemN-message]
+ *
+ * hasPluralSuffix=true
+ * Suspected CL ====================> Suspected CLs:
+ * - [item1-message]
+ * - [item2-message]
+ * [...]
+ * - [itemN-message]
+ */
+ appendMap(
+ map, hasPluralSuffix, emptyText, itemCallback, opt_this) {
+ opt_this = opt_this || this;
+ if (map.size === 0) {
+ if (emptyText) {
+ this.append(emptyText);
+ }
+ } else if (map.size === 1) {
+ this.parts_.push(' ');
+ const key = map.keys().next().value;
+ itemCallback.call(opt_this, key, map.get(key));
+ } else {
+ if (hasPluralSuffix) {
+ this.parts_.push('s');
+ }
+ this.parts_.push(':');
+ this.indent_++;
+ for (const key of map.keys()) {
+ this.parts_.push('\n', ' '.repeat(3 * (this.indent_ - 1)), ' - ');
+ itemCallback.call(opt_this, key, map.get(key));
+ }
+ this.indent_--;
+ }
+ },
+
+ appendImportanceRange(range) {
+ this.append(' (importance: ');
+ if (range.min === range.max) {
+ this.append(range.min);
+ } else {
+ this.append(range.min, EN_DASH, range.max);
+ }
+ this.append(')');
+ },
+
+ appendSizeIfDefined(size) {
+ if (size !== undefined) {
+ this.append(' (', tr.b.Unit.byName.sizeInBytes.format(size), ')');
+ }
+ },
+
+ appendSomeTimestampsQuantifier() {
+ this.append(
+ ' ', tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER);
+ },
+
+ build() {
+ return this.parts_.join('');
+ }
+ };
+
+ /** @constructor */
+ function EffectiveSizeColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.DetailsNumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ EffectiveSizeColumn.prototype = {
+ __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createLink({
+ textContent: this.name,
+ tooltip: 'Memory used by this component',
+ href: URL_TO_SIZE_VS_EFFECTIVE_SIZE
+ });
+ },
+
+ addInfos(numerics, memoryAllocatorDumps, infos) {
+ if (memoryAllocatorDumps === undefined) return;
+
+ // Quantified name of an owner dump (of the given dump) -> {count,
+ // importanceRange}.
+ const ownerNameToEntry = new Map();
+
+ // Quantified name of an owned dump (by the given dump) -> {count,
+ // importanceRange, sharerNameToEntry}, where sharerNameToEntry is a map
+ // from quantified names of other owners of the owned dump to {count,
+ // importanceRange}.
+ const ownedNameToEntry = new Map();
+
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+
+ const dump = memoryAllocatorDumps[i];
+ if (dump === SUBALLOCATION_CONTEXT) {
+ return; // No ownership of suballocation internal rows.
+ }
+
+ // Gather owners of this dump.
+ dump.ownedBy.forEach(function(ownerLink) {
+ const ownerDump = ownerLink.source;
+ this.getAndUpdateOwnershipEntry_(
+ ownerNameToEntry, ownerDump, ownerLink);
+ }, this);
+
+ // Gather dumps owned by this dump and other owner dumps sharing them
+ // (with this dump).
+ const ownedLink = dump.owns;
+ if (ownedLink !== undefined) {
+ const ownedDump = ownedLink.target;
+ const ownedEntry = this.getAndUpdateOwnershipEntry_(ownedNameToEntry,
+ ownedDump, ownedLink, true /* opt_withSharerNameToEntry */);
+ const sharerNameToEntry = ownedEntry.sharerNameToEntry;
+ ownedDump.ownedBy.forEach(function(sharerLink) {
+ const sharerDump = sharerLink.source;
+ if (sharerDump === dump) return;
+ this.getAndUpdateOwnershipEntry_(
+ sharerNameToEntry, sharerDump, sharerLink);
+ }, this);
+ }
+ }
+
+ // Emit a single info listing all owners of this dump.
+ if (ownerNameToEntry.size > 0) {
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('shared by');
+ messageBuilder.appendMap(
+ ownerNameToEntry,
+ false /* hasPluralSuffix */,
+ undefined /* emptyText */,
+ function(ownerName, ownerEntry) {
+ messageBuilder.append(ownerName);
+ if (ownerEntry.count < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ messageBuilder.appendImportanceRange(ownerEntry.importanceRange);
+ }, this);
+ infos.push({
+ message: messageBuilder.build(),
+ icon: LEFTWARDS_OPEN_HEADED_ARROW,
+ color: 'green'
+ });
+ }
+
+ // Emit a single info listing all dumps owned by this dump together
+ // with list(s) of other owner dumps sharing them with this dump.
+ if (ownedNameToEntry.size > 0) {
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('shares');
+ messageBuilder.appendMap(
+ ownedNameToEntry,
+ false /* hasPluralSuffix */,
+ undefined /* emptyText */,
+ function(ownedName, ownedEntry) {
+ messageBuilder.append(ownedName);
+ const ownedCount = ownedEntry.count;
+ if (ownedCount < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ messageBuilder.appendImportanceRange(ownedEntry.importanceRange);
+ messageBuilder.append(' with');
+ messageBuilder.appendMap(
+ ownedEntry.sharerNameToEntry,
+ false /* hasPluralSuffix */,
+ ' no other dumps',
+ function(sharerName, sharerEntry) {
+ messageBuilder.append(sharerName);
+ if (sharerEntry.count < ownedCount) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ messageBuilder.appendImportanceRange(
+ sharerEntry.importanceRange);
+ }, this);
+ }, this);
+ infos.push({
+ message: messageBuilder.build(),
+ icon: RIGHTWARDS_OPEN_HEADED_ARROW,
+ color: 'green'
+ });
+ }
+ },
+
+ getAndUpdateOwnershipEntry_(
+ map, dump, link, opt_withSharerNameToEntry) {
+ const entry = getAndUpdateEntry(map, dump.quantifiedName,
+ function(newEntry) {
+ newEntry.importanceRange = new tr.b.math.Range();
+ if (opt_withSharerNameToEntry) {
+ newEntry.sharerNameToEntry = new Map();
+ }
+ });
+ entry.importanceRange.addValue(link.importance || 0);
+ return entry;
+ }
+ };
+
+ /** @constructor */
+ function SizeColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.DetailsNumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ SizeColumn.prototype = {
+ __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createLink({
+ textContent: this.name,
+ tooltip: 'Memory requested by this component',
+ href: URL_TO_SIZE_VS_EFFECTIVE_SIZE
+ });
+ },
+
+ addInfos(numerics, memoryAllocatorDumps, infos) {
+ if (memoryAllocatorDumps === undefined) return;
+ this.addOverlapInfo_(numerics, memoryAllocatorDumps, infos);
+ this.addProvidedSizeWarningInfos_(numerics, memoryAllocatorDumps, infos);
+ },
+
+ addOverlapInfo_(numerics, memoryAllocatorDumps, infos) {
+ // Sibling allocator dump name -> {count, size}. The latter field (size)
+ // is omitted in multi-selection mode.
+ const siblingNameToEntry = new Map();
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+ const dump = memoryAllocatorDumps[i];
+ if (dump === SUBALLOCATION_CONTEXT) {
+ return; // No ownership of suballocation internal rows.
+ }
+ const ownedBySiblingSizes = dump.ownedBySiblingSizes;
+ for (const siblingDump of ownedBySiblingSizes.keys()) {
+ const siblingName = siblingDump.name;
+ getAndUpdateEntry(siblingNameToEntry, siblingName,
+ function(newEntry) {
+ if (numerics.length === 1 /* single-selection mode */) {
+ newEntry.size = ownedBySiblingSizes.get(siblingDump);
+ }
+ });
+ }
+ }
+
+ // Emit a single info describing all overlaps with siblings (if
+ // applicable).
+ if (siblingNameToEntry.size > 0) {
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('overlaps with its sibling');
+ messageBuilder.appendMap(
+ siblingNameToEntry,
+ true /* hasPluralSuffix */,
+ undefined /* emptyText */,
+ function(siblingName, siblingEntry) {
+ messageBuilder.append('\'', siblingName, '\'');
+ messageBuilder.appendSizeIfDefined(siblingEntry.size);
+ if (siblingEntry.count < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ }, this);
+ infos.push({
+ message: messageBuilder.build(),
+ icon: CIRCLED_LATIN_SMALL_LETTER_I,
+ color: 'blue'
+ });
+ }
+ },
+
+ addProvidedSizeWarningInfos_(numerics, memoryAllocatorDumps,
+ infos) {
+ // Info type (see MemoryAllocatorDumpInfoType) -> {count, providedSize,
+ // dependencySize}. The latter two fields (providedSize and
+ // dependencySize) are omitted in multi-selection mode.
+ const infoTypeToEntry = new Map();
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+ const dump = memoryAllocatorDumps[i];
+ if (dump === SUBALLOCATION_CONTEXT) {
+ return; // Suballocation internal rows have no provided size.
+ }
+ dump.infos.forEach(function(dumpInfo) {
+ getAndUpdateEntry(infoTypeToEntry, dumpInfo.type, function(newEntry) {
+ if (numerics.length === 1 /* single-selection mode */) {
+ newEntry.providedSize = dumpInfo.providedSize;
+ newEntry.dependencySize = dumpInfo.dependencySize;
+ }
+ });
+ });
+ }
+
+ // Emit a warning info for every info type.
+ for (const infoType of infoTypeToEntry.keys()) {
+ const entry = infoTypeToEntry.get(infoType);
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('provided size');
+ messageBuilder.appendSizeIfDefined(entry.providedSize);
+ let dependencyName;
+ switch (infoType) {
+ case PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN:
+ dependencyName = 'the aggregated size of the children';
+ break;
+ case PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER:
+ dependencyName = 'the size of the largest owner';
+ break;
+ default:
+ dependencyName = 'an unknown dependency';
+ break;
+ }
+ messageBuilder.append(' was less than ', dependencyName);
+ messageBuilder.appendSizeIfDefined(entry.dependencySize);
+ if (entry.count < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ infos.push(tr.ui.analysis.createWarningInfo(messageBuilder.build()));
+ }
+ }
+ };
+
+ const NUMERIC_COLUMN_RULES = [
+ {
+ condition: tr.model.MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME,
+ importance: 10,
+ columnConstructor: EffectiveSizeColumn
+ },
+ {
+ condition: tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME,
+ importance: 9,
+ columnConstructor: SizeColumn
+ },
+ {
+ condition: 'page_size',
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: /size/,
+ importance: 5,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ // All other columns.
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ }
+ ];
+
+ const DIAGNOSTIC_COLUMN_RULES = [
+ {
+ importance: 0,
+ columnConstructor: tr.ui.analysis.StringMemoryColumn
+ }
+ ];
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-allocator-details-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.memoryAllocatorDumps_ = undefined;
+ this.heapDumps_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ },
+
+ /**
+ * Sets the memory allocator dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of memory allocator
+ * dumps. All dumps are assumed to belong to the same process and have
+ * the same full name. Example:
+ *
+ * [
+ * tr.model.MemoryAllocatorDump {}, // MAD at timestamp 1.
+ * undefined, // MAD not provided at timestamp 2.
+ * tr.model.MemoryAllocatorDump {}, // MAD at timestamp 3.
+ * ]
+ */
+ set memoryAllocatorDumps(memoryAllocatorDumps) {
+ this.memoryAllocatorDumps_ = memoryAllocatorDumps;
+ this.scheduleRebuild_();
+ },
+
+ get memoryAllocatorDumps() {
+ return this.memoryAllocatorDumps_;
+ },
+
+ // TODO(petrcermak): Don't plumb the heap dumps through the allocator
+ // details pane. Maybe add support for multiple child panes to stacked pane
+ // (view) instead.
+ set heapDumps(heapDumps) {
+ this.heapDumps_ = heapDumps;
+ this.scheduleRebuild_();
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ onRebuild_() {
+ if (this.memoryAllocatorDumps_ === undefined ||
+ this.memoryAllocatorDumps_.length === 0) {
+ // Show the info text (hide the table).
+ this.$.info_text.style.display = 'block';
+ this.$.table.style.display = 'none';
+
+ this.$.table.clear();
+ this.$.table.rebuild();
+
+ // Hide the heap details pane (if applicable).
+ this.childPaneBuilder = undefined;
+ return;
+ }
+
+ // Show the table (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.table.style.display = 'block';
+
+ const rows = this.createRows_();
+ const columns = this.createColumns_(rows);
+ rows.forEach(function(rootRow) {
+ tr.ui.analysis.aggregateTableRowCellsRecursively(rootRow, columns,
+ function(contexts) {
+ // Only aggregate suballocation rows (numerics of regular rows
+ // corresponding to MADs have already been aggregated by the
+ // model in MemoryAllocatorDump.aggregateNumericsRecursively).
+ return contexts !== undefined && contexts.some(function(context) {
+ return context === SUBALLOCATION_CONTEXT;
+ });
+ });
+ });
+
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = columns;
+ this.$.table.rebuild();
+ tr.ui.analysis.expandTableRowsRecursively(this.$.table);
+
+ // Show/hide the heap details pane.
+ if (this.heapDumps_ === undefined) {
+ this.childPaneBuilder = undefined;
+ } else {
+ this.childPaneBuilder = function() {
+ const pane =
+ document.createElement('tr-ui-a-memory-dump-heap-details-pane');
+ pane.heapDumps = this.heapDumps_;
+ pane.aggregationMode = this.aggregationMode_;
+ return pane;
+ }.bind(this);
+ }
+ },
+
+ createRows_() {
+ return [
+ this.createAllocatorRowRecursively_(this.memoryAllocatorDumps_)
+ ];
+ },
+
+ createAllocatorRowRecursively_(dumps) {
+ // Get the name of the memory allocator dumps. We can use any defined
+ // dump in dumps since they all have the same name.
+ const definedDump = dumps.find(x => x);
+ const title = definedDump.name;
+ const fullName = definedDump.fullName;
+
+ // Transform a chronological list of memory allocator dumps into two
+ // dictionaries of cells (where each cell contains a chronological list
+ // of the values of one of its numerics or diagnostics).
+ const numericCells = tr.ui.analysis.createCells(dumps, function(dump) {
+ return dump.numerics;
+ });
+ const diagnosticCells = tr.ui.analysis.createCells(dumps, function(dump) {
+ return dump.diagnostics;
+ });
+
+ // Determine whether the memory allocator dump is a suballocation. A
+ // dump is assumed to be a suballocation if (1) its name starts with
+ // two underscores, (2) it has an owner from within the same process at
+ // some timestamp, and (3) it is undefined, has no owners, or has the
+ // same owner (and no other owners) at all other timestamps.
+ let suballocatedBy = undefined;
+ if (title.startsWith('__')) {
+ for (let i = 0; i < dumps.length; i++) {
+ const dump = dumps[i];
+ if (dump === undefined || dump.ownedBy.length === 0) {
+ // Ignore timestamps where the dump is undefined or doesn't
+ // have any owner.
+ continue;
+ }
+ const ownerDump = dump.ownedBy[0].source;
+ if (dump.ownedBy.length > 1 ||
+ dump.children.length > 0 ||
+ ownerDump.containerMemoryDump !== dump.containerMemoryDump) {
+ // If the dump has (1) any children, (2) multiple owners, or
+ // (3) its owner is in a different process (otherwise, the
+ // modified title would be ambiguous), then it's not considered
+ // to be a suballocation.
+ suballocatedBy = undefined;
+ break;
+ }
+ if (suballocatedBy === undefined) {
+ suballocatedBy = ownerDump.fullName;
+ } else if (suballocatedBy !== ownerDump.fullName) {
+ // The full name of the owner dump changed over time, so this
+ // dump is not a suballocation.
+ suballocatedBy = undefined;
+ break;
+ }
+ }
+ }
+
+ const row = {
+ title,
+ fullNames: [fullName],
+ contexts: dumps,
+ numericCells,
+ diagnosticCells,
+ suballocatedBy
+ };
+
+ // Child memory dump name (dict key) -> Timestamp (list index) ->
+ // Child dump.
+ const childDumpNameToDumps = tr.b.invertArrayOfDicts(dumps,
+ function(dump) {
+ const results = {};
+ for (const child of dump.children) {
+ results[child.name] = child;
+ }
+ return results;
+ });
+
+ // Recursively create sub-rows for children (if applicable).
+ const subRows = [];
+ let suballocationClassificationRootNode = undefined;
+ for (const childDumps of Object.values(childDumpNameToDumps)) {
+ const childRow = this.createAllocatorRowRecursively_(childDumps);
+ if (childRow.suballocatedBy === undefined) {
+ // Not a suballocation row: just append it.
+ subRows.push(childRow);
+ } else {
+ // Suballocation row: classify it in a tree of suballocations.
+ suballocationClassificationRootNode =
+ this.classifySuballocationRow_(
+ childRow, suballocationClassificationRootNode);
+ }
+ }
+
+ // Build the tree of suballocations (if applicable).
+ if (suballocationClassificationRootNode !== undefined) {
+ const suballocationRow = this.createSuballocationRowRecursively_(
+ 'suballocations', suballocationClassificationRootNode);
+ subRows.push(suballocationRow);
+ }
+
+ if (subRows.length > 0) {
+ row.subRows = subRows;
+ }
+
+ return row;
+ },
+
+ classifySuballocationRow_(suballocationRow, rootNode) {
+ if (rootNode === undefined) {
+ rootNode = {
+ children: {},
+ row: undefined
+ };
+ }
+
+ const suballocationLevels = suballocationRow.suballocatedBy.split('/');
+ let currentNode = rootNode;
+ for (let i = 0; i < suballocationLevels.length; i++) {
+ const suballocationLevel = suballocationLevels[i];
+ let nextNode = currentNode.children[suballocationLevel];
+ if (nextNode === undefined) {
+ currentNode.children[suballocationLevel] = nextNode = {
+ children: {},
+ row: undefined
+ };
+ }
+ currentNode = nextNode;
+ }
+
+ const existingRow = currentNode.row;
+ if (existingRow !== undefined) {
+ // On rare occasions it can happen that one dump (e.g. sqlite) owns
+ // different suballocations at different timestamps (e.g.
+ // malloc/allocated_objects/_7d35 and malloc/allocated_objects/_511e).
+ // When this happens, we merge the two suballocations into a single row
+ // (malloc/allocated_objects/suballocations/sqlite).
+ for (let i = 0; i < suballocationRow.contexts.length; i++) {
+ const newContext = suballocationRow.contexts[i];
+ if (newContext === undefined) continue;
+
+ if (existingRow.contexts[i] !== undefined) {
+ throw new Error('Multiple suballocations with the same owner name');
+ }
+
+ existingRow.contexts[i] = newContext;
+ ['numericCells', 'diagnosticCells'].forEach(function(cellKey) {
+ const suballocationCells = suballocationRow[cellKey];
+ if (suballocationCells === undefined) return;
+ for (const [cellName, cell] of Object.entries(suballocationCells)) {
+ if (cell === undefined) continue;
+ const fields = cell.fields;
+ if (fields === undefined) continue;
+ const field = fields[i];
+ if (field === undefined) continue;
+ let existingCells = existingRow[cellKey];
+ if (existingCells === undefined) {
+ existingCells = {};
+ existingRow[cellKey] = existingCells;
+ }
+ let existingCell = existingCells[cellName];
+ if (existingCell === undefined) {
+ existingCell = new tr.ui.analysis.MemoryCell(
+ new Array(fields.length));
+ existingCells[cellName] = existingCell;
+ }
+ existingCell.fields[i] = field;
+ }
+ });
+ }
+ existingRow.fullNames.push.apply(
+ existingRow.fullNames, suballocationRow.fullNames);
+ } else {
+ currentNode.row = suballocationRow;
+ }
+
+ return rootNode;
+ },
+
+ createSuballocationRowRecursively_(name, node) {
+ const childCount = Object.keys(node.children).length;
+ if (childCount === 0) {
+ if (node.row === undefined) {
+ throw new Error('Suballocation node must have a row or children');
+ }
+ // Leaf row of the suballocation tree: Change the row's title from
+ // '__MEANINGLESSHASH' to the name of the suballocation owner.
+ const row = node.row;
+ row.title = name;
+ row.suballocation = true;
+ return row;
+ }
+
+ // Internal row of the suballocation tree: Recursively create its
+ // sub-rows.
+ const subRows = [];
+ for (const [subName, subNode] of Object.entries(node.children)) {
+ subRows.push(this.createSuballocationRowRecursively_(subName, subNode));
+ }
+
+ if (node.row !== undefined) {
+ // Very unlikely case: Both an ancestor (e.g. 'skia') and one of its
+ // descendants (e.g. 'skia/sk_glyph_cache') both suballocate from the
+ // same MemoryAllocatorDump (e.g. 'malloc/allocated_objects'). In
+ // this case, the suballocation from the ancestor must be mapped to
+ // 'malloc/allocated_objects/suballocations/skia/<unspecified>' so
+ // that 'malloc/allocated_objects/suballocations/skia' could
+ // aggregate the numerics of the two suballocations properly.
+ const row = node.row;
+ row.title = '<unspecified>';
+ row.suballocation = true;
+ subRows.unshift(row);
+ }
+
+ // An internal row of the suballocation tree is assumed to be defined
+ // at a given timestamp if at least one of its sub-rows is defined at
+ // the timestamp.
+ const contexts = new Array(subRows[0].contexts.length);
+ for (let i = 0; i < subRows.length; i++) {
+ subRows[i].contexts.forEach(function(subContext, index) {
+ if (subContext !== undefined) {
+ contexts[index] = SUBALLOCATION_CONTEXT;
+ }
+ });
+ }
+
+ return {
+ title: name,
+ suballocation: true,
+ contexts,
+ subRows
+ };
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new AllocatorDumpNameColumn();
+ titleColumn.width = '200px';
+
+ const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'numericCells',
+ aggregationMode: this.aggregationMode_,
+ rules: NUMERIC_COLUMN_RULES
+ });
+ const diagnosticColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'diagnosticCells',
+ aggregationMode: this.aggregationMode_,
+ rules: DIAGNOSTIC_COLUMN_RULES
+ });
+ const fieldColumns = numericColumns.concat(diagnosticColumns);
+ tr.ui.analysis.MemoryColumn.spaceEqually(fieldColumns);
+
+ const columns = [titleColumn].concat(fieldColumns);
+ return columns;
+ }
+ });
+
+ return {
+ // All exports are for testing only.
+ SUBALLOCATION_CONTEXT,
+ AllocatorDumpNameColumn,
+ EffectiveSizeColumn,
+ SizeColumn,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html
new file mode 100644
index 00000000000..6fda765b34b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html
@@ -0,0 +1,1261 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const Scalar = tr.b.Scalar;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const HeapDump = tr.model.HeapDump;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const checkNumericFields = tr.ui.analysis.checkNumericFields;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkStringFields = tr.ui.analysis.checkStringFields;
+ const checkColumnInfosAndColor = tr.ui.analysis.checkColumnInfosAndColor;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const AllocatorDumpNameColumn = tr.ui.analysis.AllocatorDumpNameColumn;
+ const EffectiveSizeColumn = tr.ui.analysis.EffectiveSizeColumn;
+ const SizeColumn = tr.ui.analysis.SizeColumn;
+ const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+
+ const SUBALLOCATION_CONTEXT = tr.ui.analysis.SUBALLOCATION_CONTEXT;
+ 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 addRootDumps(containerMemoryDump, rootNames, addedCallback) {
+ // Test sanity check.
+ assert.isUndefined(containerMemoryDump.memoryAllocatorDumps);
+
+ const rootDumps = rootNames.map(function(rootName) {
+ return new MemoryAllocatorDump(containerMemoryDump, rootName);
+ });
+ addedCallback.apply(null, rootDumps);
+ containerMemoryDump.memoryAllocatorDumps = rootDumps;
+ }
+
+ function newSuballocationDump(ownerDump, parentDump, name, size) {
+ const suballocationDump = addChildDump(parentDump, name,
+ {numerics: {size}});
+ if (ownerDump !== undefined) {
+ addOwnershipLink(ownerDump, suballocationDump);
+ }
+ return suballocationDump;
+ }
+
+ function createProcessMemoryDumps() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(model, {ts: -10});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11});
+ pmd1.memoryAllocatorDumps = (function() {
+ const v8Dump = newAllocatorDump(pmd1, 'v8', {numerics: {
+ size: 1073741824 /* 1 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 805306368 /* 768 MiB */}});
+ addChildDump(v8HeapsDump, 'heap42',
+ {numerics: {size: 804782080 /* 767.5 MiB */}});
+
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects');
+ v8ObjectsDump.addDiagnostic('url', 'http://example.com');
+ addChildDump(v8ObjectsDump, 'foo',
+ {numerics: {size: 1022976 /* 999 KiB */}});
+ addChildDump(v8ObjectsDump, 'bar',
+ {numerics: {size: 1024000 /* 1000 KiB */}});
+
+ const oilpanDump = newAllocatorDump(pmd1, 'oilpan',
+ {numerics: {size: 125829120 /* 120 MiB */}});
+ newSuballocationDump(
+ oilpanDump, v8Dump, '__99BEAD', 150994944 /* 144 MiB */);
+
+ const oilpanSubDump = addChildDump(oilpanDump, 'animals');
+
+ const oilpanSubDump1 = addChildDump(oilpanSubDump, 'cow',
+ {numerics: {size: 33554432 /* 32 MiB */}});
+ newSuballocationDump(
+ oilpanSubDump1, v8Dump, '__42BEEF', 67108864 /* 64 MiB */);
+
+ const oilpanSubDump2 = addChildDump(oilpanSubDump, 'chicken',
+ {numerics: {size: 16777216 /* 16 MiB */}});
+ newSuballocationDump(
+ oilpanSubDump2, v8Dump, '__68DEAD', 33554432 /* 32 MiB */);
+
+ const skiaDump = newAllocatorDump(pmd1, 'skia',
+ {numerics: {size: 8388608 /* 8 MiB */}});
+ const suballocationDump = newSuballocationDump(
+ skiaDump, v8Dump, '__15FADE', 16777216 /* 16 MiB */);
+
+ const ccDump = newAllocatorDump(pmd1, 'cc',
+ {numerics: {size: 4194304 /* 4 MiB */}});
+ newSuballocationDump(
+ ccDump, v8Dump, '__12FEED', 5242880 /* 5 MiB */).addDiagnostic(
+ 'url', 'localhost:1234');
+
+ return [v8Dump, oilpanDump, skiaDump, ccDump];
+ })();
+
+ // Second timestamp.
+ const gmd2 = addGlobalMemoryDump(model, {ts: 10});
+ const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 11});
+ pmd2.memoryAllocatorDumps = (function() {
+ const v8Dump = newAllocatorDump(pmd2, 'v8', {numerics: {
+ size: 1073741824 /* 1 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects');
+ v8ObjectsDump.addDiagnostic('url', 'http://sample.net');
+ addChildDump(v8ObjectsDump, 'foo',
+ {numerics: {size: 1020928 /* 997 KiB */}});
+ addChildDump(v8ObjectsDump, 'bar',
+ {numerics: {size: 1026048 /* 1002 KiB */}});
+
+ newSuballocationDump(
+ undefined, v8Dump, '__99BEAD', 268435456 /* 256 MiB */);
+
+ const ccDump = newAllocatorDump(pmd2, 'cc',
+ {numerics: {size: 7340032 /* 7 MiB */}});
+ newSuballocationDump(
+ ccDump, v8Dump, '__13DEED', 11534336 /* 11 MiB */).addDiagnostic(
+ 'url', 'localhost:5678');
+
+ return [v8Dump, ccDump];
+ })();
+ });
+
+ return model.processes[1].memoryDumps;
+ }
+
+ function createSizeFields(values) {
+ return values.map(function(value) {
+ if (value === undefined) return undefined;
+ return new Scalar(sizeInBytes_smallerIsBetter, value);
+ });
+ }
+
+ const EXPECTED_COLUMNS = [
+ { title: 'Component', type: AllocatorDumpNameColumn, noAggregation: true },
+ { title: 'effective_size', type: EffectiveSizeColumn },
+ { title: 'size', type: SizeColumn },
+ { title: 'inner_size', type: NumericMemoryColumn },
+ { title: 'objects_count', type: NumericMemoryColumn },
+ { title: 'url', type: StringMemoryColumn }
+ ];
+
+ function checkRow(columns, row, expectations) {
+ const formattedTitle = columns[0].formatTitle(row);
+ const expectedTitle = expectations.title;
+ if (typeof expectedTitle === 'function') {
+ expectedTitle(formattedTitle);
+ } else {
+ assert.strictEqual(formattedTitle, expectedTitle);
+ }
+
+ checkSizeNumericFields(row, columns[1], expectations.size);
+ checkSizeNumericFields(row, columns[2], expectations.effective_size);
+ checkSizeNumericFields(row, columns[3], expectations.inner_size);
+ checkNumericFields(row, columns[4], expectations.objects_count,
+ unitlessNumber_smallerIsBetter);
+ checkStringFields(row, columns[5], expectations.url);
+
+ const expectedSubRowCount = expectations.sub_row_count;
+ if (expectedSubRowCount === undefined) {
+ assert.isUndefined(row.subRows);
+ } else {
+ assert.lengthOf(row.subRows, expectedSubRowCount);
+ }
+
+ const expectedContexts = expectations.contexts;
+ if (expectedContexts === undefined) {
+ assert.isUndefined(row.contexts);
+ } else {
+ assert.deepEqual(Array.from(row.contexts), expectedContexts);
+ }
+ }
+
+ function buildProcessMemoryDumps(count, preFinalizeDumpsCallback) {
+ const pmds = new Array(count);
+ tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < count; i++) {
+ const timestamp = 10 + i;
+ const gmd = addGlobalMemoryDump(model, {ts: timestamp});
+ pmds[i] = addProcessMemoryDump(gmd, process, {ts: timestamp});
+ }
+ preFinalizeDumpsCallback(pmds);
+ });
+ return pmds;
+ }
+
+ function getAllocatorDumps(pmds, fullName) {
+ return pmds.map(function(pmd) {
+ if (pmd === undefined) return undefined;
+ return pmd.getMemoryAllocatorDumpByFullName(fullName);
+ });
+ }
+
+ function checkAllocatorPaneColumnInfosAndColor(
+ column, dumps, numericName, expectedInfos) {
+ const numerics = dumps.map(function(dump) {
+ if (dump === undefined) return undefined;
+ return dump.numerics[numericName];
+ });
+ checkColumnInfosAndColor(
+ column, numerics, dumps, expectedInfos, undefined /* no color */);
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-allocator-details-pane', 'memoryAllocatorDumps',
+ function(viewEl) {
+ // Check that the info text is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.info_text));
+ assert.isFalse(isElementDisplayed(viewEl.$.table));
+ });
+ });
+
+ test('instantiate_single', function() {
+ const processMemoryDumps = createProcessMemoryDumps().slice(0, 1);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, undefined /* no aggregation */);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const rootRow = rows[0];
+ checkRow(columns, rootRow, {
+ title: 'v8',
+ size: [942619648],
+ effective_size: [1081031680],
+ inner_size: [2097152],
+ objects_count: [204],
+ sub_row_count: 3,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8'),
+ });
+
+ const heapsSubRow = rootRow.subRows[0];
+ checkRow(columns, heapsSubRow, {
+ title: 'heaps',
+ size: [805306368],
+ effective_size: [805306368],
+ sub_row_count: 2,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps'),
+ });
+
+ const heapsUnspecifiedSubRow = heapsSubRow.subRows[0];
+ checkRow(columns, heapsUnspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [524288],
+ effective_size: [524288],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps/<unspecified>'),
+ });
+
+ const suballocationsSubRow = rootRow.subRows[2];
+ checkRow(columns, suballocationsSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, 'suballocations');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [135266304],
+ effective_size: [273678336],
+ sub_row_count: 3,
+ contexts: [SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanSuballocationSubRow = suballocationsSubRow.subRows[0];
+ checkRow(columns, oilpanSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'oilpan');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [125829120],
+ effective_size: [251658240],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanUnspecifiedSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanUnspecifiedSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, '<unspecified>');
+ assert.strictEqual(formattedTitle.title, 'v8/__99BEAD');
+ },
+ size: [75497472],
+ effective_size: [150994944],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__99BEAD'),
+ });
+
+ const oilpanAnimalsSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[1];
+ checkRow(columns, oilpanAnimalsSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'animals');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [50331648],
+ effective_size: [100663296],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanCowSuballocationSubRow =
+ oilpanAnimalsSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanCowSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cow');
+ assert.strictEqual(formattedTitle.title, 'v8/__42BEEF');
+ },
+ size: [33554432],
+ effective_size: [67108864],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__42BEEF'),
+ });
+
+ const skiaSuballocationSubRow = suballocationsSubRow.subRows[1];
+ checkRow(columns, skiaSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'skia');
+ assert.strictEqual(formattedTitle.title, 'v8/__15FADE');
+ },
+ size: [8388608],
+ effective_size: [16777216],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__15FADE'),
+ });
+
+ const ccSuballocationSubRow = suballocationsSubRow.subRows[2];
+ checkRow(columns, ccSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cc');
+ assert.strictEqual(formattedTitle.title, 'v8/__12FEED');
+ },
+ size: [1048576],
+ effective_size: [5242880],
+ url: ['localhost:1234'],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__12FEED')
+ });
+ });
+
+ test('instantiate_multipleDiff', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const rootRow = rows[0];
+ checkRow(columns, rootRow, {
+ title: 'v8',
+ size: [942619648, 1066401792],
+ effective_size: [1081031680, 1073741824],
+ inner_size: [2097152, 2097152],
+ objects_count: [204, 204],
+ sub_row_count: 4,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8'),
+ });
+
+ const heapsSubRow = rootRow.subRows[0];
+ checkRow(columns, heapsSubRow, {
+ title: 'heaps',
+ size: [805306368, undefined],
+ effective_size: [805306368, undefined],
+ sub_row_count: 2,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps'),
+ });
+
+ const heapsUnspecifiedSubRow = heapsSubRow.subRows[0];
+ checkRow(columns, heapsUnspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [524288, undefined],
+ effective_size: [524288, undefined],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps/<unspecified>'),
+ });
+
+ const unspecifiedSubRow = rootRow.subRows[2];
+ checkRow(columns, unspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [undefined, 791725056],
+ effective_size: [undefined, 791725056],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/<unspecified>'),
+ });
+
+ const suballocationsSubRow = rootRow.subRows[3];
+ checkRow(columns, suballocationsSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, 'suballocations');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [135266304, 272629760],
+ effective_size: [273678336, 279969792],
+ sub_row_count: 3,
+ contexts: [SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanSuballocationSubRow = suballocationsSubRow.subRows[0];
+ checkRow(columns, oilpanSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'oilpan');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [125829120, 268435456],
+ effective_size: [251658240, 268435456],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanUnspecifiedSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanUnspecifiedSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, '<unspecified>');
+ assert.strictEqual(formattedTitle.title, 'v8/__99BEAD');
+ },
+ size: [75497472, 268435456],
+ effective_size: [150994944, 268435456],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__99BEAD'),
+ });
+
+ const oilpanAnimalsSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[1];
+ checkRow(columns, oilpanAnimalsSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'animals');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [50331648, undefined],
+ effective_size: [100663296, undefined],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT, undefined],
+ });
+
+ const oilpanCowSuballocationSubRow =
+ oilpanAnimalsSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanCowSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cow');
+ assert.strictEqual(formattedTitle.title, 'v8/__42BEEF');
+ },
+ size: [33554432, undefined],
+ effective_size: [67108864, undefined],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__42BEEF'),
+ });
+
+ const skiaSuballocationSubRow = suballocationsSubRow.subRows[1];
+ checkRow(columns, skiaSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'skia');
+ assert.strictEqual(formattedTitle.title, 'v8/__15FADE');
+ },
+ size: [8388608, undefined],
+ effective_size: [16777216, undefined],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__15FADE'),
+ });
+
+ const ccSuballocationSubRow = suballocationsSubRow.subRows[2];
+ checkRow(columns, ccSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cc');
+ assert.strictEqual(formattedTitle.title, 'v8/__12FEED, v8/__13DEED');
+ },
+ size: [1048576, 4194304],
+ effective_size: [5242880, 11534336],
+ url: ['localhost:1234', 'localhost:5678'],
+ contexts: [
+ processMemoryDumps[0].getMemoryAllocatorDumpByFullName('v8/__12FEED'),
+ processMemoryDumps[1].getMemoryAllocatorDumpByFullName('v8/__13DEED')
+ ]
+ });
+ });
+
+ test('instantiate_multipleMax', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ // Just check that the aggregation mode was propagated to the columns.
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.MAX);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+ });
+
+ test('instantiate_multipleWithUndefined', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+ processMemoryDumps.splice(1, 0, undefined);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check only a few rows of the table.
+ const rootRow = rows[0];
+ checkRow(columns, rootRow, {
+ title: 'v8',
+ size: [942619648, undefined, 1066401792],
+ effective_size: [1081031680, undefined, 1073741824],
+ inner_size: [2097152, undefined, 2097152],
+ objects_count: [204, undefined, 204],
+ sub_row_count: 4,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8'),
+ });
+
+ const unspecifiedSubRow = rootRow.subRows[2];
+ checkRow(columns, unspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [undefined, undefined, 791725056],
+ effective_size: [undefined, undefined, 791725056],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/<unspecified>'),
+ });
+
+ const suballocationsSubRow = rootRow.subRows[3];
+ checkRow(columns, suballocationsSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, 'suballocations');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [135266304, undefined, 272629760],
+ effective_size: [273678336, undefined, 279969792],
+ sub_row_count: 3,
+ contexts: [SUBALLOCATION_CONTEXT, undefined, SUBALLOCATION_CONTEXT],
+ });
+ });
+
+ test('heapDumpsPassThrough', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+ const heapDumps = processMemoryDumps.map(function(dump) {
+ if (dump === undefined) return undefined;
+ return new HeapDump(dump, 'v8');
+ });
+
+ // Start by creating a component details pane without any heap dumps.
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+
+ assert.lengthOf(viewEl.requestedChildPanes, 1);
+ assert.isUndefined(viewEl.requestedChildPanes[0]);
+
+ // Set the heap dumps. This should trigger creating a heap details pane.
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+
+ assert.lengthOf(viewEl.requestedChildPanes, 2);
+ assert.strictEqual(viewEl.requestedChildPanes[1].tagName,
+ 'TR-UI-A-MEMORY-DUMP-HEAP-DETAILS-PANE');
+ assert.strictEqual(viewEl.requestedChildPanes[1].heapDumps, heapDumps);
+ assert.strictEqual(viewEl.requestedChildPanes[1].aggregationMode,
+ AggregationMode.DIFF);
+
+ // Unset the heap dumps. This should trigger removing the heap details pane.
+ viewEl.heapDumps = undefined;
+ viewEl.rebuild();
+
+ assert.lengthOf(viewEl.requestedChildPanes, 3);
+ assert.isUndefined(viewEl.requestedChildPanes[2]);
+ });
+
+ test('allocatorDumpNameColumn', function() {
+ const c = new AllocatorDumpNameColumn();
+
+ // Regular row.
+ assert.strictEqual(c.formatTitle({title: 'Regular row'}), 'Regular row');
+
+ // Sub-allocation row.
+ const row = c.formatTitle({
+ title: 'Suballocation row',
+ suballocation: true,
+ });
+ assert.strictEqual(Polymer.dom(row).textContent, 'Suballocation row');
+ assert.strictEqual(row.style.fontStyle, 'italic');
+ });
+
+ test('effectiveSizeColumn_noContext', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128, 256, undefined, 64]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('effectiveSizeColumn_suballocationContext', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.MAX);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ [SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([undefined, 256, undefined, 64]),
+ [undefined, SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT,
+ SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('effectiveSizeColumn_dumpContext_noOwnership', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ const pmds = buildProcessMemoryDumps(4 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8'], function(v8Dump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 64}});
+ });
+ addRootDumps(pmds[2], ['v8'], function(v8Dump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 128}});
+ });
+ addRootDumps(pmds[3], ['v8'], function(v8Dump) {});
+ });
+ const v8HeapsDumps = getAllocatorDumps(pmds, 'v8/heaps');
+
+ // Single selection.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDumps[0]],
+ 'effective_size',
+ [] /* no infos */);
+
+ // Multi-selection, all dumps defined.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDumps[0], v8HeapsDumps[2]],
+ 'effective_size',
+ [] /* no infos */);
+
+ // Multi-selection, some dumps missing.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapsDumps,
+ 'effective_size',
+ [] /* no infos */);
+ });
+
+ test('effectiveSizeColumn_dumpContext_singleOwnership', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.MAX);
+ const pmds = buildProcessMemoryDumps(5 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump);
+ });
+ addRootDumps(pmds[1], ['v8'], function(v8Dump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 32}});
+ // Missing oilpan/objects dump.
+ });
+ addRootDumps(pmds[2], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ // Missing v8/heaps dump.
+ });
+ addRootDumps(pmds[3], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 32}});
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ // Missing ownership link.
+ });
+ addRootDumps(pmds[4], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 2);
+ });
+ });
+ const v8HeapsDump = getAllocatorDumps(pmds, 'v8/heaps');
+ const oilpanObjectsDump = getAllocatorDumps(pmds, 'oilpan/objects');
+
+ // Single selection.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[0]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 (importance: 0) ' +
+ 'with no other dumps',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [oilpanObjectsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by \'v8/heaps\' in Process 1 (importance: 2)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, all dumps defined.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[0], v8HeapsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 (importance: ' +
+ '0\u20132) with no other dumps',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [oilpanObjectsDump[0], oilpanObjectsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by \'v8/heaps\' in Process 1 (importance: ' +
+ '0\u20132)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, some dumps missing.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapsDump,
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 at some ' +
+ 'selected timestamps (importance: 0\u20132) with no other ' +
+ 'dumps',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ oilpanObjectsDump,
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by \'v8/heaps\' in Process 1 at some selected ' +
+ 'timestamps (importance: 0\u20132)',
+ color: 'green'
+ }
+ ]);
+ });
+
+ test('effectiveSizeColumn_dumpContext_multipleOwnerships', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ const pmds = buildProcessMemoryDumps(6 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const v8QueuesDump = addChildDump(v8Dump, 'queues',
+ {numerics: {size: 8}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump);
+ addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1);
+ });
+ addRootDumps(pmds[1], ['v8'], function(v8Dump) {});
+ addRootDumps(pmds[2], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const v8QueuesDump = addChildDump(v8Dump, 'queues',
+ {numerics: {size: 8}});
+ const v8PilesDump = addChildDump(v8Dump, 'piles',
+ {numerics: {size: 48}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 2);
+ addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1);
+ addOwnershipLink(v8PilesDump, oilpanObjectsDump);
+ });
+ addRootDumps(pmds[3], ['v8', 'blink'], function(v8Dump, blinkDump) {
+ const blinkHandlesDump = addChildDump(blinkDump, 'handles',
+ {numerics: {size: 32}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 64}});
+ const blinkObjectsDump = addChildDump(blinkDump, 'objects',
+ {numerics: {size: 32}});
+ addOwnershipLink(blinkHandlesDump, v8HeapsDump, -273);
+ addOwnershipLink(v8HeapsDump, blinkObjectsDump, 3);
+ });
+ addRootDumps(pmds[4], ['v8', 'gpu'], function(v8Dump, gpuDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 64}});
+ const gpuTile1Dump = addChildDump(gpuDump, 'tile1',
+ {numerics: {size: 100}});
+ const gpuTile2Dump = addChildDump(gpuDump, 'tile2',
+ {numerics: {size: 99}});
+ addOwnershipLink(v8HeapsDump, gpuTile1Dump, 3);
+ addOwnershipLink(gpuTile2Dump, gpuTile1Dump, -1);
+ });
+ addRootDumps(pmds[5], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const v8QueuesDump = addChildDump(v8Dump, 'queues',
+ {numerics: {size: 8}});
+ const v8PilesDump = addChildDump(v8Dump, 'piles',
+ {numerics: {size: 48}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 1);
+ addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1);
+ addOwnershipLink(v8PilesDump, oilpanObjectsDump, 7);
+ });
+ });
+ const v8HeapsDump = getAllocatorDumps(pmds, 'v8/heaps');
+ const oilpanObjectsDump = getAllocatorDumps(pmds, 'oilpan/objects');
+ const gpuTile1Dump = getAllocatorDumps(pmds, 'gpu/tile1');
+
+ // Single selection.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'gpu/tile1\' in Process 1 (importance: 3) with ' +
+ '\'gpu/tile2\' in Process 1 (importance: -1)',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [gpuTile1Dump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by:\n' +
+ ' - \'v8/heaps\' in Process 1 (importance: 3)\n' +
+ ' - \'gpu/tile2\' in Process 1 (importance: -1)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, all dumps defined.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[2], v8HeapsDump[5]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 (importance: ' +
+ '1\u20132) with:\n' +
+ ' - \'v8/queues\' in Process 1 (importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 (importance: 0\u20137)',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [oilpanObjectsDump[2], oilpanObjectsDump[5]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by:\n' +
+ ' - \'v8/heaps\' in Process 1 (importance: 1\u20132)\n' +
+ ' - \'v8/queues\' in Process 1 (importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 (importance: 0\u20137)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, some dumps missing.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapsDump,
+ 'effective_size',
+ [ // v8/objects is both owned (first info) and an owner (second info).
+ {
+ icon: '\u21FD',
+ message: 'shared by \'blink/handles\' in Process 1 at some ' +
+ 'selected timestamps (importance: -273)',
+ color: 'green'
+ },
+ {
+ icon: '\u21FE',
+ message: 'shares:\n' +
+ ' - \'oilpan/objects\' in Process 1 at some selected ' +
+ 'timestamps (importance: 0\u20132) with:\n' +
+ ' - \'v8/queues\' in Process 1 (importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 at some selected ' +
+ 'timestamps (importance: 0\u20137)\n' +
+ ' - \'blink/objects\' in Process 1 at some selected ' +
+ 'timestamps (importance: 3) with no other dumps\n' +
+ ' - \'gpu/tile1\' in Process 1 at some selected timestamps ' +
+ '(importance: 3) with \'gpu/tile2\' in Process 1 ' +
+ '(importance: -1)',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ oilpanObjectsDump,
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by:\n' +
+ ' - \'v8/heaps\' in Process 1 at some selected timestamps ' +
+ '(importance: 0\u20132)\n' +
+ ' - \'v8/queues\' in Process 1 at some selected timestamps ' +
+ '(importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 at some selected timestamps ' +
+ '(importance: 0\u20137)',
+ color: 'green'
+ }
+ ]);
+ });
+
+ test('sizeColumn_noContext', function() {
+ const c = new SizeColumn('Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128, 256, undefined, 64]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('sizeColumn_suballocationContext', function() {
+ const c = new SizeColumn('Size', 'bytes', (x => x),
+ AggregationMode.MAX);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ [SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([undefined, 256, undefined, 64]),
+ [undefined, SUBALLOCATION_CONTEXT, undefined, SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('sizeColumn_dumpContext', function() {
+ const c = new SizeColumn('Size', 'bytes', (x => x), AggregationMode.DIFF);
+ const pmds = buildProcessMemoryDumps(7 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8'], function(v8Dump) {
+ // Single direct overlap (v8/objects -> v8/heaps).
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 1536}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addOwnershipLink(v8ObjectsDump, v8HeapsDump);
+ });
+ // pmd[1] intentionally skipped.
+ addRootDumps(pmds[2], ['v8'], function(v8Dump, oilpanDump) {
+ // Single direct overlap with inconsistent owned dump size.
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 3072}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addOwnershipLink(v8ObjectsDump, v8HeapsDump);
+ });
+ addRootDumps(pmds[3], ['v8'], function(v8Dump) {
+ // Single indirect overlap (v8/objects/X -> v8/heaps/42).
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 1536}});
+ const v8ObjectsXDump = addChildDump(v8ObjectsDump, 'X',
+ {numerics: {size: 512}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ const v8Heaps42Dump = addChildDump(v8HeapsDump, '42',
+ {numerics: {size: 1024}});
+ addOwnershipLink(v8ObjectsXDump, v8Heaps42Dump);
+ });
+ addRootDumps(pmds[4], ['v8'], function(v8Dump) {
+ // Multiple overlaps.
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 1024}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+
+ const v8ObjectsXDump = addChildDump(v8ObjectsDump, 'X',
+ {numerics: {size: 512}});
+ const v8Heaps42Dump = addChildDump(v8HeapsDump, '42',
+ {numerics: {size: 1280}});
+ addOwnershipLink(v8ObjectsXDump, v8Heaps42Dump);
+
+ const v8ObjectsYDump = addChildDump(v8ObjectsDump, 'Y',
+ {numerics: {size: 128}});
+ const v8Heaps90Dump = addChildDump(v8HeapsDump, '90',
+ {numerics: {size: 256}});
+ addOwnershipLink(v8ObjectsYDump, v8Heaps90Dump);
+
+ const v8BlocksDump = addChildDump(v8Dump, 'blocks',
+ {numerics: {size: 768}});
+ addOwnershipLink(v8BlocksDump, v8Heaps42Dump);
+ });
+ addRootDumps(pmds[5], ['v8'], function(v8Dump) {
+ // No overlaps, inconsistent parent size.
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addChildDump(v8HeapsDump, '42', {numerics: {size: 1536}});
+ addChildDump(v8HeapsDump, '90', {numerics: {size: 615}});
+ });
+ addRootDumps(pmds[6], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ // No overlaps, inconsistent parent and owned dump size.
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addChildDump(v8HeapsDump, '42', {numerics: {size: 1536}});
+ addChildDump(v8HeapsDump, '90', {numerics: {size: 615}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 3072}});
+ addOwnershipLink(oilpanObjectsDump, v8HeapsDump);
+ });
+ });
+ const v8HeapDumps = getAllocatorDumps(pmds, 'v8/heaps');
+
+ // Single selection, single overlap.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[0]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its sibling \'objects\' (1.5 KiB)',
+ color: 'blue'
+ }
+ ]);
+
+ // Single selection, multiple overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[4]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its siblings:\n' +
+ ' - \'objects\' (640.0 B)\n' +
+ ' - \'blocks\' (768.0 B)',
+ color: 'blue'
+ }
+ ]);
+
+ // Single selection, warnings with no overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[6]],
+ 'size',
+ [
+ {
+ icon: '\u26A0',
+ message: 'provided size (2.0 KiB) was less than the aggregated ' +
+ 'size of the children (2.1 KiB)',
+ color: 'red'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size (2.0 KiB) was less than the size of the ' +
+ 'largest owner (3.0 KiB)',
+ color: 'red'
+ }
+ ]);
+
+ // Single selection, single overlap with a warning.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[2]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its sibling \'objects\' (3.0 KiB)',
+ color: 'blue'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size (2.0 KiB) was less than the size of the ' +
+ 'largest owner (3.0 KiB)',
+ color: 'red'
+ }
+ ]);
+
+ // Multi-selection, single overlap.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[0], v8HeapDumps[3]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its sibling \'objects\'',
+ color: 'blue'
+ }
+ ]);
+
+ // Multi-selection, multiple overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[0], v8HeapDumps[4]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its siblings:\n' +
+ ' - \'objects\'\n' +
+ ' - \'blocks\' at some selected timestamps',
+ color: 'blue'
+ }
+ ]);
+
+ // Multi-selection, warnings with no overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[5], v8HeapDumps[6]],
+ 'size',
+ [
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the aggregated ' +
+ 'size of the children',
+ color: 'red'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the size of the largest ' +
+ 'owner at some selected timestamps',
+ color: 'red'
+ }
+ ]);
+
+ // Multi-selection, multiple overlaps with warnings.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapDumps,
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its siblings:\n' +
+ ' - \'objects\' at some selected timestamps\n' +
+ ' - \'blocks\' at some selected timestamps',
+ color: 'blue'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the size of the largest ' +
+ 'owner at some selected timestamps',
+ color: 'red'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the aggregated size of ' +
+ 'the children at some selected timestamps',
+ color: 'red'
+ }
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html
new file mode 100644
index 00000000000..1141116ec86
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_overview_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<dom-module id='tr-ui-a-memory-dump-header-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ background-color: #d0d0d0;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ }
+
+ #label {
+ flex: 1 1 auto;
+ padding: 6px;
+ font-size: 15px;
+ }
+
+ #aggregation_mode_container {
+ display: none;
+ flex: 0 0 auto;
+ padding: 5px;
+ font-size: 15px;
+ }
+ </style>
+ </tr-ui-b-view-specific-brushing-state>
+ <div id="label"></div>
+ <div id="aggregation_mode_container">
+ <span>Metric aggregation:</span>
+ <!-- Aggregation mode selector (added in Polymer.ready()) -->
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ Polymer({
+ is: 'tr-ui-a-memory-dump-header-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.containerMemoryDumps_ = undefined;
+ },
+
+ ready() {
+ Polymer.dom(this.$.aggregation_mode_container).appendChild(
+ tr.ui.b.createSelector(this, 'aggregationMode',
+ 'memoryDumpHeaderPane.aggregationMode',
+ tr.ui.analysis.MemoryColumn.AggregationMode.DIFF, [
+ {
+ label: 'Diff',
+ value: tr.ui.analysis.MemoryColumn.AggregationMode.DIFF
+ },
+ {
+ label: 'Max',
+ value: tr.ui.analysis.MemoryColumn.AggregationMode.MAX
+ }
+ ]));
+ },
+
+ /**
+ * Sets the container memory dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronologically sorted list of
+ * ContainerMemoryDump objects. All of the dumps must be associated with
+ * the same container (i.e. containerMemoryDumps must be either a list of
+ * ProcessMemoryDump(s) belonging to the same process, or a list of
+ * GlobalMemoryDump(s)). Example:
+ *
+ * [
+ * tr.model.ProcessMemoryDump {}, // PMD at timestamp 1.
+ * tr.model.ProcessMemoryDump {}, // PMD at timestamp 2.
+ * tr.model.ProcessMemoryDump {} // PMD at timestamp 3.
+ * ]
+ */
+ set containerMemoryDumps(containerMemoryDumps) {
+ this.containerMemoryDumps_ = containerMemoryDumps;
+ this.scheduleRebuild_();
+ },
+
+ get containerMemoryDumps() {
+ return this.containerMemoryDumps_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ onRebuild_() {
+ this.updateLabel_();
+ this.updateAggregationModeSelector_();
+ this.changeChildPane_();
+ },
+
+ updateLabel_() {
+ Polymer.dom(this.$.label).textContent = '';
+
+ if (this.containerMemoryDumps_ === undefined ||
+ this.containerMemoryDumps_.length <= 0) {
+ Polymer.dom(this.$.label).textContent = 'No memory dumps selected';
+ return;
+ }
+
+ const containerDumpCount = this.containerMemoryDumps_.length;
+ const isMultiSelection = containerDumpCount > 1;
+
+ Polymer.dom(this.$.label).appendChild(document.createTextNode(
+ 'Selected ' + containerDumpCount + ' memory dump' +
+ (isMultiSelection ? 's' : '') +
+ ' in ' + this.containerMemoryDumps_[0].containerName + ' at '));
+ // TODO(petrcermak): Use <tr-v-ui-scalar-span> once it can be displayed
+ // inline. See https://github.com/catapult-project/catapult/issues/1371.
+ Polymer.dom(this.$.label).appendChild(document.createTextNode(
+ tr.b.Unit.byName.timeStampInMs.format(
+ this.containerMemoryDumps_[0].start)));
+ if (isMultiSelection) {
+ const ELLIPSIS = String.fromCharCode(8230);
+ Polymer.dom(this.$.label).appendChild(
+ document.createTextNode(ELLIPSIS));
+ Polymer.dom(this.$.label).appendChild(document.createTextNode(
+ tr.b.Unit.byName.timeStampInMs.format(
+ this.containerMemoryDumps_[containerDumpCount - 1].start)));
+ }
+ },
+
+ updateAggregationModeSelector_() {
+ let displayStyle;
+ if (this.containerMemoryDumps_ === undefined ||
+ this.containerMemoryDumps_.length <= 1) {
+ displayStyle = 'none';
+ } else {
+ displayStyle = 'initial';
+ }
+ this.$.aggregation_mode_container.style.display = displayStyle;
+ },
+
+ changeChildPane_() {
+ this.childPaneBuilder = function() {
+ if (this.containerMemoryDumps_ === undefined ||
+ this.containerMemoryDumps_.length <= 0) {
+ return undefined;
+ }
+
+ const overviewPane = document.createElement(
+ 'tr-ui-a-memory-dump-overview-pane');
+ overviewPane.processMemoryDumps = this.containerMemoryDumps_.map(
+ function(containerDump) {
+ return containerDump.processMemoryDumps;
+ });
+ overviewPane.aggregationMode = this.aggregationMode;
+ return overviewPane;
+ }.bind(this);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html
new file mode 100644
index 00000000000..3d5d20a7c47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/memory_dump_header_pane.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+
+ function createAndCheckMemoryDumpHeaderPane(test, containerMemoryDumps,
+ expectedLabelText, expectedChildPaneRequested, expectedSelectorVisible) {
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-header-pane');
+ viewEl.containerMemoryDumps = containerMemoryDumps;
+ viewEl.rebuild();
+ test.addHTMLOutput(viewEl);
+ checkMemoryDumpHeaderPane(viewEl, containerMemoryDumps, expectedLabelText,
+ expectedChildPaneRequested, expectedSelectorVisible);
+ }
+
+ function checkMemoryDumpHeaderPane(viewEl, containerMemoryDumps,
+ expectedLabelText, expectedChildPaneRequested, expectedSelectorVisible) {
+ // The default aggregation mode is DIFF.
+ assert.strictEqual(viewEl.aggregationMode, AggregationMode.DIFF);
+
+ // Check the text in the label.
+ assert.strictEqual(
+ Polymer.dom(viewEl.$.label).textContent, expectedLabelText);
+
+ // Check the visibility of aggregation mode selector.
+ const aggregationModeContainerVisible =
+ isElementDisplayed(viewEl.$.aggregation_mode_container);
+ const childPanes = viewEl.requestedChildPanes;
+
+ // Check the requested child panes.
+ if (containerMemoryDumps === undefined ||
+ containerMemoryDumps.length === 0) {
+ assert.isTrue(!expectedSelectorVisible); // Test sanity check.
+ assert.isFalse(aggregationModeContainerVisible);
+ assert.lengthOf(childPanes, 1);
+ assert.isUndefined(childPanes[0]);
+ return;
+ }
+
+ const expectedProcessMemoryDumps = containerMemoryDumps.map(
+ function(containerMemoryDump) {
+ return containerMemoryDump.processMemoryDumps;
+ });
+ function checkLastChildPane(expectedChildPaneCount) {
+ assert.lengthOf(childPanes, expectedChildPaneCount);
+ const lastChildPane = childPanes[expectedChildPaneCount - 1];
+ assert.strictEqual(
+ lastChildPane.tagName, 'TR-UI-A-MEMORY-DUMP-OVERVIEW-PANE');
+ assert.deepEqual(lastChildPane.processMemoryDumps,
+ expectedProcessMemoryDumps);
+ assert.strictEqual(lastChildPane.aggregationMode, viewEl.aggregationMode);
+ }
+
+ checkLastChildPane(1);
+
+ // Check the behavior of aggregation mode selector (if visible).
+ if (!expectedSelectorVisible) {
+ assert.isFalse(aggregationModeContainerVisible);
+ return;
+ }
+
+ assert.isTrue(aggregationModeContainerVisible);
+ const selector = tr.ui.b.findDeepElementMatching(viewEl, 'select');
+
+ selector.selectedValue = AggregationMode.MAX;
+ viewEl.rebuild();
+ assert.strictEqual(viewEl.aggregationMode, AggregationMode.MAX);
+ checkLastChildPane(2);
+
+ selector.selectedValue = AggregationMode.DIFF;
+ viewEl.rebuild();
+ assert.strictEqual(viewEl.aggregationMode, AggregationMode.DIFF);
+ checkLastChildPane(3);
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-header-pane', 'containerMemoryDumps',
+ function(viewEl) {
+ checkMemoryDumpHeaderPane(viewEl, [], 'No memory dumps selected',
+ false /* no child pane requested */,
+ false /* aggregation mode selector hidden */);
+ });
+ });
+
+ test('instantiate_singleGlobalMemoryDump', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ [tr.ui.analysis.createSingleTestGlobalMemoryDump()],
+ 'Selected 1 memory dump in global space at 68.000 ms',
+ true /* child pane requested */,
+ false /* aggregation mode selector hidden */);
+ });
+
+ test('instantiate_multipleGlobalMemoryDumps', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps(),
+ 'Selected 3 memory dumps in global space at 42.000 ms\u2026100.000 ms',
+ true /* child pane requested */,
+ true /* aggregation selector visible */);
+ });
+
+ test('instantiate_singleProcessMemoryDump', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()],
+ 'Selected 1 memory dump in Process 2 at 69.000 ms',
+ true /* child pane requested */,
+ false /* aggregation mode selector hidden */);
+ });
+
+ test('instantiate_multipleProcessMemoryDumps', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ tr.ui.analysis.createMultipleTestProcessMemoryDumps(),
+ 'Selected 3 memory dumps in Process 2 at 42.000 ms\u2026102.000 ms',
+ true /* child pane requested */,
+ true /* aggregation selector visible */);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html
new file mode 100644
index 00000000000..9d17e39ce85
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html
@@ -0,0 +1,354 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-breakdown-view'>
+ <template>
+ <tr-ui-b-tab-view id="tabs"></tr-ui-b-tab-view>
+ </template>
+</dom-module>
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-breakdown-view-tab'>
+ <template>
+ <tr-v-ui-scalar-context-controller></tr-v-ui-scalar-context-controller>
+ <tr-ui-b-info-bar id="info" hidden></tr-ui-b-info-bar>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const RESONABLE_NUMBER_OF_ROWS = 200;
+
+ const TabUiState = {
+ NO_LONG_TAIL: 0,
+ HIDING_LONG_TAIL: 1,
+ SHOWING_LONG_TAIL: 2,
+ };
+
+ /** @constructor */
+ function EmptyFillerColumn() {}
+
+ EmptyFillerColumn.prototype = {
+ title: '',
+
+ value() {
+ return '';
+ },
+ };
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-breakdown-view',
+ behaviors: [tr.ui.analysis.RebuildableBehavior],
+
+ created() {
+ this.displayedNode_ = undefined;
+ this.dimensionToTab_ = new Map();
+ },
+
+ ready() {
+ this.scheduleRebuild_();
+ this.root.addEventListener('keydown', this.onKeyDown_.bind(this), true);
+ },
+
+ get displayedNode() {
+ return this.displayedNode_;
+ },
+
+ set displayedNode(node) {
+ this.displayedNode_ = node;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ for (const tab of this.$.tabs.tabs) {
+ tab.aggregationMode = aggregationMode;
+ }
+ },
+
+ onRebuild_() {
+ const previouslySelectedTab = this.$.tabs.selectedSubView;
+ let previouslySelectedTabFocused = false;
+ let previouslySelectedDimension = undefined;
+ if (previouslySelectedTab) {
+ previouslySelectedTabFocused = previouslySelectedTab.isFocused;
+ previouslySelectedDimension = previouslySelectedTab.dimension;
+ }
+
+ for (const tab of this.$.tabs.tabs) {
+ tab.nodes = undefined;
+ }
+ this.$.tabs.clearSubViews();
+
+ if (this.displayedNode_ === undefined) {
+ this.$.tabs.label = 'No heap node provided.';
+ return;
+ }
+
+ for (const [dimension, children] of this.displayedNode_.childNodes) {
+ if (!this.dimensionToTab_.has(dimension)) {
+ this.dimensionToTab_.set(dimension, document.createElement(
+ 'tr-ui-a-memory-dump-heap-details-breakdown-view-tab'));
+ }
+ const tab = this.dimensionToTab_.get(dimension);
+ tab.aggregationMode = this.aggregationMode_;
+ tab.dimension = dimension;
+ tab.nodes = children;
+ this.$.tabs.addSubView(tab);
+ tab.rebuild();
+ if (dimension === previouslySelectedDimension) {
+ this.$.tabs.selectedSubView = tab;
+ if (previouslySelectedTabFocused) {
+ tab.focus();
+ }
+ }
+ }
+
+ if (this.$.tabs.tabs.length > 0) {
+ this.$.tabs.label = 'Break selected node further by:';
+ } else {
+ this.$.tabs.label = 'Selected node cannot be broken down any further.';
+ }
+ },
+
+ onKeyDown_(keyEvent) {
+ if (!this.displayedNode_) return;
+
+ let keyHandled = false;
+ switch (keyEvent.keyCode) {
+ case 8: {
+ // Backspace.
+ if (!this.displayedNode_.parentNode) break;
+
+ // Enter the parent node upon pressing backspace.
+ const viewEvent = new tr.b.Event('enter-node');
+ viewEvent.node = this.displayedNode_.parentNode;
+ this.dispatchEvent(viewEvent);
+ keyHandled = true;
+ break;
+ }
+
+ case 37: // Left arrow.
+ case 39: // Right arrow.
+ {
+ const wasFocused = this.$.tabs.selectedSubView.isFocused;
+ keyHandled = keyEvent.keyCode === 37 ?
+ this.$.tabs.selectPreviousTabIfPossible() :
+ this.$.tabs.selectNextTabIfPossible();
+ if (wasFocused && keyHandled) {
+ this.$.tabs.selectedSubView.focus(); // Restore focus to new tab.
+ }
+ }
+ }
+
+ if (!keyHandled) return;
+ keyEvent.stopPropagation();
+ keyEvent.preventDefault();
+ }
+ });
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-breakdown-view-tab',
+ behaviors: [tr.ui.analysis.RebuildableBehavior],
+
+ created() {
+ this.dimension_ = undefined;
+ this.nodes_ = undefined;
+ this.aggregationMode_ = undefined;
+ this.displayLongTail_ = false;
+ },
+
+ ready() {
+ this.$.table.addEventListener('step-into', function(tableEvent) {
+ const viewEvent = new tr.b.Event('enter-node');
+ viewEvent.node = tableEvent.tableRow;
+ this.dispatchEvent(viewEvent);
+ }.bind(this));
+ },
+
+ get displayLongTail() {
+ return this.displayLongTail_;
+ },
+
+ set displayLongTail(newValue) {
+ if (this.displayLongTail === newValue) return;
+ this.displayLongTail_ = newValue;
+ this.scheduleRebuild_();
+ },
+
+ get dimension() {
+ return this.dimension_;
+ },
+
+ set dimension(dimension) {
+ this.dimension_ = dimension;
+ this.scheduleRebuild_();
+ },
+
+ get nodes() {
+ return this.nodes_;
+ },
+
+ set nodes(nodes) {
+ this.nodes_ = nodes;
+ this.scheduleRebuild_();
+ },
+
+ get nodes() {
+ return this.nodes_ || [];
+ },
+
+ get dimensionLabel_() {
+ if (this.dimension_ === undefined) return '(undefined)';
+ return this.dimension_.label;
+ },
+
+ get tabLabel() {
+ let nodeCount = 0;
+ if (this.nodes_) {
+ nodeCount = this.nodes_.length;
+ }
+ return this.dimensionLabel_ + ' (' + nodeCount + ')';
+ },
+
+ get tabIcon() {
+ if (this.dimension_ === undefined ||
+ this.dimension_ === tr.ui.analysis.HeapDetailsRowDimension.ROOT) {
+ return undefined;
+ }
+ return {
+ text: this.dimension_.symbol,
+ style: 'color: ' + tr.b.ColorScheme.getColorForReservedNameAsString(
+ this.dimension_.color) + ';'
+ };
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ focus() {
+ this.$.table.focus();
+ },
+
+ blur() {
+ this.$.table.blur();
+ },
+
+ get isFocused() {
+ return this.$.table.isFocused;
+ },
+
+ onRebuild_() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.emptyValue = 'Cannot break down by ' +
+ this.dimensionLabel_.toLowerCase() + ' any further.';
+ const [state, rows] = this.getRows_();
+ const total = this.nodes.length;
+ const displayed = rows.length;
+ const hidden = total - displayed;
+ this.updateInfoBar_(state, [total, displayed, hidden]);
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = this.createColumns_(rows);
+ if (this.$.table.sortColumnIndex === undefined) {
+ this.$.table.sortColumnIndex = 0;
+ this.$.table.sortDescending = false;
+ }
+ this.$.table.rebuild();
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new tr.ui.analysis.HeapDetailsTitleColumn(
+ this.dimensionLabel_);
+ titleColumn.width = '400px';
+
+ const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'cells',
+ aggregationMode: this.aggregationMode_,
+ rules: tr.ui.analysis.HEAP_DETAILS_COLUMN_RULES,
+ shouldSetContextGroup: true
+ });
+ if (numericColumns.length === 0) {
+ numericColumns.push(new EmptyFillerColumn());
+ }
+ tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns);
+
+ const columns = [titleColumn].concat(numericColumns);
+ return columns;
+ },
+
+ getRows_() {
+ let rows = this.nodes;
+ if (rows.length <= RESONABLE_NUMBER_OF_ROWS) {
+ return [TabUiState.NO_LONG_TAIL, rows];
+ } else if (this.displayLongTail) {
+ return [TabUiState.SHOWING_LONG_TAIL, rows];
+ }
+ const absSize = row => Math.max(row.cells.Size.fields[0].value);
+ rows.sort((a, b) => absSize(b) - absSize(a));
+ rows = rows.slice(0, RESONABLE_NUMBER_OF_ROWS);
+ return [TabUiState.HIDING_LONG_TAIL, rows];
+ },
+
+ updateInfoBar_(state, rowStats) {
+ if (state === TabUiState.SHOWING_LONG_TAIL) {
+ this.longTailVisibleInfoBar_(rowStats);
+ } else if (state === TabUiState.HIDING_LONG_TAIL) {
+ this.longTailHiddenInfoBar_(rowStats);
+ } else {
+ this.hideInfoBar_();
+ }
+ },
+
+ longTailVisibleInfoBar_(rowStats) {
+ const [total, visible, hidden] = rowStats;
+ const couldHide = total - RESONABLE_NUMBER_OF_ROWS;
+ this.$.info.message = 'Showing ' + total + ' rows. This may be slow.';
+ this.$.info.removeAllButtons();
+ const buttonText = 'Hide ' + couldHide + ' rows.';
+ this.$.info.addButton(buttonText, () => this.displayLongTail = false);
+ this.$.info.visible = true;
+ },
+
+ longTailHiddenInfoBar_(rowStats) {
+ const [total, visible, hidden] = rowStats;
+ this.$.info.message = 'Hiding the smallest ' + hidden + ' rows.';
+ this.$.info.removeAllButtons();
+ this.$.info.addButton('Show all.', () => this.displayLongTail = true);
+ this.$.info.visible = true;
+ },
+
+ hideInfoBar_() {
+ this.$.info.visible = false;
+ },
+
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html
new file mode 100644
index 00000000000..a43fdaa8189
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_path_view.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #header {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ }
+
+ #label {
+ flex: 1 1 auto;
+ padding: 8px;
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #view_mode_container {
+ display: none;
+ flex: 0 0 auto;
+ padding: 5px;
+ font-size: 15px;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #split_view {
+ display: none; /* Hide until memory allocator dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ flex-direction: row;
+ }
+
+ #path_view {
+ width: 50%;
+ }
+
+ #breakdown_view {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ #path_view, #breakdown_view {
+ overflow-x: auto; /* Show scrollbar if necessary. */
+ }
+ </style>
+ <div id="header">
+ <div id="label">Heap details</div>
+ <div id="view_mode_container">
+ <span>View mode:</span>
+ <!-- View mode selector (added in Polymer.ready()) -->
+ </div>
+ </div>
+ <div id="contents">
+ <tr-ui-b-info-bar id="info_bar" hidden>
+ </tr-ui-b-info-bar>
+
+ <div id="info_text">No heap dump selected</div>
+
+ <div id="split_view">
+ <tr-ui-a-memory-dump-heap-details-path-view id="path_view">
+ </tr-ui-a-memory-dump-heap-details-path-view>
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
+ <tr-ui-a-memory-dump-heap-details-breakdown-view id="breakdown_view">
+ </tr-ui-a-memory-dump-heap-details-breakdown-view>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
+ const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
+ const TotalState = tr.b.MultiDimensionalViewNode.TotalState;
+
+ /** @{constructor} */
+ function HeapDumpTreeNode(
+ stackFrameNodes, dimension, title, heavyView, parentNode) {
+ this.dimension = dimension;
+ this.title = title;
+ this.parentNode = parentNode;
+
+ this.heavyView_ = heavyView;
+ this.stackFrameNodes_ = stackFrameNodes;
+ this.lazyCells_ = undefined;
+ this.lazyChildNodes_ = undefined;
+ }
+
+ HeapDumpTreeNode.prototype = {
+ get minDisplayedTotalState_() {
+ if (this.heavyView_) {
+ // Show lower-bound and exact values in heavy views.
+ return TotalState.LOWER_BOUND;
+ }
+ // Show only exact values in tree view.
+ return TotalState.EXACT;
+ },
+
+ get childNodes() {
+ if (!this.lazyChildNodes_) {
+ this.lazyChildNodes_ = new Map();
+ this.addDimensionChildNodes_(
+ tr.ui.analysis.HeapDetailsRowDimension.STACK_FRAME, 0);
+ this.addDimensionChildNodes_(
+ tr.ui.analysis.HeapDetailsRowDimension.OBJECT_TYPE, 1);
+ this.releaseStackFrameNodesIfPossible_();
+ }
+ return this.lazyChildNodes_;
+ },
+
+ get cells() {
+ if (!this.lazyCells_) {
+ this.addCells_();
+ this.releaseStackFrameNodesIfPossible_();
+ }
+ return this.lazyCells_;
+ },
+
+ releaseStackFrameNodesIfPossible_() {
+ if (this.lazyCells_ && this.lazyChildNodes_) {
+ // Don't unnecessarily hold a reference to the stack frame nodes when
+ // we don't need them anymore.
+ this.stackFrameNodes_ = undefined;
+ }
+ },
+
+ addDimensionChildNodes_(dimension, dimensionIndex) {
+ // Child title -> Timestamp (list index) -> Child
+ // MultiDimensionalViewNode.
+ const dimensionChildTitleToStackFrameNodes = tr.b.invertArrayOfDicts(
+ this.stackFrameNodes_,
+ node => this.convertStackFrameNodeDimensionToChildDict_(
+ node, dimensionIndex));
+
+ // Child title (list index) -> Child HeapDumpTreeNode.
+ const dimensionChildNodes = [];
+ for (const [childTitle, childStackFrameNodes] of
+ Object.entries(dimensionChildTitleToStackFrameNodes)) {
+ dimensionChildNodes.push(new HeapDumpTreeNode(childStackFrameNodes,
+ dimension, childTitle, this.heavyView_, this));
+ }
+ this.lazyChildNodes_.set(dimension, dimensionChildNodes);
+ },
+
+ convertStackFrameNodeDimensionToChildDict_(
+ stackFrameNode, dimensionIndex) {
+ const childDict = {};
+ let displayedChildrenTotalSize = 0;
+ let displayedChildrenTotalCount = 0;
+ let hasDisplayedChildren = false;
+ let allDisplayedChildrenHaveDisplayedCounts = true;
+ for (const child of stackFrameNode.children[dimensionIndex].values()) {
+ if (child.values[0].totalState < this.minDisplayedTotalState_) {
+ continue;
+ }
+ if (child.values[1].totalState < this.minDisplayedTotalState_) {
+ allDisplayedChildrenHaveDisplayedCounts = false;
+ }
+ childDict[child.title[dimensionIndex]] = child;
+ displayedChildrenTotalSize += child.values[0].total;
+ displayedChildrenTotalCount += child.values[1].total;
+ hasDisplayedChildren = true;
+ }
+
+ const nodeTotalSize = stackFrameNode.values[0].total;
+ const nodeTotalCount = stackFrameNode.values[1].total;
+
+ // Add '<other>' node if necessary in tree-view.
+ const hasUnclassifiedSizeOrCount =
+ displayedChildrenTotalSize < nodeTotalSize ||
+ displayedChildrenTotalCount < nodeTotalCount;
+ if (!this.heavyView_ && hasUnclassifiedSizeOrCount &&
+ hasDisplayedChildren) {
+ const otherTitle = stackFrameNode.title.slice();
+ otherTitle[dimensionIndex] = '<other>';
+ const otherNode = new tr.b.MultiDimensionalViewNode(otherTitle, 2);
+ childDict[otherTitle[dimensionIndex]] = otherNode;
+
+ // '<other>' node size.
+ otherNode.values[0].total = nodeTotalSize - displayedChildrenTotalSize;
+ otherNode.values[0].totalState = this.minDisplayedTotalState_;
+
+ // '<other>' node allocation count.
+ otherNode.values[1].total =
+ nodeTotalCount - displayedChildrenTotalCount;
+ // Don't show allocation count of the '<other>' node if there is a
+ // displayed child node that did NOT display allocation count.
+ otherNode.values[1].totalState =
+ allDisplayedChildrenHaveDisplayedCounts ?
+ this.minDisplayedTotalState_ : TotalState.NOT_PROVIDED;
+ }
+
+ return childDict;
+ },
+
+ addCells_() {
+ // Transform a chronological list of heap stack frame tree nodes into a
+ // dictionary of cells (where each cell contains a chronological list
+ // of the values of its numeric).
+ this.lazyCells_ = tr.ui.analysis.createCells(this.stackFrameNodes_,
+ function(stackFrameNode) {
+ const size = stackFrameNode.values[0].total;
+ const numerics = {
+ 'Size': new Scalar(sizeInBytes_smallerIsBetter, size)
+ };
+ const countValue = stackFrameNode.values[1];
+ if (countValue.totalState >= this.minDisplayedTotalState_) {
+ const count = countValue.total;
+ numerics.Count = new Scalar(
+ count_smallerIsBetter, count);
+ }
+ return numerics;
+ }, this);
+ }
+ };
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.heapDumps_ = undefined;
+ this.viewMode_ = undefined;
+ this.aggregationMode_ = undefined;
+ this.cachedBuilders_ = new Map();
+ },
+
+ ready() {
+ this.$.info_bar.message = 'Note: Values displayed in the heavy view ' +
+ 'are lower bounds (except for the root).';
+
+ Polymer.dom(this.$.view_mode_container).appendChild(
+ tr.ui.b.createSelector(
+ this, 'viewMode', 'memoryDumpHeapDetailsPane.viewMode',
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW,
+ [
+ {
+ label: 'Top-down (Tree)',
+ value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW
+ },
+ {
+ label: 'Top-down (Heavy)',
+ value:
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW
+ },
+ {
+ label: 'Bottom-up (Heavy)',
+ value:
+ MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW
+ }
+ ]));
+
+ this.$.drag_handle.target = this.$.path_view;
+ this.$.drag_handle.horizontal = false;
+
+ // If the user selects a node in the path view, show its children in the
+ // breakdown view.
+ this.$.path_view.addEventListener('selected-node-changed', (function(e) {
+ this.$.breakdown_view.displayedNode = this.$.path_view.selectedNode;
+ }).bind(this));
+
+ // If the user double-clicks on a node in the breakdown view, select the
+ // node in the path view.
+ this.$.breakdown_view.addEventListener('enter-node', (function(e) {
+ this.$.path_view.selectedNode = e.node;
+ }).bind(this));
+ },
+
+ /**
+ * Sets the heap dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of heap dumps. All
+ * dumps are assumed to belong to the same process and belong to the same
+ * allocator. Example:
+ *
+ * [
+ * tr.model.HeapDump {}, // Heap dump at timestamp 1.
+ * undefined, // Heap dump not provided at timestamp 2.
+ * tr.model.HeapDump {}, // Heap dump at timestamp 3.
+ * ]
+ */
+ set heapDumps(heapDumps) {
+ this.heapDumps_ = heapDumps;
+ this.scheduleRebuild_();
+ },
+
+ get heapDumps() {
+ return this.heapDumps_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.$.path_view.aggregationMode = aggregationMode;
+ this.$.breakdown_view.aggregationMode = aggregationMode;
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set viewMode(viewMode) {
+ this.viewMode_ = viewMode;
+ this.scheduleRebuild_();
+ },
+
+ get viewMode() {
+ return this.viewMode_;
+ },
+
+ get heavyView() {
+ switch (this.viewMode) {
+ case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW:
+ case MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW:
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ onRebuild_() {
+ if (this.heapDumps_ === undefined ||
+ this.heapDumps_.length === 0) {
+ // Show the info text (hide the table and the view mode selector).
+ this.$.info_text.style.display = 'block';
+ this.$.split_view.style.display = 'none';
+ this.$.view_mode_container.style.display = 'none';
+ this.$.info_bar.hidden = true;
+ this.$.path_view.selectedNode = undefined;
+ return;
+ }
+
+ // Show the table and the view mode selector (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.split_view.style.display = 'flex';
+ this.$.view_mode_container.style.display = 'block';
+
+ // Show the info bar if in heavy view mode.
+ this.$.info_bar.hidden = !this.heavyView;
+
+ this.$.path_view.selectedNode = this.createHeapTree_();
+ this.$.path_view.rebuild();
+ this.$.breakdown_view.rebuild();
+ },
+
+ createHeapTree_() {
+ const definedHeapDump = this.heapDumps_.find(x => x);
+ if (definedHeapDump === undefined) return undefined;
+
+ // The title of the root node is the name of the allocator.
+ const rootRowTitle = definedHeapDump.allocatorName;
+
+ const stackFrameTrees = this.createStackFrameTrees_(this.heapDumps_);
+
+ return new HeapDumpTreeNode(stackFrameTrees,
+ tr.ui.analysis.HeapDetailsRowDimension.ROOT, rootRowTitle,
+ this.heavyView);
+ },
+
+ createStackFrameTrees_(heapDumps) {
+ const builders = heapDumps.map(heapDump => this.createBuilder_(heapDump));
+ const views = builders.map(builder => {
+ if (builder === undefined) return undefined;
+ return builder.buildView(this.viewMode);
+ });
+ return views;
+ },
+
+ createBuilder_(heapDump) {
+ if (heapDump === undefined) return undefined;
+
+ if (this.cachedBuilders_.has(heapDump)) {
+ return this.cachedBuilders_.get(heapDump);
+ }
+
+ const dimensions = 2; // stack frames, object type
+ const valueCount = 2; // size, count
+ const builder = new MultiDimensionalViewBuilder(dimensions, valueCount);
+
+ // Build the heap tree.
+ for (const entry of heapDump.entries) {
+ const leafStackFrame = entry.leafStackFrame;
+ const stackTracePath = leafStackFrame === undefined ?
+ [] : leafStackFrame.getUserFriendlyStackTrace().reverse();
+
+ const objectTypeName = entry.objectTypeName;
+ const objectTypeNamePath = objectTypeName === undefined ?
+ [] : [objectTypeName];
+
+ const valueKind = entry.valuesAreTotals ?
+ MultiDimensionalViewBuilder.ValueKind.TOTAL :
+ MultiDimensionalViewBuilder.ValueKind.SELF;
+
+ builder.addPath([stackTracePath, objectTypeNamePath],
+ [entry.size, entry.count],
+ valueKind);
+ }
+
+ builder.complete = heapDump.isComplete;
+ this.cachedBuilders_.set(heapDump, builder);
+ return builder;
+ },
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html
new file mode 100644
index 00000000000..947cc532e7a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html
@@ -0,0 +1,4045 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel='import' href='/tracing/base/multi_dimensional_view.html'>
+<link rel='import' href='/tracing/base/unit.html'>
+<link rel='import' href='/tracing/base/utils.html'>
+<link rel='import' href='/tracing/core/test_utils.html'>
+<link rel='import' href='/tracing/model/heap_dump.html'>
+<link rel='import' href='/tracing/model/memory_dump_test_utils.html'>
+<link rel='import'
+ href='/tracing/ui/analysis/memory_dump_heap_details_pane.html'>
+<link rel='import'
+ href='/tracing/ui/analysis/memory_dump_sub_view_test_utils.html'>
+<link rel='import' href='/tracing/ui/analysis/memory_dump_sub_view_util.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ViewType = tr.b.MultiDimensionalViewBuilder.ViewType;
+ const TOP_DOWN_TREE_VIEW = ViewType.TOP_DOWN_TREE_VIEW;
+ const TOP_DOWN_HEAVY_VIEW = ViewType.TOP_DOWN_HEAVY_VIEW;
+ const BOTTOM_UP_HEAVY_VIEW = ViewType.BOTTOM_UP_HEAVY_VIEW;
+ const HeapDump = tr.model.HeapDump;
+ const HeapDetailsRowDimension = tr.ui.analysis.HeapDetailsRowDimension;
+ const ROOT = HeapDetailsRowDimension.ROOT;
+ const STACK_FRAME = HeapDetailsRowDimension.STACK_FRAME;
+ const OBJECT_TYPE = HeapDetailsRowDimension.OBJECT_TYPE;
+ const TitleColumn = tr.ui.analysis.TitleColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const checkNumericFields = tr.ui.analysis.checkNumericFields;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
+
+ function createHeapDumps(withCount) {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(1);
+
+ function addHeapEntry(heapDump, stackFrames, objectTypeName, size, count) {
+ const leafStackFrame = stackFrames === undefined ? undefined :
+ tr.c.TestUtils.newStackTrace(model, stackFrames);
+ heapDump.addEntry(leafStackFrame, objectTypeName, size,
+ withCount ? count : undefined);
+ }
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(model, {ts: -10});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11});
+ const hd1 = new HeapDump(pmd1, 'partition_alloc');
+
+ addHeapEntry(hd1, undefined /* sum over all traces */,
+ undefined /* sum over all types */, 4194304 /* 4 MiB */, 1000);
+ addHeapEntry(hd1, undefined /* sum over all traces */, 'v8::Context',
+ 1048576 /* 1 MiB */, 200);
+ addHeapEntry(hd1, undefined /* sum over all traces */, 'blink::Node',
+ 331776 /* 324 KiB */, 10);
+ addHeapEntry(hd1, ['MessageLoop::RunTask'],
+ undefined /* sum over all types */, 4194304 /* 4 MiB */, 1000);
+ addHeapEntry(hd1, ['MessageLoop::RunTask'], 'v8::Context',
+ 1048576 /* 1 MiB */, 200);
+
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'],
+ undefined /* sum over all types */, 1406976 /* 1.3 MiB */, 299);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'],
+ 'blink::Node', 331776 /* 324 KiB */, 10);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'], 'v8::Context',
+ 1024000 /* 1000 KiB */, 176);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', '<self>'],
+ undefined /* sum over all types */, 102400 /* 100 KiB */, 30);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ 'v8::Context', 716800 /* 700 KiB */, 100);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 1048576 /* 1 MiB */, 101);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ undefined /* sum over all types */,
+ 153600 /* 150 KiB, lower than the actual sum (should be ignored) */,
+ 25 /* the allocation count should, however, NOT be ignored */);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ 'v8::Context', 153600 /* 150 KiB */, 15);
+
+ // The following entry should not appear in the tree-view because there is
+ // no entry for its parent stack frame.
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'MissingParent', 'FunctionCall'],
+ undefined /* sum over all types */, 10 /* 10 B */, 2);
+
+ // The following entry should not appear in the tree-view because there is
+ // no sum over all types (for the given stack trace). However, it will lead
+ // to a visible increase of the (incorrectly provided) sum over all types
+ // of MessageLoop::RunTask -> FunctionCall -> FunctionCall by 50 KiB.
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall',
+ 'FunctionCall'],
+ 'MissingSumOverAllTypes', 51200 /* 50 KiB */, 9);
+
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute'],
+ undefined /* sum over all types */, 2404352 /* 2.3 MiB */, 399);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ undefined /* sum over all types */, 2404352 /* 2.3 MiB */, 399);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ 'v8::Context', 20480 /* 20 KiB */, 6);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', '<self>'],
+ 'v8::Context', 15360 /* 15 KiB */, 5);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 2097152 /* 2 MiB */, 99);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute',
+ 'V8.Execute'],
+ undefined /* sum over all types */, 2097152 /* 2 MiB */, 99);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', '<self>'],
+ undefined /* sum over all types */, 307200 /* 300 KiB */, 300);
+
+ // Second timestamp.
+ const gmd2 = addGlobalMemoryDump(model, {ts: 10});
+ const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 11});
+ const hd2 = new HeapDump(pmd2, 'partition_alloc');
+
+ addHeapEntry(hd2, undefined /* sum over all traces */,
+ undefined /* sum over all types */,
+ 3145728 /* 3 MiB, lower than the actual sum (should be ignored) */,
+ 900 /* the allocation count should, however, NOT be ignored */);
+ addHeapEntry(hd2, undefined /* sum over all traces */,
+ 'v8::Context', 1258291 /* 1.2 MiB */, 520);
+ addHeapEntry(hd2, undefined /* sum over all traces */,
+ 'blink::Node', 1048576 /* 1 MiB */, 5);
+ addHeapEntry(hd2, ['<self>'], undefined /* sum over all types */,
+ 131072 /* 128 KiB */, 16);
+ addHeapEntry(hd2, ['<self>'], 'v8::Context', 131072 /* 128 KiB */, 16);
+ addHeapEntry(hd2, ['MessageLoop::RunTask'],
+ undefined /* sum over all types */, 4823449 /* 4.6 MiB */, 884);
+ addHeapEntry(hd2, ['MessageLoop::RunTask'], 'v8::Context',
+ 1127219 /* 1.1 MiB */, 317);
+
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'],
+ undefined /* sum over all types */, 2170880 /* 2.1 MiB */, 600);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], 'v8::Context',
+ 1024000 /* 1000 KiB */, 500);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], 'blink::Node',
+ 819200 /* 800 KiB */, 4);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 1572864 /* 1.5 MiB */, 270);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ 'v8::Context', 614400 /* 600 KiB */, 123);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ 'blink::Node', 819200 /* 800 KiB */, 4);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ undefined /* sum over all types */, 204800 /* 200 KiB */, 313);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ 'v8::Context', 122880 /* 120 KiB */, 270);
+ addHeapEntry(hd2,
+ ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall',
+ 'FunctionCall'],
+ undefined /* sum over all types */, 204800 /* 200 KiB */, 313);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', '<self>'],
+ undefined /* sum over all types */, 393216 /* 384 KiB */, 17);
+
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute'],
+ undefined /* sum over all types */, 2621440 /* 2.5 MiB */, 199);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ undefined /* sum over all types */, 2621440 /* 2.5 MiB */, 199);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ 'v8::Context', 20480 /* 20 KiB */, 4);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ 'WTF::StringImpl', 126362 /* 123.4 KiB */, 56);
+ addHeapEntry(hd2,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 2516582 /* 2.4 MiB */, 158);
+
+ return [hd1, hd2];
+ }
+
+ function createSelfHeapDumps(withCount) {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(1);
+
+ function addHeapEntry(heapDump, stackFrames, objectTypeName, size, count) {
+ const leafStackFrame = stackFrames === undefined ? undefined :
+ tr.c.TestUtils.newStackTrace(model, stackFrames);
+ heapDump.addEntry(leafStackFrame, objectTypeName, size,
+ withCount ? count : undefined, false /* valuesAreTotals */);
+ }
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(model, {ts: -10});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11});
+ const hd1 = new HeapDump(pmd1, 'partition_alloc');
+ hd1.isComplete = true;
+
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'AllocSomething'],
+ 'v8::Context', 1024, 100);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'b', 'AllocSomething'],
+ 'v8::Context', 1024, 100);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'b', 'c', 'AllocSomething'],
+ 'v8::Context', 1024, 100);
+
+ return [hd1];
+ }
+
+
+ function checkDisplayedElements(viewEl, displayExpectations) {
+ assert.strictEqual(isElementDisplayed(viewEl.$.info_text),
+ displayExpectations.infoText);
+ assert.strictEqual(isElementDisplayed(viewEl.$.info_bar),
+ displayExpectations.infoBar);
+ assert.strictEqual(isElementDisplayed(viewEl.$.split_view),
+ displayExpectations.tableAndSplitView);
+ assert.strictEqual(isElementDisplayed(viewEl.$.view_mode_container),
+ displayExpectations.tableAndSplitView);
+ }
+
+ const EXPECTED_COLUMNS_WITHOUT_COUNT = [
+ { title: 'Current path', type: TitleColumn, noAggregation: true },
+ { title: 'Size', type: NumericMemoryColumn }
+ ];
+
+ const EXPECTED_COLUMNS_WITH_COUNT = EXPECTED_COLUMNS_WITHOUT_COUNT.concat([
+ { title: 'Count', type: NumericMemoryColumn },
+ ]);
+
+ const EXPECTED_CELLS = ['Size', 'Count'];
+
+ function checkNode(node, expectedNodeStructure, expectedParentNode) {
+ assert.strictEqual(node.title, expectedNodeStructure.title);
+ assert.strictEqual(node.dimension, expectedNodeStructure.dimension);
+ assert.strictEqual(node.parentNode, expectedParentNode);
+
+ // Check that there AREN'T any cells that we are NOT expecting.
+ const cells = node.cells;
+ assert.includeMembers(EXPECTED_CELLS, Object.keys(cells));
+
+ const sizeCell = cells.Size;
+ const sizeFields = sizeCell ? sizeCell.fields : undefined;
+ checkSizeNumericFields(sizeFields, undefined, expectedNodeStructure.size);
+
+ const countCell = cells.Count;
+ const countFields = countCell ? countCell.fields : undefined;
+ checkNumericFields(countFields, undefined, expectedNodeStructure.count,
+ count_smallerIsBetter);
+
+ assert.strictEqual(node.childNodes.size, 2);
+
+ // If |expectedNodeStructure.children| is undefined, check that there are
+ // no child nodes.
+ if (!expectedNodeStructure.children) {
+ assert.lengthOf(node.childNodes.get(STACK_FRAME), 0);
+ assert.lengthOf(node.childNodes.get(OBJECT_TYPE), 0);
+ return;
+ }
+
+ // If |expectedNodeStructure.children| is just a number, check total number
+ // of child nodes.
+ if (typeof expectedNodeStructure.children === 'number') {
+ assert.strictEqual(expectedNodeStructure.children,
+ node.childNodes.get(STACK_FRAME).length +
+ node.childNodes.get(OBJECT_TYPE).length);
+ return;
+ }
+
+ // Check child nodes wrt both dimensions.
+ checkNodes(node.childNodes.get(STACK_FRAME),
+ expectedNodeStructure.children.filter(c => c.dimension === STACK_FRAME),
+ node);
+ checkNodes(node.childNodes.get(OBJECT_TYPE),
+ expectedNodeStructure.children.filter(c => c.dimension === OBJECT_TYPE),
+ node);
+ }
+
+ function checkNodes(nodes, expectedStructure, expectedParentNode) {
+ assert.lengthOf(nodes, expectedStructure.length);
+ for (let i = 0; i < expectedStructure.length; i++) {
+ checkNode(nodes[i], expectedStructure[i], expectedParentNode);
+ }
+ }
+
+ function checkSplitView(viewEl, expectedConfig, expectedStructure) {
+ checkDisplayedElements(viewEl, {
+ infoText: false,
+ tableAndSplitView: true,
+ infoBar: !!expectedConfig.expectedInfoBarDisplayed
+ });
+
+ // Both the split view and breakdown view should be displaying the same
+ // node.
+ const selectedNode = viewEl.$.path_view.selectedNode;
+ assert.strictEqual(viewEl.$.breakdown_view.displayedNode, selectedNode);
+ checkNodes([selectedNode], expectedStructure,
+ undefined /* expectedParentNode */);
+
+ // TODO: Add proper tests for tr-ui-a-memory-dump-heap-details-path-view
+ // and tr-ui-a-memory-dump-heap-details-breakdown-view.
+ const expectedColumns = expectedConfig.expectedCountColumns ?
+ EXPECTED_COLUMNS_WITH_COUNT : EXPECTED_COLUMNS_WITHOUT_COUNT;
+ checkColumns(viewEl.$.path_view.$.table.tableColumns, expectedColumns,
+ expectedConfig.expectedAggregationMode);
+ }
+
+ function changeView(viewEl, viewType) {
+ tr.ui.b.findDeepElementMatching(viewEl, 'select').selectedValue = viewType;
+ viewEl.rebuild();
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-heap-details-pane', 'heapDumps',
+ function(viewEl) {
+ // Check that the info text is shown.
+ checkDisplayedElements(viewEl, {
+ infoText: true,
+ tableAndSplitView: false,
+ infoBar: false
+ });
+ });
+ });
+
+ test('instantiate_noEntries', function() {
+ const heapDumps = createHeapDumps(false).slice(0, 1);
+ heapDumps[0].entries = [];
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { /* empty expectedConfig */ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [0],
+ defined: [true]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [0],
+ defined: [true]
+ }
+ ]);
+
+ changeView(viewEl, BOTTOM_UP_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [0],
+ defined: [true]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_TREE_VIEW);
+ });
+
+ test('instantiate_single', function() {
+ const heapDumps = createHeapDumps(false).slice(0, 1);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { /* empty expectedConfig */ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600 + 51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [51200],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [291840],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [5120],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2383872],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [382976],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [3145728],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2813952],
+ defined: [true],
+ }
+ ]
+ }
+ ]);
+
+ changeView(viewEl, BOTTOM_UP_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [3811338],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1406976],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [204800],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [153600],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [10],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1044480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1024000],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [153600],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [409600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [409600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [102400],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [3452928],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [3145728],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2097152],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2097152],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [737280],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [716800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [10],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1044480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1024000],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [153600],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [737280],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [716800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true }, [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600 + 51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976 + 10 + 2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400 + 307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576 + 2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600 + 51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400 + 307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576 + 2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+ });
+
+ test('instantiate_multipleDiff', function() {
+ const heapDumps = createHeapDumps(true /* with allocation counts */);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ {
+ expectedAggregationMode: AggregationMode.DIFF,
+ expectedInfoBarDisplayed: true,
+ expectedCountColumns: true
+ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, 4954521],
+ count: [1000, 900],
+ averageSize: [4194304 / 1000, 4954521 / 900],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304, 4823449],
+ count: [1000, 884],
+ averageSize: [4194304 / 1000, 4823449 / 884],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976, 2170880],
+ count: [299, 600],
+ averageSize: [1406976 / 299, 2170880 / 600],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400, 393216],
+ count: [30, 17],
+ averageSize: [102400 / 30, 393216 / 17],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576, 1572864],
+ count: [101, 270],
+ averageSize: [1048576 / 101, 1572864 / 270],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800, 204800],
+ count: [25, 313],
+ averageSize: [204800 / 25, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, 204800],
+ count: [9, 313],
+ averageSize: [51200 / 9, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200, undefined],
+ count: [300, undefined],
+ averageSize: [307200 / 300, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, 2516582],
+ count: [99, 158],
+ averageSize: [2097152 / 99, 2516582 / 158],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [3811338, 4792320],
+ count: [700, 799],
+ averageSize: [3811338 / 700, 4792320 / 799],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [409600, 393216],
+ count: [330, 17],
+ averageSize: [409600 / 330, 393216 / 17],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [3145728, 4089446],
+ count: [200, 428],
+ averageSize: [3145728 / 200, 4089446 / 428],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800, 204800],
+ count: [25, 313],
+ averageSize: [204800 / 25, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, 204800],
+ count: [9, 313],
+ averageSize: [51200 / 9, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1044480, 1044480],
+ count: [182, 504],
+ averageSize: [1044480 / 182, 1044480 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [409600, 524288],
+ count: [330, 33],
+ averageSize: [409600 / 330, 524288 / 33],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, 131072],
+ count: [5, 16],
+ averageSize: [15360 / 5, 131072 / 16],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [3452928, 4194304],
+ count: [500, 469],
+ averageSize: [3452928 / 500, 4194304 / 469],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200, undefined],
+ count: [300, undefined],
+ averageSize: [307200 / 300, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, 2516582],
+ count: [99, 158],
+ averageSize: [2097152 / 99, 2516582 / 158],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [737280, 634880],
+ count: [106, 127],
+ averageSize: [737280 / 106, 634880 / 127],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1258291],
+ count: [200, 520],
+ averageSize: [1048576 / 200, 1258291 / 520],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1044480, 1044480],
+ count: [182, 504],
+ averageSize: [1044480 / 182, 1044480 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, 131072],
+ count: [5, 16],
+ averageSize: [15360 / 5, 131072 / 16],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [737280, 634880],
+ count: [106, 127],
+ averageSize: [737280 / 106, 634880 / 127],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 1048576],
+ count: [10, 5],
+ averageSize: [331776 / 10, 1048576 / 5],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_TREE_VIEW);
+ checkSplitView(viewEl,
+ {
+ expectedAggregationMode: AggregationMode.DIFF,
+ expectedCountColumns: true
+ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, 4954521],
+ count: [1000, 900],
+ averageSize: [4194304 / 1000, 4954521 / 900],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304, 4823449],
+ count: [1000, 884],
+ averageSize: [4194304 / 1000, 4823449 / 884],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976, 2170880],
+ count: [299, 600],
+ averageSize: [1406976 / 299, 2170880 / 600],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400, 393216],
+ count: [30, 17],
+ averageSize: [102400 / 30, 393216 / 17],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576, 1572864],
+ count: [101, 270],
+ averageSize: [1048576 / 101, 1572864 / 270],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [331776, 139264],
+ count: [1, 143],
+ averageSize: [331776 / 1, 139264 / 143],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800, 204800],
+ count: [25, 313],
+ averageSize: [204800 / 25, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 204800],
+ count: [undefined, 313],
+ averageSize: [undefined, 204800 / 313],
+ defined: [false, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200, 81920],
+ count: [10, 43],
+ averageSize: [51200 / 10, 81920 / 43],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [51200, undefined],
+ count: [143, undefined],
+ averageSize: [51200 / 143, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600, 286720],
+ count: [61, 107],
+ averageSize: [153600 / 61, 286720 / 107],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200, 327680],
+ count: [113, 96],
+ averageSize: [51200 / 113, 327680 / 96],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200, undefined],
+ count: [300, undefined],
+ averageSize: [307200 / 300, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [291840, undefined],
+ count: [295, undefined],
+ averageSize: [291840 / 295, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, 2516582],
+ count: [99, 158],
+ averageSize: [2097152 / 99, 2516582 / 158],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [undefined, 104858],
+ count: [undefined, 41],
+ averageSize: [undefined, 104858 / 41],
+ defined: [false, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [5120, undefined],
+ count: [1, undefined],
+ averageSize: [5120 / 1, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2383872, 2474598],
+ count: [393, 139],
+ averageSize: [2383872 / 393, 2474598 / 139],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [382976, 31129],
+ count: [302, 85],
+ averageSize: [382976 / 302, 31129 / 85],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600, 286720],
+ count: [61, 107],
+ averageSize: [153600 / 61, 286720 / 107],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576, 103219],
+ count: [24, 4],
+ averageSize: [24576 / 24, 103219 / 4],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [3145728, 3696230],
+ count: [800, 380],
+ averageSize: [3145728 / 800, 3696230 / 380],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [undefined, 131072],
+ count: [undefined, 16],
+ averageSize: [undefined, 131072 / 16],
+ defined: [false, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [undefined, 131072],
+ count: [undefined, 16],
+ averageSize: [undefined, 131072 / 16],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1258291],
+ count: [200, 520],
+ averageSize: [1048576 / 200, 1258291 / 520],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600, 286720],
+ count: [61, 107],
+ averageSize: [153600 / 61, 286720 / 107],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576, 103219],
+ count: [24, 4],
+ averageSize: [24576 / 24, 103219 / 4],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [undefined, 131072],
+ count: [undefined, 16],
+ averageSize: [undefined, 131072 / 16],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 1048576],
+ count: [10, 5],
+ averageSize: [331776 / 10, 1048576 / 5],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2813952, 2647654],
+ count: [790, 375],
+ averageSize: [2813952 / 790, 2647654 / 375],
+ defined: [true, true]
+ }
+ ]
+ }
+ ]);
+ });
+
+ test('instantiate_multipleMax', function() {
+ const heapDumps = createHeapDumps(false);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ {
+ expectedAggregationMode: AggregationMode.MAX,
+ expectedInfoBarDisplayed: true
+ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, 4954521],
+ defined: [true, true],
+ children: 9 // No need to check the full structure again.
+ }
+ ]);
+ });
+
+ test('instantiate_multipleWithUndefined', function() {
+ const heapDumps = createHeapDumps(false);
+ heapDumps.splice(1, 0, undefined);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { expectedAggregationMode: AggregationMode.DIFF },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, undefined, 4954521],
+ defined: [true, false, true],
+ children: 5 // No need to check the full structure again.
+ }
+ ]);
+ });
+
+ test('instantiate_selfHeapSingle', function() {
+ const heapDumps = createSelfHeapDumps(true).slice(0, 1);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { expectedCountColumns: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [1024 * 3],
+ count: [300],
+ defined: [true],
+ children: 2 // No need to check the full structure again.
+ }
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html
new file mode 100644
index 00000000000..1cc3c8d7e5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-path-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ </style>
+ <tr-v-ui-scalar-context-controller></tr-v-ui-scalar-context-controller>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const DOWNWARDS_ARROW_WITH_TIP_RIGHTWARDS = String.fromCharCode(0x21B3);
+
+ function HeapDetailsPathColumn(title) {
+ tr.ui.analysis.HeapDetailsTitleColumn.call(this, title);
+ }
+
+ HeapDetailsPathColumn.prototype = {
+ __proto__: tr.ui.analysis.HeapDetailsTitleColumn.prototype,
+
+ formatTitle(row) {
+ const title = tr.ui.analysis.HeapDetailsTitleColumn.prototype.
+ formatTitle.call(this, row);
+ if (row.dimension === tr.ui.analysis.HeapDetailsRowDimension.ROOT) {
+ return title;
+ }
+
+ const arrowEl = document.createElement('span');
+ Polymer.dom(arrowEl).textContent = DOWNWARDS_ARROW_WITH_TIP_RIGHTWARDS;
+ arrowEl.style.paddingRight = '2px';
+ arrowEl.style.fontWeight = 'bold';
+ arrowEl.style.color = tr.b.ColorScheme.getColorForReservedNameAsString(
+ 'heap_dump_child_node_arrow');
+
+ const rowEl = document.createElement('span');
+ Polymer.dom(rowEl).appendChild(arrowEl);
+ Polymer.dom(rowEl).appendChild(tr.ui.b.asHTMLOrTextNode(title));
+ return rowEl;
+ }
+ };
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-path-view',
+ behaviors: [tr.ui.analysis.RebuildableBehavior],
+
+ created() {
+ this.selectedNode_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.addEventListener('selection-changed', function(event) {
+ this.selectedNode_ = this.$.table.selectedTableRow;
+ this.didSelectedNodeChange_();
+ }.bind(this));
+ },
+
+ didSelectedNodeChange_() {
+ this.dispatchEvent(new tr.b.Event('selected-node-changed'));
+ },
+
+ get selectedNode() {
+ return this.selectedNode_;
+ },
+
+ set selectedNode(node) {
+ this.selectedNode_ = node;
+ this.didSelectedNodeChange_();
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ onRebuild_() {
+ if (this.selectedNode_ === undefined) {
+ this.$.table.clear();
+ return;
+ }
+
+ if (this.$.table.tableRows.includes(this.selectedNode_)) {
+ this.$.table.selectedTableRow = this.selectedNode_;
+ return;
+ }
+
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.userCanModifySortOrder = false;
+ const rows = this.createRows_(this.selectedNode_);
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = this.createColumns_(rows);
+ this.$.table.selectedTableRow = rows[rows.length - 1];
+ },
+
+ createRows_(node) {
+ const rows = [];
+ while (node) {
+ rows.push(node);
+ node = node.parentNode;
+ }
+ rows.reverse();
+ return rows;
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new HeapDetailsPathColumn('Current path');
+ titleColumn.width = '200px';
+
+ const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'cells',
+ aggregationMode: this.aggregationMode_,
+ rules: tr.ui.analysis.HEAP_DETAILS_COLUMN_RULES,
+ shouldSetContextGroup: true
+ });
+ tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns);
+
+ return [titleColumn].concat(numericColumns);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html
new file mode 100644
index 00000000000..2cf6c6ec8b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const LATIN_SMALL_LETTER_F_WITH_HOOK = String.fromCharCode(0x0192);
+ const CIRCLED_LATIN_CAPITAL_LETTER_T = String.fromCharCode(0x24C9);
+
+ /** @{enum} */
+ const HeapDetailsRowDimension = {
+ ROOT: {},
+ STACK_FRAME: {
+ label: 'Stack frame',
+ symbol: LATIN_SMALL_LETTER_F_WITH_HOOK,
+ color: 'heap_dump_stack_frame'
+ },
+ OBJECT_TYPE: {
+ label: 'Object type',
+ symbol: CIRCLED_LATIN_CAPITAL_LETTER_T,
+ color: 'heap_dump_object_type'
+ }
+ };
+
+ /** @{constructor} */
+ function HeapDetailsTitleColumn(title) {
+ tr.ui.analysis.TitleColumn.call(this, title);
+ }
+
+ HeapDetailsTitleColumn.prototype = {
+ __proto__: tr.ui.analysis.TitleColumn.prototype,
+
+ formatTitle(row) {
+ if (row.dimension === HeapDetailsRowDimension.ROOT) {
+ return row.title;
+ }
+
+ const symbolEl = document.createElement('span');
+ Polymer.dom(symbolEl).textContent = row.dimension.symbol;
+ symbolEl.title = row.dimension.label;
+ symbolEl.style.color = tr.b.ColorScheme.getColorForReservedNameAsString(
+ row.dimension.color);
+ symbolEl.style.paddingRight = '4px';
+ symbolEl.style.cursor = 'help';
+ symbolEl.style.fontWeight = 'bold';
+
+ const titleEl = document.createElement('span');
+ Polymer.dom(titleEl).appendChild(symbolEl);
+ Polymer.dom(titleEl).appendChild(document.createTextNode(row.title));
+
+ return titleEl;
+ }
+ };
+
+ /** @constructor */
+ function AllocationCountColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.DetailsNumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ AllocationCountColumn.prototype = {
+ __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
+
+ getFormattingContext(unit) {
+ return { minimumFractionDigits: 0 };
+ }
+ };
+
+ const HEAP_DETAILS_COLUMN_RULES = [
+ {
+ condition: 'Size',
+ importance: 2,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Count',
+ importance: 1,
+ columnConstructor: AllocationCountColumn
+ },
+ {
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ }
+ ];
+
+ return {
+ HeapDetailsRowDimension,
+ HeapDetailsTitleColumn,
+ AllocationCountColumn,
+ HEAP_DETAILS_COLUMN_RULES,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html
new file mode 100644
index 00000000000..5df80bb88b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html
@@ -0,0 +1,774 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/fixed_color_scheme.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/color_legend.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/view_specific_brushing_state.html">
+
+<dom-module id='tr-ui-a-memory-dump-overview-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #label {
+ flex: 0 0 auto;
+ padding: 8px;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #label a {
+ font-weight: normal;
+ float: right;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ overflow: auto;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #table {
+ display: none; /* Hide until memory dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-view-specific-brushing-state id="state"
+ view-id="analysis.memory_dump_overview_pane">
+ </tr-ui-b-view-specific-brushing-state>
+ <div id="label">Overview <a href="https://chromium.googlesource.com/chromium/src/+/master/docs/memory-infra">Help</a></div>
+ <div id="contents">
+ <div id="info_text">No memory memory dumps selected</div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const MemoryColumnColorScheme = tr.b.MemoryColumnColorScheme;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+
+ const PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX = '_bytes';
+
+ const DISPLAYED_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
+ const SOME_TIMESTAMPS_INFO_QUANTIFIER =
+ tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER;
+
+ // Unicode symbols used for memory cell info icons and messages.
+ const RIGHTWARDS_ARROW_WITH_HOOK = String.fromCharCode(0x21AA);
+ const RIGHTWARDS_ARROW_FROM_BAR = String.fromCharCode(0x21A6);
+ const GREATER_THAN_OR_EQUAL_TO = String.fromCharCode(0x2265);
+ const UNMARRIED_PARTNERSHIP_SYMBOL = String.fromCharCode(0x26AF);
+ const TRIGRAM_FOR_HEAVEN = String.fromCharCode(0x2630);
+
+ function lazyMap(list, fn, opt_this) {
+ opt_this = opt_this || this;
+ let result = undefined;
+ list.forEach(function(item, index) {
+ const value = fn.call(opt_this, item, index);
+ if (value === undefined) return;
+ if (result === undefined) {
+ result = new Array(list.length);
+ }
+ result[index] = value;
+ });
+ return result;
+ }
+
+ /** @constructor */
+ function ProcessNameColumn() {
+ tr.ui.analysis.TitleColumn.call(this, 'Process');
+ }
+
+ ProcessNameColumn.prototype = {
+ __proto__: tr.ui.analysis.TitleColumn.prototype,
+
+ formatTitle(row) {
+ if (row.contexts === undefined) {
+ return row.title; // Total row.
+ }
+ const titleEl = document.createElement('tr-ui-b-color-legend');
+ titleEl.label = row.title;
+ return titleEl;
+ }
+ };
+
+ /** @constructor */
+ function UsedMemoryColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.NumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ UsedMemoryColumn.COLOR =
+ MemoryColumnColorScheme.getColor('used_memory_column').toString();
+ UsedMemoryColumn.OLDER_COLOR =
+ MemoryColumnColorScheme.getColor('older_used_memory_column').toString();
+
+ UsedMemoryColumn.prototype = {
+ __proto__: tr.ui.analysis.NumericMemoryColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createSpan({
+ textContent: this.name,
+ color: UsedMemoryColumn.COLOR
+ });
+ },
+
+ getFormattingContext(unit) {
+ return { unitPrefix: tr.b.UnitPrefixScale.BINARY.MEBI };
+ },
+
+ color(numerics, processMemoryDumps) {
+ return UsedMemoryColumn.COLOR;
+ },
+
+ getChildPaneBuilder(processMemoryDumps) {
+ if (processMemoryDumps === undefined) return undefined;
+
+ const vmRegions = lazyMap(processMemoryDumps, function(pmd) {
+ if (pmd === undefined) return undefined;
+ return pmd.mostRecentVmRegions;
+ });
+ if (vmRegions === undefined) return undefined;
+
+ return function() {
+ const pane = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ pane.vmRegions = vmRegions;
+ pane.aggregationMode = this.aggregationMode;
+ return pane;
+ }.bind(this);
+ }
+ };
+
+ /** @constructor */
+ function PeakMemoryColumn(name, cellPath, aggregationMode) {
+ UsedMemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ PeakMemoryColumn.prototype = {
+ __proto__: UsedMemoryColumn.prototype,
+
+ addInfos(numerics, processMemoryDumps, infos) {
+ if (processMemoryDumps === undefined) return; // Total row.
+
+ let resettableValueCount = 0;
+ let nonResettableValueCount = 0;
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+ if (processMemoryDumps[i].arePeakResidentBytesResettable) {
+ resettableValueCount++;
+ } else {
+ nonResettableValueCount++;
+ }
+ }
+
+ if (resettableValueCount > 0 && nonResettableValueCount > 0) {
+ infos.push(tr.ui.analysis.createWarningInfo('Both resettable and ' +
+ 'non-resettable peak RSS values were provided by the process'));
+ } else if (resettableValueCount > 0) {
+ infos.push({
+ icon: RIGHTWARDS_ARROW_WITH_HOOK,
+ message: 'Peak RSS since previous memory dump.'
+ });
+ } else {
+ infos.push({
+ icon: RIGHTWARDS_ARROW_FROM_BAR,
+ message: 'Peak RSS since process startup. Finer grained ' +
+ 'peaks require a Linux kernel version ' +
+ GREATER_THAN_OR_EQUAL_TO + ' 4.0.'
+ });
+ }
+ }
+ };
+
+ /** @constructor */
+ function ByteStatColumn(name, cellPath, aggregationMode) {
+ UsedMemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ ByteStatColumn.prototype = {
+ __proto__: UsedMemoryColumn.prototype,
+
+ color(numerics, processMemoryDumps) {
+ if (processMemoryDumps === undefined) {
+ return UsedMemoryColumn.COLOR; // Total row.
+ }
+
+ const allOlderValues = processMemoryDumps.every(
+ function(processMemoryDump) {
+ if (processMemoryDump === undefined) return true;
+ return !processMemoryDump.hasOwnVmRegions;
+ });
+
+ // Show the cell in lighter blue if all values were older (i.e. none of
+ // the defined process memory dumps had own VM regions).
+ if (allOlderValues) {
+ return UsedMemoryColumn.OLDER_COLOR;
+ }
+ return UsedMemoryColumn.COLOR;
+ },
+
+ addInfos(numerics, processMemoryDumps, infos) {
+ if (processMemoryDumps === undefined) return; // Total row.
+
+ let olderValueCount = 0;
+ for (let i = 0; i < numerics.length; i++) {
+ const processMemoryDump = processMemoryDumps[i];
+ if (processMemoryDump !== undefined &&
+ !processMemoryDump.hasOwnVmRegions) {
+ olderValueCount++;
+ }
+ }
+
+ if (olderValueCount === 0) {
+ return; // There are no older values.
+ }
+
+ const infoQuantifier = olderValueCount < numerics.length ?
+ ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : /* some values are older */
+ ''; /* all values are older */
+
+ // Emit an info if there was at least one older value (i.e. at least one
+ // defined process memory dump did not have own VM regions).
+ infos.push({
+ message: 'Older value' + infoQuantifier +
+ ' (only heavy (purple) memory dumps contain memory maps).',
+ icon: UNMARRIED_PARTNERSHIP_SYMBOL
+ });
+ }
+ };
+
+ // Rules for constructing and sorting used memory columns.
+ UsedMemoryColumn.RULES = [
+ {
+ condition: 'Total resident',
+ importance: 10,
+ columnConstructor: UsedMemoryColumn
+ },
+ {
+ condition: 'Peak total resident',
+ importance: 9,
+ columnConstructor: PeakMemoryColumn
+ },
+ {
+ condition: 'PSS',
+ importance: 8,
+ columnConstructor: ByteStatColumn
+ },
+ {
+ condition: 'Private dirty',
+ importance: 7,
+ columnConstructor: ByteStatColumn
+ },
+ {
+ condition: 'Swapped',
+ importance: 6,
+ columnConstructor: ByteStatColumn
+ },
+ {
+ // All other columns.
+ importance: 0,
+ columnConstructor: UsedMemoryColumn
+ }
+ ];
+
+ // Map from ProcessMemoryDump totals fields to column names.
+ UsedMemoryColumn.TOTALS_MAP = {
+ 'residentBytes': 'Total resident',
+ 'peakResidentBytes': 'Peak total resident',
+ 'privateFootprintBytes': 'Private footprint',
+ };
+
+ // Map from ProcessMemoryDump platform-specific totals fields to column names.
+ UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP = {
+ 'vm': 'Total virtual',
+ 'swp': 'Swapped',
+ 'pc': 'Private clean',
+ 'pd': 'Private dirty',
+ 'sc': 'Shared clean',
+ 'sd': 'Shared dirty',
+ 'gpu_egl': 'GPU EGL',
+ 'gpu_egl_pss': 'GPU EGL PSS',
+ 'gpu_gl': 'GPU GL',
+ 'gpu_gl_pss': 'GPU GL PSS',
+ 'gpu_etc': 'GPU Other',
+ 'gpu_etc_pss': 'GPU Other PSS',
+ };
+
+ // Map from VMRegionByteStats field names to column names.
+ UsedMemoryColumn.BYTE_STAT_MAP = {
+ 'proportionalResident': 'PSS',
+ 'privateDirtyResident': 'Private dirty',
+ 'swapped': 'Swapped'
+ };
+
+ /** @constructor */
+ function AllocatorColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.NumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ AllocatorColumn.prototype = {
+ __proto__: tr.ui.analysis.NumericMemoryColumn.prototype,
+
+ get title() {
+ const titleEl = document.createElement('tr-ui-b-color-legend');
+ titleEl.label = this.name;
+ return titleEl;
+ },
+
+ getFormattingContext(unit) {
+ return { unitPrefix: tr.b.UnitPrefixScale.BINARY.MEBI };
+ },
+
+ addInfos(numerics, processMemoryDumps, infos) {
+ if (processMemoryDumps === undefined) return;
+
+ let heapDumpCount = 0;
+ let missingSizeCount = 0;
+
+ for (let i = 0; i < processMemoryDumps.length; i++) {
+ const processMemoryDump = processMemoryDumps[i];
+ if (processMemoryDump === undefined) continue;
+
+ const heapDumps = processMemoryDump.heapDumps;
+ if (heapDumps !== undefined && heapDumps[this.name] !== undefined) {
+ heapDumpCount++;
+ }
+ const allocatorDump =
+ processMemoryDump.getMemoryAllocatorDumpByFullName(this.name);
+
+ if (allocatorDump !== undefined &&
+ allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME] === undefined) {
+ missingSizeCount++;
+ }
+ }
+
+ // Emit a heap dump info if at least one of the process memory dumps has
+ // a heap dump associated with this allocator.
+ if (heapDumpCount > 0) {
+ const infoQuantifier = heapDumpCount < numerics.length ?
+ ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : '';
+ infos.push({
+ message: 'Heap dump provided' + infoQuantifier + '.',
+ icon: TRIGRAM_FOR_HEAVEN
+ });
+ }
+
+ // Emit a warning if this allocator did not provide size in at least one
+ // of the process memory dumps.
+ if (missingSizeCount > 0) {
+ const infoQuantifier = missingSizeCount < numerics.length ?
+ ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : '';
+ infos.push(tr.ui.analysis.createWarningInfo(
+ 'Size was not provided' + infoQuantifier + '.'));
+ }
+ },
+
+ getChildPaneBuilder(processMemoryDumps) {
+ if (processMemoryDumps === undefined) return undefined;
+
+ const memoryAllocatorDumps = lazyMap(processMemoryDumps, function(pmd) {
+ if (pmd === undefined) return undefined;
+ return pmd.getMemoryAllocatorDumpByFullName(this.name);
+ }, this);
+ if (memoryAllocatorDumps === undefined) return undefined;
+
+ const heapDumps = lazyMap(processMemoryDumps, function(pmd) {
+ if (pmd === undefined || pmd.heapDumps === undefined) return undefined;
+ return pmd.heapDumps[this.name];
+ }, this);
+
+ return function() {
+ const pane = document.createElement(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ pane.memoryAllocatorDumps = memoryAllocatorDumps;
+ pane.heapDumps = heapDumps;
+ pane.aggregationMode = this.aggregationMode;
+ return pane;
+ }.bind(this);
+ }
+ };
+
+ /** @constructor */
+ function TracingColumn(name, cellPath, aggregationMode) {
+ AllocatorColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ TracingColumn.COLOR =
+ MemoryColumnColorScheme.getColor('tracing_memory_column').toString();
+
+ TracingColumn.prototype = {
+ __proto__: AllocatorColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createSpan({
+ textContent: this.name,
+ color: TracingColumn.COLOR
+ });
+ },
+
+ color(numerics, processMemoryDumps) {
+ return TracingColumn.COLOR;
+ }
+ };
+
+ // Rules for constructing and sorting allocator columns.
+ AllocatorColumn.RULES = [
+ {
+ condition: 'tracing',
+ importance: 0,
+ columnConstructor: TracingColumn
+ },
+ {
+ // All other columns.
+ importance: 1,
+ columnConstructor: AllocatorColumn
+ }
+ ];
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-overview-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.processMemoryDumps_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.CELL;
+ this.$.table.addEventListener('selection-changed',
+ function(tableEvent) {
+ tableEvent.stopPropagation();
+ this.changeChildPane_();
+ }.bind(this));
+ },
+
+ /**
+ * Sets the process memory dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of dictionaries
+ * mapping process IDs to process memory dumps. Example:
+ *
+ * [
+ * {
+ * // PMDs at timestamp 1.
+ * 42: tr.model.ProcessMemoryDump {}
+ * },
+ * {
+ * // PMDs at timestamp 2.
+ * 42: tr.model.ProcessMemoryDump {},
+ * 89: tr.model.ProcessMemoryDump {}
+ * }
+ * ]
+ */
+ set processMemoryDumps(processMemoryDumps) {
+ this.processMemoryDumps_ = processMemoryDumps;
+ this.scheduleRebuild_();
+ },
+
+ get processMemoryDumps() {
+ return this.processMemoryDumps_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ get selectedMemoryCell() {
+ if (this.processMemoryDumps_ === undefined ||
+ this.processMemoryDumps_.length === 0) {
+ return undefined;
+ }
+
+ const selectedTableRow = this.$.table.selectedTableRow;
+ if (!selectedTableRow) return undefined;
+
+ const selectedColumnIndex = this.$.table.selectedColumnIndex;
+ if (selectedColumnIndex === undefined) return undefined;
+
+ const selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
+ const selectedMemoryCell = selectedColumn.cell(selectedTableRow);
+ return selectedMemoryCell;
+ },
+
+ changeChildPane_() {
+ this.storeSelection_();
+ this.childPaneBuilder = this.determineChildPaneBuilderFromSelection_();
+ },
+
+ determineChildPaneBuilderFromSelection_() {
+ if (this.processMemoryDumps_ === undefined ||
+ this.processMemoryDumps_.length === 0) {
+ return undefined;
+ }
+
+ const selectedTableRow = this.$.table.selectedTableRow;
+ if (!selectedTableRow) return undefined;
+
+ const selectedColumnIndex = this.$.table.selectedColumnIndex;
+ if (selectedColumnIndex === undefined) return undefined;
+ const selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
+
+ return selectedColumn.getChildPaneBuilder(selectedTableRow.contexts);
+ },
+
+ onRebuild_() {
+ if (this.processMemoryDumps_ === undefined ||
+ this.processMemoryDumps_.length === 0) {
+ // Show the info text (hide the table).
+ this.$.info_text.style.display = 'block';
+ this.$.table.style.display = 'none';
+
+ this.$.table.clear();
+ this.$.table.rebuild();
+ return;
+ }
+
+ // Show the table (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.table.style.display = 'block';
+
+ const rows = this.createRows_();
+ const columns = this.createColumns_(rows);
+ const footerRows = this.createFooterRows_(rows, columns);
+
+ this.$.table.tableRows = rows;
+ this.$.table.footerRows = footerRows;
+ this.$.table.tableColumns = columns;
+ this.$.table.rebuild();
+
+ this.restoreSelection_();
+ },
+
+ createRows_() {
+ // Timestamp (list index) -> Process ID (dict key) -> PMD.
+ const timeToPidToProcessMemoryDump = this.processMemoryDumps_;
+
+ // Process ID (dict key) -> Timestamp (list index) -> PMD or undefined.
+ const pidToTimeToProcessMemoryDump = tr.b.invertArrayOfDicts(
+ timeToPidToProcessMemoryDump);
+
+ // Process (list index) -> Component (dict key) -> Cell.
+ const rows = [];
+ for (const [pid, timeToDump] of
+ Object.entries(pidToTimeToProcessMemoryDump)) {
+ // Get the process associated with the dumps. We can use any defined
+ // process memory dump in timeToDump since they all have the same
+ // pid.
+ const process = timeToDump.find(x => x).process;
+
+ // Used memory (total resident, PSS, ...).
+ const usedMemoryCells = tr.ui.analysis.createCells(timeToDump,
+ function(dump) {
+ const sizes = {};
+
+ const totals = dump.totals;
+ if (totals !== undefined) {
+ // Common totals.
+ for (const [totalName, cellName] of
+ Object.entries(UsedMemoryColumn.TOTALS_MAP)) {
+ const total = totals[totalName];
+ if (total === undefined) continue;
+ sizes[cellName] = new Scalar(
+ sizeInBytes_smallerIsBetter, total);
+ }
+
+ // Platform-specific totals (e.g. private resident on Mac).
+ const platformSpecific = totals.platformSpecific;
+ if (platformSpecific !== undefined) {
+ for (const [name, size] of Object.entries(platformSpecific)) {
+ let newName = name;
+ if (UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP[name] ===
+ undefined) {
+ // Change raw OS-specific total name to a friendly
+ // column title (e.g. 'private_bytes' -> 'Private').
+ if (name.endsWith(PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX)) {
+ newName = name.substring(0, name.length -
+ PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX.length);
+ }
+ newName = newName.replace('_', ' ').trim();
+ newName =
+ newName.charAt(0).toUpperCase() + newName.slice(1);
+ } else {
+ newName =
+ UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP[name];
+ }
+ sizes[newName] = new Scalar(
+ sizeInBytes_smallerIsBetter, size);
+ }
+ }
+ }
+
+ // VM regions byte stats.
+ const vmRegions = dump.mostRecentVmRegions;
+ if (vmRegions !== undefined) {
+ for (const [byteStatName, cellName] of
+ Object.entries(UsedMemoryColumn.BYTE_STAT_MAP)) {
+ const byteStat = vmRegions.byteStats[byteStatName];
+ if (byteStat === undefined) continue;
+ sizes[cellName] = new Scalar(
+ sizeInBytes_smallerIsBetter, byteStat);
+ }
+ }
+
+ return sizes;
+ });
+
+ // Allocator memory (v8, oilpan, ...).
+ const allocatorCells = tr.ui.analysis.createCells(timeToDump,
+ function(dump) {
+ const memoryAllocatorDumps = dump.memoryAllocatorDumps;
+ if (memoryAllocatorDumps === undefined) return undefined;
+
+ const sizes = {};
+ memoryAllocatorDumps.forEach(function(allocatorDump) {
+ let rootDisplayedSizeNumeric = allocatorDump.numerics[
+ DISPLAYED_SIZE_NUMERIC_NAME];
+ if (rootDisplayedSizeNumeric === undefined) {
+ rootDisplayedSizeNumeric =
+ new Scalar(sizeInBytes_smallerIsBetter, 0);
+ }
+ sizes[allocatorDump.fullName] = rootDisplayedSizeNumeric;
+ });
+ return sizes;
+ });
+
+ rows.push({
+ title: process.userFriendlyName,
+ contexts: timeToDump,
+ usedMemoryCells,
+ allocatorCells
+ });
+ }
+ return rows;
+ },
+
+ createFooterRows_(rows, columns) {
+ // Add a 'Total' row if there are at least two process memory dumps.
+ if (rows.length <= 1) return [];
+
+ const totalRow = {title: 'Total'};
+ tr.ui.analysis.aggregateTableRowCells(totalRow, rows, columns);
+
+ return [totalRow];
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new ProcessNameColumn();
+ titleColumn.width = '200px';
+
+ const usedMemorySizeColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'usedMemoryCells',
+ aggregationMode: this.aggregationMode_,
+ rules: UsedMemoryColumn.RULES
+ });
+
+ const allocatorSizeColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'allocatorCells',
+ aggregationMode: this.aggregationMode_,
+ rules: AllocatorColumn.RULES
+ });
+
+ const sizeColumns = usedMemorySizeColumns.concat(allocatorSizeColumns);
+ tr.ui.analysis.MemoryColumn.spaceEqually(sizeColumns);
+
+ const columns = [titleColumn].concat(sizeColumns);
+ return columns;
+ },
+
+ storeSelection_() {
+ let selectedRowTitle;
+ const selectedRow = this.$.table.selectedTableRow;
+ if (selectedRow !== undefined) {
+ selectedRowTitle = selectedRow.title;
+ }
+
+ let selectedColumnName;
+ const selectedColumnIndex = this.$.table.selectedColumnIndex;
+ if (selectedColumnIndex !== undefined) {
+ const selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
+ selectedColumnName = selectedColumn.name;
+ }
+
+ this.$.state.set(
+ {rowTitle: selectedRowTitle, columnName: selectedColumnName});
+ },
+
+ restoreSelection_() {
+ const settings = this.$.state.get();
+ if (settings === undefined || settings.rowTitle === undefined ||
+ settings.columnName === undefined) {
+ return;
+ }
+
+ const selectedColumnIndex = this.$.table.tableColumns.findIndex(
+ col => col.name === settings.columnName);
+ if (selectedColumnIndex === -1) return;
+
+ const selectedRowTitle = settings.rowTitle;
+ const selectedRow = this.$.table.tableRows.find(
+ row => row.title === selectedRowTitle);
+ if (selectedRow === undefined) return;
+
+ this.$.table.selectedTableRow = selectedRow;
+ this.$.table.selectedColumnIndex = selectedColumnIndex;
+ }
+ });
+
+ return {
+ // All exports are for testing only.
+ ProcessNameColumn,
+ UsedMemoryColumn,
+ PeakMemoryColumn,
+ ByteStatColumn,
+ AllocatorColumn,
+ TracingColumn,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html
new file mode 100644
index 00000000000..80127e020a8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html
@@ -0,0 +1,840 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_overview_pane.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const HeapDump = tr.model.HeapDump;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkColor = tr.ui.analysis.checkColor;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const checkColumnInfosAndColor = tr.ui.analysis.checkColumnInfosAndColor;
+ const convertToProcessMemoryDumps =
+ tr.ui.analysis.convertToProcessMemoryDumps;
+ const extractProcessMemoryDumps = tr.ui.analysis.extractProcessMemoryDumps;
+ const extractVmRegions = tr.ui.analysis.extractVmRegions;
+ const extractMemoryAllocatorDumps =
+ tr.ui.analysis.extractMemoryAllocatorDumps;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const ProcessNameColumn = tr.ui.analysis.ProcessNameColumn;
+ const UsedMemoryColumn = tr.ui.analysis.UsedMemoryColumn;
+ const PeakMemoryColumn = tr.ui.analysis.PeakMemoryColumn;
+ const ByteStatColumn = tr.ui.analysis.ByteStatColumn;
+ const AllocatorColumn = tr.ui.analysis.AllocatorColumn;
+ const TracingColumn = tr.ui.analysis.TracingColumn;
+
+ function spanMatcher(expectedTitle) {
+ return function(actualTitle) {
+ assert.instanceOf(actualTitle, HTMLElement);
+ assert.strictEqual(actualTitle.tagName, 'SPAN');
+ assert.strictEqual(Polymer.dom(actualTitle).textContent, expectedTitle);
+ };
+ }
+
+ function colorLegendMatcher(expectedTitle) {
+ return function(actualTitle) {
+ assert.instanceOf(actualTitle, HTMLElement);
+ assert.strictEqual(actualTitle.tagName, 'TR-UI-B-COLOR-LEGEND');
+ assert.strictEqual(actualTitle.label, expectedTitle);
+ };
+ }
+
+ const EXPECTED_COLUMNS = [
+ { title: 'Process', type: ProcessNameColumn, noAggregation: true },
+ { title: spanMatcher('Total resident'), type: UsedMemoryColumn },
+ { title: spanMatcher('Peak total resident'), type: PeakMemoryColumn },
+ { title: spanMatcher('PSS'), type: ByteStatColumn },
+ { title: spanMatcher('Private dirty'), type: ByteStatColumn },
+ { title: spanMatcher('Swapped'), type: ByteStatColumn },
+ { title: spanMatcher('Private'), type: UsedMemoryColumn },
+ { title: spanMatcher('Private footprint'), type: UsedMemoryColumn },
+ { title: colorLegendMatcher('blink'), type: AllocatorColumn },
+ { title: colorLegendMatcher('gpu'), type: AllocatorColumn },
+ { title: colorLegendMatcher('malloc'), type: AllocatorColumn },
+ { title: colorLegendMatcher('oilpan'), type: AllocatorColumn },
+ { title: colorLegendMatcher('v8'), type: AllocatorColumn },
+ { title: spanMatcher('tracing'), type: TracingColumn }
+ ];
+
+ function checkRow(columns, row, expectedTitle, expectedSizes,
+ expectedContexts) {
+ // Check title.
+ const formattedTitle = columns[0].formatTitle(row);
+ if (typeof expectedTitle === 'function') {
+ expectedTitle(formattedTitle);
+ } else {
+ assert.strictEqual(formattedTitle, expectedTitle);
+ }
+
+ // Check all sizes. The first assert below is a test sanity check.
+ assert.lengthOf(expectedSizes, columns.length - 1 /* all except title */);
+ for (let i = 0; i < expectedSizes.length; i++) {
+ checkSizeNumericFields(row, columns[i + 1], expectedSizes[i]);
+ }
+
+ // There should be no row nesting on the overview pane.
+ assert.isUndefined(row.subRows);
+
+ if (expectedContexts) {
+ assert.deepEqual(Array.from(row.contexts), expectedContexts);
+ } else {
+ assert.isUndefined(row.contexts);
+ }
+ }
+
+ function checkRows(columns, actualRows, expectedRows) {
+ if (expectedRows === undefined) {
+ assert.isUndefined(actualRows);
+ return;
+ }
+ assert.lengthOf(actualRows, expectedRows.length);
+ for (let i = 0; i < expectedRows.length; i++) {
+ const actualRow = actualRows[i];
+ const expectedRow = expectedRows[i];
+ checkRow(columns, actualRow, expectedRow.title, expectedRow.sizes,
+ expectedRow.contexts);
+ }
+ }
+
+ function checkSpanWithColor(span, expectedText, expectedColor) {
+ assert.strictEqual(span.tagName, 'SPAN');
+ assert.strictEqual(Polymer.dom(span).textContent, expectedText);
+ checkColor(span.style.color, expectedColor);
+ }
+
+ function checkColorLegend(legend, expectedLabel) {
+ assert.strictEqual(legend.tagName, 'TR-UI-B-COLOR-LEGEND');
+ assert.strictEqual(legend.label, expectedLabel);
+ }
+
+ function createAndCheckMemoryDumpOverviewPane(
+ test, processMemoryDumps, expectedRows, expectedFooterRows,
+ aggregationMode) {
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane');
+ viewEl.processMemoryDumps = processMemoryDumps;
+ viewEl.aggregationMode = aggregationMode;
+ viewEl.rebuild();
+ test.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ assert.isUndefined(viewEl.createChildPane());
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, aggregationMode);
+ const rows = table.tableRows;
+
+ checkRows(columns, table.tableRows, expectedRows);
+ checkRows(columns, table.footerRows, expectedFooterRows);
+ }
+
+ const FIELD = 1 << 0;
+ const DUMP = 1 << 1;
+
+ function checkOverviewColumnInfosAndColor(column, fieldAndDumpMask,
+ dumpCreatedCallback, expectedInfos, expectedColorReservedName) {
+ const fields = fieldAndDumpMask.map(function(mask, index) {
+ return mask & FIELD ?
+ new Scalar(sizeInBytes_smallerIsBetter, 1024 + 32 * index) :
+ undefined;
+ });
+
+ let contexts;
+ if (dumpCreatedCallback === undefined) {
+ contexts = undefined;
+ } else {
+ tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ fieldAndDumpMask.forEach(function(mask, i) {
+ const timestamp = 10 + i;
+ const gmd = addGlobalMemoryDump(model, {ts: timestamp});
+ if (mask & DUMP) {
+ const pmd = addProcessMemoryDump(gmd, process, {ts: timestamp});
+ dumpCreatedCallback(pmd, mask);
+ }
+ });
+ contexts = model.globalMemoryDumps.map(function(gmd) {
+ return gmd.processMemoryDumps[1];
+ });
+ });
+ }
+
+ checkColumnInfosAndColor(
+ column, fields, contexts, expectedInfos, expectedColorReservedName);
+ }
+
+ test('colorsAreDefined', function() {
+ // We use these constants in the code and the tests so here we guard
+ // against them being undefined and causing all the tests to still
+ // pass while the we end up with no colors.
+ assert.isDefined(UsedMemoryColumn.COLOR);
+ assert.isDefined(UsedMemoryColumn.OLDER_COLOR);
+ assert.isDefined(TracingColumn.COLOR);
+ });
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-overview-pane', 'processMemoryDumps',
+ function(viewEl) {
+ // Check that the info text is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.info_text));
+ assert.isFalse(isElementDisplayed(viewEl.$.table));
+ });
+ });
+
+ test('instantiate_singleGlobalMemoryDump', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ [tr.ui.analysis.createSingleTestGlobalMemoryDump()]);
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 1'),
+ sizes: [[29884416], undefined, [9437184], [5767168], undefined,
+ undefined, undefined, undefined, undefined, [7340032], undefined,
+ undefined, [2097152]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 1)
+ },
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[17825792], [39845888], [18350080], [0], [32], [8912896],
+ [15728640], [7340032], [0], [1048576], [1], [5242880],
+ [1572864]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ },
+ {
+ title: colorLegendMatcher('Process 4'),
+ sizes: [undefined, [17825792], undefined, undefined, undefined,
+ undefined, undefined, undefined, undefined, undefined,
+ undefined, undefined, undefined],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 4)
+ }
+ ],
+ [ // Footer rows.
+ {
+ title: 'Total',
+ sizes: [[47710208], [57671680], [27787264], [5767168], [32],
+ [8912896], [15728640], [7340032], [0], [8388608], [1],
+ [5242880], [3670016]],
+ contexts: undefined
+ }
+ ],
+ undefined /* no aggregation */);
+ });
+
+ test('instantiate_multipleGlobalMemoryDumps', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps());
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 1'),
+ sizes: [[31457280, 29884416, undefined], undefined,
+ [10485760, 9437184, undefined], [8388608, 5767168, undefined],
+ undefined, undefined, undefined, undefined, undefined,
+ [undefined, 7340032, undefined], undefined, undefined,
+ [undefined, 2097152, undefined]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 1)
+ },
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[19398656, 17825792, 15728640],
+ [40370176, 39845888, 40894464], [18350080, 18350080, 18350080],
+ [0, 0, -2621440], [32, 32, 64], [10485760, 8912896, 7340032],
+ [15728640, 15728640, 15728640], [undefined, 7340032, 6291456],
+ [undefined, 0, 1048576], [2097152, 1048576, 786432],
+ [undefined, 1, undefined], [5242880, 5242880, 5767168],
+ [1048576, 1572864, 2097152]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ },
+ {
+ title: colorLegendMatcher('Process 3'),
+ sizes: [undefined, undefined, undefined, undefined, undefined,
+ undefined, undefined, undefined, undefined, undefined,
+ [2147483648, undefined, 1073741824],
+ [1073741824, undefined, 2147483648], undefined],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 3)
+ },
+ {
+ title: colorLegendMatcher('Process 4'),
+ sizes: [undefined, [undefined, 17825792, 17825792], undefined,
+ undefined, undefined, undefined, undefined, undefined,
+ undefined, undefined, undefined, undefined, undefined],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 4)
+ }
+ ],
+ [ // Footer rows.
+ {
+ title: 'Total',
+ sizes: [[50855936, 47710208, 15728640],
+ [40370176, 57671680, 58720256], [28835840, 27787264, 18350080],
+ [8388608, 5767168, -2621440], [32, 32, 64],
+ [10485760, 8912896, 7340032], [15728640, 15728640, 15728640],
+ [undefined, 7340032, 6291456], [undefined, 0, 1048576],
+ [2097152, 8388608, 786432], [2147483648, 1, 1073741824],
+ [1078984704, 5242880, 2153250816],
+ [1048576, 3670016, 2097152]],
+ contexts: undefined
+ }
+ ],
+ AggregationMode.DIFF);
+ });
+
+ test('instantiate_singleProcessMemoryDump', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()]);
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[17825792], [39845888], [18350080], [0], [32], [8912896],
+ [15728640], [7340032], [0], [1048576], [1], [5242880],
+ [1572864]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ }
+ ],
+ [] /* footer rows */,
+ undefined /* no aggregation */);
+ });
+
+ test('instantiate_multipleProcessMemoryDumps', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestProcessMemoryDumps());
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[19398656, 17825792, 15728640],
+ [40370176, 39845888, 40894464], [18350080, 18350080, 18350080],
+ [0, 0, -2621440], [32, 32, 64], [10485760, 8912896, 7340032],
+ [15728640, 15728640, 15728640], [undefined, 7340032, 6291456],
+ [undefined, 0, 1048576], [2097152, 1048576, 786432],
+ [undefined, 1, undefined], [5242880, 5242880, 5767168],
+ [1048576, 1572864, 2097152]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ }
+ ],
+ [] /* footer rows */,
+ AggregationMode.MAX);
+ });
+
+ test('selection', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps());
+
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane');
+ viewEl.processMemoryDumps = processMemoryDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ const table = viewEl.$.table;
+
+ // Simulate clicking on the 'malloc' cell of the second process.
+ table.selectedTableRow = table.tableRows[1];
+ table.selectedColumnIndex = 10;
+ assert.lengthOf(viewEl.requestedChildPanes, 2);
+ let lastChildPane = viewEl.requestedChildPanes[1];
+ assert.strictEqual(
+ lastChildPane.tagName, 'TR-UI-A-MEMORY-DUMP-ALLOCATOR-DETAILS-PANE');
+ assert.strictEqual(lastChildPane.aggregationMode, AggregationMode.DIFF);
+ assert.deepEqual(lastChildPane.memoryAllocatorDumps,
+ extractMemoryAllocatorDumps(processMemoryDumps, 2, 'malloc'));
+
+ // Simulate clicking on the 'Oilpan' cell of the second process.
+ table.selectedColumnIndex = 10;
+ assert.lengthOf(viewEl.requestedChildPanes, 3);
+ lastChildPane = viewEl.requestedChildPanes[2];
+ assert.isUndefined(viewEl.lastChildPane);
+ });
+
+ test('memory', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps());
+ const containerEl = document.createElement('div');
+ containerEl.brushingStateController =
+ new tr.c.BrushingStateController(undefined);
+
+ function simulateView(pids, aggregationMode,
+ expectedSelectedCellFieldValues, expectedSelectedRowTitle,
+ expectedSelectedColumnIndex, callback) {
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane');
+ const table = viewEl.$.table;
+ Polymer.dom(containerEl).textContent = '';
+ Polymer.dom(containerEl).appendChild(viewEl);
+
+ const displayedProcessMemoryDumps = processMemoryDumps.map(
+ function(memoryDumps) {
+ const result = {};
+ for (const [pid, pmd] of Object.entries(memoryDumps)) {
+ if (pids.includes(pmd.process.pid)) result[pid] = pmd;
+ }
+ return result;
+ });
+ viewEl.processMemoryDumps = displayedProcessMemoryDumps;
+ viewEl.aggregationMode = aggregationMode;
+ viewEl.rebuild();
+
+ if (expectedSelectedCellFieldValues === undefined) {
+ assert.isUndefined(viewEl.childPaneBuilder);
+ } else {
+ checkSizeNumericFields(table.selectedTableRow,
+ table.tableColumns[table.selectedColumnIndex],
+ expectedSelectedCellFieldValues);
+ }
+
+ assert.strictEqual(
+ table.selectedColumnIndex, expectedSelectedColumnIndex);
+ if (expectedSelectedRowTitle === undefined) {
+ assert.isUndefined(table.selectedTableRow);
+ } else {
+ assert.strictEqual(
+ table.selectedTableRow.title, expectedSelectedRowTitle);
+ }
+
+ callback(viewEl, viewEl.$.table);
+ }
+
+ simulateView(
+ [1, 2, 3, 4], // All processes.
+ AggregationMode.DIFF,
+ undefined, undefined, undefined, // No cell should be selected.
+ function(view, table) {
+ assert.isUndefined(view.createChildPane());
+
+ // Select the 'PSS' column of the second process.
+ table.selectedTableRow = table.tableRows[1];
+ table.selectedColumnIndex = 3;
+ });
+
+ simulateView(
+ [2, 3],
+ AggregationMode.MAX,
+ [18350080, 18350080, 18350080], 'Process 2', 3, /* PSS */
+ function(view, table) {
+ const childPane = view.createChildPane();
+ assert.strictEqual(
+ childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE');
+ assert.deepEqual(Array.from(childPane.vmRegions),
+ extractVmRegions(processMemoryDumps, 2));
+ assert.strictEqual(childPane.aggregationMode, AggregationMode.MAX);
+ });
+
+ simulateView(
+ [3],
+ undefined, /* No aggregation */
+ undefined, undefined, undefined, // No cell selected.
+ function(view, table) {
+ assert.isUndefined(view.createChildPane());
+ });
+
+ simulateView(
+ [1, 2, 3, 4],
+ AggregationMode.DIFF,
+ [18350080, 18350080, 18350080], 'Process 2', 3, /* PSS */
+ function(view, table) {
+ const childPane = view.createChildPane();
+ assert.strictEqual(
+ childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE');
+ assert.deepEqual(Array.from(childPane.vmRegions),
+ extractVmRegions(processMemoryDumps, 2));
+ assert.strictEqual(childPane.aggregationMode, AggregationMode.DIFF);
+
+ // Select the 'v8' column of the first process (empty cell).
+ table.selectedTableRow = table.tableRows[0];
+ table.selectedColumnIndex = 11;
+ });
+
+ simulateView(
+ [1],
+ undefined, /* No aggregation */
+ undefined, undefined, undefined, // No cell should selected.
+ function(view, table) {
+ assert.isUndefined(view.createChildPane());
+
+ // Select 'Total resident' column of the first process.
+ table.selectedTableRow = table.tableRows[0];
+ table.selectedColumnIndex = 1;
+ });
+
+ simulateView(
+ [1, 2, 3, 4],
+ AggregationMode.MAX,
+ [31457280, 29884416, undefined], 'Process 1', 1, /* Total resident */
+ function(view, table) {
+ const childPane = view.createChildPane();
+ assert.strictEqual(
+ childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE');
+ assert.deepEqual(Array.from(childPane.vmRegions),
+ extractVmRegions(processMemoryDumps, 1));
+ assert.strictEqual(childPane.aggregationMode, AggregationMode.MAX);
+ });
+ });
+
+ test('processNameColumn_formatTitle', function() {
+ const c = new ProcessNameColumn();
+
+ // With context (total row).
+ assert.strictEqual(c.formatTitle({
+ title: 'Total',
+ usedMemoryCells: {}
+ }), 'Total');
+
+ // Without context (process row).
+ const title = c.formatTitle({
+ title: 'Process 1',
+ usedMemoryCells: {},
+ contexts: [tr.ui.analysis.createSingleTestProcessMemoryDump()]
+ });
+ checkColorLegend(title, 'Process 1');
+ });
+
+ test('usedMemoryColumn', function() {
+ const c = new UsedMemoryColumn('Private', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ checkSpanWithColor(c.title, 'Private',
+ UsedMemoryColumn.COLOR /* blue (column title) */);
+ checkColor(c.color(undefined /* contexts */),
+ UsedMemoryColumn.COLOR /* blue (column cells) */);
+ });
+
+ test('peakMemoryColumn', function() {
+ const c = new PeakMemoryColumn('Peak', 'bytes', (x => x),
+ AggregationMode.MAX);
+ checkSpanWithColor(c.title, 'Peak',
+ UsedMemoryColumn.COLOR /* blue (column title) */);
+ checkColor(c.color(undefined) /* contexts */,
+ UsedMemoryColumn.COLOR /* blue (column cells) */);
+
+ const RESETTABLE_PEAK = 1 << 2;
+ const NON_RESETTABLE_PEAK = 1 << 3;
+ function checkPeakColumnInfosAndColor(fieldAndDumpMask, expectedInfos) {
+ checkOverviewColumnInfosAndColor(c,
+ fieldAndDumpMask,
+ function(pmd, mask) {
+ if (mask & RESETTABLE_PEAK) {
+ assert.strictEqual(
+ mask & NON_RESETTABLE_PEAK, 0); // Test sanity check.
+ pmd.arePeakResidentBytesResettable = true;
+ } else if (mask & NON_RESETTABLE_PEAK) {
+ pmd.arePeakResidentBytesResettable = false;
+ }
+ },
+ expectedInfos,
+ UsedMemoryColumn.COLOR);
+ }
+
+ // No context.
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD, FIELD, 0, FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+
+ // All resettable.
+ const EXPECTED_RESETTABLE_INFO = {
+ icon: '\u21AA',
+ message: 'Peak RSS since previous memory dump.'
+ };
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | RESETTABLE_PEAK
+ ], [EXPECTED_RESETTABLE_INFO]);
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | RESETTABLE_PEAK,
+ DUMP /* ignored because there's no field */,
+ 0,
+ FIELD | DUMP | RESETTABLE_PEAK
+ ], [EXPECTED_RESETTABLE_INFO]);
+
+ // All non-resettable.
+ const EXPECTED_NON_RESETTABLE_INFO = {
+ icon: '\u21A6',
+ message: 'Peak RSS since process startup. Finer grained peaks require ' +
+ 'a Linux kernel version \u2265 4.0.'
+ };
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | NON_RESETTABLE_PEAK
+ ], [EXPECTED_NON_RESETTABLE_INFO]);
+ checkPeakColumnInfosAndColor([
+ 0,
+ DUMP | RESETTABLE_PEAK /* ignored because there's no field */,
+ FIELD | DUMP | NON_RESETTABLE_PEAK,
+ FIELD | DUMP | NON_RESETTABLE_PEAK
+ ], [EXPECTED_NON_RESETTABLE_INFO]);
+
+ // Combination (warning).
+ const EXPECTED_COMBINATION_INFO = {
+ icon: '\u26A0',
+ message: 'Both resettable and non-resettable peak RSS values were ' +
+ 'provided by the process',
+ color: 'red'
+ };
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | NON_RESETTABLE_PEAK,
+ 0,
+ FIELD | DUMP | RESETTABLE_PEAK,
+ 0
+ ], [EXPECTED_COMBINATION_INFO]);
+ });
+
+ test('byteStatColumn', function() {
+ const c = new ByteStatColumn('Stat', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ checkSpanWithColor(c.title, 'Stat',
+ UsedMemoryColumn.COLOR /* blue (column title) */);
+
+ const HAS_OWN_VM_REGIONS = 1 << 2;
+ function checkByteStatColumnInfosAndColor(
+ fieldAndDumpMask, expectedInfos, expectedIsOlderColor) {
+ checkOverviewColumnInfosAndColor(c,
+ fieldAndDumpMask,
+ function(pmd, mask) {
+ if (mask & HAS_OWN_VM_REGIONS) {
+ pmd.vmRegions = [];
+ }
+ },
+ expectedInfos,
+ expectedIsOlderColor ?
+ UsedMemoryColumn.OLDER_COLOR /* light blue */ :
+ UsedMemoryColumn.COLOR /* blue color */);
+ }
+
+ const EXPECTED_ALL_OLDER_VALUES = {
+ icon: '\u26AF',
+ message: 'Older value (only heavy (purple) memory dumps contain ' +
+ 'memory maps).'
+ };
+ const EXPECTED_SOME_OLDER_VALUES = {
+ icon: '\u26AF',
+ message: 'Older value at some selected timestamps (only heavy ' +
+ '(purple) memory dumps contain memory maps).'
+ };
+
+ // No context.
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD, FIELD, 0, FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+
+ // All process memory dumps have own VM regions.
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP | HAS_OWN_VM_REGIONS
+ ], [] /* no infos */, false /* blue color */);
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP | HAS_OWN_VM_REGIONS,
+ FIELD | DUMP | HAS_OWN_VM_REGIONS,
+ 0,
+ FIELD | DUMP | HAS_OWN_VM_REGIONS
+ ], [] /* no infos */, false /* blue color */);
+
+ // No process memory dumps have own VM regions.
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP
+ ], [EXPECTED_ALL_OLDER_VALUES], true /* light blue */);
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP,
+ FIELD | DUMP
+ ], [EXPECTED_ALL_OLDER_VALUES], true /* light blue */);
+
+ // Some process memory dumps don't have own VM regions.
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP,
+ 0,
+ FIELD | DUMP
+ ], [EXPECTED_SOME_OLDER_VALUES], true /* light blue */);
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP | HAS_OWN_VM_REGIONS,
+ FIELD | DUMP,
+ FIELD | DUMP | HAS_OWN_VM_REGIONS
+ ], [EXPECTED_SOME_OLDER_VALUES], false /* blue */);
+ });
+
+ test('allocatorColumn', function() {
+ const c = new AllocatorColumn('Allocator', 'bytes', (x => x),
+ AggregationMode.MAX);
+ checkColorLegend(c.title, 'Allocator');
+ checkColor(c.color(undefined /* contexts */),
+ undefined /* no color (column cells) */);
+
+ const HAS_HEAP_DUMPS = 1 << 2;
+ const HAS_ALLOCATOR_HEAP_DUMP = 1 << 3;
+ const MISSING_SIZE = 1 << 4;
+ function checkAllocatorColumnInfosAndColor(fieldAndDumpMask,
+ expectedInfos) {
+ checkOverviewColumnInfosAndColor(c,
+ fieldAndDumpMask,
+ function(pmd, mask) {
+ if (mask & HAS_HEAP_DUMPS) {
+ pmd.heapDumps = {};
+ }
+ if (mask & HAS_ALLOCATOR_HEAP_DUMP) {
+ pmd.heapDumps.Allocator = new HeapDump(pmd, 'Allocator');
+ }
+ const mad = new MemoryAllocatorDump(pmd, 'Allocator');
+ if (!(mask & MISSING_SIZE)) {
+ mad.addNumeric('size',
+ new Scalar(sizeInBytes_smallerIsBetter, 7));
+ }
+ pmd.memoryAllocatorDumps = [mad];
+ },
+ expectedInfos,
+ undefined /* no color */);
+ }
+
+ // No context.
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD, FIELD, 0, FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // No infos.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP
+ ], [] /* no infos */);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS,
+ 0,
+ FIELD | DUMP
+ ], [] /* infos */);
+
+ const EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP = {
+ icon: '\u2630',
+ message: 'Heap dump provided.'
+ };
+ const EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP = {
+ icon: '\u2630',
+ message: 'Heap dump provided at some selected timestamps.'
+ };
+
+ // All process memory dumps have heap dumps.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP]);
+
+ // Some process memory dumps have heap dumps.
+ checkAllocatorColumnInfosAndColor([
+ 0,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP]);
+
+ const EXPECTED_ALL_MISSING_SIZE = {
+ icon: '\u26A0',
+ message: 'Size was not provided.',
+ color: 'red'
+ };
+ const EXPECTED_SOME_MISSING_SIZE = {
+ icon: '\u26A0',
+ message: 'Size was not provided at some selected timestamps.',
+ color: 'red'
+ };
+
+ // All process memory dumps are missing allocator size.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_ALL_MISSING_SIZE]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE,
+ FIELD | DUMP | MISSING_SIZE,
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_ALL_MISSING_SIZE]);
+
+ // Some process memory dumps use Android memtrack PSS fallback.
+ checkAllocatorColumnInfosAndColor([
+ 0,
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_SOME_MISSING_SIZE]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE,
+ FIELD | DUMP,
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_SOME_MISSING_SIZE]);
+
+ // Combination of heap dump and memtrack fallback infos.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE | HAS_HEAP_DUMPS |
+ HAS_ALLOCATOR_HEAP_DUMP
+ ], [
+ EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP,
+ EXPECTED_ALL_MISSING_SIZE
+ ]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP,
+ FIELD | DUMP | MISSING_SIZE
+ ], [
+ EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP,
+ EXPECTED_SOME_MISSING_SIZE
+ ]);
+ });
+
+ test('tracingColumn', function() {
+ const c = new TracingColumn('Tracing', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ checkSpanWithColor(c.title, 'Tracing',
+ TracingColumn.COLOR /* expected column title gray color */);
+ checkColor(c.color(undefined /* contexts */),
+ TracingColumn.COLOR /* expected column cells gray color */);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html
new file mode 100644
index 00000000000..2f702242140
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html
@@ -0,0 +1,593 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for memory dump analysis sub-view tests.
+ */
+tr.exportTo('tr.ui.analysis', function() {
+ const Color = tr.b.Color;
+ const ColorScheme = tr.b.ColorScheme;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const HeapDump = tr.model.HeapDump;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+
+ function createMultipleTestGlobalMemoryDumps() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const pA = model.getOrCreateProcess(1);
+ const pB = model.getOrCreateProcess(2);
+ const pC = model.getOrCreateProcess(3);
+ const pD = model.getOrCreateProcess(4);
+
+ // ======================================================================
+ // First timestamp.
+ // ======================================================================
+ const gmd1 = addGlobalMemoryDump(model, {ts: 42});
+
+ // Totals and VM regions.
+ const pmd1A = addProcessMemoryDump(gmd1, pA, {ts: 41});
+ pmd1A.totals = {residentBytes: 31457280 /* 30 MiB */};
+ pmd1A.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 1024,
+ sizeInBytes: 20971520, /* 20 MiB */
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack]',
+ byteStats: {
+ privateDirtyResident: 8388608, /* 8 MiB */
+ sharedCleanResident: 12582912, /* 12 MiB */
+ proportionalResident: 10485760 /* 10 MiB */
+ }
+ })
+ ]);
+
+ // Everything.
+ const pmd1B = addProcessMemoryDump(gmd1, pB, {ts: 42});
+ pmd1B.totals = {
+ residentBytes: 20971520, /* 20 MiB */
+ peakResidentBytes: 41943040, /* 40 MiB */
+ arePeakResidentBytesResettable: false,
+ privateFootprintBytes: 15728640, /* 15 MiB */
+ platformSpecific: {
+ private_bytes: 10485760 /* 10 MiB */
+ }
+ };
+ pmd1B.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 256,
+ sizeInBytes: 6000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ mappedFile: '[stack:20310]',
+ byteStats: {
+ proportionalResident: 15728640, /* 15 MiB */
+ privateDirtyResident: 1572864, /* 1.5 MiB */
+ swapped: 32 /* 32 B */
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 100000,
+ sizeInBytes: 4096,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '/usr/lib/libwtf.so',
+ byteStats: {
+ proportionalResident: 4194304, /* 4 MiB */
+ privateDirtyResident: 0,
+ swapped: 0 /* 32 B */
+ }
+ })
+ ]);
+ pmd1B.memoryAllocatorDumps = [
+ newAllocatorDump(pmd1B, 'malloc',
+ {numerics: {size: 3145728 /* 3 MiB */}}),
+ newAllocatorDump(pmd1B, 'v8', {numerics: {size: 5242880 /* 5 MiB */}}),
+ newAllocatorDump(pmd1B, 'tracing', {numerics: {
+ size: 1048576 /* 1 MiB */,
+ resident_size: 1572864 /* 1.5 MiB */
+ }})
+ ];
+
+ // Allocator dumps only.
+ const pmd1C = addProcessMemoryDump(gmd1, pC, {ts: 43});
+ pmd1C.memoryAllocatorDumps = (function() {
+ const oilpanDump = newAllocatorDump(pmd1C, 'oilpan', {numerics: {
+ size: 3221225472 /* 3 GiB */,
+ inner_size: 5242880 /* 5 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 2015)
+ }});
+ const v8Dump = newAllocatorDump(pmd1C, 'v8', {numerics: {
+ size: 1073741824 /* 1 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ addOwnershipLink(v8Dump, oilpanDump);
+
+ return [oilpanDump, v8Dump];
+ })();
+ pmd1C.heapDumps = {
+ 'v8': (function() {
+ const v8HeapDump = new HeapDump(pmd1C, 'v8');
+ v8HeapDump.addEntry(
+ tr.c.TestUtils.newStackTrace(model,
+ ['V8.Execute', 'UpdateLayoutTree']),
+ undefined /* sum over all object types */,
+ 536870912 /* 512 MiB */);
+ return v8HeapDump;
+ })()
+ };
+
+ // ======================================================================
+ // Second timestamp.
+ // ======================================================================
+ const gmd2 = addGlobalMemoryDump(model, {ts: 68});
+
+ // Everything.
+ const pmd2A = addProcessMemoryDump(gmd2, pA, {ts: 67});
+ pmd2A.totals = {residentBytes: 32505856 /* 31 MiB */};
+ pmd2A.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 1024,
+ sizeInBytes: 20971520, /* 20 MiB */
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack]',
+ byteStats: {
+ privateDirtyResident: 8388608, /* 8 MiB */
+ sharedCleanResident: 11534336, /* 11 MiB */
+ proportionalResident: 11534336 /* 11 MiB */
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 104857600,
+ sizeInBytes: 5242880, /* 5 MiB */
+ protectionFlags: VMRegion.PROTECTION_FLAG_EXECUTE,
+ mappedFile: '/usr/bin/google-chrome',
+ byteStats: {
+ privateDirtyResident: 0,
+ sharedCleanResident: 4194304, /* 4 MiB */
+ proportionalResident: 524288 /* 512 KiB */
+ }
+ })
+ ]);
+ pmd2A.memoryAllocatorDumps = [
+ newAllocatorDump(pmd2A, 'malloc', {numerics: {
+ size: 9437184 /* 9 MiB */
+ }}),
+ newAllocatorDump(pmd2A, 'tracing', {numerics: {
+ size: 2097152 /* 2 MiB */,
+ resident_size: 2621440 /* 2.5 MiB */
+ }})
+ ];
+
+ // Totals and allocator dumps only.
+ const pmd2B = addProcessMemoryDump(gmd2, pB, {ts: 69});
+ pmd2B.totals = {
+ residentBytes: 19922944, /* 19 MiB */
+ peakResidentBytes: 41943040, /* 40 MiB */
+ arePeakResidentBytesResettable: false,
+ privateFootprintBytes: 15728640, /* 15 MiB */
+ platformSpecific: {
+ private_bytes: 8912896 /* 8.5 MiB */
+ }
+ };
+ pmd2B.memoryAllocatorDumps = [
+ newAllocatorDump(pmd2B, 'malloc', {numerics: {
+ size: 2621440 /* 2.5 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'v8', {numerics: {
+ size: 5242880 /* 5 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'blink', {numerics: {
+ size: 7340032 /* 7 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'oilpan', {numerics: {size: 1}}),
+ newAllocatorDump(pmd2B, 'tracing', {numerics: {
+ size: 1572864 /* 1.5 MiB */,
+ resident_size: 2097152 /* 2 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'gpu', {numerics: {
+ memtrack_pss: 524288 /* 512 KiB */
+ }})
+ ];
+
+ // Resettable peak total size only.
+ const pmd2D = addProcessMemoryDump(gmd2, pD, {ts: 71});
+ pmd2D.totals = {
+ peakResidentBytes: 17825792, /* 17 MiB */
+ arePeakResidentBytesResettable: true
+ };
+
+ // ======================================================================
+ // Third timestamp.
+ // ======================================================================
+ const gmd3 = addGlobalMemoryDump(model, {ts: 100});
+
+ // Everything.
+ const pmd3B = addProcessMemoryDump(gmd3, pB, {ts: 102});
+ pmd3B.totals = {
+ residentBytes: 18874368, /* 18 MiB */
+ peakResidentBytes: 44040192, /* 42 MiB */
+ privateFootprintBytes: 15728640, /* 16 MiB */
+ arePeakResidentBytesResettable: false,
+ platformSpecific: {
+ private_bytes: 7340032 /* 7 MiB */
+ }
+ };
+ pmd3B.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 256,
+ sizeInBytes: 6000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ mappedFile: '[stack:20310]',
+ byteStats: {
+ proportionalResident: 21495808, /* 20.5 MiB */
+ privateDirtyResident: 524288, /* 0.5 MiB */
+ swapped: 64 /* 32 B */
+ }
+ })
+ ]);
+ pmd3B.memoryAllocatorDumps = [
+ newAllocatorDump(pmd3B, 'malloc', {numerics: {
+ size: 2883584 /* 2.75 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'v8', {numerics: {
+ size: 5767168 /* 5.5 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'blink', {numerics: {
+ size: 6291456 /* 7 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'tracing', {numerics: {
+ size: 2097152 /* 2 MiB */,
+ resident_size: 3145728 /* 3 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'gpu', {numerics: {
+ size: 1048576 /* 1 MiB */,
+ memtrack_pss: 786432 /* 768 KiB */
+ }})
+ ];
+
+ // Allocator dumps only.
+ const pmd3C = addProcessMemoryDump(gmd3, pC, {ts: 100});
+ pmd3C.memoryAllocatorDumps = (function() {
+ const oilpanDump = newAllocatorDump(pmd3C, 'oilpan', {numerics: {
+ size: 3221225472 /* 3 GiB */,
+ inner_size: 5242880 /* 5 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 2015)
+ }});
+ const v8Dump = newAllocatorDump(pmd3C, 'v8', {numerics: {
+ size: 2147483648 /* 2 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ addOwnershipLink(v8Dump, oilpanDump);
+
+ return [oilpanDump, v8Dump];
+ })();
+ pmd3C.heapDumps = {
+ 'v8': (function() {
+ const v8HeapDump = new HeapDump(pmd1C, 'v8');
+ v8HeapDump.addEntry(
+ tr.c.TestUtils.newStackTrace(model,
+ ['V8.Execute', 'UpdateLayoutTree']),
+ undefined /* sum over all object types */,
+ 268435456 /* 256 MiB */);
+ v8HeapDump.addEntry(
+ tr.c.TestUtils.newStackTrace(model,
+ ['V8.Execute', 'FrameView::layout']),
+ undefined /* sum over all object types */,
+ 134217728 /* 128 MiB */);
+ return v8HeapDump;
+ })()
+ };
+
+ // Resettable peak total size only.
+ const pmd3D = addProcessMemoryDump(gmd3, pD, {ts: 99});
+ pmd3D.totals = {
+ peakResidentBytes: 17825792, /* 17 MiB */
+ arePeakResidentBytesResettable: true
+ };
+ });
+
+ return model.globalMemoryDumps;
+ }
+
+ function createSingleTestGlobalMemoryDump() {
+ return createMultipleTestGlobalMemoryDumps()[1];
+ }
+
+ function createMultipleTestProcessMemoryDumps() {
+ return createMultipleTestGlobalMemoryDumps().map(function(gmd) {
+ return gmd.processMemoryDumps[2];
+ });
+ }
+
+ function createSingleTestProcessMemoryDump() {
+ return createMultipleTestProcessMemoryDumps()[1];
+ }
+
+ function checkNumericFields(row, column, expectedValues, expectedUnit) {
+ let fields;
+ if (column === undefined) {
+ fields = row;
+ } else {
+ fields = column.fields(row);
+ }
+
+ if (expectedValues === undefined) {
+ assert.isUndefined(fields);
+ return;
+ }
+
+ assert.lengthOf(fields, expectedValues.length);
+ for (let i = 0; i < fields.length; i++) {
+ const field = fields[i];
+ const expectedValue = expectedValues[i];
+ if (expectedValue === undefined) {
+ assert.isUndefined(field);
+ } else {
+ assert.isDefined(expectedUnit); // Test sanity check.
+ assert.instanceOf(field, Scalar);
+ assert.strictEqual(field.value, expectedValue);
+ assert.strictEqual(field.unit, expectedUnit);
+ }
+ }
+ }
+
+ function checkSizeNumericFields(row, column, expectedValues) {
+ checkNumericFields(row, column, expectedValues,
+ sizeInBytes_smallerIsBetter);
+ }
+
+ function checkStringFields(row, column, expectedStrings) {
+ const fields = column.fields(row);
+
+ if (expectedStrings === undefined) {
+ assert.isUndefined(fields);
+ return;
+ }
+
+ assert.deepEqual(Array.from(fields), expectedStrings);
+ }
+
+ /**
+ * Check the titles, types and aggregation modes of a list of columns.
+ * expectedColumns is a list of dictionaries with the following fields:
+ *
+ * - title: Either the expected title (string), or a matcher for it
+ * (function that accepts the actual title as its argument).
+ * - type: The expected class of the column.
+ * - noAggregation: If true, the column is expected to have no aggregation
+ * mode (regardless of expectedAggregationMode).
+ */
+ function checkColumns(columns, expectedColumns, expectedAggregationMode) {
+ assert.lengthOf(columns, expectedColumns.length);
+ for (let i = 0; i < expectedColumns.length; i++) {
+ const actualColumn = columns[i];
+ const expectedColumn = expectedColumns[i];
+ const expectedTitle = expectedColumn.title;
+ if (typeof expectedTitle === 'function') {
+ expectedTitle(actualColumn.title); // Custom title matcher.
+ } else if (actualColumn.title.innerText) {
+ // HTML title.
+ assert.strictEqual(actualColumn.title.innerText, expectedTitle);
+ } else {
+ assert.strictEqual(actualColumn.title, expectedTitle); // String title.
+ }
+ assert.instanceOf(actualColumn, expectedColumn.type);
+ assert.strictEqual(actualColumn.aggregationMode,
+ expectedColumn.noAggregation ? undefined : expectedAggregationMode);
+ }
+ }
+
+ function checkColumnInfosAndColor(
+ column, fields, contexts, expectedInfos, expectedColorReservedName) {
+ // Test sanity checks.
+ assert.isDefined(fields);
+ if (contexts !== undefined) {
+ assert.lengthOf(contexts, fields.length);
+ }
+
+ // Check infos.
+ const infos = [];
+ column.addInfos(fields, contexts, infos);
+ assert.lengthOf(infos, expectedInfos.length);
+ for (let i = 0; i < expectedInfos.length; i++) {
+ assert.deepEqual(infos[i], expectedInfos[i]);
+ }
+
+ // Check color.
+ const actualColor = typeof column.color === 'function' ?
+ column.color(fields, contexts) :
+ column.color;
+ checkColor(actualColor, expectedColorReservedName);
+ }
+
+ function checkColor(actualColorString, expectedColorString) {
+ if (actualColorString === undefined) {
+ assert.isUndefined(expectedColorString);
+ return;
+ }
+ const actualColor = Color.fromString(actualColorString);
+ const expectedColor = Color.fromString(expectedColorString);
+ assert.deepEqual(actualColor, expectedColor);
+ }
+
+ function createAndCheckEmptyPanes(
+ test, paneTagName, propertyName, opt_callback) {
+ // Unset property.
+ const unsetViewEl = createTestPane(paneTagName);
+ unsetViewEl.rebuild();
+ assert.isUndefined(unsetViewEl.createChildPane());
+ test.addHTMLOutput(unsetViewEl);
+
+ // Undefined property.
+ const undefinedViewEl = createTestPane(paneTagName);
+ undefinedViewEl[propertyName] = undefined;
+ undefinedViewEl.rebuild();
+ assert.isUndefined(undefinedViewEl.createChildPane());
+ test.addHTMLOutput(undefinedViewEl);
+
+ // Empty property.
+ const emptyViewEl = createTestPane(paneTagName);
+ emptyViewEl[propertyName] = [];
+ emptyViewEl.rebuild();
+ assert.isUndefined(undefinedViewEl.createChildPane());
+ test.addHTMLOutput(emptyViewEl);
+
+ // Check that all the panes have the same dimensions.
+ const unsetBounds = unsetViewEl.getBoundingClientRect();
+ const undefinedBounds = undefinedViewEl.getBoundingClientRect();
+ const emptyBounds = emptyViewEl.getBoundingClientRect();
+ assert.strictEqual(undefinedBounds.width, unsetBounds.width);
+ assert.strictEqual(emptyBounds.width, unsetBounds.width);
+ assert.strictEqual(undefinedBounds.height, unsetBounds.height);
+ assert.strictEqual(emptyBounds.height, unsetBounds.height);
+
+ // Custom checks (if provided).
+ if (opt_callback) {
+ opt_callback(unsetViewEl);
+ opt_callback(undefinedViewEl);
+ opt_callback(emptyViewEl);
+ }
+ }
+
+ function createTestPane(tagName) {
+ const paneEl = document.createElement(tagName);
+
+ // Store a list of requested child panes (for inspection in tests).
+ paneEl.requestedChildPanes = [];
+ paneEl.addEventListener('request-child-pane-change', function() {
+ paneEl.requestedChildPanes.push(paneEl.createChildPane());
+ });
+
+ paneEl.createChildPane = function() {
+ const childPaneBuilder = this.childPaneBuilder;
+ if (childPaneBuilder === undefined) return undefined;
+ return childPaneBuilder();
+ };
+
+ return paneEl;
+ }
+
+ // TODO(petrcermak): Consider moving this to tracing/ui/base/dom_helpers.html.
+ function isElementDisplayed(element) {
+ const style = getComputedStyle(element);
+ const displayed = style.display;
+ if (displayed === undefined) return true;
+ return displayed.indexOf('none') === -1;
+ }
+
+ /**
+ * Convert a list of ContainerMemoryDump(s) to a list of dictionaries of the
+ * underlying ProcessMemoryDump(s).
+ */
+ function convertToProcessMemoryDumps(containerMemoryDumps) {
+ return containerMemoryDumps.map(function(containerMemoryDump) {
+ return containerMemoryDump.processMemoryDumps;
+ });
+ }
+
+ /**
+ * Extract a chronological list of ProcessMemoryDump(s) (for a given process)
+ * from a chronological list of dictionaries of ProcessMemoryDump(s).
+ */
+ function extractProcessMemoryDumps(processMemoryDumps, pid) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ return memoryDumps[pid];
+ });
+ }
+
+ /**
+ * Extract a chronological list of lists of VMRegion(s) (for a given process)
+ * from a chronological list of dictionaries of ProcessMemoryDump(s).
+ */
+ function extractVmRegions(processMemoryDumps, pid) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ const processMemoryDump = memoryDumps[pid];
+ if (processMemoryDump === undefined) return undefined;
+ return processMemoryDump.mostRecentVmRegions;
+ });
+ }
+
+ /**
+ * Extract a chronological list of MemoryAllocatorDump(s) (for a given
+ * process and allocator name) from a chronological list of dictionaries of
+ * ProcessMemoryDump(s).
+ */
+ function extractMemoryAllocatorDumps(processMemoryDumps, pid, allocatorName) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ const processMemoryDump = memoryDumps[pid];
+ if (processMemoryDump === undefined) return undefined;
+ return processMemoryDump.getMemoryAllocatorDumpByFullName(allocatorName);
+ });
+ }
+
+ /**
+ * Extract a chronological list of HeapDump(s) (for a given process and
+ * allocator name) from a chronological list of dictionaries of
+ * ProcessMemoryDump(s).
+ */
+ function extractHeapDumps(processMemoryDumps, pid, allocatorName) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ const processMemoryDump = memoryDumps[pid];
+ if (processMemoryDump === undefined ||
+ processMemoryDump.heapDumps === undefined) {
+ return undefined;
+ }
+ return processMemoryDump.heapDumps[allocatorName];
+ });
+ }
+
+ return {
+ createSingleTestGlobalMemoryDump,
+ createMultipleTestGlobalMemoryDumps,
+ createSingleTestProcessMemoryDump,
+ createMultipleTestProcessMemoryDumps,
+ checkNumericFields,
+ checkSizeNumericFields,
+ checkStringFields,
+ checkColumns,
+ checkColumnInfosAndColor,
+ checkColor,
+ createAndCheckEmptyPanes,
+ createTestPane,
+ isElementDisplayed,
+ convertToProcessMemoryDumps,
+ extractProcessMemoryDumps,
+ extractVmRegions,
+ extractMemoryAllocatorDumps,
+ extractHeapDumps,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html
new file mode 100644
index 00000000000..654ce0ea73f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html
@@ -0,0 +1,915 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper code for memory dump sub-views.
+ */
+tr.exportTo('tr.ui.analysis', function() {
+ const NO_BREAK_SPACE = String.fromCharCode(160);
+ const RIGHTWARDS_ARROW = String.fromCharCode(8594);
+
+ const COLLATOR = new Intl.Collator(undefined, {numeric: true});
+
+ /**
+ * A table column for displaying memory dump row titles.
+ *
+ * @constructor
+ */
+ function TitleColumn(title) {
+ this.title = title;
+ }
+
+ TitleColumn.prototype = {
+ supportsCellSelection: false,
+
+ /**
+ * Get the title associated with a given row.
+ *
+ * This method will decorate the title with color and '+++'/'---' prefix if
+ * appropriate (as determined by the optional row.contexts field).
+ * Examples:
+ *
+ * +----------------------+-----------------+--------+--------+
+ * | Contexts provided at | Interpretation | Prefix | Color |
+ * +----------------------+-----------------+--------+--------+
+ * | 1111111111 | always present | | |
+ * | 0000111111 | added | +++ | red |
+ * | 1111111000 | deleted | --- | green |
+ * | 1100111111* | flaky | | purple |
+ * | 0001001111 | added + flaky | +++ | purple |
+ * | 1111100010 | deleted + flaky | --- | purple |
+ * +----------------------+-----------------+--------+--------+
+ *
+ * *) This means that, given a selection of 10 memory dumps, a particular
+ * row (e.g. a process) was present in the first 2 and last 6 of them
+ * (but not in the third and fourth dump).
+ *
+ * This method should therefore NOT be overriden by subclasses. The
+ * formatTitle method should be overriden instead when necessary.
+ */
+ value(row) {
+ const formattedTitle = this.formatTitle(row);
+
+ const contexts = row.contexts;
+ if (contexts === undefined || contexts.length === 0) {
+ return formattedTitle;
+ }
+
+ // Determine if the row was provided in the first and last row and how
+ // many times it changed between being provided and not provided.
+ const firstContext = contexts[0];
+ const lastContext = contexts[contexts.length - 1];
+ let changeDefinedContextCount = 0;
+ for (let i = 1; i < contexts.length; i++) {
+ if ((contexts[i] === undefined) !== (contexts[i - 1] === undefined)) {
+ changeDefinedContextCount++;
+ }
+ }
+
+ // Determine the color and prefix of the title.
+ let color = undefined;
+ let prefix = undefined;
+ if (!firstContext && lastContext) {
+ // The row was added.
+ color = 'red';
+ prefix = '+++';
+ } else if (firstContext && !lastContext) {
+ // The row was removed.
+ color = 'green';
+ prefix = '---';
+ }
+ if (changeDefinedContextCount > 1) {
+ // The row was flaky (added/removed more than once).
+ color = 'purple';
+ }
+
+ if (color === undefined && prefix === undefined) {
+ return formattedTitle;
+ }
+
+ const titleEl = document.createElement('span');
+ if (prefix !== undefined) {
+ const prefixEl = tr.ui.b.createSpan({textContent: prefix});
+ // Enforce same width of '+++' and '---'.
+ prefixEl.style.fontFamily = 'monospace';
+ Polymer.dom(titleEl).appendChild(prefixEl);
+ Polymer.dom(titleEl).appendChild(
+ tr.ui.b.asHTMLOrTextNode(NO_BREAK_SPACE));
+ }
+ if (color !== undefined) {
+ titleEl.style.color = color;
+ }
+ Polymer.dom(titleEl).appendChild(
+ tr.ui.b.asHTMLOrTextNode(formattedTitle));
+ return titleEl;
+ },
+
+ /**
+ * Format the title associated with a given row. This method is intended to
+ * be overriden by subclasses.
+ */
+ formatTitle(row) {
+ return row.title;
+ },
+
+ cmp(rowA, rowB) {
+ return COLLATOR.compare(rowA.title, rowB.title);
+ }
+ };
+
+ /**
+ * Abstract table column for displaying memory dump data.
+ *
+ * @constructor
+ */
+ function MemoryColumn(name, cellPath, aggregationMode) {
+ this.name = name;
+ this.cellPath = cellPath;
+ this.shouldSetContextGroup = false;
+
+ // See MemoryColumn.AggregationMode enum in this file.
+ this.aggregationMode = aggregationMode;
+ }
+
+ /**
+ * Construct columns from cells in a hierarchy of rows and a list of rules.
+ *
+ * The list of rules contains objects with three fields:
+ *
+ * condition: Optional string or regular expression matched against the
+ * name of a cell. If omitted, the rule will match any cell.
+ * importance: Mandatory number which determines the final order of the
+ * columns. The column with the highest importance will be first in the
+ * returned array.
+ * columnConstructor: Mandatory memory column constructor.
+ *
+ * Example:
+ *
+ * const importanceRules = [
+ * {
+ * condition: 'page_size',
+ * columnConstructor: NumericMemoryColumn,
+ * importance: 8
+ * },
+ * {
+ * condition: /size/,
+ * columnConstructor: CustomNumericMemoryColumn,
+ * importance: 10
+ * },
+ * {
+ * // No condition: matches all columns.
+ * columnConstructor: NumericMemoryColumn,
+ * importance: 9
+ * }
+ * ];
+ *
+ * Given a name of a cell, the corresponding column constructor and
+ * importance are determined by the first rule whose condition matches the
+ * column's name. For example, given a cell with name 'inner_size', the
+ * corresponding column will be constructed using CustomNumericMemoryColumn
+ * and its importance (for sorting purposes) will be 10 (second rule).
+ *
+ * After columns are constructed for all cell names, they are sorted in
+ * descending order of importance and the resulting list is returned. In the
+ * example above, the constructed columns will be sorted into three groups as
+ * follows:
+ *
+ * [most important, left in the resulting table]
+ * 1. columns whose name contains 'size' excluding 'page_size' because it
+ * would have already matched the first rule (Note that string matches
+ * must be exact so a column named 'page_size2' would not match the
+ * first rule and would therefore belong to this group).
+ * 2. columns whose name does not contain 'size'.
+ * 3. columns whose name is 'page_size'.
+ * [least important, right in the resulting table]
+ *
+ * where columns will be sorted alphabetically within each group.
+ *
+ * @param {!Array.<!Object>} rows
+ * @param {!Object} config
+ * @param {string} config.cellKey
+ * @param {!MemoryColumn.AggregationMode=} config.aggregationMode
+ * @param {!Array.<!{
+ * condition: (string|!RegExp)=,
+ * importance: number,
+ * columnConstructor: !function(new: MemoryColumn, ...)=,
+ * shouldSetContextGroup: boolean=
+ * }>} config.rules
+ */
+ MemoryColumn.fromRows = function(rows, config) {
+ // Recursively find the names of all cells of the rows (and their sub-rows).
+ const cellNames = new Set();
+ function gatherCellNames(rows) {
+ rows.forEach(function(row) {
+ if (row === undefined) return;
+ const fieldCells = row[config.cellKey];
+ if (fieldCells !== undefined) {
+ for (const [fieldName, fieldCell] of Object.entries(fieldCells)) {
+ if (fieldCell === undefined || fieldCell.fields === undefined) {
+ continue;
+ }
+ cellNames.add(fieldName);
+ }
+ }
+ const subRows = row.subRows;
+ if (subRows !== undefined) {
+ gatherCellNames(subRows);
+ }
+ });
+ }
+ gatherCellNames(rows);
+
+ // Based on the provided list of rules, construct the columns and calculate
+ // their importance.
+ const positions = [];
+ cellNames.forEach(function(cellName) {
+ const cellPath = [config.cellKey, cellName];
+ const matchingRule = MemoryColumn.findMatchingRule(
+ cellName, config.rules);
+ const constructor = matchingRule.columnConstructor;
+ const column = new constructor(
+ cellName, cellPath, config.aggregationMode);
+ column.shouldSetContextGroup = !!config.shouldSetContextGroup;
+ positions.push({
+ importance: matchingRule.importance,
+ column
+ });
+ });
+
+ positions.sort(function(a, b) {
+ // Sort columns with the same importance alphabetically.
+ if (a.importance === b.importance) {
+ return COLLATOR.compare(a.column.name, b.column.name);
+ }
+
+ // Sort columns in descending order of importance.
+ return b.importance - a.importance;
+ });
+
+ return positions.map(function(position) { return position.column; });
+ };
+
+ MemoryColumn.spaceEqually = function(columns) {
+ const columnWidth = (100 / columns.length).toFixed(3) + '%';
+ columns.forEach(function(column) {
+ column.width = columnWidth;
+ });
+ };
+
+ MemoryColumn.findMatchingRule = function(name, rules) {
+ for (let i = 0; i < rules.length; i++) {
+ const rule = rules[i];
+ if (MemoryColumn.nameMatchesCondition(name, rule.condition)) {
+ return rule;
+ }
+ }
+ return undefined;
+ };
+
+ MemoryColumn.nameMatchesCondition = function(name, condition) {
+ // Rules without conditions match all columns.
+ if (condition === undefined) return true;
+
+ // String conditions must match the column name exactly.
+ if (typeof(condition) === 'string') return name === condition;
+
+ // If the condition is not a string, assume it is a RegExp.
+ return condition.test(name);
+ };
+
+ /** @enum */
+ MemoryColumn.AggregationMode = {
+ DIFF: 0,
+ MAX: 1
+ };
+
+ MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER = 'at some selected timestamps';
+
+ MemoryColumn.prototype = {
+ get title() {
+ return this.name;
+ },
+
+ cell(row) {
+ let cell = row;
+ const cellPath = this.cellPath;
+ for (let i = 0; i < cellPath.length; i++) {
+ if (cell === undefined) return undefined;
+ cell = cell[cellPath[i]];
+ }
+ return cell;
+ },
+
+ aggregateCells(row, subRows) {
+ // No generic aggregation.
+ },
+
+ fields(row) {
+ const cell = this.cell(row);
+ if (cell === undefined) return undefined;
+ return cell.fields;
+ },
+
+ /**
+ * Format a cell associated with this column from the given row. This
+ * method is not intended to be overriden.
+ */
+ value(row) {
+ const fields = this.fields(row);
+ if (this.hasAllRelevantFieldsUndefined(fields)) return '';
+
+ // Determine the color and infos of the resulting element.
+ const contexts = row.contexts;
+ const color = this.color(fields, contexts);
+ const infos = [];
+ this.addInfos(fields, contexts, infos);
+
+ // Format the actual fields.
+ const formattedFields = this.formatFields(fields);
+
+ // If no color is specified and there are no infos, there is no need to
+ // wrap the value in a span element.#
+ if ((color === undefined || formattedFields === '') &&
+ infos.length === 0) {
+ return formattedFields;
+ }
+
+ const fieldEl = document.createElement('span');
+ fieldEl.style.display = 'flex';
+ fieldEl.style.alignItems = 'center';
+ fieldEl.style.justifyContent = 'flex-end';
+ Polymer.dom(fieldEl).appendChild(
+ tr.ui.b.asHTMLOrTextNode(formattedFields));
+
+ // Add info icons with tooltips.
+ infos.forEach(function(info) {
+ const infoEl = document.createElement('span');
+ infoEl.style.paddingLeft = '4px';
+ infoEl.style.cursor = 'help';
+ infoEl.style.fontWeight = 'bold';
+ Polymer.dom(infoEl).textContent = info.icon;
+ if (info.color !== undefined) {
+ infoEl.style.color = info.color;
+ }
+ infoEl.title = info.message;
+ Polymer.dom(fieldEl).appendChild(infoEl);
+ }, this);
+
+ // Set the color of the element.
+ if (color !== undefined) {
+ fieldEl.style.color = color;
+ }
+
+ return fieldEl;
+ },
+
+ /**
+ * Returns true iff all fields of a row which are relevant for the current
+ * aggregation mode (e.g. first and last field for diff mode) are undefined.
+ */
+ hasAllRelevantFieldsUndefined(fields) {
+ if (fields === undefined) return true;
+
+ switch (this.aggregationMode) {
+ case MemoryColumn.AggregationMode.DIFF:
+ // Only the first and last field are relevant.
+ return fields[0] === undefined &&
+ fields[fields.length - 1] === undefined;
+
+ case MemoryColumn.AggregationMode.MAX:
+ default:
+ // All fields are relevant.
+ return fields.every(function(field) { return field === undefined; });
+ }
+ },
+
+ /**
+ * Get the color of the given fields formatted by this column. At least one
+ * field relevant for the current aggregation mode is guaranteed to be
+ * defined.
+ */
+ color(fields, contexts) {
+ return undefined;
+ },
+
+ /**
+ * Format an arbitrary number of fields. At least one field relevant for
+ * the current aggregation mode is guaranteed to be defined.
+ */
+ formatFields(fields) {
+ if (fields.length === 1) {
+ return this.formatSingleField(fields[0]);
+ }
+ return this.formatMultipleFields(fields);
+ },
+
+ /**
+ * Format a single defined field.
+ *
+ * This method is intended to be overriden by field type specific columns
+ * (e.g. show '1.0 KiB' instead of '1024' for Scalar(s) representing
+ * bytes).
+ */
+ formatSingleField(field) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Format multiple fields. At least one field relevant for the current
+ * aggregation mode is guaranteed to be defined.
+ *
+ * The aggregation mode specializations of this method (e.g.
+ * formatMultipleFieldsDiff) are intended to be overriden by field type
+ * specific columns.
+ */
+ formatMultipleFields(fields) {
+ switch (this.aggregationMode) {
+ case MemoryColumn.AggregationMode.DIFF:
+ return this.formatMultipleFieldsDiff(
+ fields[0], fields[fields.length - 1]);
+
+ case MemoryColumn.AggregationMode.MAX:
+ return this.formatMultipleFieldsMax(fields);
+
+ default:
+ return tr.ui.b.createSpan({
+ textContent: '(unsupported aggregation mode)',
+ italic: true
+ });
+ }
+ },
+
+ formatMultipleFieldsDiff(firstField, lastField) {
+ throw new Error('Not implemented');
+ },
+
+ formatMultipleFieldsMax(fields) {
+ return this.formatSingleField(this.getMaxField(fields));
+ },
+
+ cmp(rowA, rowB) {
+ const fieldsA = this.fields(rowA);
+ const fieldsB = this.fields(rowB);
+
+ // Sanity check.
+ if (fieldsA !== undefined && fieldsB !== undefined &&
+ fieldsA.length !== fieldsB.length) {
+ throw new Error('Different number of fields');
+ }
+
+ // Handle empty fields.
+ const undefinedA = this.hasAllRelevantFieldsUndefined(fieldsA);
+ const undefinedB = this.hasAllRelevantFieldsUndefined(fieldsB);
+ if (undefinedA && undefinedB) return 0;
+ if (undefinedA) return -1;
+ if (undefinedB) return 1;
+
+ return this.compareFields(fieldsA, fieldsB);
+ },
+
+ /**
+ * Compare a pair of single or multiple fields. At least one field relevant
+ * for the current aggregation mode is guaranteed to be defined in each of
+ * the two lists.
+ */
+ compareFields(fieldsA, fieldsB) {
+ if (fieldsA.length === 1) {
+ return this.compareSingleFields(fieldsA[0], fieldsB[0]);
+ }
+ return this.compareMultipleFields(fieldsA, fieldsB);
+ },
+
+ /**
+ * Compare a pair of single defined fields.
+ *
+ * This method is intended to be overriden by field type specific columns.
+ */
+ compareSingleFields(fieldA, fieldB) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Compare a pair of multiple fields. At least one field relevant for the
+ * current aggregation mode is guaranteed to be defined in each of the two
+ * lists.
+ *
+ * The aggregation mode specializations of this method (e.g.
+ * compareMultipleFieldsDiff) are intended to be overriden by field type
+ * specific columns.
+ */
+ compareMultipleFields(fieldsA, fieldsB) {
+ switch (this.aggregationMode) {
+ case MemoryColumn.AggregationMode.DIFF:
+ return this.compareMultipleFieldsDiff(
+ fieldsA[0], fieldsA[fieldsA.length - 1],
+ fieldsB[0], fieldsB[fieldsB.length - 1]);
+
+ case MemoryColumn.AggregationMode.MAX:
+ return this.compareMultipleFieldsMax(fieldsA, fieldsB);
+
+ default:
+ return 0;
+ }
+ },
+
+ compareMultipleFieldsDiff(firstFieldA, lastFieldA, firstFieldB,
+ lastFieldB) {
+ throw new Error('Not implemented');
+ },
+
+ compareMultipleFieldsMax(fieldsA, fieldsB) {
+ return this.compareSingleFields(
+ this.getMaxField(fieldsA), this.getMaxField(fieldsB));
+ },
+
+ getMaxField(fields) {
+ return fields.reduce(function(accumulator, field) {
+ if (field === undefined) {
+ return accumulator;
+ }
+ if (accumulator === undefined ||
+ this.compareSingleFields(field, accumulator) > 0) {
+ return field;
+ }
+ return accumulator;
+ }.bind(this), undefined);
+ },
+
+ addInfos(fields, contexts, infos) {
+ // No generic infos.
+ },
+
+ getImportance(importanceRules) {
+ if (importanceRules.length === 0) return 0;
+
+ // Find the first matching rule.
+ const matchingRule =
+ MemoryColumn.findMatchingRule(this.name, importanceRules);
+ if (matchingRule !== undefined) {
+ return matchingRule.importance;
+ }
+
+ // No matching rule. Return lower importance than all rules.
+ let minImportance = importanceRules[0].importance;
+ for (let i = 1; i < importanceRules.length; i++) {
+ minImportance = Math.min(minImportance, importanceRules[i].importance);
+ }
+ return minImportance - 1;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function StringMemoryColumn(name, cellPath, aggregationMode) {
+ MemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ StringMemoryColumn.prototype = {
+ __proto__: MemoryColumn.prototype,
+
+ formatSingleField(string) {
+ return string;
+ },
+
+ formatMultipleFieldsDiff(firstString, lastString) {
+ if (firstString === undefined) {
+ // String was added ("+NEW_VALUE" in red).
+ const spanEl = tr.ui.b.createSpan({color: 'red'});
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('+'));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(lastString)));
+ return spanEl;
+ } else if (lastString === undefined) {
+ // String was removed ("-OLD_VALUE" in green).
+ const spanEl = tr.ui.b.createSpan({color: 'green'});
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('-'));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(firstString)));
+ return spanEl;
+ } else if (firstString === lastString) {
+ // String didn't change ("VALUE" with unchanged color).
+ return this.formatSingleField(firstString);
+ }
+ // String changed ("OLD_VALUE -> NEW_VALUE" in orange).
+ const spanEl = tr.ui.b.createSpan({color: 'DarkOrange'});
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(firstString)));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ ' ' + RIGHTWARDS_ARROW + ' '));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(lastString)));
+ return spanEl;
+ },
+
+ compareSingleFields(stringA, stringB) {
+ return COLLATOR.compare(stringA, stringB);
+ },
+
+ compareMultipleFieldsDiff(firstStringA, lastStringA, firstStringB,
+ lastStringB) {
+ // If one of the strings was added (and the other one wasn't), mark the
+ // corresponding diff as greater.
+ if (firstStringA === undefined && firstStringB !== undefined) {
+ return 1;
+ }
+ if (firstStringA !== undefined && firstStringB === undefined) {
+ return -1;
+ }
+
+ // If both strings were added, compare the last values (greater last
+ // value implies greater diff).
+ if (firstStringA === undefined && firstStringB === undefined) {
+ return this.compareSingleFields(lastStringA, lastStringB);
+ }
+
+ // If one of the strings was removed (and the other one wasn't), mark the
+ // corresponding diff as lower.
+ if (lastStringA === undefined && lastStringB !== undefined) {
+ return -1;
+ }
+ if (lastStringA !== undefined && lastStringB === undefined) {
+ return 1;
+ }
+
+ // If both strings were removed, compare the first values (greater first
+ // value implies smaller (!) diff).
+ if (lastStringA === undefined && lastStringB === undefined) {
+ return this.compareSingleFields(firstStringB, firstStringA);
+ }
+
+ const areStringsAEqual = firstStringA === lastStringA;
+ const areStringsBEqual = firstStringB === lastStringB;
+
+ // Consider diffs of strings that did not change to be smaller than diffs
+ // of strings that did change.
+ if (areStringsAEqual && areStringsBEqual) return 0;
+ if (areStringsAEqual) return -1;
+ if (areStringsBEqual) return 1;
+
+ // Both strings changed. We are unable to determine the ordering of the
+ // diffs.
+ return 0;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function NumericMemoryColumn(name, cellPath, aggregationMode) {
+ MemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ // Avoid tiny positive/negative diffs (displayed in the UI as '+0.0 B' and
+ // '-0.0 B') due to imprecise floating-point arithmetic by treating all diffs
+ // within the (-DIFF_EPSILON, DIFF_EPSILON) range as zeros.
+ NumericMemoryColumn.DIFF_EPSILON = 0.0001;
+
+ NumericMemoryColumn.prototype = {
+ __proto__: MemoryColumn.prototype,
+
+ align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT,
+
+ aggregateCells(row, subRows) {
+ const subRowCells = subRows.map(this.cell, this);
+
+ // Determine if there is at least one defined numeric in the sub-row
+ // cells and the timestamp count.
+ let hasDefinedSubRowNumeric = false;
+ let timestampCount = undefined;
+ subRowCells.forEach(function(subRowCell) {
+ if (subRowCell === undefined) return;
+
+ const subRowNumerics = subRowCell.fields;
+ if (subRowNumerics === undefined) return;
+
+ if (timestampCount === undefined) {
+ timestampCount = subRowNumerics.length;
+ } else if (timestampCount !== subRowNumerics.length) {
+ throw new Error('Sub-rows have different numbers of timestamps');
+ }
+
+ if (hasDefinedSubRowNumeric) {
+ return; // Avoid unnecessary traversals of the numerics.
+ }
+ hasDefinedSubRowNumeric = subRowNumerics.some(function(numeric) {
+ return numeric !== undefined;
+ });
+ });
+ if (!hasDefinedSubRowNumeric) {
+ return; // No numeric to aggregate.
+ }
+
+ // Get or create the row cell.
+ const cellPath = this.cellPath;
+ let rowCell = row;
+ for (let i = 0; i < cellPath.length; i++) {
+ const nextStepName = cellPath[i];
+ let nextStep = rowCell[nextStepName];
+ if (nextStep === undefined) {
+ if (i < cellPath.length - 1) {
+ nextStep = {};
+ } else {
+ nextStep = new MemoryCell(undefined);
+ }
+ rowCell[nextStepName] = nextStep;
+ }
+ rowCell = nextStep;
+ }
+ if (rowCell.fields === undefined) {
+ rowCell.fields = new Array(timestampCount);
+ } else if (rowCell.fields.length !== timestampCount) {
+ throw new Error(
+ 'Row has a different number of timestamps than sub-rows');
+ }
+
+ for (let i = 0; i < timestampCount; i++) {
+ if (rowCell.fields[i] !== undefined) continue;
+ rowCell.fields[i] = tr.model.MemoryAllocatorDump.aggregateNumerics(
+ subRowCells.map(function(subRowCell) {
+ if (subRowCell === undefined || subRowCell.fields === undefined) {
+ return undefined;
+ }
+ return subRowCell.fields[i];
+ }));
+ }
+ },
+
+ formatSingleField(numeric) {
+ return tr.v.ui.createScalarSpan(numeric, {
+ context: this.getFormattingContext(numeric.unit),
+ contextGroup: this.shouldSetContextGroup ? this.name : undefined,
+ inline: true,
+ });
+ },
+
+ getFormattingContext(unit) {
+ return undefined;
+ },
+
+ formatMultipleFieldsDiff(firstNumeric, lastNumeric) {
+ return this.formatSingleField(
+ this.getDiffField_(firstNumeric, lastNumeric));
+ },
+
+ compareSingleFields(numericA, numericB) {
+ return numericA.value - numericB.value;
+ },
+
+ compareMultipleFieldsDiff(firstNumericA, lastNumericA,
+ firstNumericB, lastNumericB) {
+ return this.getDiffFieldValue_(firstNumericA, lastNumericA) -
+ this.getDiffFieldValue_(firstNumericB, lastNumericB);
+ },
+
+ getDiffField_(firstNumeric, lastNumeric) {
+ const definedNumeric = firstNumeric || lastNumeric;
+ return new tr.b.Scalar(definedNumeric.unit.correspondingDeltaUnit,
+ this.getDiffFieldValue_(firstNumeric, lastNumeric));
+ },
+
+ getDiffFieldValue_(firstNumeric, lastNumeric) {
+ const firstValue = firstNumeric === undefined ? 0 : firstNumeric.value;
+ const lastValue = lastNumeric === undefined ? 0 : lastNumeric.value;
+ const diff = lastValue - firstValue;
+ return Math.abs(diff) < NumericMemoryColumn.DIFF_EPSILON ? 0 : diff;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function MemoryCell(fields) {
+ this.fields = fields;
+ }
+
+ MemoryCell.extractFields = function(cell) {
+ if (cell === undefined) return undefined;
+ return cell.fields;
+ };
+
+ /** Limit for the number of sub-rows for recursive table row expansion. */
+ const RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT = 10;
+
+ function expandTableRowsRecursively(table) {
+ let currentLevelRows = table.tableRows;
+ let totalVisibleRowCount = currentLevelRows.length;
+
+ while (currentLevelRows.length > 0) {
+ // Calculate the total number of sub-rows on the current level.
+ let nextLevelRowCount = 0;
+ currentLevelRows.forEach(function(currentLevelRow) {
+ const subRows = currentLevelRow.subRows;
+ if (subRows === undefined || subRows.length === 0) return;
+ nextLevelRowCount += subRows.length;
+ });
+
+ // Determine whether expanding all rows on the current level would cause
+ // the total number of visible rows go over the limit.
+ if (totalVisibleRowCount + nextLevelRowCount >
+ RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT) {
+ break;
+ }
+
+ // Expand all rows on the current level and gather their sub-rows.
+ const nextLevelRows = new Array(nextLevelRowCount);
+ let nextLevelRowIndex = 0;
+ currentLevelRows.forEach(function(currentLevelRow) {
+ const subRows = currentLevelRow.subRows;
+ if (subRows === undefined || subRows.length === 0) return;
+ table.setExpandedForTableRow(currentLevelRow, true);
+ subRows.forEach(function(subRow) {
+ nextLevelRows[nextLevelRowIndex++] = subRow;
+ });
+ });
+
+ // Update the total number of visible rows and progress to the next level.
+ totalVisibleRowCount += nextLevelRowCount;
+ currentLevelRows = nextLevelRows;
+ }
+ }
+
+ function aggregateTableRowCellsRecursively(row, columns, opt_predicate) {
+ const subRows = row.subRows;
+ if (subRows === undefined || subRows.length === 0) return;
+
+ subRows.forEach(function(subRow) {
+ aggregateTableRowCellsRecursively(subRow, columns, opt_predicate);
+ });
+
+ if (opt_predicate === undefined || opt_predicate(row.contexts)) {
+ aggregateTableRowCells(row, subRows, columns);
+ }
+ }
+
+ function aggregateTableRowCells(row, subRows, columns) {
+ columns.forEach(function(column) {
+ if (!(column instanceof MemoryColumn)) return;
+ column.aggregateCells(row, subRows);
+ });
+ }
+
+ function createCells(timeToValues, valueFieldsGetter, opt_this) {
+ opt_this = opt_this || this;
+ const fieldNameToFields = tr.b.invertArrayOfDicts(
+ timeToValues, valueFieldsGetter, opt_this);
+ const result = {};
+ for (const [fieldName, fields] of Object.entries(fieldNameToFields)) {
+ result[fieldName] = new tr.ui.analysis.MemoryCell(fields);
+ }
+ return result;
+ }
+
+ function createWarningInfo(message) {
+ return {
+ message,
+ icon: String.fromCharCode(9888),
+ color: 'red'
+ };
+ }
+
+ // TODO(petrcermak): Use a context manager instead
+ // (https://github.com/catapult-project/catapult/issues/2420).
+ function DetailsNumericMemoryColumn(name, cellPath, aggregationMode) {
+ NumericMemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ DetailsNumericMemoryColumn.prototype = {
+ __proto__: NumericMemoryColumn.prototype,
+
+ getFormattingContext(unit) {
+ if (unit.baseUnit === tr.b.Unit.byName.sizeInBytes) {
+ return { unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI };
+ }
+ return undefined;
+ }
+ };
+
+ return {
+ TitleColumn,
+ MemoryColumn,
+ StringMemoryColumn,
+ NumericMemoryColumn,
+ MemoryCell,
+ expandTableRowsRecursively,
+ aggregateTableRowCellsRecursively,
+ aggregateTableRowCells,
+ createCells,
+ createWarningInfo,
+ DetailsNumericMemoryColumn,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html
new file mode 100644
index 00000000000..859f78433d6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html
@@ -0,0 +1,1241 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TitleColumn = tr.ui.analysis.TitleColumn;
+ const MemoryColumn = tr.ui.analysis.MemoryColumn;
+ const AggregationMode = MemoryColumn.AggregationMode;
+ const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const MemoryCell = tr.ui.analysis.MemoryCell;
+ const expandTableRowsRecursively = tr.ui.analysis.expandTableRowsRecursively;
+ const aggregateTableRowCells = tr.ui.analysis.aggregateTableRowCells;
+ const aggregateTableRowCellsRecursively =
+ tr.ui.analysis.aggregateTableRowCellsRecursively;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkNumericFields = tr.ui.analysis.checkNumericFields;
+ const checkStringFields = tr.ui.analysis.checkStringFields;
+ const createCells = tr.ui.analysis.createCells;
+ const createWarningInfo = tr.ui.analysis.createWarningInfo;
+
+ function checkPercent(string, expectedPercent) {
+ assert.strictEqual(Number(string.slice(0, -1)), expectedPercent);
+ assert.strictEqual(string.slice(-1), '%');
+ }
+
+ function checkMemoryColumnFieldFormat(test, column, fields,
+ expectedTextContent, opt_expectedColor) {
+ const value = column.formatMultipleFields(fields);
+ if (expectedTextContent === undefined) {
+ assert.strictEqual(value, '');
+ assert.isUndefined(opt_expectedColor); // Test sanity check.
+ return;
+ }
+
+ const node = tr.ui.b.asHTMLOrTextNode(value);
+ const spanEl = document.createElement('span');
+ Polymer.dom(spanEl).appendChild(node);
+ test.addHTMLOutput(spanEl);
+
+ assert.strictEqual(Polymer.dom(node).textContent, expectedTextContent);
+ if (opt_expectedColor === undefined) {
+ assert.notInstanceOf(node, HTMLElement);
+ } else {
+ assert.strictEqual(node.style.color, opt_expectedColor);
+ }
+ }
+
+ function checkCompareFieldsEqual(column, fieldValuesA, fieldValuesB) {
+ assert.strictEqual(column.compareFields(fieldValuesA, fieldValuesB), 0);
+ }
+
+ function checkCompareFieldsLess(column, fieldValuesA, fieldValuesB) {
+ assert.isBelow(column.compareFields(fieldValuesA, fieldValuesB), 0);
+ assert.isAbove(column.compareFields(fieldValuesB, fieldValuesA), 0);
+ }
+
+ function checkNumericMemoryColumnFieldFormat(test, column, fieldValues, unit,
+ expectedValue) {
+ const value = column.formatMultipleFields(
+ buildScalarCell(unit, fieldValues).fields);
+ if (expectedValue === undefined) {
+ assert.strictEqual(value, '');
+ assert.isUndefined(expectedUnits); // Test sanity check.
+ return;
+ }
+
+ test.addHTMLOutput(value);
+ assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(value.value, expectedValue);
+ assert.strictEqual(value.unit, unit);
+ }
+
+ function buildScalarCell(unit, values) {
+ return new MemoryCell(values.map(function(value) {
+ if (value === undefined) return undefined;
+ return new Scalar(unit, value);
+ }));
+ }
+
+ function buildTestRows() {
+ return [
+ {
+ title: 'Row 1',
+ fields: {
+ 'cpu_temperature': new MemoryCell(['below zero', 'absolute zero'])
+ },
+ subRows: [
+ {
+ title: 'Row 1A',
+ fields: {
+ 'page_size': buildScalarCell(sizeInBytes_smallerIsBetter,
+ [1024, 1025])
+ }
+ },
+ {
+ title: 'Row 1B',
+ fields: {
+ 'page_size': buildScalarCell(sizeInBytes_smallerIsBetter,
+ [512, 513]),
+ 'mixed': new MemoryCell(['0.01', '0.10']),
+ 'mixed2': new MemoryCell([
+ new Scalar(tr.b.Unit.byName.powerInWatts, 2.43e18),
+ new Scalar(tr.b.Unit.byName.powerInWatts, 0.5433)
+ ])
+ }
+ }
+ ]
+ },
+ {
+ title: 'Row 2',
+ fields: {
+ 'cpu_temperature': undefined,
+ 'mixed': buildScalarCell(tr.b.Unit.byName.timeDurationInMs,
+ [0.99, 0.999])
+ }
+ }
+ ];
+ }
+
+ function checkCellValue(
+ test, value, expectedText, expectedColor, opt_expectedInfos) {
+ const expectedInfos = opt_expectedInfos || [];
+ assert.lengthOf(Polymer.dom(value).childNodes, 1 + expectedInfos.length);
+ assert.strictEqual(value.style.color, expectedColor);
+ if (typeof expectedText === 'string') {
+ assert.strictEqual(
+ Polymer.dom(Polymer.dom(value).childNodes[0]).textContent,
+ expectedText);
+ } else {
+ expectedText(Polymer.dom(value).childNodes[0]);
+ }
+ for (let i = 0; i < expectedInfos.length; i++) {
+ const expectedInfo = expectedInfos[i];
+ const infoEl = Polymer.dom(value).childNodes[i + 1];
+ assert.strictEqual(Polymer.dom(infoEl).textContent, expectedInfo.icon);
+ assert.strictEqual(infoEl.title, expectedInfo.message);
+ assert.strictEqual(infoEl.style.color, expectedInfo.color || '');
+ }
+ test.addHTMLOutput(value);
+ }
+
+ function sizeSpanMatcher(
+ expectedValue, opt_expectedIsDelta, opt_expectedContext) {
+ return function(element) {
+ assert.strictEqual(element.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(element.value, expectedValue);
+ assert.strictEqual(element.unit, opt_expectedIsDelta ?
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter :
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter);
+ assert.deepEqual(element.context, opt_expectedContext);
+ };
+ }
+
+ test('checkTitleColumn_value', function() {
+ const column = new TitleColumn('column_title');
+ assert.strictEqual(column.title, 'column_title');
+ assert.isFalse(column.supportsCellSelection);
+
+ let row = {title: 'undefined', contexts: undefined};
+ assert.strictEqual(column.formatTitle(row), 'undefined');
+ assert.strictEqual(column.value(row), 'undefined');
+
+ row = {title: 'constant', contexts: [{}, {}, {}, {}]};
+ assert.strictEqual(column.formatTitle(row), 'constant');
+ assert.strictEqual(column.value(row), 'constant');
+
+ row = {title: 'added', contexts: [undefined, undefined, undefined, {}]};
+ assert.strictEqual(column.formatTitle(row), 'added');
+ let value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, '+++\u00A0added');
+ assert.strictEqual(value.style.color, 'red');
+
+ row = {title: 'removed', contexts: [true, true, undefined, undefined]};
+ assert.strictEqual(column.formatTitle(row), 'removed');
+ value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, '---\u00A0removed');
+ assert.strictEqual(value.style.color, 'green');
+
+ row = {title: 'flaky', contexts: [true, undefined, true, true]};
+ assert.strictEqual(column.formatTitle(row), 'flaky');
+ value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, 'flaky');
+ assert.strictEqual(value.style.color, 'purple');
+
+ row = {title: 'added-flaky', contexts: [undefined, {}, undefined, true]};
+ assert.strictEqual(column.formatTitle(row), 'added-flaky');
+ value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, '+++\u00A0added-flaky');
+ assert.strictEqual(value.style.color, 'purple');
+
+ row = {title: 'removed-flaky', contexts: [true, undefined, {}, undefined]};
+ assert.strictEqual(column.formatTitle(row), 'removed-flaky');
+ value = column.value(row);
+ assert.strictEqual(
+ Polymer.dom(value).textContent, '---\u00A0removed-flaky');
+ assert.strictEqual(value.style.color, 'purple');
+ });
+
+ test('checkTitleColumn_cmp', function() {
+ const column = new TitleColumn('column_title');
+
+ assert.isBelow(column.cmp({title: 'a'}, {title: 'b'}), 0);
+ assert.strictEqual(column.cmp({title: 'cc'}, {title: 'cc'}), 0);
+ assert.isAbove(column.cmp({title: '10'}, {title: '2'}), 0);
+ });
+
+ test('checkMemoryColumn_fromRows', function() {
+ function MockColumn0() {
+ MemoryColumn.apply(this, arguments);
+ }
+ MockColumn0.prototype = {
+ __proto__: MemoryColumn.prototype,
+ get title() { return 'MockColumn0'; }
+ };
+
+ function MockColumn1() {
+ MemoryColumn.apply(this, arguments);
+ }
+ MockColumn1.prototype = {
+ __proto__: MemoryColumn.prototype,
+ get title() { return 'MockColumn1'; }
+ };
+
+ function MockColumn2() {
+ MemoryColumn.apply(this, arguments);
+ }
+ MockColumn2.prototype = {
+ __proto__: MemoryColumn.prototype,
+ get title() { return 'MockColumn2'; }
+ };
+
+ const rules = [
+ {
+ condition: /size/,
+ importance: 10,
+ columnConstructor: MockColumn0
+ },
+ {
+ condition: 'cpu_temperature',
+ importance: 0,
+ columnConstructor: MockColumn1
+ },
+ {
+ condition: 'unmatched',
+ importance: -1,
+ get columnConstructor() {
+ throw new Error('The constructor should never be retrieved');
+ }
+ },
+ {
+ importance: 1,
+ columnConstructor: MockColumn2
+ }
+ ];
+
+ const rows = buildTestRows();
+ const columns = MemoryColumn.fromRows(rows, {
+ cellKey: 'fields',
+ aggregationMode: AggregationMode.MAX,
+ rules,
+ shouldSetContextGroup: true
+ });
+ assert.lengthOf(columns, 4);
+
+ const pageSizeColumn = columns[0];
+ assert.strictEqual(pageSizeColumn.name, 'page_size');
+ assert.strictEqual(pageSizeColumn.title, 'MockColumn0');
+ assert.strictEqual(pageSizeColumn.aggregationMode, AggregationMode.MAX);
+ assert.strictEqual(pageSizeColumn.cell({fields: {page_size: 'large'}}),
+ 'large');
+ assert.isTrue(pageSizeColumn.shouldSetContextGroup);
+ assert.instanceOf(pageSizeColumn, MockColumn0);
+
+ const mixedColumn = columns[1];
+ assert.strictEqual(mixedColumn.name, 'mixed');
+ assert.strictEqual(mixedColumn.title, 'MockColumn2');
+ assert.strictEqual(mixedColumn.aggregationMode, AggregationMode.MAX);
+ assert.strictEqual(mixedColumn.cell({fields: {mixed: 89}}), 89);
+ assert.isTrue(mixedColumn.shouldSetContextGroup);
+ assert.instanceOf(mixedColumn, MockColumn2);
+
+ const mixed2Column = columns[2];
+ assert.strictEqual(mixed2Column.name, 'mixed2');
+ assert.strictEqual(mixed2Column.title, 'MockColumn2');
+ assert.strictEqual(mixed2Column.aggregationMode, AggregationMode.MAX);
+ assert.strictEqual(mixed2Column.cell({fields: {mixed2: 'invalid'}}),
+ 'invalid');
+ assert.isTrue(mixed2Column.shouldSetContextGroup);
+ assert.instanceOf(mixed2Column, MockColumn2);
+
+ const cpuTemperatureColumn = columns[3];
+ assert.strictEqual(cpuTemperatureColumn.name, 'cpu_temperature');
+ assert.strictEqual(cpuTemperatureColumn.title, 'MockColumn1');
+ assert.strictEqual(cpuTemperatureColumn.aggregationMode,
+ AggregationMode.MAX);
+ assert.strictEqual(
+ cpuTemperatureColumn.cell({fields: {cpu_temperature: 42}}), 42);
+ assert.isTrue(cpuTemperatureColumn.shouldSetContextGroup);
+ assert.instanceOf(cpuTemperatureColumn, MockColumn1);
+ });
+
+ test('checkMemoryColumn_spaceEqually', function() {
+ // Zero columns.
+ let columns = [];
+ MemoryColumn.spaceEqually(columns);
+
+ // One column.
+ columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; }
+ }
+ ];
+ MemoryColumn.spaceEqually(columns);
+ checkPercent(columns[0].width, 100);
+
+ // Two columns.
+ columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; }
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.firstData; }
+ }
+ ];
+ MemoryColumn.spaceEqually(columns);
+ checkPercent(columns[0].width, 50);
+ checkPercent(columns[1].width, 50);
+ });
+
+ test('checkMemoryColumn_instantiate', function() {
+ const c = new MemoryColumn('test_column', ['x'], AggregationMode.MAX);
+ assert.strictEqual(c.name, 'test_column');
+ assert.strictEqual(c.title, 'test_column');
+ assert.strictEqual(c.cell({x: 95}), 95);
+ assert.isUndefined(c.width);
+ assert.isUndefined(c.color());
+ });
+
+ test('checkMemoryColumn_cell', function() {
+ const c = new MemoryColumn('test_column', ['a', 'b'], AggregationMode.MAX);
+ const cell = new MemoryCell(undefined);
+
+ assert.isUndefined(c.cell(undefined));
+ assert.isUndefined(c.cell({b: cell}));
+ assert.isUndefined(c.cell({a: {c: cell}}));
+ assert.strictEqual(c.cell({a: {b: cell, c: 42}}), cell);
+ });
+
+ test('checkMemoryColumn_fields', function() {
+ const c = new MemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ // Undefined cell or field inside cell.
+ assert.isUndefined(c.fields({}));
+ assert.isUndefined(c.fields({x: new MemoryCell(undefined)}));
+
+ // Defined field(s) inside cell.
+ const field1 = new Scalar(tr.b.Unit.byName.powerInWatts, 1013.25);
+ const field2 = new Scalar(tr.b.Unit.byName.powerInWatts, 1065);
+ const row1 = {x: new MemoryCell([field1])};
+ const row2 = {x: new MemoryCell([field1, field2])};
+ assert.deepEqual(c.fields(row1), [field1]);
+ assert.deepEqual(c.fields(row2), [field1, field2]);
+ });
+
+ test('checkMemoryColumn_hasAllRelevantFieldsUndefined', function() {
+ // Single field.
+ const c1 = new MemoryColumn('single_column', ['x'],
+ undefined /* aggregation mode */);
+ assert.isTrue(c1.hasAllRelevantFieldsUndefined([undefined]));
+ assert.isFalse(c1.hasAllRelevantFieldsUndefined(
+ [new Scalar(sizeInBytes_smallerIsBetter, 16)]));
+
+ // Multiple fields, diff aggregation mode.
+ const c2 = new MemoryColumn('diff_column', ['x'],
+ AggregationMode.DIFF);
+ assert.isTrue(c2.hasAllRelevantFieldsUndefined([undefined, undefined]));
+ assert.isTrue(c2.hasAllRelevantFieldsUndefined(
+ [undefined, undefined, undefined]));
+ assert.isTrue(c2.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 16), undefined]));
+ assert.isFalse(c2.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 32)]));
+ assert.isFalse(c2.hasAllRelevantFieldsUndefined(
+ [new Scalar(sizeInBytes_smallerIsBetter, 32), undefined, undefined]));
+ assert.isFalse(c2.hasAllRelevantFieldsUndefined([
+ new Scalar(sizeInBytes_smallerIsBetter, 16),
+ undefined,
+ new Scalar(sizeInBytes_smallerIsBetter, 32)
+ ]));
+
+ // Multiple fields, max aggregation mode.
+ const c3 = new MemoryColumn('max_column', ['x'],
+ AggregationMode.MAX);
+ assert.isTrue(c3.hasAllRelevantFieldsUndefined([undefined, undefined]));
+ assert.isTrue(c3.hasAllRelevantFieldsUndefined(
+ [undefined, undefined, undefined]));
+ assert.isFalse(c3.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 16), undefined]));
+ assert.isFalse(c3.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 32)]));
+ assert.isFalse(c3.hasAllRelevantFieldsUndefined([
+ new Scalar(sizeInBytes_smallerIsBetter, 32),
+ undefined,
+ new Scalar(sizeInBytes_smallerIsBetter, 16)
+ ]));
+ });
+
+ test('checkMemoryColumn_value_allFieldsUndefined', function() {
+ const c1 = new MemoryColumn('no_color', ['x'],
+ AggregationMode.MAX);
+ const c2 = new MemoryColumn('color', ['x'],
+ AggregationMode.DIFF);
+ Object.defineProperty(c2, 'color', {
+ get() {
+ throw new Error('The color should never be retrieved');
+ }
+ });
+
+ // Infos should be completely ignored.
+ c1.addInfos = c2.addInfos = function() {
+ throw new Error('This method should never be called');
+ };
+
+ [c1, c2].forEach(function(c) {
+ assert.strictEqual(c.value({}), '');
+ assert.strictEqual(c.value({x: new MemoryCell(undefined)}), '');
+ assert.strictEqual(c.value({x: new MemoryCell([undefined])}), '');
+ assert.strictEqual(
+ c.value({x: new MemoryCell([undefined, undefined])}), '');
+ });
+
+ // Diff should only take into account the first and last field value.
+ assert.strictEqual(c2.value({
+ x: new MemoryCell([
+ undefined,
+ new Scalar(sizeInBytes_smallerIsBetter, 16),
+ undefined
+ ])
+ }), '');
+ });
+
+ test('checkMemoryColumn_getImportance', function() {
+ const c = new NumericMemoryColumn('test_column', ['x']);
+
+ const rules1 = [];
+ assert.strictEqual(c.getImportance(rules1), 0);
+
+ const rules2 = [
+ {
+ condition: 'test',
+ importance: 4
+ },
+ {
+ condition: /test$/,
+ importance: 2
+ }
+ ];
+ assert.strictEqual(c.getImportance(rules2), 1);
+
+ const rules3 = [
+ {
+ condition: 'test_column',
+ importance: 10
+ },
+ {
+ importance: 5
+ }
+ ];
+ assert.strictEqual(c.getImportance(rules3), 10);
+
+ const rules4 = [
+ {
+ condition: 'test_column2',
+ importance: 8
+ },
+ {
+ condition: /column/,
+ importance: 12
+ }
+ ];
+ assert.strictEqual(c.getImportance(rules4), 12);
+ });
+
+ test('checkMemoryColumn_nameMatchesCondition', function() {
+ const c = new NumericMemoryColumn('test_column', ['x']);
+
+ assert.isTrue(MemoryColumn.nameMatchesCondition('test_column', undefined));
+
+ assert.isFalse(MemoryColumn.nameMatchesCondition('test_column', 'test'));
+ assert.isTrue(
+ MemoryColumn.nameMatchesCondition('test_column', 'test_column'));
+ assert.isFalse(
+ MemoryColumn.nameMatchesCondition('test_column', 'test_column2'));
+
+ assert.isTrue(MemoryColumn.nameMatchesCondition('test_column', /test/));
+ assert.isTrue(
+ MemoryColumn.nameMatchesCondition('test_column', /^[^_]*_[^_]*$/));
+ assert.isFalse(MemoryColumn.nameMatchesCondition('test_column', /test$/));
+ });
+
+ test('checkStringMemoryColumn_value_singleField', function() {
+ const c = new StringMemoryColumn('', ['x'], AggregationMode.MAX);
+ c.color = function(fields, contexts) {
+ if (fields[0] < '0') return 'green';
+ if (contexts && contexts[0] % 2 === 0) return 'red';
+ return undefined;
+ };
+
+ const infos1 = [{ icon: '\u{1F648}', message: 'Some info', color: 'blue' }];
+ const infos2 = [
+ { icon: '\u{1F649}', message: 'Start', color: 'cyan' },
+ { icon: '\u{1F64A}', message: 'Stop' }
+ ];
+ c.addInfos = function(fields, contexts, infos) {
+ if (fields[0] < '0') {
+ infos.push.apply(infos, infos1);
+ } else if (contexts && contexts[0] % 2 === 0) {
+ infos.push.apply(infos, infos2);
+ }
+ };
+
+ let row = {x: new MemoryCell(['123'])};
+ assert.strictEqual(c.value(row), '123');
+
+ row = {x: new MemoryCell(['-123']), contexts: [undefined]};
+ checkCellValue(this, c.value(row), '-123', 'green', infos1);
+
+ row = {x: new MemoryCell(['123']), contexts: [42]};
+ checkCellValue(this, c.value(row), '123', 'red', infos2);
+ });
+
+ test('checkStringMemoryColumn_value_multipleFields', function() {
+ const c1 = new StringMemoryColumn('test_column1', ['x'],
+ undefined /* aggregation mode */);
+ const c2 = new StringMemoryColumn('test_column2', ['x'],
+ AggregationMode.DIFF);
+ c2.color = function(fields, contexts) {
+ return '#009999';
+ };
+ const c3 = new StringMemoryColumn('test_column3', ['x'],
+ AggregationMode.MAX);
+ c3.color = function(fields, contexts) {
+ if (fields[0] < '0') {
+ return 'green';
+ } else if (contexts && contexts[contexts.length - 1] % 2 === 0) {
+ return 'red';
+ }
+ return undefined;
+ };
+
+ const infos1 = [{ icon: '\u{1F648}', message: 'Some info', color: 'blue' }];
+ const infos2 = [
+ { icon: '\u{1F649}', message: 'Start', color: 'cyan' },
+ { icon: '\u{1F64A}', message: 'Stop' }
+ ];
+ c1.addInfos = c2.addInfos = c3.addInfos =
+ function(fields, contexts, infos) {
+ if (fields[0] < '0') {
+ infos.push.apply(infos, infos1);
+ } else if (contexts && contexts[contexts.length - 1] % 2 === 0) {
+ infos.push.apply(infos, infos2);
+ }
+ };
+
+ let row = {x: new MemoryCell(['123', '456'])};
+ checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '');
+ checkCellValue(this, c2.value(row), '123 \u2192 456', 'rgb(0, 153, 153)');
+ assert.strictEqual(c3.value(row), '456');
+
+ row = {
+ x: new MemoryCell(['-123', undefined, '+123']),
+ contexts: [12, 14, undefined]
+ };
+ checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '',
+ infos1);
+ checkCellValue(this, c2.value(row), '-123 \u2192 +123', 'rgb(0, 153, 153)',
+ infos1);
+ checkCellValue(this, c3.value(row), '+123', 'green', infos1);
+
+ row = {
+ x: new MemoryCell(['123', undefined, '456']),
+ contexts: [31, 7, -2]
+ };
+ checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '',
+ infos2);
+ checkCellValue(this, c2.value(row), '123 \u2192 456', 'rgb(0, 153, 153)',
+ infos2);
+ checkCellValue(this, c3.value(row), '456', 'red', infos2);
+ });
+
+ test('checkStringMemoryColumn_formatSingleField', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ undefined /* aggregation mode */);
+
+ assert.strictEqual(c.formatSingleField('1024'), '1024');
+ assert.strictEqual(c.formatSingleField('~10'), '~10');
+ });
+
+ test('checkStringMemoryColumn_formatMultipleFields_diff', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ // Added value.
+ checkMemoryColumnFieldFormat(this, c, [undefined, 'few'], '+few', 'red');
+ checkMemoryColumnFieldFormat(this, c, [undefined, 64, 32], '+32', 'red');
+
+ // Removed value.
+ checkMemoryColumnFieldFormat(this, c, ['00', undefined], '-00', 'green');
+ checkMemoryColumnFieldFormat(this, c, [1, undefined, 2, undefined], '-1',
+ 'green');
+
+ // Identical values.
+ checkMemoryColumnFieldFormat(this, c, ['Unchanged', 'Unchanged'],
+ 'Unchanged', undefined /* unchanged color (not an HTML element) */);
+ checkMemoryColumnFieldFormat(this, c, [16, 32, undefined, 64, 16], '16',
+ undefined /* unchanged color (not an HTML element) */);
+
+ // Different values.
+ checkMemoryColumnFieldFormat(this, c, ['A', 'C', undefined, 'C', 'B'],
+ 'A \u2192 B', 'darkorange');
+ checkMemoryColumnFieldFormat(this, c, [16, undefined, 64], '16 \u2192 64',
+ 'darkorange');
+ });
+
+ test('checkStringMemoryColumn_formatMultipleFields_max', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ // Different values.
+ checkMemoryColumnFieldFormat(this, c, ['A', 'B', 'A'], 'B',
+ undefined /* unchanged color (not an HTML element) */);
+ checkMemoryColumnFieldFormat(this, c, [16, 16, undefined, 17], '17',
+ undefined /* unchanged color (not an HTML element) */);
+
+ // Identical values.
+ checkMemoryColumnFieldFormat(this, c, ['X', 'X'], 'X',
+ undefined /* unchanged color (not an HTML element) */);
+ checkMemoryColumnFieldFormat(this, c, [7, undefined, 7, undefined, 7], '7',
+ undefined /* unchanged color (not an HTML element) */);
+ });
+
+ test('checkStringMemoryColumn_compareSingleFields', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ undefined /* aggregation mode */);
+
+ assert.isBelow(c.compareSingleFields(
+ new Scalar(sizeInBytes_smallerIsBetter, 2),
+ new Scalar(sizeInBytes_smallerIsBetter, 10)), 0);
+ assert.strictEqual(c.compareSingleFields('equal', 'equal'), 0);
+ assert.isAbove(c.compareSingleFields('100', '99'), 0);
+ });
+
+ test('checkStringMemoryColumn_compareMultipleFields_diff', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ // One field was added.
+ checkCompareFieldsLess(c, [-10, 10], [undefined, 5]);
+ checkCompareFieldsLess(c,
+ [-100, undefined, undefined], [undefined, 4, 5]);
+ checkCompareFieldsLess(c,
+ [1, 2, 3, 4], [undefined, 'x', undefined, 'y']);
+
+ // Both fields were added.
+ checkCompareFieldsEqual(c,
+ [undefined, 'C', undefined, 'A'], [undefined, 'B', 'D', 'A']);
+ checkCompareFieldsLess(c, [undefined, 1], [undefined, 2]);
+ checkCompareFieldsLess(c, [undefined, 6, 3], [undefined, 5, 4]);
+
+ // One field was removed (neither was added).
+ checkCompareFieldsLess(c, ['B', undefined], ['A', 'A']);
+ checkCompareFieldsLess(c,
+ [5, undefined, undefined], [undefined, -5, -10]);
+
+ // Both fields were removed (neither was added)
+ checkCompareFieldsEqual(c, ['T', 'A', undefined, undefined],
+ ['T', 'B', 'C', undefined]);
+ checkCompareFieldsLess(c, [5, undefined], [4, undefined]);
+
+ // Neither field was added or removed.
+ checkCompareFieldsLess(c, ['BB', 'BB'], ['AA', 'CC']);
+ checkCompareFieldsEqual(c, [7, 8, 9], [6, 9, 10]);
+ checkCompareFieldsEqual(c, [5, undefined, 5], [4, 3, 4]);
+ });
+
+ test('checkStringMemoryColumn_compareMultipleFields_max', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ // At least one field has multiple values.
+ checkCompareFieldsEqual(c, [0, 1, 3], [1, 3, 2]);
+ checkCompareFieldsLess(c, ['4', undefined, '4'], ['3', '4', '5']);
+ checkCompareFieldsLess(c, [3, 3, 3], [9, undefined, 10]);
+
+ // Both fields have single values.
+ checkCompareFieldsEqual(c,
+ [undefined, 'ttt', undefined], ['ttt', 'ttt', undefined]);
+ checkCompareFieldsLess(c, [undefined, -1, undefined], [-2, -2, -2]);
+ checkCompareFieldsLess(c, ['Q', 'Q', undefined], ['X', undefined, 'X']);
+ });
+
+ test('checkStringMemoryColumn_cmp', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ // Cell (or the associated field) undefined in one or both rows.
+ assert.strictEqual(c.cmp({}, {y: new MemoryCell([undefined])}), 0);
+ assert.strictEqual(c.cmp({x: new MemoryCell(undefined)}, {}), 0);
+ assert.strictEqual(
+ c.cmp({x: new MemoryCell([undefined, undefined])}, {}), 0);
+ assert.isAbove(c.cmp({x: new MemoryCell(['negative'])}, {}), 0);
+ assert.isAbove(c.cmp({x: new MemoryCell(['negative'])},
+ {x: new MemoryCell([undefined])}), 0);
+ assert.isBelow(c.cmp({}, {x: new MemoryCell(['positive'])}), 0);
+ assert.isBelow(c.cmp({x: new MemoryCell(undefined)},
+ {x: new MemoryCell(['positive'])}), 0);
+
+ // Single field.
+ assert.strictEqual(c.cmp({x: new MemoryCell(['equal'])},
+ {x: new MemoryCell(['equal'])}), 0);
+ assert.isAbove(c.cmp({x: new MemoryCell(['bigger'])},
+ {x: new MemoryCell(['BIG'])}), 0);
+ assert.isBelow(c.cmp({x: new MemoryCell(['small'])},
+ {x: new MemoryCell(['smaLL'])}), 0);
+
+ // Multiple fields.
+ assert.isBelow(c.cmp(
+ {x: new MemoryCell(['MemoryColumn', 'supports*', undefined])},
+ {x: new MemoryCell(['comparing', 'multiple', 'values :-)'])}), 0);
+ });
+
+ test('checkNumericMemoryColumn_value', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+ c.color = function(fields, contexts) {
+ return '#009999';
+ };
+ const infos1 = [createWarningInfo('Attention!')];
+ c.addInfos = function(fields, contexts, infos) {
+ infos.push.apply(infos, infos1);
+ };
+
+ // Undefined field values.
+ let row = {x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [undefined, 1, undefined])};
+ assert.strictEqual(c.value(row), '');
+
+ // Single field value.
+ row = {x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [5.4975581e13/* 50 TiB */])};
+ checkCellValue(this, c.value(row), sizeSpanMatcher(5.4975581e13),
+ 'rgb(0, 153, 153)', infos1);
+
+ // Multiple field values.
+ row = {
+ x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [5.4975581e13/* 50 TiB */, undefined, 2.1990233e13/* 20 TiB */])
+ };
+ checkCellValue(this, c.value(row),
+ sizeSpanMatcher(-3.2985348e13, true /* opt_expectedIsDelta */),
+ 'rgb(0, 153, 153)', infos1);
+
+ // With custom formatting context.
+ c.getFormattingContext = function(unit) {
+ assert.strictEqual(unit,
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter);
+ return { minimumFractionDigits: 2 };
+ };
+ checkCellValue(this, c.value(row),
+ sizeSpanMatcher(-3.2985348e13, true /* opt_expectedIsDelta */,
+ { minimumFractionDigits: 2 }),
+ 'rgb(0, 153, 153)', infos1);
+ });
+
+ test('checkNumericMemoryColumn_formatSingleField', function() {
+ let c = new NumericMemoryColumn('non_bytes_column', ['x'],
+ undefined /* aggregation mode */);
+ let value = c.formatSingleField(new Scalar(
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 123));
+ assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(value.value, 123);
+ assert.strictEqual(value.unit,
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter);
+ assert.isUndefined(value.contextGroup);
+ this.addHTMLOutput(value);
+
+ c = new NumericMemoryColumn('bytes_column', ['x'],
+ undefined /* aggregation mode */);
+ c.shouldSetContextGroup = true;
+ value = c.formatSingleField(new Scalar(
+ sizeInBytes_smallerIsBetter, 456));
+ assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(value.value, 456);
+ assert.strictEqual(value.unit,
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter);
+ assert.strictEqual(value.contextGroup, 'bytes_column');
+ this.addHTMLOutput(value);
+ });
+
+ test('checkNumericMemoryColumn_formatMultipleFields_diff',
+ function() {
+ let c = new NumericMemoryColumn(
+ 'non_bytes_column', ['x'], AggregationMode.DIFF);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 2);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, -10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 0);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [2.71828, 2.71829] /* diff within epsilon */,
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 0);
+
+ c = new NumericMemoryColumn(
+ 'bytes_column', ['x'], AggregationMode.DIFF);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 2);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, -10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 0);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [1.41421, 1.41422] /* diff within epsilon */,
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 0);
+ });
+
+ test('checkNumericMemoryColumn_formatMultipleFields_max',
+ function() {
+ let c = new NumericMemoryColumn(
+ 'non_bytes_column', ['x'], AggregationMode.MAX);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 3);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 60);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [undefined, 10, 20, undefined],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 20);
+
+ c = new NumericMemoryColumn(
+ 'bytes_column', ['x'], AggregationMode.MAX);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 3);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 60);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [undefined, 10, 20, undefined],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 20);
+ });
+
+ test('checkNumericMemoryColumn_cmp', function() {
+ const c = new NumericMemoryColumn(
+ 'test_column', ['x'], AggregationMode.DIFF);
+
+ // Undefined field values.
+ assert.isAbove(c.cmp({x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [-9999999999])},
+ {x: undefined}), 0);
+ assert.isBelow(c.cmp({x: new MemoryCell(undefined)},
+ {x: buildScalarCell(sizeInBytes_smallerIsBetter, [748, 749])}), 0);
+ assert.strictEqual(
+ c.cmp({}, {
+ x: buildScalarCell(
+ sizeInBytes_smallerIsBetter, [undefined, undefined])
+ }), 0);
+
+ // Single field value.
+ assert.isBelow(c.cmp(
+ {x: buildScalarCell(sizeInBytes_smallerIsBetter, [16384])},
+ {x: buildScalarCell(sizeInBytes_smallerIsBetter, [32768])}), 0);
+
+ // Multiple field values.
+ assert.strictEqual(c.cmp(
+ {x: buildScalarCell(
+ sizeInBytes_smallerIsBetter, [999, undefined, 1001])},
+ {x: buildScalarCell(
+ sizeInBytes_smallerIsBetter, [undefined, 5, 2])}), 0);
+ });
+
+ test('checkNumericMemoryColumn_compareSingleFields', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ undefined /* aggregation mode */);
+
+ assert.isBelow(c.compareSingleFields(
+ new Scalar(
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, 99),
+ new Scalar(
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, 100)), 0);
+ assert.strictEqual(c.compareSingleFields(
+ new Scalar(tr.b.Unit.byName.unitlessNumber, 0xEEE),
+ new Scalar(tr.b.Unit.byName.unitlessNumber, 0xEEE)), 0);
+ assert.isAbove(c.compareSingleFields(
+ new Scalar(sizeInBytes_smallerIsBetter, 10),
+ new Scalar(sizeInBytes_smallerIsBetter, 2)), 0);
+ });
+
+ test('checkNumericMemoryColumn_compareMultipleFields_diff', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ assert.isBelow(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10000, 10001, 10002] /* diff +2 */).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [5, 7, 8] /* diff +3 */).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [4, undefined] /* diff -4 */).fields,
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [999, 995] /* diff -4 */).fields), 0);
+ assert.isAbove(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10, undefined, 12] /* diff +2 */).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [11, 50, 12] /* diff +1 */).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [17, undefined, 17] /* diff 0 */).fields,
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [undefined, 100, undefined] /* diff 0 */).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [3.14159, undefined, 3.14160] /* diff within epsilon */).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [100, 100, 100] /* diff 0 */).fields), 0);
+ });
+
+ test('checkNumericMemoryColumn_compareMultipleFields_max', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ assert.isBelow(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10, undefined, 12]).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter, [11, 50, 12]).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [999, undefined, -8888]).fields,
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [undefined, 999, undefined]).fields), 0);
+ assert.isAbove(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10000, 10001, 10002]).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter, [5, 7, 8]).fields), 0);
+ assert.isBelow(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [17, undefined, 17]).fields,
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [undefined, 100, undefined]).fields), 0);
+ });
+
+ test('checkNumericMemoryColumn_getDiffFieldValue', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+ function checkDiffValue(first, last, expectedDiffValue) {
+ const actualDiffValue = c.getDiffFieldValue_(
+ first === undefined ? undefined :
+ new Scalar(sizeInBytes_smallerIsBetter, first),
+ last === undefined ? undefined :
+ new Scalar(sizeInBytes_smallerIsBetter, last));
+ assert.closeTo(actualDiffValue, expectedDiffValue, 1e-8);
+ }
+
+ // Diff outside epsilon range.
+ checkDiffValue(0, 0.0002, 0.0002);
+ checkDiffValue(undefined, 0.0003, 0.0003);
+ checkDiffValue(0.3334, 0.3332, -0.0002);
+ checkDiffValue(0.0005, undefined, -0.0005);
+
+ // Diff inside epsilon range.
+ checkDiffValue(5, 5.00009, 0);
+ checkDiffValue(undefined, 0.0000888, 0);
+ checkDiffValue(0.29999, 0.3, 0);
+ checkDiffValue(0.00009, undefined, 0);
+ checkDiffValue(0.777777, 0.777777, 0);
+ checkDiffValue(undefined, undefined, 0);
+ });
+
+ test('checkExpandTableRowsRecursively', function() {
+ const columns = [
+ {
+ title: 'Single column',
+ value(row) { return row.data; },
+ width: '100px'
+ }
+ ];
+
+ const rows = [
+ {
+ data: 'allocated',
+ subRows: [
+ {
+ data: 'v8',
+ subRows: []
+ },
+ {
+ data: 'oilpan',
+ subRows: [
+ {
+ data: 'still_visible',
+ subRows: [
+ {
+ data: 'not_visible_any_more'
+ }
+ ]
+ },
+ {
+ data: 'also_visible'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ data: 'no_sub_rows'
+ },
+ {
+ data: 'fragmentation',
+ subRows: [
+ {
+ data: 'internal'
+ },
+ {
+ data: 'external',
+ subRows: [
+ {
+ data: 'unexpanded'
+ }
+ ]
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ expandTableRowsRecursively(table);
+
+ function isExpanded(row) { return table.getExpandedForTableRow(row); }
+
+ // Level 0 (3 rows) should be expanded (except for nodes which have no
+ // sub-rows).
+ assert.isTrue(isExpanded(rows[0] /* allocated */));
+ assert.isFalse(isExpanded(rows[1] /* no_sub_rows */));
+ assert.isTrue(isExpanded(rows[2] /* overhead */));
+
+ // Level 1 (4 rows) should be expanded (except for nodes which have no
+ // sub-rows).
+ assert.isFalse(isExpanded(rows[0].subRows[0] /* allocated/v8 */));
+ assert.isTrue(isExpanded(rows[0].subRows[1] /* allocated/oilpan */));
+ assert.isFalse(isExpanded(rows[2].subRows[0] /* fragmentation/internal */));
+ assert.isTrue(isExpanded(rows[2].subRows[1] /* fragmentation/external */));
+
+ // Level 2 (3 rows) should not be expanded any more.
+ assert.isFalse(isExpanded(
+ rows[0].subRows[1].subRows[0] /* allocated/oilpan/still_visible */));
+ assert.isFalse(isExpanded(
+ rows[0].subRows[1].subRows[1] /* allocated/oilpan/also_visible */));
+ assert.isFalse(isExpanded(
+ rows[2].subRows[1].subRows[0] /* fragmentation/external/unexpanded */));
+ });
+
+ test('checkMemoryCell_extractFields', function() {
+ assert.isUndefined(MemoryCell.extractFields(undefined));
+
+ assert.isUndefined(MemoryCell.extractFields(new MemoryCell(undefined)));
+
+ const fields = [new Scalar(sizeInBytes_smallerIsBetter, 1024)];
+ assert.strictEqual(
+ MemoryCell.extractFields(new MemoryCell(fields)), fields);
+ });
+
+ test('checkAggregateTableRowCellsRecursively', function() {
+ const row = {
+ testCells: {
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [17])
+ },
+ subRows: [
+ {
+ // Intentionally no testCells.
+ subRows: [
+ {
+ testCells: {
+ b: buildScalarCell(sizeInBytes_smallerIsBetter, [103]),
+ c: new MemoryCell(['should-not-propagate-upwards']),
+ d: buildScalarCell(sizeInBytes_smallerIsBetter, [-200])
+ }
+ // Intentionally no subRows.
+ },
+ {
+ testCells: {},
+ subRows: []
+ }
+ ],
+ contexts: ['skip-row-when-using-predicate']
+ },
+ {
+ testCells: {
+ b: buildScalarCell(sizeInBytes_smallerIsBetter, [20]),
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [13]),
+ e: buildScalarCell(sizeInBytes_smallerIsBetter, [-300])
+ },
+ contexts: ['don\'t-skip']
+ }
+ ]
+ };
+
+ // Without a predicate.
+ const ca = new NumericMemoryColumn('column_a', ['testCells', 'a']);
+ const cb = new NumericMemoryColumn('column_b', ['testCells', 'b']);
+ const cc = new StringMemoryColumn('column_c', ['testCells', 'c']);
+ aggregateTableRowCellsRecursively(row, [ca, cb, cc]);
+ checkSizeNumericFields(row, ca, [17]);
+ checkSizeNumericFields(row, cb, [123]);
+ checkStringFields(row, cc, undefined);
+
+ // With a predicate.
+ const cd = new NumericMemoryColumn('column_d', ['testCells', 'd']);
+ const ce = new NumericMemoryColumn('column_e', ['testCells', 'e']);
+ aggregateTableRowCellsRecursively(row, [cd, ce], function(contexts) {
+ return contexts === undefined || !contexts[0].startsWith('skip');
+ });
+ checkSizeNumericFields(row, cd, undefined);
+ checkSizeNumericFields(row, ce, [-300]);
+ });
+
+ test('checkAggregateTableRowCells', function() {
+ const row = {
+ // Intentionally no testCells.
+ otherCells: {
+ a: buildScalarCell(tr.b.Unit.byName.unitlessNumber,
+ [5, undefined, undefined])
+ }
+ };
+ const subRows = [
+ {
+ testCells: {
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [1, 9])
+ },
+ subRows: [
+ {
+ testCells: {
+ c: buildScalarCell(sizeInBytes_smallerIsBetter, [13])
+ }
+ }
+ ]
+ },
+ {
+ testCells: {
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [2, 17]),
+ b: buildScalarCell(sizeInBytes_smallerIsBetter, [5])
+ },
+ otherCells: {
+ a: buildScalarCell(tr.b.Unit.byName.unitlessNumber,
+ [153, undefined, 257]),
+ b: new MemoryCell(['field-should-not-propagate-upwards', ''])
+ }
+ }
+ ];
+
+ const cta = new NumericMemoryColumn('column_test_a', ['testCells', 'a']);
+ const ctb = new NumericMemoryColumn('column_test_b', ['testCells', 'b']);
+ const ctc = new NumericMemoryColumn('column_test_c', ['testCells', 'c']);
+ const coa = new NumericMemoryColumn('column_other_a', ['otherCells', 'a']);
+ const cob = new StringMemoryColumn('column_other_b', ['otherCells', 'b']);
+
+ aggregateTableRowCells(row, subRows, [cta, ctb, ctc, coa, cob]);
+
+ checkSizeNumericFields(row, cta, [3, 26]);
+ checkSizeNumericFields(row, ctb, [5]);
+ checkSizeNumericFields(row, ctc, undefined);
+
+ checkNumericFields(row, coa, [5, undefined, 257],
+ tr.b.Unit.byName.unitlessNumber);
+ checkStringFields(row, cob, undefined);
+ });
+
+ test('checkCreateCells', function() {
+ const values = [
+ {
+ a: 9,
+ b: 314
+ },
+ {
+ b: 159,
+ c: undefined
+ },
+ undefined,
+ {
+ b: 265,
+ d: 0
+ }
+ ];
+
+ const mockColumn = new MemoryColumn('', [], undefined);
+
+ const cells = createCells(values, function(dict) {
+ const fields = {};
+ for (const [key, value] of Object.entries(dict)) {
+ if (value === undefined) continue;
+ fields[key] = new Scalar(sizeInBytes_smallerIsBetter, value);
+ }
+ return fields;
+ });
+ assert.deepEqual(Object.keys(cells), ['a', 'b', 'd']);
+ checkSizeNumericFields(
+ cells.a, mockColumn, [9, undefined, undefined, undefined]);
+ checkSizeNumericFields(cells.b, mockColumn, [314, 159, undefined, 265]);
+ checkSizeNumericFields(
+ cells.d, mockColumn, [undefined, undefined, undefined, 0]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html
new file mode 100644
index 00000000000..2a20bb3c27e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html
@@ -0,0 +1,382 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-memory-dump-vm-regions-details-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #label {
+ flex: 0 0 auto;
+ padding: 8px;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #table {
+ display: none; /* Hide until memory dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <div id="label">Memory maps</div>
+ <div id="contents">
+ <div id="info_text">No memory maps selected</div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+
+ const CONSTANT_COLUMN_RULES = [
+ {
+ condition: 'Start address',
+ importance: 0,
+ columnConstructor: tr.ui.analysis.StringMemoryColumn
+ }
+ ];
+
+ const VARIABLE_COLUMN_RULES = [
+ {
+ condition: 'Virtual size',
+ importance: 7,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Protection flags',
+ importance: 6,
+ columnConstructor: tr.ui.analysis.StringMemoryColumn
+ },
+ {
+ condition: 'PSS',
+ importance: 5,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Private dirty',
+ importance: 4,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Private clean',
+ importance: 3,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Shared dirty',
+ importance: 2,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Shared clean',
+ importance: 1,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Swapped',
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ }
+ ];
+
+ const BYTE_STAT_COLUMN_MAP = {
+ 'proportionalResident': 'PSS',
+ 'privateDirtyResident': 'Private dirty',
+ 'privateCleanResident': 'Private clean',
+ 'sharedDirtyResident': 'Shared dirty',
+ 'sharedCleanResident': 'Shared clean',
+ 'swapped': 'Swapped'
+ };
+
+ function hexString(address, is64BitAddress) {
+ if (address === undefined) return undefined;
+ const hexPadding = is64BitAddress ? '0000000000000000' : '00000000';
+ return (hexPadding + address.toString(16)).substr(-hexPadding.length);
+ }
+
+ function pruneEmptyRuleRows(row) {
+ if (row.subRows === undefined || row.subRows.length === 0) return;
+
+ // Either all sub-rows are rule rows, or all sub-rows are VM region rows.
+ if (row.subRows[0].rule === undefined) {
+ // VM region rows: Early out to avoid filtering a large array for
+ // performance reasons (no sub-rows would be removed, but the whole array
+ // would be unnecessarily copied to a new array).
+ return;
+ }
+
+ row.subRows.forEach(pruneEmptyRuleRows);
+ row.subRows = row.subRows.filter(function(subRow) {
+ return subRow.subRows.length > 0;
+ });
+ }
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-vm-regions-details-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.vmRegions_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ },
+
+ /**
+ * Sets the VM regions and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of lists of VM
+ * regions. All VM regions are assumed to belong to the same process.
+ * Example:
+ *
+ * [
+ * [
+ * // VM regions at timestamp 1.
+ * tr.model.VMRegion {},
+ * tr.model.VMRegion {},
+ * tr.model.VMRegion {}
+ * ],
+ * undefined, // No VM regions provided at timestamp 2.
+ * [
+ * // VM regions at timestamp 3.
+ * tr.model.VMRegion {},
+ * tr.model.VMRegion {}
+ * ]
+ * ]
+ */
+ set vmRegions(vmRegions) {
+ this.vmRegions_ = vmRegions;
+ this.scheduleRebuild_();
+ },
+
+ get vmRegions() {
+ return this.vmRegions_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ onRebuild_() {
+ if (this.vmRegions_ === undefined || this.vmRegions_.length === 0) {
+ // Show the info text (hide the table).
+ this.$.info_text.style.display = 'block';
+ this.$.table.style.display = 'none';
+
+ this.$.table.clear();
+ this.$.table.rebuild();
+ return;
+ }
+
+ // Show the table (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.table.style.display = 'block';
+
+ const rows = this.createRows_(this.vmRegions_);
+ const columns = this.createColumns_(rows);
+
+ // Note: There is no need to aggregate fields of the VM regions because
+ // the classification tree already takes care of that.
+
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = columns;
+
+ // TODO(petrcermak): This can be quite slow. Consider doing this somehow
+ // asynchronously.
+ this.$.table.rebuild();
+
+ tr.ui.analysis.expandTableRowsRecursively(this.$.table);
+ },
+
+ createRows_(timeToVmRegionTree) {
+ // Determine if any start address is outside the 32-bit range.
+ const is64BitAddress = timeToVmRegionTree.some(function(vmRegionTree) {
+ if (vmRegionTree === undefined) return false;
+ return vmRegionTree.someRegion(function(region) {
+ if (region.startAddress === undefined) return false;
+ return region.startAddress >= 4294967296; /* 2^32 */
+ });
+ });
+
+ return [
+ this.createClassificationNodeRow(timeToVmRegionTree, is64BitAddress)
+ ];
+ },
+
+ createClassificationNodeRow(timeToNode, is64BitAddress) {
+ // Get any defined classification node so that we can extract the
+ // properties which don't change over time.
+ const definedNode = timeToNode.find(x => x);
+
+ // Child node ID (list index) -> Timestamp (list index) ->
+ // VM region classification node.
+ const childNodeIdToTimeToNode = Object.values(
+ tr.b.invertArrayOfDicts(timeToNode, function(node) {
+ const children = node.children;
+ if (children === undefined) return undefined;
+ const childMap = {};
+ children.forEach(function(childNode) {
+ if (!childNode.hasRegions) return;
+ childMap[childNode.title] = childNode;
+ });
+ return childMap;
+ }));
+ const childNodeSubRows = childNodeIdToTimeToNode.map(
+ function(timeToChildNode) {
+ return this.createClassificationNodeRow(
+ timeToChildNode, is64BitAddress);
+ }, this);
+
+ // Region ID (list index) -> Timestamp (list index) -> VM region.
+ const regionIdToTimeToRegion = Object.values(
+ tr.b.invertArrayOfDicts(timeToNode, function(node) {
+ const regions = node.regions;
+ if (regions === undefined) return undefined;
+
+ const results = {};
+ for (const region of regions) {
+ results[region.uniqueIdWithinProcess] = region;
+ }
+ return results;
+ }));
+ const regionSubRows = regionIdToTimeToRegion.map(function(timeToRegion) {
+ return this.createRegionRow_(timeToRegion, is64BitAddress);
+ }, this);
+
+ const subRows = childNodeSubRows.concat(regionSubRows);
+
+ return {
+ title: definedNode.title,
+ contexts: timeToNode,
+ variableCells: this.createVariableCells_(timeToNode),
+ subRows
+ };
+ },
+
+ createRegionRow_(timeToRegion, is64BitAddress) {
+ // Get any defined VM region so that we can extract the properties which
+ // don't change over time.
+ const definedRegion = timeToRegion.find(x => x);
+
+ return {
+ title: definedRegion.mappedFile,
+ contexts: timeToRegion,
+ constantCells: this.createConstantCells_(definedRegion, is64BitAddress),
+ variableCells: this.createVariableCells_(timeToRegion)
+ };
+ },
+
+ /**
+ * Create cells for VM region properties which DON'T change over time.
+ *
+ * Note that there are currently no such properties of classification nodes.
+ */
+ createConstantCells_(definedRegion, is64BitAddress) {
+ return tr.ui.analysis.createCells([definedRegion], function(region) {
+ const startAddress = region.startAddress;
+ if (startAddress === undefined) return undefined;
+ return { 'Start address': hexString(startAddress, is64BitAddress) };
+ });
+ },
+
+ /**
+ * Create cells for VM region (classification node) properties which DO
+ * change over time.
+ */
+ createVariableCells_(timeToRegion) {
+ return tr.ui.analysis.createCells(timeToRegion, function(region) {
+ const fields = {};
+
+ const sizeInBytes = region.sizeInBytes;
+ if (sizeInBytes !== undefined) {
+ fields['Virtual size'] = new Scalar(
+ sizeInBytes_smallerIsBetter, sizeInBytes);
+ }
+ const protectionFlags = region.protectionFlagsToString;
+ if (protectionFlags !== undefined) {
+ fields['Protection flags'] = protectionFlags;
+ }
+
+ for (const [byteStatName, columnName] of
+ Object.entries(BYTE_STAT_COLUMN_MAP)) {
+ const byteStat = region.byteStats[byteStatName];
+ if (byteStat === undefined) continue;
+ fields[columnName] = new Scalar(
+ sizeInBytes_smallerIsBetter, byteStat);
+ }
+
+ return fields;
+ });
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new tr.ui.analysis.TitleColumn('Mapped file');
+ titleColumn.width = '200px';
+
+ const constantColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'constantCells',
+ aggregationMode: undefined,
+ rules: CONSTANT_COLUMN_RULES
+ });
+ const variableColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'variableCells',
+ aggregationMode: this.aggregationMode_,
+ rules: VARIABLE_COLUMN_RULES
+ });
+ const fieldColumns = constantColumns.concat(variableColumns);
+ tr.ui.analysis.MemoryColumn.spaceEqually(fieldColumns);
+
+ const columns = [titleColumn].concat(fieldColumns);
+ return columns;
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html
new file mode 100644
index 00000000000..7534727091b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html
@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const TitleColumn = tr.ui.analysis.TitleColumn;
+ const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkStringFields = tr.ui.analysis.checkStringFields;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ function createVMRegions() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(
+ model, {ts: 42, levelOfDetail: DETAILED});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: 42});
+ pmd1.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ mappedFile: '/lib/chrome.so',
+ startAddress: 65536,
+ sizeInBytes: 536870912,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 8192
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0',
+ startAddress: 140296983150592,
+ sizeInBytes: 2097152,
+ protectionFlags: 0,
+ byteStats: {
+ proportionalResident: 0
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 10995116277760,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 12094627905536,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/dalvik-zygote space',
+ startAddress: 13194139533312,
+ sizeInBytes: 100,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 100,
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/libc malloc',
+ startAddress: 14293651161088,
+ sizeInBytes: 200,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 200,
+ privateDirtyResident: 96,
+ swapped: 0
+ }
+ })
+ ]);
+
+ // This is here so that we could test that tracing is discounted from the
+ // 'Native heap' category.
+ pmd1.memoryAllocatorDumps = [
+ newAllocatorDump(pmd1, 'tracing',
+ {numerics: {size: 500, resident_size: 32}})
+ ];
+
+ // Second timestamp.
+ const gmd2 = addGlobalMemoryDump(
+ model, {ts: 42, levelOfDetail: DETAILED});
+ const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 42});
+ pmd2.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ mappedFile: '/lib/chrome.so',
+ startAddress: 65536,
+ sizeInBytes: 536870912,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 9216
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/lib/chrome.so',
+ startAddress: 140296983150592,
+ sizeInBytes: 536870912,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 10240
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 10995116277760,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 32
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 12094627905536,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/dalvik-zygote space',
+ startAddress: 13194139533312,
+ sizeInBytes: 100,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 0,
+ privateDirtyResident: 100,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/libc malloc',
+ startAddress: 14293651161088,
+ sizeInBytes: 200,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 100,
+ privateDirtyResident: 96,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/usr/share/fonts/DejaVuSansMono.ttf',
+ startAddress: 140121259503616,
+ sizeInBytes: 335872,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ byteStats: {
+ proportionalResident: 22528
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: 'another-map',
+ startAddress: 52583094233905872,
+ sizeInBytes: 1,
+ byteStats: {
+ proportionalResident: 1,
+ privateDirtyResident: 1,
+ swapped: 1
+ }
+ })
+ ]);
+ });
+
+ return model.processes[1].memoryDumps.map(function(pmd) {
+ return pmd.mostRecentVmRegions;
+ });
+ }
+
+ const EXPECTED_COLUMNS = [
+ { title: 'Mapped file', type: TitleColumn, noAggregation: true },
+ { title: 'Start address', type: StringMemoryColumn, noAggregation: true },
+ { title: 'Virtual size', type: NumericMemoryColumn },
+ { title: 'Protection flags', type: StringMemoryColumn },
+ { title: 'PSS', type: NumericMemoryColumn },
+ { title: 'Private dirty', type: NumericMemoryColumn },
+ { title: 'Swapped', type: NumericMemoryColumn }
+ ];
+
+ function checkRow(columns, row, expectedTitle, expectedStartAddress,
+ expectedVirtualSize, expectedProtectionFlags,
+ expectedProportionalResidentValues, expectedPrivateDirtyResidentValues,
+ expectedSwappedValues, expectedSubRowCount, expectedContexts) {
+ assert.strictEqual(columns[0].formatTitle(row), expectedTitle);
+ checkStringFields(row, columns[1], expectedStartAddress);
+ checkSizeNumericFields(row, columns[2], expectedVirtualSize);
+ checkStringFields(row, columns[3], expectedProtectionFlags);
+ checkSizeNumericFields(row, columns[4], expectedProportionalResidentValues);
+ checkSizeNumericFields(row, columns[5], expectedPrivateDirtyResidentValues);
+ checkSizeNumericFields(row, columns[6], expectedSwappedValues);
+
+ if (expectedSubRowCount === undefined) {
+ assert.isUndefined(row.subRows);
+ } else {
+ assert.lengthOf(row.subRows, expectedSubRowCount);
+ }
+
+ if (typeof expectedContexts === 'function') {
+ expectedContexts(row.contexts);
+ } else if (expectedContexts !== undefined) {
+ assert.deepEqual(Array.from(row.contexts), expectedContexts);
+ } else {
+ assert.isUndefined(row.contexts);
+ }
+ }
+
+ function genericMatcher(callback, defined) {
+ return function(actualValues) {
+ assert.lengthOf(actualValues, defined.length);
+ for (let i = 0; i < defined.length; i++) {
+ const actualValue = actualValues[i];
+ if (defined[i]) {
+ callback(actualValue);
+ } else {
+ assert.isUndefined(actualValue);
+ }
+ }
+ };
+ }
+
+ function vmRegionsMatcher(expectedMappedFile, expectedStartAddress, defined) {
+ return genericMatcher(function(actualRegion) {
+ assert.instanceOf(actualRegion, VMRegion);
+ assert.strictEqual(actualRegion.mappedFile, expectedMappedFile);
+ assert.strictEqual(actualRegion.startAddress, expectedStartAddress);
+ }, defined);
+ }
+
+ function classificationNodesMatcher(expectedTitle, defined) {
+ return genericMatcher(function(actualNode) {
+ assert.instanceOf(actualNode, VMRegionClassificationNode);
+ assert.strictEqual(actualNode.title, expectedTitle);
+ }, defined);
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-vm-regions-details-pane', 'vmRegions',
+ function(viewEl) {
+ // Check that the info text is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.info_text));
+ assert.isFalse(isElementDisplayed(viewEl.$.table));
+ });
+ });
+
+ test('instantiate_single', function() {
+ const vmRegions = createVMRegions().slice(0, 1);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, undefined /* no aggregation */);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const totalRow = rows[0];
+ checkRow(columns, totalRow, 'Total', undefined, [4833935160], undefined,
+ [8460], [64], [0], 3, vmRegions);
+
+ const androidRow = totalRow.subRows[0];
+ checkRow(columns, androidRow, 'Android', undefined, [100], undefined,
+ [100], [0], [0], 1, classificationNodesMatcher('Android', [true]));
+
+ const javaRuntimeRow = androidRow.subRows[0];
+ checkRow(columns, javaRuntimeRow, 'Java runtime', undefined, [100],
+ undefined, [100], [0], [0], 1,
+ classificationNodesMatcher('Java runtime', [true]));
+
+ const spacesRow = javaRuntimeRow.subRows[0];
+ checkRow(columns, spacesRow, 'Spaces', undefined, [100], undefined, [100],
+ [0], [0], 1, classificationNodesMatcher('Spaces', [true]));
+
+ const nativeHeapRow = totalRow.subRows[1];
+ checkRow(columns, nativeHeapRow, 'Native heap', undefined, [4294966996],
+ undefined, [168], [64], [0], 4,
+ classificationNodesMatcher('Native heap', [true]));
+
+ const discountedTracingOverheadRow = nativeHeapRow.subRows[3];
+ checkRow(columns, discountedTracingOverheadRow,
+ '[discounted tracing overhead]', undefined, [-500], undefined, [-32],
+ [-32], undefined, undefined,
+ vmRegionsMatcher('[discounted tracing overhead]', undefined, [true]));
+
+ const filesRow = totalRow.subRows[2];
+ checkRow(columns, filesRow, 'Files', undefined, [538968064], undefined,
+ [8192], undefined, undefined, 1,
+ classificationNodesMatcher('Files', [true]));
+
+ const soRow = filesRow.subRows[0];
+ checkRow(columns, soRow, 'so', undefined, [538968064], undefined,
+ [8192], undefined, undefined, 2,
+ classificationNodesMatcher('so', [true]));
+
+ const mmapChromeRow = soRow.subRows[0];
+ checkRow(columns, mmapChromeRow, '/lib/chrome.so', ['0000000000010000'],
+ [536870912], ['r-xp'], [8192], undefined, undefined, undefined,
+ vmRegionsMatcher('/lib/chrome.so', 65536, [true]));
+
+ const mmapLibX11Row = soRow.subRows[1];
+ checkRow(columns, mmapLibX11Row,
+ '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', ['00007f996fd80000'],
+ [2097152], ['---p'], [0], undefined, undefined, undefined,
+ vmRegionsMatcher('/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0',
+ 140296983150592, [true]));
+ });
+
+ test('instantiate_multipleDiff', function() {
+ const vmRegions = createVMRegions();
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const totalRow = rows[0];
+ checkRow(columns, totalRow, 'Total', undefined, [4833935160, 5369045293],
+ undefined, [8460, 42085], [64, 197], [0, 33], 4, vmRegions);
+
+ const androidRow = totalRow.subRows[0];
+ checkRow(columns, androidRow, 'Android', undefined, [100, 100], undefined,
+ [100, 0], [0, 100], [0, 0], 1,
+ classificationNodesMatcher('Android', [true, true]));
+
+ const javaRuntimeRow = androidRow.subRows[0];
+ checkRow(columns, javaRuntimeRow, 'Java runtime', undefined, [100, 100],
+ undefined, [100, 0], [0, 100], [0, 0], 1,
+ classificationNodesMatcher('Java runtime', [true, true]));
+
+ const spacesRow = javaRuntimeRow.subRows[0];
+ checkRow(columns, spacesRow, 'Spaces', undefined, [100, 100], undefined,
+ [100, 0], [0, 100], [0, 0], 1,
+ classificationNodesMatcher('Spaces', [true, true]));
+
+ const nativeHeapRow = totalRow.subRows[1];
+ checkRow(columns, nativeHeapRow, 'Native heap', undefined,
+ [4294966996, 4294967496], undefined, [168, 100], [64, 96], [0, 32], 4,
+ classificationNodesMatcher('Native heap', [true, true]));
+
+ const discountedTracingOverheadRow = nativeHeapRow.subRows[3];
+ checkRow(columns, discountedTracingOverheadRow,
+ '[discounted tracing overhead]', undefined, [-500, undefined],
+ undefined, [-32, undefined], [-32, undefined], undefined, undefined,
+ vmRegionsMatcher('[discounted tracing overhead]', undefined,
+ [true, false]));
+
+ const filesRow = totalRow.subRows[2];
+ checkRow(columns, filesRow, 'Files', undefined, [538968064, 1074077696],
+ undefined, [8192, 41984], undefined, undefined, 2,
+ classificationNodesMatcher('Files', [true, true]));
+
+ const soRow = filesRow.subRows[0];
+ checkRow(columns, soRow, 'so', undefined, [538968064, 1073741824],
+ undefined, [8192, 19456], undefined, undefined, 3,
+ classificationNodesMatcher('so', [true, true]));
+
+ const mmapChromeRow = soRow.subRows[0];
+ checkRow(columns, mmapChromeRow, '/lib/chrome.so', ['0000000000010000'],
+ [536870912, 536870912], ['r-xp', 'r-xp'], [8192, 9216], undefined,
+ undefined, undefined,
+ vmRegionsMatcher('/lib/chrome.so', 65536, [true, true]));
+
+ const mmapLibX11Row = soRow.subRows[1];
+ checkRow(columns, mmapLibX11Row,
+ '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', ['00007f996fd80000'],
+ [2097152, undefined], ['---p', undefined], [0, undefined], undefined,
+ undefined, undefined,
+ vmRegionsMatcher('/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0',
+ 140296983150592, [true, false]));
+
+ const otherRow = totalRow.subRows[3];
+ checkRow(columns, otherRow, 'Other', undefined, [undefined, 1], undefined,
+ [undefined, 1], [undefined, 1], [undefined, 1], 1,
+ classificationNodesMatcher('Other', [false, true]));
+
+ const anotherMapRow = otherRow.subRows[0];
+ checkRow(columns, anotherMapRow, 'another-map', ['00bad00bad00bad0'],
+ [undefined, 1], undefined, [undefined, 1], [undefined, 1],
+ [undefined, 1], undefined,
+ vmRegionsMatcher('another-map', 52583094233905872, [false, true]));
+ });
+
+ test('instantiate_multipleMax', function() {
+ const vmRegions = createVMRegions();
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ // Just check that the aggregation mode was propagated to the columns.
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.MAX);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+ });
+
+ test('instantiate_multipleWithUndefined', function() {
+ const vmRegions = createVMRegions();
+ vmRegions.splice(1, 0, undefined);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ // Just check that the table has the right shape.
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html
new file mode 100644
index 00000000000..0bb39b7e0f2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+
+<dom-module id='tr-ui-a-multi-async-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #container {
+ display: flex;
+ flex: 1 1 auto;
+ }
+ #events {
+ margin-left: 8px;
+ flex: 0 1 200px;
+ }
+ </style>
+ <div id="container">
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ <div id="events">
+ <tr-ui-a-related-events id="relatedEvents"></tr-ui-a-related-events>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-async-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ this.$.relatedEvents.setRelatedEvents(selection);
+ if (this.$.relatedEvents.hasRelatedEvents()) {
+ this.$.relatedEvents.style.display = '';
+ } else {
+ this.$.relatedEvents.style.display = 'none';
+ }
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.$.content.selection) return undefined;
+
+ const selection = new tr.model.EventSet();
+ this.$.content.selection.forEach(function(asyncEvent) {
+ if (!asyncEvent.associatedEvents) return;
+
+ asyncEvent.associatedEvents.forEach(function(event) {
+ selection.push(event);
+ });
+ });
+ if (selection.length) return selection;
+ return undefined;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-async-slice-sub-view',
+ tr.model.AsyncSlice,
+ {
+ multi: true,
+ title: 'Async Slices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html
new file mode 100644
index 00000000000..20fb52c058f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_async_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'a',
+ start: 10,
+ end: 20,
+ startThread: t1,
+ endThread: t1
+ }));
+ t1.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'b',
+ start: 25,
+ end: 40,
+ startThread: t1,
+ endThread: t1
+ }));
+
+ const selection = new tr.model.EventSet();
+ selection.push(t1.asyncSliceGroup.slices[0]);
+ selection.push(t1.asyncSliceGroup.slices[1]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-async-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html
new file mode 100644
index 00000000000..4525df0e8c2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-cpu-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #content {
+ flex: 1 1 auto;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-cpu-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.$.content.eventsHaveSubRows = false;
+ },
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.setSelectionWithoutErrorChecks(selection);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-cpu-slice-sub-view',
+ tr.model.CpuSlice,
+ {
+ multi: true,
+ title: 'CPU Slices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html
new file mode 100644
index 00000000000..36dc99bd338
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_cpu_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('instantiate', function() {
+ const m = createBasicModel();
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+
+ const selection = new tr.model.EventSet();
+ selection.push(cpu.slices[0]);
+ selection.push(cpu.slices[1]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-cpu-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html
new file mode 100644
index 00000000000..52908c620bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html
@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary_table.html">
+<link rel="import" href="/tracing/ui/analysis/selection_summary_table.html">
+<link rel="import" href="/tracing/ui/base/radio_picker.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/value/diagnostics/scalar.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/histogram_span.html">
+
+<dom-module id='tr-ui-a-multi-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ overflow: auto;
+ }
+ #content {
+ display: flex;
+ flex-direction: column;
+ flex: 0 1 auto;
+ align-self: stretch;
+ }
+ #content > * {
+ flex: 0 0 auto;
+ align-self: stretch;
+ }
+ #histogramContainer {
+ display: flex;
+ }
+
+ tr-ui-a-multi-event-summary-table {
+ border-bottom: 1px solid #aaa;
+ }
+
+ tr-ui-a-selection-summary-table {
+ margin-top: 1.25em;
+ border-top: 1px solid #aaa;
+ background-color: #eee;
+ font-weight: bold;
+ margin-bottom: 1.25em;
+ border-bottom: 1px solid #aaa;
+ }
+ </style>
+ <div id="content">
+ <tr-ui-a-multi-event-summary-table id="eventSummaryTable">
+ </tr-ui-a-multi-event-summary-table>
+ <tr-ui-a-selection-summary-table id="selectionSummaryTable">
+ </tr-ui-a-selection-summary-table>
+ <tr-ui-b-radio-picker id="radioPicker">
+ </tr-ui-b-radio-picker>
+ <div id="histogramContainer">
+ <tr-v-ui-histogram-span id="histogramSpan">
+ </tr-v-ui-histogram-span>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'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));
+ }
+
+ 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 {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html
new file mode 100644
index 00000000000..9958b7db81c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const Thread = tr.model.Thread;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('differentTitles', function() {
+ const model = new Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0.0, duration: 0.04}));
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0.12, duration: 0.06}));
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'aa', start: 0.5, duration: 0.5}));
+ t53.sliceGroup.createSubSlices();
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ selection.push(t53.sliceGroup.slices[1]);
+ selection.push(t53.sliceGroup.slices[2]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+
+ const summaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-multi-event-summary-table');
+ assert.isTrue(summaryTableEl.showTotals);
+ assert.lengthOf(Object.keys(summaryTableEl.eventsByTitle), 2);
+
+ const selectionSummaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-selection-summary-table');
+ assert.strictEqual(selectionSummaryTableEl.selection, selection);
+
+ const radioPickerEl =
+ tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-radio-picker');
+ assert.strictEqual(radioPickerEl.style.display, 'none');
+ });
+
+ test('sameTitles', function() {
+ const model = new Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'c', start: 0.0, duration: 0.04}));
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'c', start: 0.12, duration: 0.06}));
+ t53.sliceGroup.createSubSlices();
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ selection.push(t53.sliceGroup.slices[1]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+
+ const summaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-multi-event-summary-table');
+ assert.isFalse(summaryTableEl.showTotals);
+ assert.lengthOf(Object.keys(summaryTableEl.eventsByTitle), 1);
+
+ const selectionSummaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-selection-summary-table');
+ assert.strictEqual(selectionSummaryTableEl.selection, selection);
+
+ const radioPickerEl =
+ tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-radio-picker');
+ assert.strictEqual(radioPickerEl.style.display, 'block');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html
new file mode 100644
index 00000000000..886e315863e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.analysis', function() {
+ function MultiEventSummary(title, events) {
+ this.title = title;
+ this.duration_ = undefined;
+ this.selfTime_ = undefined;
+ this.events_ = events;
+
+ this.cpuTimesComputed_ = false;
+ this.cpuSelfTime_ = undefined;
+ this.cpuDuration_ = undefined;
+
+ this.maxDuration_ = undefined;
+ this.maxCpuDuration_ = undefined;
+ this.maxSelfTime_ = undefined;
+ this.maxCpuSelfTime_ = undefined;
+
+ this.untotallableArgs_ = [];
+ this.totalledArgs_ = undefined;
+ }
+ MultiEventSummary.prototype = {
+
+ set title(title) {
+ if (title === 'Totals') {
+ this.totalsRow = true;
+ }
+ this.title_ = title;
+ },
+
+ get title() {
+ return this.title_;
+ },
+
+ get duration() {
+ if (this.duration_ === undefined) {
+ this.duration_ = tr.b.math.Statistics.sum(
+ this.events_, function(event) {
+ return event.duration;
+ });
+ }
+ return this.duration_;
+ },
+
+ get cpuSelfTime() {
+ this.computeCpuTimesIfNeeded_();
+ return this.cpuSelfTime_;
+ },
+
+ get cpuDuration() {
+ this.computeCpuTimesIfNeeded_();
+ return this.cpuDuration_;
+ },
+
+ computeCpuTimesIfNeeded_() {
+ if (this.cpuTimesComputed_) return;
+ this.cpuTimesComputed_ = true;
+
+ let cpuSelfTime = 0;
+ let cpuDuration = 0;
+ let hasCpuData = false;
+ for (const event of this.events_) {
+ if (event.cpuDuration !== undefined) {
+ cpuDuration += event.cpuDuration;
+ hasCpuData = true;
+ }
+
+ if (event.cpuSelfTime !== undefined) {
+ cpuSelfTime += event.cpuSelfTime;
+ hasCpuData = true;
+ }
+ }
+ if (hasCpuData) {
+ this.cpuDuration_ = cpuDuration;
+ this.cpuSelfTime_ = cpuSelfTime;
+ }
+ },
+
+ get selfTime() {
+ if (this.selfTime_ === undefined) {
+ this.selfTime_ = 0;
+ for (const event of this.events_) {
+ if (event.selfTime !== undefined) {
+ this.selfTime_ += event.selfTime;
+ }
+ }
+ }
+ return this.selfTime_;
+ },
+
+ get events() {
+ return this.events_;
+ },
+
+ get numEvents() {
+ return this.events_.length;
+ },
+
+ get numAlerts() {
+ if (this.numAlerts_ === undefined) {
+ this.numAlerts_ = tr.b.math.Statistics.sum(this.events_, event =>
+ event.associatedAlerts.length
+ );
+ }
+ return this.numAlerts_;
+ },
+
+ get untotallableArgs() {
+ this.updateArgsIfNeeded_();
+ return this.untotallableArgs_;
+ },
+
+ get totalledArgs() {
+ this.updateArgsIfNeeded_();
+ return this.totalledArgs_;
+ },
+
+
+ get maxDuration() {
+ if (this.maxDuration_ === undefined) {
+ this.maxDuration_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.duration;
+ });
+ }
+ return this.maxDuration_;
+ },
+
+
+ get maxCpuDuration() {
+ if (this.maxCpuDuration_ === undefined) {
+ this.maxCpuDuration_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.cpuDuration;
+ });
+ }
+ return this.maxCpuDuration_;
+ },
+
+
+ get maxSelfTime() {
+ if (this.maxSelfTime_ === undefined) {
+ this.maxSelfTime_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.selfTime;
+ });
+ }
+ return this.maxSelfTime_;
+ },
+
+
+ get maxCpuSelfTime() {
+ if (this.maxCpuSelfTime_ === undefined) {
+ this.maxCpuSelfTime_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.cpuSelfTime;
+ });
+ }
+ return this.maxCpuSelfTime_;
+ },
+
+
+ updateArgsIfNeeded_() {
+ if (this.totalledArgs_ !== undefined) return;
+
+ const untotallableArgs = {};
+ const totalledArgs = {};
+ for (const event of this.events_) {
+ for (const argName in event.args) {
+ const argVal = event.args[argName];
+ const type = typeof argVal;
+ if (type !== 'number') {
+ untotallableArgs[argName] = true;
+ delete totalledArgs[argName];
+ continue;
+ }
+ if (untotallableArgs[argName]) {
+ continue;
+ }
+
+ if (totalledArgs[argName] === undefined) {
+ totalledArgs[argName] = 0;
+ }
+ totalledArgs[argName] += argVal;
+ }
+ }
+ this.untotallableArgs_ = Object.keys(untotallableArgs);
+ this.totalledArgs_ = totalledArgs;
+ }
+ };
+
+ return {
+ MultiEventSummary,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html
new file mode 100644
index 00000000000..1b32d606f61
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html
@@ -0,0 +1,358 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-multi-event-summary-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-event-summary-table',
+
+ ready() {
+ this.showTotals_ = false;
+ this.eventsHaveDuration_ = true;
+ this.eventsHaveSubRows_ = true;
+ this.eventsByTitle_ = undefined;
+ },
+
+ updateTableColumns_(rows, maxValues) {
+ let hasCpuData = false;
+ let hasAlerts = false;
+ rows.forEach(function(row) {
+ if (row.cpuDuration !== undefined) {
+ hasCpuData = true;
+ }
+ if (row.cpuSelfTime !== undefined) {
+ hasCpuData = true;
+ }
+ if (row.numAlerts) {
+ hasAlerts = true;
+ }
+ });
+
+ const ownerDocument = this.ownerDocument;
+
+ const columns = [];
+
+ columns.push({
+ title: 'Name',
+ value(row) {
+ if (row.title === 'Totals') return 'Totals';
+ const container = document.createElement('div');
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(row.events);
+ }, row.title);
+ container.appendChild(linkEl);
+
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ const link = document.createElement('tr-ui-e-chrome-codesearch');
+ link.searchPhrase = row.title;
+ container.appendChild(link);
+ }
+ return container;
+ },
+ width: '350px',
+ cmp(rowA, rowB) {
+ return rowA.title.localeCompare(rowB.title);
+ }
+ });
+ if (this.eventsHaveDuration_) {
+ columns.push({
+ title: 'Wall Duration',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.duration, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.duration),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.duration - rowB.duration;
+ }
+ });
+ }
+
+ if (this.eventsHaveDuration_ && hasCpuData) {
+ columns.push({
+ title: 'CPU Duration',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.cpuDuration, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.cpuDuration),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.cpuDuration - rowB.cpuDuration;
+ }
+ });
+ }
+
+ if (this.eventsHaveSubRows_ && this.eventsHaveDuration_) {
+ columns.push({
+ title: 'Self time',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.selfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.selfTime),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.selfTime - rowB.selfTime;
+ }
+ });
+ }
+
+ if (this.eventsHaveSubRows_ && this.eventsHaveDuration_ && hasCpuData) {
+ columns.push({
+ title: 'CPU Self Time',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.cpuSelfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.cpuSelfTime),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.cpuSelfTime - rowB.cpuSelfTime;
+ }
+ });
+ }
+
+ if (this.eventsHaveDuration_) {
+ columns.push({
+ title: 'Average ' + (hasCpuData ? 'CPU' : 'Wall') + ' Duration',
+ value(row) {
+ const totalDuration = hasCpuData ? row.cpuDuration : row.duration;
+ return tr.v.ui.createScalarSpan(totalDuration / row.numEvents, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.duration),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ if (hasCpuData) {
+ return rowA.cpuDuration / rowA.numEvents -
+ rowB.cpuDuration / rowB.numEvents;
+ }
+ return rowA.duration / rowA.numEvents -
+ rowB.duration / rowB.numEvents;
+ }
+ });
+ }
+
+ columns.push({
+ title: 'Occurrences',
+ value(row) {
+ return row.numEvents;
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.numEvents - rowB.numEvents;
+ }
+ });
+
+ let alertsColumnIndex;
+ if (hasAlerts) {
+ columns.push({
+ title: 'Num Alerts',
+ value(row) {
+ return row.numAlerts;
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.numAlerts - rowB.numAlerts;
+ }
+ });
+ alertsColumnIndex = columns.length - 1;
+ }
+ let colWidthPercentage;
+ if (columns.length === 1) {
+ colWidthPercentage = '100%';
+ } else {
+ colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+ }
+
+ for (let i = 1; i < columns.length; i++) {
+ columns[i].width = colWidthPercentage;
+ }
+
+ this.$.table.tableColumns = columns;
+
+ if (hasAlerts) {
+ this.$.table.sortColumnIndex = alertsColumnIndex;
+ this.$.table.sortDescending = true;
+ }
+ },
+
+ configure(config) {
+ if (config.eventsByTitle === undefined) {
+ throw new Error('Required: eventsByTitle');
+ }
+
+ if (config.showTotals !== undefined) {
+ this.showTotals_ = config.showTotals;
+ } else {
+ this.showTotals_ = true;
+ }
+
+ if (config.eventsHaveDuration !== undefined) {
+ this.eventsHaveDuration_ = config.eventsHaveDuration;
+ } else {
+ this.eventsHaveDuration_ = true;
+ }
+
+ if (config.eventsHaveSubRows !== undefined) {
+ this.eventsHaveSubRows_ = config.eventsHaveSubRows;
+ } else {
+ this.eventsHaveSubRows_ = true;
+ }
+
+ this.eventsByTitle_ = config.eventsByTitle;
+ this.updateContents_();
+ },
+
+ get showTotals() {
+ return this.showTotals_;
+ },
+
+ set showTotals(showTotals) {
+ this.showTotals_ = showTotals;
+ this.updateContents_();
+ },
+
+ get eventsHaveDuration() {
+ return this.eventsHaveDuration_;
+ },
+
+ set eventsHaveDuration(eventsHaveDuration) {
+ this.eventsHaveDuration_ = eventsHaveDuration;
+ this.updateContents_();
+ },
+
+ get eventsHaveSubRows() {
+ return this.eventsHaveSubRows_;
+ },
+
+ set eventsHaveSubRows(eventsHaveSubRows) {
+ this.eventsHaveSubRows_ = eventsHaveSubRows;
+ this.updateContents_();
+ },
+
+ get eventsByTitle() {
+ return this.eventsByTitle_;
+ },
+
+ set eventsByTitle(eventsByTitle) {
+ this.eventsByTitle_ = eventsByTitle;
+ this.updateContents_();
+ },
+
+ get selectionBounds() {
+ return this.selectionBounds_;
+ },
+
+ set selectionBounds(selectionBounds) {
+ this.selectionBounds_ = selectionBounds;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ let eventsByTitle;
+ if (this.eventsByTitle_ !== undefined) {
+ eventsByTitle = this.eventsByTitle_;
+ } else {
+ eventsByTitle = [];
+ }
+
+ const allEvents = new tr.model.EventSet();
+ const rows = [];
+ for (const [title, eventsOfSingleTitle] of Object.entries(eventsByTitle)) {
+ for (const event of eventsOfSingleTitle) allEvents.push(event);
+ const row = new tr.ui.analysis.MultiEventSummary(
+ title, eventsOfSingleTitle);
+ rows.push(row);
+ }
+
+ this.updateTableColumns_(rows);
+ this.$.table.tableRows = rows;
+
+ const maxValues = {
+ duration: undefined,
+ selfTime: undefined,
+ cpuSelfTime: undefined,
+ cpuDuration: undefined
+ };
+
+ if (this.eventsHaveDuration) {
+ for (const column in maxValues) {
+ maxValues[column] = tr.b.math.Statistics.max(rows, function(event) {
+ return event[column];
+ });
+ }
+ }
+
+ const footerRows = [];
+
+ if (this.showTotals_) {
+ const multiEventSummary = new tr.ui.analysis.MultiEventSummary(
+ 'Totals', allEvents);
+ footerRows.push(multiEventSummary);
+ }
+
+
+ this.updateTableColumns_(rows, maxValues);
+ this.$.table.tableRows = rows;
+
+ // TODO(selection bounds).
+
+ // TODO(sorting)
+
+ this.$.table.footerRows = footerRows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html
new file mode 100644
index 00000000000..32efc0de1ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary_table.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('basicNoCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, duration: 0.5}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0.5}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0.5}));
+ tsg.createSubSlices();
+
+ const threadTrack = {};
+ threadTrack.thread = thread;
+
+ const selection = new EventSet(tsg.slices);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-summary-table');
+ viewEl.configure({
+ showTotals: true,
+ eventsHaveDuration: true,
+ eventsByTitle: selection.getEventsOrganizedByTitle()
+ });
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('basicWithCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+ cpuStart: 0, cpuEnd: 3}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+ cpuStart: 1, cpuEnd: 1.75}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+ cpuStart: 3, cpuEnd: 3.75}));
+ tsg.createSubSlices();
+
+ const threadTrack = {};
+ threadTrack.thread = thread;
+
+ const selection = new EventSet(tsg.slices);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-summary-table');
+ viewEl.configure({
+ showTotals: true,
+ eventsHaveDuration: true,
+ eventsByTitle: selection.getEventsOrganizedByTitle()
+ });
+ this.addHTMLOutput(viewEl);
+
+ const totals = tr.ui.b.findDeepElementMatchingPredicate(
+ viewEl, e => e.tagName === 'TFOOT');
+ const scalars = tr.ui.b.findDeepElementsMatchingPredicate(
+ totals, e => e.tagName === 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(scalars[0].value, 5);
+ assert.closeTo(scalars[1].value, 4.5, 1e-6);
+ assert.strictEqual(scalars[2].value, 4);
+ assert.closeTo(scalars[3].value, 3.75, 1e-6);
+ assert.closeTo(scalars[4].value, 1.5, 1e-6);
+ assert.strictEqual('3', totals.children[0].children[6].textContent);
+ });
+
+ test('noSelfTimeNoSubRows', function() {
+ const model = new Model();
+
+ const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+ const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+
+ // Make reading some properties an explosion, as a way to ensure that they
+ // aren't read. Note that 'duration' is read since it is used by the
+ // EventSet to get the range.
+ const failProp = {
+ get() {
+ throw new Error('Should not be called');
+ }
+ };
+ Object.defineProperty(fe1, 'subRows', failProp);
+ Object.defineProperty(fe2, 'subRows', failProp);
+
+ Object.defineProperty(fe1, 'selfTime', failProp);
+ Object.defineProperty(fe2, 'selfTime', failProp);
+
+ model.flowEvents.push(fe1);
+ model.flowEvents.push(fe2);
+
+ const selection = new EventSet([fe1, fe2]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-summary-table');
+ viewEl.configure({
+ showTotals: true,
+ eventsHaveDuration: false,
+ eventsHaveSubRows: false,
+ eventsByTitle: selection.getEventsOrganizedByTitle()
+ });
+ this.addHTMLOutput(viewEl);
+ });
+
+ // TODO(nduca): Tooltippish stuff.
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html
new file mode 100644
index 00000000000..fcc73e1d608
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('summaryRowNoCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+ tsg.pushSlice(newSliceEx({title: 'bb', start: 1, end: 2}));
+ tsg.pushSlice(newSliceEx({title: 'bb', start: 4, end: 5}));
+ tsg.createSubSlices();
+
+ const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+ assert.strictEqual(row.duration, 5);
+ assert.strictEqual(row.selfTime, 4);
+ assert.isUndefined(row.cpuDuration);
+ assert.isUndefined(row.cpuSelfTime);
+ });
+
+ test('summaryRowWithCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+ cpuStart: 0, cpuEnd: 3}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+ cpuStart: 1, cpuEnd: 1.75}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+ cpuStart: 3, cpuEnd: 3.75}));
+ tsg.createSubSlices();
+
+ const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+ assert.strictEqual(row.duration, 5);
+ assert.strictEqual(row.selfTime, 4);
+ assert.strictEqual(row.cpuDuration, 4.5);
+ assert.strictEqual(row.cpuSelfTime, 3.75);
+ assert.strictEqual(row.maxDuration, 3);
+ assert.strictEqual(row.maxSelfTime, 2);
+ assert.strictEqual(row.maxCpuDuration, 3);
+ assert.strictEqual(row.maxCpuSelfTime, 2.25);
+ });
+
+ test('summaryRowNonSlice', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+ const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+ model.flowEvents.push(fe1);
+ model.flowEvents.push(fe2);
+
+ const row = new tr.ui.analysis.MultiEventSummary('a', [fe1, fe2]);
+ assert.strictEqual(row.duration, 0);
+ assert.strictEqual(row.selfTime, 0);
+ assert.isUndefined(row.cpuDuration);
+ assert.isUndefined(row.cpuSelfTime);
+ assert.strictEqual(row.maxDuration, 0);
+ });
+
+ test('summaryNumAlerts', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert');
+
+ const alert = new tr.model.Alert(ALERT_INFO_1, 5, [slice]);
+
+ const row = new tr.ui.analysis.MultiEventSummary('a', [slice]);
+ assert.strictEqual(row.numAlerts, 1);
+ });
+
+ test('argSummary', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+ args: {value1: 3, value2: 'x', value3: 1}}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+ args: {value1: 3, value2: 'y', value3: 2}}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+ args: {value1: 3, value2: 'z', value3: 'x'}}));
+ tsg.createSubSlices();
+
+ const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+ assert.deepEqual(row.totalledArgs, {value1: 9});
+ assert.deepEqual(row.untotallableArgs, ['value2', 'value3']);
+ assert.strictEqual(row.maxDuration, 3);
+ assert.strictEqual(row.maxSelfTime, 2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html
new file mode 100644
index 00000000000..3e509508732
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-flow-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-flow-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.$.content.eventsHaveDuration = false;
+ this.$.content.eventsHaveSubRows = false;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ },
+
+ get selection() {
+ return this.$.content.selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-flow-event-sub-view',
+ tr.model.FlowEvent,
+ {
+ multi: true,
+ title: 'Flow Events',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html
new file mode 100644
index 00000000000..8b1d98d7bd1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = new Model();
+
+ const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+ const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+ model.flowEvents.push(fe1);
+ model.flowEvents.push(fe2);
+
+ const selection = new EventSet();
+ selection.push(fe1);
+ selection.push(fe2);
+ assert.strictEqual(selection.length, 2);
+
+ const subView = document.createElement('tr-ui-a-multi-flow-event-sub-view');
+ subView.selection = selection;
+
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html
new file mode 100644
index 00000000000..cdf71245df3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-frame-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ Polymer.dom(this).textContent = '';
+ const realView = document.createElement('tr-ui-a-multi-event-sub-view');
+ realView.eventsHaveDuration = false;
+ realView.eventsHaveSubRows = false;
+
+ Polymer.dom(this).appendChild(realView);
+ realView.setSelectionWithoutErrorChecks(selection);
+
+ this.currentSelection_ = selection;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+
+ const selection = new tr.model.EventSet();
+ this.currentSelection_.forEach(function(frameEvent) {
+ frameEvent.associatedEvents.forEach(function(event) {
+ selection.push(event);
+ });
+ });
+ return selection;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-frame-sub-view',
+ tr.model.Frame,
+ {
+ multi: true,
+ title: 'Frames',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html
new file mode 100644
index 00000000000..c2252f90e14
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-instant-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ <div id='content'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-instant-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ Polymer.dom(this.$.content).textContent = '';
+ const realView = document.createElement('tr-ui-a-multi-event-sub-view');
+ realView.eventsHaveDuration = false;
+ realView.eventsHaveSubRows = false;
+
+ Polymer.dom(this.$.content).appendChild(realView);
+ realView.setSelectionWithoutErrorChecks(selection);
+
+ this.currentSelection_ = selection;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html
new file mode 100644
index 00000000000..ca228515de5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = new Model();
+ const p52 = model.getOrCreateProcess(52);
+ const t53 = p52.getOrCreateThread(53);
+
+ const ie1 = new tr.model.ProcessInstantEvent('cat', 'title', 7, 10, {});
+ const ie2 = new tr.model.ProcessInstantEvent('cat', 'title', 7, 20, {});
+ p52.instantEvents.push(ie1);
+ p52.instantEvents.push(ie2);
+
+
+ const selection = new EventSet();
+ selection.push(ie1);
+ selection.push(ie2);
+ assert.strictEqual(selection.length, 2);
+
+ const subView =
+ document.createElement('tr-ui-a-multi-instant-event-sub-view');
+ subView.selection = selection;
+
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html
new file mode 100644
index 00000000000..089f4b011a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-multi-object-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="content"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-object-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ ready() {
+ this.$.content.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+
+ const objectEvents = Array.from(selection).sort(
+ tr.b.math.Range.compareByMinTimes);
+
+ const timeSpanConfig = {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ };
+ const table = this.$.content;
+ table.tableColumns = [
+ {
+ title: 'First',
+ value(event) {
+ if (event instanceof tr.model.ObjectSnapshot) {
+ return tr.v.ui.createScalarSpan(event.ts, timeSpanConfig);
+ }
+
+ const spanEl = document.createElement('span');
+ Polymer.dom(spanEl).appendChild(tr.v.ui.createScalarSpan(
+ event.creationTs, timeSpanConfig));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.createSpan({
+ textContent: '-',
+ marginLeft: '4px',
+ marginRight: '4px'
+ }));
+ if (event.deletionTs !== Number.MAX_VALUE) {
+ Polymer.dom(spanEl).appendChild(tr.v.ui.createScalarSpan(
+ event.deletionTs, timeSpanConfig));
+ }
+ return spanEl;
+ },
+ width: '200px'
+ },
+ {
+ title: 'Second',
+ value(event) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(event);
+ }, event.userFriendlyName);
+ return linkEl;
+ },
+ width: '100%'
+ }
+ ];
+ table.tableRows = objectEvents;
+ table.rebuild();
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-object-sub-view',
+ tr.model.ObjectInstance,
+ {
+ multi: true,
+ title: 'Object Instances',
+ });
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-object-sub-view',
+ tr.model.ObjectSnapshot,
+ {
+ multi: true,
+ title: 'Object Snapshots',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html
new file mode 100644
index 00000000000..18e62170e7d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ test('instantiate_analysisWithObjects', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const objects = p1.objects;
+ const i10 = objects.idWasCreated(
+ '0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10);
+ const s10 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl',
+ 10, 'snapshot-1');
+ const s25 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl',
+ 25, 'snapshot-2');
+ const s40 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl',
+ 40, 'snapshot-3');
+ objects.idWasDeleted('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 45);
+
+ const track = {};
+ const selection = new EventSet();
+ selection.push(i10);
+ selection.push(s10);
+ selection.push(s25);
+ selection.push(s40);
+
+ const analysisEl = document.createElement('tr-ui-a-multi-object-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html
new file mode 100644
index 00000000000..32315c220a1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html">
+<link rel="import" href="/tracing/ui/analysis/power_sample_summary_table.html">
+
+<dom-module id='tr-ui-a-multi-power-sample-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #tables {
+ display: flex;
+ flex-direction: column;
+ width: 50%;
+ }
+ #chart {
+ width: 50%;
+ }
+ </style>
+ <div id="tables">
+ <tr-ui-a-power-sample-summary-table id="summaryTable">
+ </tr-ui-a-power-sample-summary-table>
+ </div>
+ <tr-ui-a-frame-power-usage-chart id="chart">
+ </tr-ui-a-frame-power-usage-chart>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+// TODO(charliea): Add a dropdown that allows the user to select which type of
+// power sample analysis view they want (e.g. table of samples, graph).
+Polymer({
+ is: 'tr-ui-a-multi-power-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const samples = this.selection;
+ const vSyncTimestamps = (!samples ? [] :
+ tr.b.getFirstElement(samples).series.device.vSyncTimestamps);
+
+ this.$.summaryTable.samples = samples;
+ this.$.chart.setData(this.selection, vSyncTimestamps);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-power-sample-sub-view',
+ tr.model.PowerSample,
+ {
+ multi: true,
+ title: 'Power Samples',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html
new file mode 100644
index 00000000000..f7759572e28
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_noSamplesOrVSyncs', function() {
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-power-sample-sub-view');
+ viewEl.selection = undefined;
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('instantiate_noVSyncs', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+
+ model.device.vSyncTimestamps = [];
+ series.addPowerSample(1, 1);
+ series.addPowerSample(2, 2);
+ series.addPowerSample(3, 3);
+ series.addPowerSample(4, 2);
+
+ const view = document.createElement('tr-ui-a-multi-power-sample-sub-view');
+ const eventSet = new tr.model.EventSet(series.samples);
+ view.selection = eventSet;
+
+ this.addHTMLOutput(view);
+
+ assert.deepEqual(view.$.chart.samples, eventSet);
+ assert.sameDeepMembers(view.$.chart.vSyncTimestamps, []);
+ });
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+
+ model.device.vSyncTimestamps = [0];
+ series.addPowerSample(1, 1);
+ series.addPowerSample(2, 2);
+ series.addPowerSample(3, 3);
+ series.addPowerSample(4, 2);
+
+ const view = document.createElement('tr-ui-a-multi-power-sample-sub-view');
+ const eventSet = new tr.model.EventSet(series.samples);
+ view.selection = eventSet;
+
+ this.addHTMLOutput(view);
+
+ assert.deepEqual(view.$.chart.samples, eventSet);
+ assert.sameDeepMembers(view.$.chart.vSyncTimestamps, [0]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html
new file mode 100644
index 00000000000..1737894f875
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-multi-sample-sub-view'>
+ <template>
+ <style>
+ :host { display: block; }
+ #control {
+ background-color: #e6e6e6;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%,
+ from(#E5E5E5), to(#D1D1D1));
+ flex: 0 0 auto;
+ overflow-x: auto;
+ }
+ #control::-webkit-scrollbar { height: 0px; }
+ #control {
+ font-size: 12px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ margin: 1px;
+ margin-right: 2px;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <div id="control">
+ Sample View Option
+ </div>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+(function() {
+ const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
+
+ Polymer({
+ is: 'tr-ui-a-multi-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.viewOption_ = undefined;
+ this.selection_ = undefined;
+ },
+
+ ready() {
+ const viewSelector = tr.ui.b.createSelector(
+ this, 'viewOption', 'tracing.ui.analysis.multi_sample_sub_view',
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW,
+ [
+ {
+ label: 'Top-down (Tree)',
+ value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW
+ },
+ {
+ label: 'Top-down (Heavy)',
+ value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW
+ },
+ {
+ label: 'Bottom-up (Heavy)',
+ value: MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW
+ }
+ ]);
+ Polymer.dom(this.$.control).appendChild(viewSelector);
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ this.updateContents_();
+ },
+
+ get viewOption() {
+ return this.viewOption_;
+ },
+
+ set viewOption(viewOption) {
+ this.viewOption_ = viewOption;
+ this.updateContents_();
+ },
+
+ createSamplingSummary_(selection, viewOption) {
+ const builder = new MultiDimensionalViewBuilder(
+ 1 /* dimensions */, 1 /* valueCount */);
+ const samples = selection.filter(
+ event => event instanceof tr.model.Sample);
+
+ samples.forEach(function(sample) {
+ builder.addPath([sample.userFriendlyStack.reverse()],
+ [1], MultiDimensionalViewBuilder.ValueKind.SELF);
+ });
+
+ return builder.buildView(viewOption);
+ },
+
+ processSampleRows_(rows) {
+ for (const row of rows) {
+ let title = row.title[0];
+ let results = /(.*) (Deoptimized reason: .*)/.exec(title);
+ if (results !== null) {
+ row.deoptReason = results[2];
+ title = results[1];
+ }
+ results = /(.*) url: (.*)/.exec(title);
+ if (results !== null) {
+ row.functionName = results[1];
+ row.url = results[2];
+ if (row.functionName === '') {
+ row.functionName = '(anonymous function)';
+ }
+ if (row.url === '') {
+ row.url = 'unknown';
+ }
+ } else {
+ row.functionName = title;
+ row.url = 'unknown';
+ }
+ this.processSampleRows_(row.subRows);
+ }
+ },
+
+ updateContents_() {
+ if (this.selection === undefined) {
+ this.$.table.tableColumns = [];
+ this.$.table.tableRows = [];
+ this.$.table.rebuild();
+ return;
+ }
+
+ const samplingData = this.createSamplingSummary_(
+ this.selection, this.viewOption);
+ const total = samplingData.values[0].total;
+ const columns = [
+ this.createPercentColumn_('Total', total),
+ this.createSamplesColumn_('Total'),
+ this.createPercentColumn_('Self', total),
+ this.createSamplesColumn_('Self'),
+ {
+ title: 'Function Name',
+ value(row) {
+ // For function that got deoptimized, show function name
+ // as red italic with a tooltip
+ if (row.deoptReason !== undefined) {
+ const spanEl = tr.ui.b.createSpan({
+ italic: true,
+ color: '#F44336',
+ tooltip: row.deoptReason
+ });
+ spanEl.innerText = row.functionName;
+ return spanEl;
+ }
+ return row.functionName;
+ },
+ width: '150px',
+ cmp: (a, b) => a.functionName.localeCompare(b.functionName),
+ showExpandButtons: true
+ },
+ {
+ title: 'Location',
+ value(row) { return row.url; },
+ width: '250px',
+ cmp: (a, b) => a.url.localeCompare(b.url),
+ }
+ ];
+
+ this.processSampleRows_(samplingData.subRows);
+ this.$.table.tableColumns = columns;
+ this.$.table.sortColumnIndex = 1; /* Total samples */
+ this.$.table.sortDescending = true;
+ this.$.table.tableRows = samplingData.subRows;
+ this.$.table.rebuild();
+ },
+
+ createPercentColumn_(title, samplingDataTotal) {
+ const field = title.toLowerCase();
+ return {
+ title: title + ' percent',
+ value(row) {
+ return tr.v.ui.createScalarSpan(
+ row.values[0][field] / samplingDataTotal, {
+ customContextRange: tr.b.math.Range.PERCENT_RANGE,
+ unit: tr.b.Unit.byName.normalizedPercentage,
+ context: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
+ });
+ },
+ width: '60px',
+ cmp: (a, b) => a.values[0][field] - b.values[0][field]
+ };
+ },
+
+ createSamplesColumn_(title) {
+ const field = title.toLowerCase();
+ return {
+ title: title + ' samples',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.values[0][field], {
+ unit: tr.b.Unit.byName.unitlessNumber,
+ context: { maximumFractionDigits: 0 },
+ });
+ },
+ width: '60px',
+ cmp: (a, b) => a.values[0][field] - b.values[0][field]
+ };
+ }
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-sample-sub-view',
+ tr.model.Sample,
+ {
+ multi: true,
+ title: 'Samples',
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html
new file mode 100644
index 00000000000..e148a2d7c95
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/multi_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSampleNamed = tr.c.TestUtils.newSampleNamed;
+
+ function instantiateWithTraces(traces) {
+ let t53;
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ traces.forEach(function(trace, index) {
+ model.samples.push(
+ newSampleNamed(t53, 'X', 'cat', trace, index * 0.02));
+ });
+ }
+ });
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < t53.samples.length; i++) {
+ selection.push(t53.samples[i]);
+ }
+
+ const view = document.createElement('tr-ui-a-multi-sample-sub-view');
+ view.style.height = '500px';
+ this.addHTMLOutput(view);
+ view.selection = selection;
+ return view;
+ }
+
+ test('instantiate_flat', function() {
+ instantiateWithTraces.call(this, [
+ ['BBB'],
+ ['AAA'],
+ ['AAA'],
+ ['Sleeping'],
+ ['BBB'],
+ ['AAA'],
+ ['CCC'],
+ ['Sleeping']
+ ]);
+ });
+
+ test('instantiate_nested', function() {
+ instantiateWithTraces.call(this, [
+ ['AAA', 'BBB'],
+ ['AAA', 'BBB', 'CCC'],
+ ['AAA', 'BBB'],
+ ['BBB', 'AAA', 'BBB'],
+ ['BBB', 'AAA', 'BBB'],
+ ['BBB', 'AAA', 'BBB']
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html
new file mode 100644
index 00000000000..b896a7bf699
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+
+<dom-module id='tr-ui-a-multi-thread-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #content {
+ display: flex;
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ #content > tr-ui-a-related-events {
+ margin-left: 8px;
+ flex: 0 1 200px;
+ }
+ </style>
+ <div id="content"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.selection_ = undefined;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+
+ // TODO(nduca): This is a gross hack for cc Frame Viewer, but its only
+ // the frame viewer that needs this feature, so ~shrug~.
+ // We check for its presence so that we do not have a hard dependency
+ // on frame viewer.
+ if (tr.isExported('tr.ui.e.chrome.cc.RasterTaskSelection')) {
+ if (tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection)) {
+ const ltvSelection = new tr.ui.e.chrome.cc.RasterTaskSelection(
+ selection);
+
+ const ltv = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView();
+ ltv.objectSnapshot = ltvSelection.containingSnapshot;
+ ltv.selection = ltvSelection;
+ ltv.extraHighlightsByLayerId = ltvSelection.extraHighlightsByLayerId;
+
+ Polymer.dom(this.$.content).textContent = '';
+ Polymer.dom(this.$.content).appendChild(ltv);
+
+ this.requiresTallView_ = true;
+ return;
+ }
+ }
+
+ Polymer.dom(this.$.content).textContent = '';
+
+ const mesv = document.createElement('tr-ui-a-multi-event-sub-view');
+ mesv.selection = selection;
+ Polymer.dom(this.$.content).appendChild(mesv);
+
+ const relatedEvents = document.createElement('tr-ui-a-related-events');
+ relatedEvents.setRelatedEvents(selection);
+
+ if (relatedEvents.hasRelatedEvents()) {
+ Polymer.dom(this.$.content).appendChild(relatedEvents);
+ }
+ },
+
+ get requiresTallView() {
+ if (this.$.content.children.length === 0) return false;
+ const childTagName = this.$.content.children[0].tagName;
+ if (childTagName === 'TR-UI-A-MULTI-EVENT-SUB-VIEW') {
+ return false;
+ }
+
+ // Using raster task view.
+ return true;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-thread-slice-sub-view',
+ tr.model.ThreadSlice,
+ {
+ multi: true,
+ title: 'Slices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..a44419cf536
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/analysis/multi_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0.0, duration: 0.5,
+ type: tr.model.ThreadSlice}));
+ t53.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 1.0, duration: 2,
+ type: tr.model.ThreadSlice}));
+ t53.sliceGroup.createSubSlices();
+
+ const selection = new tr.model.EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ selection.push(t53.sliceGroup.slices[1]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('withFlows', function() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+
+ m.t2 = m.p1.getOrCreateThread(2);
+ m.t3 = m.p1.getOrCreateThread(3);
+ m.t4 = m.p1.getOrCreateThread(4);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5,
+ type: tr.model.ThreadSlice}));
+ m.sB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15,
+ type: tr.model.ThreadSlice}));
+ m.sC = m.t4.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20,
+ type: tr.model.ThreadSlice}));
+
+ m.t2.createSubSlices();
+ m.t3.createSubSlices();
+ m.t4.createSubSlices();
+
+ m.f1 = newFlowEventEx({
+ title: 'flowish', start: 0, end: 10,
+ startSlice: m.sA,
+ endSlice: m.sB
+ });
+ m.f2 = newFlowEventEx({
+ title: 'flowish', start: 15, end: 21,
+ startSlice: m.sB,
+ endSlice: m.sC
+ });
+ });
+
+ const selection = new tr.model.EventSet();
+ selection.push(m.sA);
+ selection.push(m.sB);
+ selection.push(m.sC);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html
new file mode 100644
index 00000000000..f1f0666fc43
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-thread-time-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #content {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-thread-time-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.$.content.eventsHaveSubRows = false;
+ },
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.setSelectionWithoutErrorChecks(selection);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-thread-time-slice-sub-view',
+ tr.model.ThreadTimeSlice,
+ {
+ multi: true,
+ title: 'Thread Timeslices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html
new file mode 100644
index 00000000000..e3489fe1c9c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_thread_time_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('instantiate', function() {
+ const m = createBasicModel();
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+
+ const selection = new tr.model.EventSet();
+ selection.push(thread.timeSlices[0]);
+ selection.push(thread.timeSlices[1]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-thread-time-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html
new file mode 100644
index 00000000000..b89e3fd0724
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/user_expectation_related_samples_table.html">
+
+<dom-module id='tr-ui-a-multi-user-expectation-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex: 1 1 auto;
+ }
+ #events {
+ margin-left: 8px;
+ flex: 0 1 200px;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="realView"></tr-ui-a-multi-event-sub-view>
+ <div id="events">
+ <tr-ui-a-user-expectation-related-samples-table id="relatedSamples"></tr-ui-a-user-expectation-related-samples-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-interaction-record-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.$.realView.setSelectionWithoutErrorChecks(selection);
+
+ this.currentSelection_ = selection;
+
+ this.$.relatedSamples.selection = selection;
+ if (this.$.relatedSamples.hasRelatedSamples()) {
+ this.$.events.style.display = '';
+ } else {
+ this.$.events.style.display = 'none';
+ }
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+
+ const selection = new tr.model.EventSet();
+ this.currentSelection_.forEach(function(ir) {
+ ir.associatedEvents.forEach(function(event) {
+ selection.push(event);
+ });
+ });
+ return selection;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-user-expectation-sub-view',
+ tr.model.um.UserExpectation,
+ {
+ multi: true,
+ title: 'User Expectations',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html
new file mode 100644
index 00000000000..3c76dc8c9e3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const ObjectInstanceView = tr.ui.b.define('object-instance-view');
+
+ ObjectInstanceView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.objectInstance_ = undefined;
+ },
+
+ get requiresTallView() {
+ return true;
+ },
+
+ set modelEvent(obj) {
+ this.objectInstance = obj;
+ },
+
+ get modelEvent() {
+ return this.objectInstance;
+ },
+
+ get objectInstance() {
+ return this.objectInstance_;
+ },
+
+ set objectInstance(i) {
+ this.objectInstance_ = i;
+ this.updateContents();
+ },
+
+ updateContents() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = ObjectInstanceView;
+ options.defaultMetadata = {
+ showInTrackView: true
+ };
+ tr.b.decorateExtensionRegistry(ObjectInstanceView, options);
+
+ return {
+ ObjectInstanceView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html
new file mode 100644
index 00000000000..a42ed0ec02b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const ObjectSnapshotView = tr.ui.b.define('object-snapshot-view');
+
+ ObjectSnapshotView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.objectSnapshot_ = undefined;
+ },
+
+ get requiresTallView() {
+ return true;
+ },
+
+ set modelEvent(obj) {
+ this.objectSnapshot = obj;
+ },
+
+ get modelEvent() {
+ return this.objectSnapshot;
+ },
+
+ get objectSnapshot() {
+ return this.objectSnapshot_;
+ },
+
+ set objectSnapshot(i) {
+ this.objectSnapshot_ = i;
+ this.updateContents();
+ },
+
+ updateContents() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = ObjectSnapshotView;
+ options.defaultMetadata = {
+ showInstances: true,
+ showInTrackView: true
+ };
+ tr.b.decorateExtensionRegistry(ObjectSnapshotView, options);
+
+ return {
+ ObjectSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html
new file mode 100644
index 00000000000..337e4f5ba56
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-power-sample-summary-table'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-power-sample-summary-table',
+
+ ready() {
+ this.$.table.tableColumns = [
+ {
+ title: 'Min power',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.powerInWatts.format(row.min);
+ }
+ },
+ {
+ title: 'Max power',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.powerInWatts.format(row.max);
+ }
+ },
+ {
+ title: 'Time-weighted average',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.powerInWatts.format(
+ row.timeWeightedAverageInW);
+ }
+ },
+ {
+ title: 'Energy consumed',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.energyInJoules.format(row.energyConsumedInJ);
+ }
+ },
+ {
+ title: 'Sample count',
+ width: '100%',
+ value(row) { return row.sampleCount; }
+ }
+ ];
+ this.samples = new tr.model.EventSet();
+ },
+
+ get samples() {
+ return this.samples_;
+ },
+
+ set samples(samples) {
+ if (samples === this.samples) return;
+
+ this.samples_ =
+ (samples === undefined) ? new tr.model.EventSet() : samples;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.samples.length === 0) {
+ this.$.table.tableRows = [];
+ } else {
+ this.$.table.tableRows = [{
+ min: this.getMin(),
+ max: this.getMax(),
+ timeWeightedAverageInW: this.getTimeWeightedAverageInW(),
+ energyConsumedInJ: this.getEnergyConsumedInJ(),
+ sampleCount: this.samples.length
+ }];
+ }
+
+ this.$.table.rebuild();
+ },
+
+ getMin() {
+ return Math.min.apply(null, this.samples.map(function(sample) {
+ return sample.powerInW;
+ }));
+ },
+
+ getMax() {
+ return Math.max.apply(null, this.samples.map(function(sample) {
+ return sample.powerInW;
+ }));
+ },
+
+ /**
+ * Returns a time-weighted average of the power consumption (Watts)
+ * in between the first sample (inclusive) and last sample (exclusive).
+ */
+ getTimeWeightedAverageInW() {
+ const energyConsumedInJ = this.getEnergyConsumedInJ();
+
+ if (energyConsumedInJ === 'N/A') return 'N/A';
+
+ const durationInS = tr.b.convertUnit(this.samples.bounds.duration,
+ tr.b.UnitPrefixScale.METRIC.MILLI,
+ tr.b.UnitPrefixScale.METRIC.NONE);
+
+ return energyConsumedInJ / durationInS;
+ },
+
+
+ getEnergyConsumedInJ() {
+ if (this.samples.length < 2) return 'N/A';
+
+ const bounds = this.samples.bounds;
+ const series = tr.b.getFirstElement(this.samples).series;
+ return series.getEnergyConsumedInJ(bounds.min, bounds.max);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html
new file mode 100644
index 00000000000..f5bf8c7d5a0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/power_sample_summary_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ test('instantiate', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 3);
+ series.addPowerSample(3000, 4);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ this.addHTMLOutput(table);
+ });
+
+ test('setSamples_undefinedPowerSamples', function() {
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = undefined;
+
+ assert.lengthOf(table.$.table.tableRows, 0);
+ });
+
+ test('setSamples_noPowerSamples', function() {
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet([]);
+
+ assert.lengthOf(table.$.table.tableRows, 0);
+ });
+
+ test('setSamples_onePowerSample', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ assert.lengthOf(table.$.table.tableRows, 1);
+ assert.strictEqual(table.$.table.tableRows[0].min, 1);
+ assert.strictEqual(table.$.table.tableRows[0].max, 1);
+ assert.strictEqual(
+ table.$.table.tableRows[0].timeWeightedAverageInW, 'N/A');
+ assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 'N/A');
+ assert.strictEqual(table.$.table.tableRows[0].sampleCount, 1);
+ });
+
+ test('setSamples_twoPowerSamples', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ assert.lengthOf(table.$.table.tableRows, 1);
+ assert.strictEqual(table.$.table.tableRows[0].min, 1);
+ assert.strictEqual(table.$.table.tableRows[0].max, 2);
+ assert.strictEqual(table.$.table.tableRows[0].timeWeightedAverageInW, 1);
+ assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 1);
+ assert.strictEqual(table.$.table.tableRows[0].sampleCount, 2);
+ });
+
+ test('setSamples_threePowerSamples', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 3);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ assert.lengthOf(table.$.table.tableRows, 1);
+ assert.strictEqual(table.$.table.tableRows[0].min, 1);
+ assert.strictEqual(table.$.table.tableRows[0].max, 3);
+ assert.strictEqual(table.$.table.tableRows[0].timeWeightedAverageInW, 1.5);
+ assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 3);
+ assert.strictEqual(table.$.table.tableRows[0].sampleCount, 3);
+ });
+
+ test('setSamples_columnsInitialized', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 3);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ const row = table.$.table.tableRows[0];
+ const columns = table.$.table.tableColumns;
+
+ assert.lengthOf(columns, 5);
+
+ assert.strictEqual(columns[0].title, 'Min power');
+ assert.strictEqual(columns[0].width, '100px');
+ assert.strictEqual(columns[0].value(row), '1.000 W');
+
+ assert.strictEqual(columns[1].title, 'Max power');
+ assert.strictEqual(columns[1].width, '100px');
+ assert.strictEqual(columns[1].value(row), '3.000 W');
+
+ assert.strictEqual(columns[2].title, 'Time-weighted average');
+ assert.strictEqual(columns[2].width, '100px');
+ assert.strictEqual(columns[2].value(row), '1.500 W');
+
+ assert.strictEqual(columns[3].title, 'Energy consumed');
+ assert.strictEqual(columns[3].width, '100px');
+ assert.strictEqual(columns[3].value(row), '3.000 J');
+
+ assert.strictEqual(columns[4].title, 'Sample count');
+ assert.strictEqual(columns[4].width, '100%');
+ assert.strictEqual(columns[4].value(row), 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html
new file mode 100644
index 00000000000..62abbe8076b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const RebuildableBehavior = {
+ rebuild() {
+ /**
+ * Rebuild the pane if necessary.
+ *
+ * This method is not intended to be overriden by subclasses. Please
+ * override scheduleRebuild_() instead.
+ */
+ if (!this.paneDirty_) {
+ // Avoid rebuilding unnecessarily as it breaks things like table
+ // selection.
+ return;
+ }
+
+ this.paneDirty_ = false;
+ this.onRebuild_();
+ },
+
+ /**
+ * Mark the UI state of the pane as dirty and schedule a rebuild.
+ *
+ * This method is intended to be called by subclasses.
+ */
+ scheduleRebuild_() {
+ if (this.paneDirty_) return;
+ this.paneDirty_ = true;
+ tr.b.requestAnimationFrame(this.rebuild.bind(this));
+ },
+
+ /**
+ * Called when the pane is dirty and a rebuild is triggered.
+ *
+ * This method is intended to be overriden by subclasses (instead of
+ * directly overriding rebuild()).
+ */
+ onRebuild_() {
+ }
+ };
+
+ return {
+ RebuildableBehavior,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html
new file mode 100644
index 00000000000..2a2f083ceb3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ Polymer({
+ is: 'tr-ui-analysis-rebuildable-test-element',
+ behaviors: [tr.ui.analysis.RebuildableBehavior]
+ });
+
+ test('rebuild', function() {
+ const el = document.createElement(
+ 'tr-ui-analysis-rebuildable-test-element');
+ let didFireOnRebuild;
+ el.onRebuild_ = function() {
+ assert.strictEqual(this, el);
+ didFireOnRebuild = true;
+ };
+
+ function checkManualRebuild(expectedDidFireOnRebuild) {
+ didFireOnRebuild = false;
+ el.rebuild();
+ assert.strictEqual(didFireOnRebuild, expectedDidFireOnRebuild);
+ }
+
+ function checkRAFRebuild(expectedDidFireOnRebuild) {
+ didFireOnRebuild = false;
+ tr.b.forcePendingRAFTasksToRun();
+ assert.strictEqual(didFireOnRebuild, expectedDidFireOnRebuild);
+ }
+
+ // No rebuilds should occur when not scheduled.
+ checkManualRebuild(false);
+ checkRAFRebuild(false);
+
+ // Single rebuild should occur when scheduled once.
+ el.scheduleRebuild_();
+ checkManualRebuild(true);
+ checkManualRebuild(false);
+
+ el.scheduleRebuild_();
+ checkRAFRebuild(true);
+ checkRAFRebuild(false);
+
+ // Only a single rebuild should occur even when scheduled multiple times.
+ el.scheduleRebuild_();
+ el.scheduleRebuild_();
+ checkManualRebuild(true);
+ checkRAFRebuild(false);
+ checkManualRebuild(false);
+
+ el.scheduleRebuild_();
+ el.scheduleRebuild_();
+ checkRAFRebuild(true);
+ checkRAFRebuild(false);
+ checkManualRebuild(false);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html
new file mode 100644
index 00000000000..b4036837bf5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html
@@ -0,0 +1,354 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/flow_classifier.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-related-events'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+function* getEventInFlowEvents(event) {
+ if (!event.inFlowEvents) return;
+ yield* event.inFlowEvents;
+}
+
+function* getEventOutFlowEvents(event) {
+ if (!event.outFlowEvents) return;
+ yield* event.outFlowEvents;
+}
+
+function* getEventAncestors(event) {
+ if (!event.enumerateAllAncestors) return;
+ yield* event.enumerateAllAncestors();
+}
+
+function* getEventDescendents(event) {
+ if (!event.enumerateAllDescendents) return;
+ yield* event.enumerateAllDescendents();
+}
+
+Polymer({
+ is: 'tr-ui-a-related-events',
+
+ ready() {
+ this.eventGroups_ = [];
+ this.cancelFunctions_ = [];
+
+ this.$.table.tableColumns = [
+ {
+ title: 'Event(s)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.type;
+ if (row.tooltip) {
+ typeEl.title = row.tooltip;
+ }
+ return typeEl;
+ },
+ width: '150px'
+ },
+ {
+ title: 'Link',
+ width: '100%',
+ value(row) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ if (row.name) {
+ linkEl.setSelectionAndContent(row.selection, row.name);
+ } else {
+ linkEl.selection = row.selection;
+ }
+ return linkEl;
+ }
+ }
+ ];
+ },
+
+ hasRelatedEvents() {
+ return (this.eventGroups_ && this.eventGroups_.length > 0);
+ },
+
+ setRelatedEvents(eventSet) {
+ this.cancelAllTasks_();
+ this.eventGroups_ = [];
+ this.addRuntimeCallStats_(eventSet);
+ this.addOverlappingV8ICStats_(eventSet);
+ this.addV8GCObjectStats_(eventSet);
+ this.addV8Slices_(eventSet);
+ this.addConnectedFlows_(eventSet);
+ this.addConnectedEvents_(eventSet);
+ this.addOverlappingSamples_(eventSet);
+ this.updateContents_();
+ },
+
+ addConnectedFlows_(eventSet) {
+ const classifier = new tr.ui.analysis.FlowClassifier();
+ eventSet.forEach(function(slice) {
+ if (slice.inFlowEvents) {
+ slice.inFlowEvents.forEach(function(flow) {
+ classifier.addInFlow(flow);
+ });
+ }
+ if (slice.outFlowEvents) {
+ slice.outFlowEvents.forEach(function(flow) {
+ classifier.addOutFlow(flow);
+ });
+ }
+ });
+ if (!classifier.hasEvents()) return;
+
+ const addToEventGroups = function(type, flowEvent) {
+ this.eventGroups_.push({
+ type,
+ selection: new tr.model.EventSet(flowEvent),
+ name: flowEvent.title
+ });
+ };
+
+ classifier.inFlowEvents.forEach(
+ addToEventGroups.bind(this, 'Incoming flow'));
+ classifier.outFlowEvents.forEach(
+ addToEventGroups.bind(this, 'Outgoing flow'));
+ classifier.internalFlowEvents.forEach(
+ addToEventGroups.bind(this, 'Internal flow'));
+ },
+
+ cancelAllTasks_() {
+ this.cancelFunctions_.forEach(function(cancelFunction) {
+ cancelFunction();
+ });
+ this.cancelFunctions_ = [];
+ },
+
+ addConnectedEvents_(eventSet) {
+ this.cancelFunctions_.push(this.createEventsLinkIfNeeded_(
+ 'Preceding events',
+ 'Add all events that have led to the selected one(s), connected by ' +
+ 'flow arrows or by call stack.',
+ eventSet,
+ function* (event) {
+ yield* getEventInFlowEvents(event);
+ yield* getEventAncestors(event);
+ if (event.startSlice) {
+ yield event.startSlice;
+ }
+ }.bind(this)));
+ this.cancelFunctions_.push(this.createEventsLinkIfNeeded_(
+ 'Following events',
+ 'Add all events that have been caused by the selected one(s), ' +
+ 'connected by flow arrows or by call stack.',
+ eventSet,
+ function* (event) {
+ yield* getEventOutFlowEvents(event);
+ yield* getEventDescendents(event);
+ if (event.endSlice) {
+ yield event.endSlice;
+ }
+ }.bind(this)));
+ this.cancelFunctions_.push(this.createEventsLinkIfNeeded_(
+ 'All connected events',
+ 'Add all events connected to the selected one(s) by flow arrows or ' +
+ 'by call stack.',
+ eventSet,
+ function* (event) {
+ yield* getEventInFlowEvents(event);
+ yield* getEventOutFlowEvents(event);
+ yield* getEventAncestors(event);
+ yield* getEventDescendents(event);
+ if (event.startSlice) {
+ yield event.startSlice;
+ }
+ if (event.endSlice) {
+ yield event.endSlice;
+ }
+ }.bind(this)));
+ },
+
+ createEventsLinkIfNeeded_(title, tooltip, events, connectedFn) {
+ events = new tr.model.EventSet(events);
+ const eventsToProcess = new Set(events);
+ // for (let event of events)
+ // eventsToProcess.add(event);
+ let wasChanged = false;
+ let task;
+ let isCanceled = false;
+ function addEventsUntilTimeout() {
+ if (isCanceled) return;
+ // Let's grant ourselves a budget of 8 ms. If time runs out, then
+ // create another task to do the rest.
+ const timeout = window.performance.now() + 8;
+ // TODO(alexandermont): Don't check window.performance.now
+ // every iteration.
+ while (eventsToProcess.size > 0 &&
+ window.performance.now() <= timeout) {
+ // Get the next event.
+ const nextEvent = tr.b.getFirstElement(eventsToProcess);
+ eventsToProcess.delete(nextEvent);
+
+ // Add the connected events to the list.
+ for (const eventToAdd of connectedFn(nextEvent)) {
+ if (!events.contains(eventToAdd)) {
+ events.push(eventToAdd);
+ eventsToProcess.add(eventToAdd);
+ wasChanged = true;
+ }
+ }
+ }
+ if (eventsToProcess.size > 0) {
+ // There are still events to process, but we ran out of time. Post
+ // more work for later.
+ const newTask = new tr.b.Task(
+ addEventsUntilTimeout.bind(this), this);
+ task.after(newTask);
+ task = newTask;
+ return;
+ }
+ // Went through all events, add the link.
+ if (!wasChanged) return;
+ this.eventGroups_.push({
+ type: title,
+ tooltip,
+ selection: events
+ });
+ this.updateContents_();
+ }
+ function cancelTask() {
+ isCanceled = true;
+ }
+ task = new tr.b.Task(addEventsUntilTimeout.bind(this), this);
+ tr.b.Task.RunWhenIdle(task);
+ return cancelTask;
+ },
+
+ addOverlappingSamples_(eventSet) {
+ const samples = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (!slice.parentContainer || !slice.parentContainer.samples) {
+ continue;
+ }
+ const candidates = slice.parentContainer.samples;
+ const range = tr.b.math.Range.fromExplicitRange(
+ slice.start, slice.start + slice.duration);
+ const filteredSamples = range.filterArray(
+ candidates, function(value) {return value.start;});
+ for (const sample of filteredSamples) {
+ samples.push(sample);
+ }
+ }
+ if (samples.length > 0) {
+ this.eventGroups_.push({
+ type: 'Overlapping samples',
+ tooltip: 'All samples overlapping the selected slice(s).',
+ selection: samples
+ });
+ }
+ },
+
+ addV8Slices_(eventSet) {
+ const v8Slices = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (slice.category === 'v8') {
+ v8Slices.push(slice);
+ }
+ }
+ if (v8Slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'V8 Slices',
+ tooltip: 'All V8 slices in the selected slice(s).',
+ selection: v8Slices
+ });
+ }
+ },
+
+ addRuntimeCallStats_(eventSet) {
+ const slices = eventSet.filter(function(slice) {
+ return (slice.category === 'v8' ||
+ slice.category === 'disabled-by-default-v8.runtime_stats') &&
+ slice.runtimeCallStats;
+ });
+ if (slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'Runtime call stats table',
+ // eslint-disable-next-line
+ tooltip: 'All V8 slices containing runtime call stats table in the selected slice(s).',
+ selection: slices
+ });
+ }
+ },
+
+ addV8GCObjectStats_(eventSet) {
+ const slices = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (slice.title === 'V8.GC_Objects_Stats') {
+ slices.push(slice);
+ }
+ }
+ if (slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'V8 GC stats table',
+ tooltip: 'All V8 GC statistics slices in the selected set.',
+ selection: slices
+ });
+ }
+ },
+
+ addOverlappingV8ICStats_(eventSet) {
+ const slices = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (!slice.parentContainer || !slice.parentContainer.sliceGroup) {
+ continue;
+ }
+ const sliceGroup = slice.parentContainer.sliceGroup.slices;
+ const range = tr.b.math.Range.fromExplicitRange(
+ slice.start, slice.start + slice.duration);
+ const filteredSlices = range.filterArray(
+ sliceGroup, value => value.start);
+ const icSlices = filteredSlices.filter(x => x.title === 'V8.ICStats');
+ for (const icSlice of icSlices) {
+ slices.push(icSlice);
+ }
+ }
+ if (slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'Overlapping V8 IC stats',
+ tooltip: 'All V8 IC statistics overlapping the selected set.',
+ selection: slices
+ });
+ }
+ },
+
+ updateContents_() {
+ const table = this.$.table;
+ if (this.eventGroups_ === undefined) {
+ table.tableRows = [];
+ } else {
+ table.tableRows = this.eventGroups_.slice();
+ }
+ table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html
new file mode 100644
index 00000000000..5d14d68faa3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+
+ m.t2 = m.p1.getOrCreateThread(2);
+ m.t3 = m.p1.getOrCreateThread(3);
+ m.t4 = m.p1.getOrCreateThread(4);
+ const node = tr.c.TestUtils.newProfileNodes(m, ['fake']);
+
+ // Setup samples and slices in this way:
+ // 0 5 10 15 20
+ // _____________________________
+ // t2 *
+ // [ a ][ ]aa
+ // -----------------------------
+ // t3 * * * * *
+ // * *
+ // [ b ]
+ // [bb]
+ // []bbb
+ // -----------------------------
+ // t4 |c
+ // -----------------------------
+ m.samples.push(
+ new tr.model.Sample(10, 'b10_1', node, m.t3),
+ new tr.model.Sample(7, 'b7', node, m.t3),
+ new tr.model.Sample(12, 'b12', node, m.t3),
+ new tr.model.Sample(20, 'b20', node, m.t3),
+ new tr.model.Sample(10, 'b10_2', node, m.t3),
+ new tr.model.Sample(15, 'b15_1', node, m.t3),
+ new tr.model.Sample(15, 'b15_2', node, m.t3),
+ new tr.model.Sample(12, 'a12', node, m.t2)
+ );
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5,
+ type: tr.model.ThreadSlice}));
+ m.sAA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'aa', start: 6, end: 8,
+ type: tr.model.ThreadSlice}));
+ m.sB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15,
+ type: tr.model.ThreadSlice}));
+ m.sBB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'bb', start: 11, end: 14,
+ type: tr.model.ThreadSlice}));
+ m.sBBB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'bbb', start: 12, end: 13,
+ type: tr.model.ThreadSlice}));
+ m.sC = m.t4.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20,
+ type: tr.model.ThreadSlice}));
+
+ m.t2.createSubSlices();
+ m.t3.createSubSlices();
+ m.t4.createSubSlices();
+
+ // Add flow events.
+ m.f0 = newFlowEventEx({
+ title: 'a_aa', start: 5, end: 6,
+ startSlice: m.sA,
+ endSlice: m.sAA
+ });
+ m.f1 = newFlowEventEx({
+ title: 'a_b', start: 0, end: 10,
+ startSlice: m.sA,
+ endSlice: m.sB
+ });
+ m.f2 = newFlowEventEx({
+ title: 'b_bbb', start: 10, end: 12,
+ startSlice: m.sB,
+ endSlice: m.sBBB
+ });
+ m.f3 = newFlowEventEx({
+ title: 'bbb_c', start: 13, end: 20,
+ startSlice: m.sBBB,
+ endSlice: m.sC
+ });
+ });
+ return m;
+ }
+
+ test('instantiate', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ const selection = new tr.model.EventSet(
+ [m.sA, m.f0, m.sAA, m.f1, m.sB, m.f2, m.sBB, m.sBBB, m.f3, m.sC]);
+ viewEl.setRelatedEvents(selection);
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ // Check that the element handles multiple setRelatedEvents calls correctly.
+ assert.lengthOf(viewEl.$.table.tableRows, 5);
+ viewEl.setRelatedEvents(selection);
+ assert.lengthOf(viewEl.$.table.tableRows, 5);
+ });
+
+ test('validateFlows', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ viewEl.setRelatedEvents(new tr.model.EventSet([m.sB, m.sBB, m.sBBB]));
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let inFlows;
+ let outFlows;
+ let internalFlows;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Incoming flow') {
+ assert.isUndefined(inFlows);
+ inFlows = row.selection;
+ }
+ if (row.type === 'Outgoing flow') {
+ assert.isUndefined(outFlows);
+ outFlows = row.selection;
+ }
+ if (row.type === 'Internal flow') {
+ assert.isUndefined(internalFlows);
+ internalFlows = row.selection;
+ }
+ });
+ assert.strictEqual(inFlows.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(inFlows).title, 'a_b');
+ assert.strictEqual(outFlows.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(outFlows).title, 'bbb_c');
+ assert.strictEqual(internalFlows.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(internalFlows).title, 'b_bbb');
+ });
+
+ test('validateConnectedEvents', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ viewEl.setRelatedEvents(new tr.model.EventSet([m.sBB]));
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let precedingEvents;
+ let followingEvents;
+ let allEvents;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Preceding events') {
+ assert.isUndefined(precedingEvents);
+ precedingEvents = row.selection;
+ }
+ if (row.type === 'Following events') {
+ assert.isUndefined(followingEvents);
+ followingEvents = row.selection;
+ }
+ if (row.type === 'All connected events') {
+ assert.isUndefined(allEvents);
+ allEvents = row.selection;
+ }
+ });
+
+ const precedingTitles = precedingEvents.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(precedingTitles, ['a', 'a_b', 'b', 'bb']);
+
+ const followingTitles = followingEvents.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(followingTitles, ['bb', 'bbb', 'bbb_c', 'c']);
+
+ const allTitles = allEvents.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(allTitles,
+ ['a', 'a_aa', 'aa', 'a_b', 'b', 'bb', 'bbb', 'b_bbb', 'bbb_c', 'c']);
+ });
+
+ test('validateOverlappingSamples', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ viewEl.setRelatedEvents(new tr.model.EventSet([m.sB]));
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let overlappingSamples;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Overlapping samples') {
+ assert.isUndefined(overlappingSamples);
+ overlappingSamples = row.selection;
+ }
+ });
+
+ const samplesTitles = overlappingSamples.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(samplesTitles,
+ ['b10_1', 'b10_2', 'b12', 'b15_1', 'b15_2']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html
new file mode 100644
index 00000000000..68ca4d533d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-selection-summary-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-selection-summary-table',
+ created() {
+ this.selection_ = new tr.b.math.Range();
+ },
+
+ ready() {
+ this.$.table.showHeader = false;
+ this.$.table.tableColumns = [
+ {
+ title: 'Name',
+ value(row) { return row.title; },
+ width: '350px'
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) {
+ return row.value;
+ }
+ }
+ ];
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const selection = this.selection_;
+ const rows = [];
+ let hasRange;
+ if (this.selection_ && (!selection.bounds.isEmpty)) {
+ hasRange = true;
+ } else {
+ hasRange = false;
+ }
+
+ rows.push({
+ title: 'Selection start',
+ value: hasRange ? tr.v.ui.createScalarSpan(
+ selection.bounds.min, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ }) : '<empty>'
+ });
+ rows.push({
+ title: 'Selection extent',
+ value: hasRange ? tr.v.ui.createScalarSpan(
+ selection.bounds.range, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ ownerDocument: this.ownerDocument
+ }) : '<empty>'
+ });
+
+ this.$.table.tableRows = rows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html
new file mode 100644
index 00000000000..20a8daf3b47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/selection_summary_table.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('noSelection', function() {
+ const summaryTable =
+ document.createElement('tr-ui-a-selection-summary-table');
+ summaryTable.selection = undefined;
+ this.addHTMLOutput(summaryTable);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(
+ summaryTable, 'tr-ui-b-table');
+ assert.strictEqual(tableEl.tableRows[0].value, '<empty>');
+ assert.strictEqual(tableEl.tableRows[1].value, '<empty>');
+ });
+
+ test('emptySelection', function() {
+ const summaryTable =
+ document.createElement('tr-ui-a-selection-summary-table');
+ const selection = new EventSet();
+ summaryTable.selection = selection;
+ this.addHTMLOutput(summaryTable);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(
+ summaryTable, 'tr-ui-b-table');
+ assert.strictEqual(tableEl.tableRows[0].value, '<empty>');
+ assert.strictEqual(tableEl.tableRows[1].value, '<empty>');
+ });
+
+ test('selection', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2}));
+
+ const selection = new EventSet();
+ selection.push(tsg.slices[0]);
+ selection.push(tsg.slices[1]);
+
+ const summaryTable =
+ document.createElement('tr-ui-a-selection-summary-table');
+ summaryTable.selection = selection;
+ this.addHTMLOutput(summaryTable);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(
+ summaryTable, 'tr-ui-b-table');
+ assert.strictEqual(tableEl.tableRows[0].value.value, 0);
+ assert.strictEqual(tableEl.tableRows[0].value.unit,
+ tr.b.Unit.byName.timeStampInMs);
+ assert.strictEqual(tableEl.tableRows[1].value.value, 3);
+ assert.strictEqual(tableEl.tableRows[1].value.unit,
+ tr.b.Unit.byName.timeDurationInMs);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html
new file mode 100644
index 00000000000..cc7f7b840dd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-async-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #events {
+ display:flex;
+ flex-direction: column;
+ }
+ </style>
+ <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view>
+ <div id="events">
+ <tr-ui-a-related-events id="relatedEvents"></tr-ui-a-related-events>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-async-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ if (selection.length !== 1) {
+ throw new Error('Only supports single slices');
+ }
+ this.$.content.setSelectionWithoutErrorChecks(selection);
+ this.$.relatedEvents.setRelatedEvents(selection);
+ if (this.$.relatedEvents.hasRelatedEvents()) {
+ this.$.relatedEvents.style.display = '';
+ } else {
+ this.$.relatedEvents.style.display = 'none';
+ }
+ },
+
+ getEventRows_(event) {
+ // TODO(nduca): Figure out if there is a cleaner way to do this.
+ const rows = this.__proto__.__proto__.getEventRows_(event);
+
+ // Put the ID up top.
+ rows.splice(0, 0, {
+ name: 'ID',
+ value: event.id
+ });
+ return rows;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ return tr.b.getOnlyElement(this.currentSelection_).associatedEvents;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-async-slice-sub-view',
+ tr.model.AsyncSlice,
+ {
+ multi: false,
+ title: 'Async Slice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html
new file mode 100644
index 00000000000..6cd341011bb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_async_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.asyncSliceGroup.push(newAsyncSliceEx({
+ id: 31415,
+ title: 'a',
+ start: 10,
+ duration: 20,
+ startThread: t1,
+ endThread: t1
+ }));
+
+ const selection = new tr.model.EventSet();
+ selection.push(t1.asyncSliceGroup.slices[0]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-single-async-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html
new file mode 100644
index 00000000000..12040ca9bad
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-cpu-slice-sub-view'>
+ <template>
+ <style>
+ table {
+ border-collapse: collapse;
+ border-width: 0;
+ margin-bottom: 25px;
+ width: 100%;
+ }
+
+ table tr > td:first-child {
+ padding-left: 2px;
+ }
+
+ table tr > td {
+ padding: 2px 4px 2px 4px;
+ vertical-align: text-top;
+ width: 150px;
+ }
+
+ table td td {
+ padding: 0 0 0 0;
+ width: auto;
+ }
+ tr {
+ vertical-align: top;
+ }
+
+ tr:nth-child(2n+0) {
+ background-color: #e2e2e2;
+ }
+ </style>
+ <table>
+ <tr>
+ <td>Running process:</td><td id="process-name"></td>
+ </tr>
+ <tr>
+ <td>Running thread:</td><td id="thread-name"></td>
+ </tr>
+ <tr>
+ <td>Start:</td>
+ <td>
+ <tr-v-ui-scalar-span id="start">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+ <tr>
+ <td>Duration:</td>
+ <td>
+ <tr-v-ui-scalar-span id="duration">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+ <tr>
+ <td>Active slices:</td><td id="running-thread"></td>
+ </tr>
+ <tr>
+ <td>Args:</td>
+ <td>
+ <tr-ui-a-generic-object-view id="args">
+ </tr-ui-a-generic-object-view>
+ </td>
+ </tr>
+ </table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-cpu-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const cpuSlice = tr.b.getOnlyElement(selection);
+ if (!(cpuSlice instanceof tr.model.CpuSlice)) {
+ throw new Error('Only supports thread time slices');
+ }
+
+ this.currentSelection_ = selection;
+
+ const thread = cpuSlice.threadThatWasRunning;
+
+ const root = Polymer.dom(this.root);
+ if (thread) {
+ Polymer.dom(root.querySelector('#process-name')).textContent =
+ thread.parent.userFriendlyName;
+ Polymer.dom(root.querySelector('#thread-name')).textContent =
+ thread.userFriendlyName;
+ } else {
+ root.querySelector('#process-name').parentElement.style.display =
+ 'none';
+ Polymer.dom(root.querySelector('#thread-name')).textContent =
+ cpuSlice.title;
+ }
+
+ root.querySelector('#start').setValueAndUnit(
+ cpuSlice.start, tr.b.Unit.byName.timeStampInMs);
+ root.querySelector('#duration').setValueAndUnit(
+ cpuSlice.duration, tr.b.Unit.byName.timeDurationInMs);
+
+ const runningThreadEl = root.querySelector('#running-thread');
+
+ const timeSlice = cpuSlice.getAssociatedTimeslice();
+ if (!timeSlice) {
+ runningThreadEl.parentElement.style.display = 'none';
+ } else {
+ const threadLink = document.createElement('tr-ui-a-analysis-link');
+ threadLink.selection = new tr.model.EventSet(timeSlice);
+ Polymer.dom(threadLink).textContent = 'Click to select';
+ runningThreadEl.parentElement.style.display = '';
+ Polymer.dom(runningThreadEl).textContent = '';
+ Polymer.dom(runningThreadEl).appendChild(threadLink);
+ }
+
+ root.querySelector('#args').object = cpuSlice.args;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-cpu-slice-sub-view',
+ tr.model.CpuSlice,
+ {
+ multi: false,
+ title: 'CPU Slice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html
new file mode 100644
index 00000000000..ee47fe5c58a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_cpu_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('cpuSliceView_withCpuSliceOnExistingThread', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+ const cpuSlice = cpu.slices[0];
+ assert.strictEqual('Binder_1', cpuSlice.title);
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+ assert.isDefined(thread);
+ assert.strictEqual(cpuSlice.threadThatWasRunning, thread);
+
+ const view = document.createElement('tr-ui-a-single-cpu-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(cpuSlice);
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ // Clicking the analysis link should focus the Binder1's timeslice.
+ let didSelectionChangeHappen = false;
+ view.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(
+ new tr.model.EventSet(thread.timeSlices[0])));
+ didSelectionChangeHappen = true;
+ });
+ Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click();
+ assert.isTrue(didSelectionChangeHappen);
+ });
+
+ test('cpuSliceViewWithCpuSliceOnMissingThread', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+ const cpuSlice = cpu.slices[1];
+ assert.strictEqual('Android.launcher', cpuSlice.title);
+ assert.isUndefined(cpuSlice.thread);
+
+ const selection = new tr.model.EventSet();
+ selection.push(cpuSlice);
+
+ const view = document.createElement('tr-ui-a-single-cpu-slice-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html
new file mode 100644
index 00000000000..d49125af9e3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html
@@ -0,0 +1,356 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/stack_frame.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex: 0 1;
+ flex-direction: column;
+ }
+ #table {
+ flex: 0 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ properties: {
+ isFlow: {
+ type: Boolean,
+ value: false
+ }
+ },
+
+ ready() {
+ this.currentSelection_ = undefined;
+ this.$.table.tableColumns = [
+ {
+ title: 'Label',
+ value(row) { return row.name; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) { return row.value; }
+ }
+ ];
+ this.$.table.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ if (selection.length !== 1) {
+ throw new Error('Only supports single slices');
+ }
+ this.setSelectionWithoutErrorChecks(selection);
+ },
+
+ setSelectionWithoutErrorChecks(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ getFlowEventRows_(event) {
+ // TODO(nduca): Figure out if there is a cleaner way to do this.
+
+ const rows = this.getEventRowsHelper_(event);
+
+ // Put the ID up top.
+ rows.splice(0, 0, {
+ name: 'ID',
+ value: event.id
+ });
+
+ function createLinkTo(slice) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(slice);
+ });
+ Polymer.dom(linkEl).textContent = slice.userFriendlyName;
+ return linkEl;
+ }
+
+ rows.push({
+ name: 'From',
+ value: createLinkTo(event.startSlice)
+ });
+ rows.push({
+ name: 'To',
+ value: createLinkTo(event.endSlice)
+ });
+ return rows;
+ },
+
+ getEventRowsHelper_(event) {
+ const rows = [];
+
+ if (event.error) {
+ rows.push({ name: 'Error', value: event.error });
+ }
+
+ if (event.title) {
+ let title = event.title;
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ const container = document.createElement('div');
+ container.appendChild(document.createTextNode(title));
+ const link = document.createElement('tr-ui-e-chrome-codesearch');
+ link.searchPhrase = title;
+ container.appendChild(link);
+ title = container;
+ }
+ rows.push({ name: 'Title', value: title });
+ }
+
+ if (event.category) {
+ rows.push({ name: 'Category', value: event.category });
+ }
+
+ if (event.model !== undefined) {
+ const ufc = event.model.getUserFriendlyCategoryFromEvent(event);
+ if (ufc !== undefined) {
+ rows.push({ name: 'User Friendly Category', value: ufc });
+ }
+ }
+
+ if (event.name) {
+ rows.push({ name: 'Name', value: event.name });
+ }
+
+ rows.push({
+ name: 'Start',
+ value: tr.v.ui.createScalarSpan(event.start, {
+ unit: tr.b.Unit.byName.timeStampInMs
+ })
+ });
+
+ if (event.duration) {
+ rows.push({
+ name: 'Wall Duration',
+ value: tr.v.ui.createScalarSpan(event.duration, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ if (event.cpuDuration) {
+ rows.push({
+ name: 'CPU Duration',
+ value: tr.v.ui.createScalarSpan(event.cpuDuration, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ if (event.subSlices !== undefined && event.subSlices.length !== 0) {
+ if (event.selfTime) {
+ rows.push({
+ name: 'Self Time',
+ value: tr.v.ui.createScalarSpan(event.selfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ if (event.cpuSelfTime) {
+ const cpuSelfTimeEl = tr.v.ui.createScalarSpan(event.cpuSelfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ });
+ if (event.cpuSelfTime > event.selfTime) {
+ cpuSelfTimeEl.warning =
+ ' Note that CPU Self Time is larger than Self Time. ' +
+ 'This is a known limitation of this system, which occurs ' +
+ 'due to several subslices, rounding issues, and imprecise ' +
+ 'time at which we get cpu- and real-time.';
+ }
+ rows.push({ name: 'CPU Self Time', value: cpuSelfTimeEl });
+ }
+ }
+
+ if (event.durationInUserTime) {
+ rows.push({
+ name: 'Duration (U)',
+ value: tr.v.ui.createScalarSpan(event.durationInUserTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ function createStackFrameEl(sf) {
+ const sfEl = document.createElement('tr-ui-a-stack-frame');
+ sfEl.stackFrame = sf;
+ return sfEl;
+ }
+ if (event.startStackFrame && event.endStackFrame) {
+ if (event.startStackFrame === event.endStackFrame) {
+ rows.push({name: 'Start+End Stack Trace',
+ value: createStackFrameEl(event.startStackFrame)});
+ } else {
+ rows.push({ name: 'Start Stack Trace',
+ value: createStackFrameEl(event.startStackFrame)});
+ rows.push({ name: 'End Stack Trace',
+ value: createStackFrameEl(event.endStackFrame)});
+ }
+ } else if (event.startStackFrame) {
+ rows.push({ name: 'Start Stack Trace',
+ value: createStackFrameEl(event.startStackFrame)});
+ } else if (event.endStackFrame) {
+ rows.push({ name: 'End Stack Trace',
+ value: createStackFrameEl(event.endStackFrame)});
+ }
+
+ if (event.info) {
+ const descriptionEl = tr.ui.b.createDiv({
+ textContent: event.info.description,
+ maxWidth: '300px'
+ });
+ rows.push({
+ name: 'Description',
+ value: descriptionEl
+ });
+
+
+ if (event.info.docLinks) {
+ event.info.docLinks.forEach(function(linkObject) {
+ const linkEl = document.createElement('a');
+ linkEl.target = '_blank';
+ linkEl.href = linkObject.href;
+ Polymer.dom(linkEl).textContent = Polymer.dom(linkObject).textContent;
+ rows.push({
+ name: linkObject.label,
+ value: linkEl
+ });
+ });
+ }
+ }
+
+ if (event.associatedAlerts.length) {
+ const alertSubRows = [];
+ event.associatedAlerts.forEach(function(alert) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(alert);
+ }, alert.info.description);
+ alertSubRows.push({
+ name: alert.title,
+ value: linkEl
+ });
+ });
+
+ rows.push({
+ name: 'Alerts', value: '',
+ isExpanded: true, subRows: alertSubRows
+ });
+ }
+ return rows;
+ },
+
+ getEventRows_(event) {
+ if (this.isFlow) {
+ return this.getFlowEventRows_(event);
+ }
+
+ return this.getEventRowsHelper_(event);
+ },
+
+ addArgsToRows_(rows, args) {
+ let n = 0;
+ for (const argName in args) {
+ n += 1;
+ }
+ if (n > 0) {
+ const subRows = [];
+ for (const argName in args) {
+ n += 1;
+ }
+ if (n > 0) {
+ const subRows = [];
+ for (const argName in args) {
+ const argView =
+ document.createElement('tr-ui-a-generic-object-view');
+ argView.object = args[argName];
+ subRows.push({name: argName, value: argView});
+ }
+ rows.push({
+ name: 'Args',
+ value: '',
+ isExpanded: true,
+ subRows
+ });
+ }
+ }
+ },
+
+ addContextsToRows_(rows, contexts) {
+ if (contexts.length) {
+ const subRows = contexts.map(function(context) {
+ const contextView =
+ document.createElement('tr-ui-a-generic-object-view');
+ contextView.object = context;
+ return {name: 'Context', value: contextView};
+ });
+ rows.push({
+ name: 'Contexts',
+ value: '',
+ isExpanded: true,
+ subRows
+ });
+ }
+ },
+
+ updateContents_() {
+ if (this.currentSelection_ === undefined) {
+ this.$.table.rows = [];
+ this.$.table.rebuild();
+ return;
+ }
+
+ const event = tr.b.getOnlyElement(this.currentSelection_);
+
+ const rows = this.getEventRows_(event);
+ if (event.argsStripped) {
+ rows.push({ name: 'Args', value: 'Stripped' });
+ } else {
+ this.addArgsToRows_(rows, event.args);
+ }
+ this.addContextsToRows_(rows, event.contexts);
+
+ const customizeRowsEvent = new tr.b.Event('customize-rows');
+ customizeRowsEvent.rows = rows;
+ this.dispatchEvent(customizeRowsEvent);
+
+ this.$.table.tableRows = rows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html
new file mode 100644
index 00000000000..41c42308e3e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html
@@ -0,0 +1,277 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const Thread = tr.model.Thread;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createSelection(customizeThreadCallback) {
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ customizeModelCallback(model) {
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ customizeThreadCallback(t53, model);
+ }
+ });
+
+ const t53 = model.processes[52].threads[53];
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ assert.strictEqual(selection.length, 1);
+
+ return selection;
+ }
+
+ function createSelectionWithSingleSlice(opt_options) {
+ const options = opt_options || {};
+ return createSelection(function(t53, model) {
+ let fA;
+ let fB;
+ if (options.withStartStackFrame || options.withEndStackFrame) {
+ fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2']);
+ fB = tr.c.TestUtils.newStackTrace(model, ['b1', 'b2']);
+ }
+
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+ slice.category = options.withCategory ? 'foo' : '';
+
+ if (options.withStartStackFrame) {
+ slice.startStackFrame = options.withStartStackFrame === 'a' ? fA : fB;
+ }
+
+ if (options.withEndStackFrame) {
+ slice.endStackFrame = options.withEndStackFrame === 'a' ? fA : fB;
+ }
+
+ t53.sliceGroup.pushSlice(slice);
+ });
+ }
+
+ test('instantiate_withSingleSlice', function() {
+ const selection = createSelectionWithSingleSlice();
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+
+ test('alerts', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert');
+
+ const alert = new tr.model.Alert(ALERT_INFO_1, 5, [slice]);
+
+ const selection = new EventSet();
+ selection.push(slice);
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+
+ test('instantiate_withSingleSliceWithArg', function() {
+ const selection = createSelection(function(t53) {
+ const slice = newSliceEx({title: 'my_slice', start: 0, duration: 1.0});
+ slice.args = {
+ 'complex': {
+ 'b': '2 as a string',
+ 'c': [3, 4, 5]
+ }
+ };
+ t53.sliceGroup.pushSlice(slice);
+ });
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const gov = tr.ui.b.findDeepElementMatching(subView,
+ 'tr-ui-a-generic-object-view');
+ assert.isDefined(gov);
+ });
+
+
+ test('instantiate_withSingleSliceCategory', function() {
+ const selection = createSelectionWithSingleSlice({withCategory: true});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+
+ test('instantiate_withSingleStartStackFrame', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withStartStackFrame: 'a'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const e = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /Start Stack Trace/);
+ assert.isDefined(e);
+ assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame);
+ });
+
+ test('instantiate_withSingleEndStackFrame', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withEndStackFrame: 'b'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const e = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /End Stack Trace/);
+ assert.isDefined(e);
+ assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(e).nextSibling.children[0].stackFrame.title, 'b2');
+ });
+
+ test('instantiate_withDifferentStartAndEndStackFrames', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withStartStackFrame: 'a',
+ withEndStackFrame: 'b'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const eA = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /Start Stack Trace/);
+ assert.isDefined(eA);
+ assert.isDefined(Polymer.dom(eA).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(eA).nextSibling.children[0].stackFrame.title, 'a2');
+
+ const eB = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /End Stack Trace/);
+ assert.isDefined(eB);
+ assert.isDefined(Polymer.dom(eB).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(eB).nextSibling.children[0].stackFrame.title, 'b2');
+ });
+
+ test('instantiate_withSameStartAndEndStackFrames', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withStartStackFrame: 'a',
+ withEndStackFrame: 'a'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const e = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /Start\+End Stack Trace/);
+ assert.isDefined(e);
+ assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(e).nextSibling.children[0].stackFrame.title, 'a2');
+ });
+
+ test('analyzeSelectionWithSingleSlice', function() {
+ const selection = createSelectionWithSingleSlice();
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ subView, 'tr-ui-b-table');
+ assert.strictEqual(table.tableRows.length, 3);
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ assert.strictEqual(table.tableRows[0].value.innerText, 'b');
+ } else {
+ assert.strictEqual(table.tableRows[0].value, 'b');
+ }
+ assert.strictEqual(table.tableRows[1].value.value, 0);
+ assert.strictEqual(table.tableRows[1].value.unit,
+ tr.b.Unit.byName.timeStampInMs);
+ assert.strictEqual(table.tableRows[2].value.value, 0.002);
+ assert.strictEqual(table.tableRows[2].value.unit,
+ tr.b.Unit.byName.timeDurationInMs);
+ });
+
+ test('analyzeSelectionWithSingleSliceCategory', function() {
+ const selection = createSelectionWithSingleSlice({withCategory: true});
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ subView, 'tr-ui-b-table');
+ assert.strictEqual(table.tableRows.length, 4);
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ assert.strictEqual(table.tableRows[0].value.innerText, 'b');
+ } else {
+ assert.strictEqual(table.tableRows[0].value, 'b');
+ }
+ assert.strictEqual(table.tableRows[1].value, 'foo');
+ assert.strictEqual(table.tableRows[2].value.value, 0);
+ assert.strictEqual(table.tableRows[2].value.unit,
+ tr.b.Unit.byName.timeStampInMs);
+ assert.strictEqual(table.tableRows[3].value.value, 0.002);
+ assert.strictEqual(table.tableRows[3].value.unit,
+ tr.b.Unit.byName.timeDurationInMs);
+ });
+
+ test('instantiate_withSingleSliceContainingIDRef', function() {
+ const model = new Model();
+ const p1 = model.getOrCreateProcess(1);
+ const myObjectSlice = p1.objects.addSnapshot(
+ '0x1000', 'cat', 'my_object', 0);
+
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(newSliceEx({title: 'b', start: 0, duration: 2}));
+ t1.sliceGroup.slices[0].args.my_object = myObjectSlice;
+
+ const t1track = {};
+ t1track.thread = t1;
+
+ const selection = new EventSet();
+ selection.push(t1.sliceGroup.slices[0]);
+ assert.strictEqual(selection.length, 1);
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const analysisLink = tr.ui.b.findDeepElementMatching(subView,
+ 'tr-ui-a-analysis-link');
+ assert.isDefined(analysisLink);
+ });
+
+ test('instantiate_withSingleSliceContainingInfo', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 1});
+ slice.info = new tr.model.EventInfo(
+ 'Info title', 'Description');
+
+ const selection = new EventSet();
+ selection.push(slice);
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html
new file mode 100644
index 00000000000..b201b161ffd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id="tr-ui-a-single-flow-event-sub-view">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ <tr-ui-a-single-event-sub-view id="singleEventSubView">
+ </tr-ui-a-single-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+function createAnalysisLinkTo(event) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(
+ new tr.model.EventSet(event), event.userFriendlyName);
+ return linkEl;
+}
+
+Polymer({
+ is: 'tr-ui-a-single-flow-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ listeners: {
+ 'singleEventSubView.customize-rows': 'onCustomizeRows_'
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.$.singleEventSubView.setSelectionWithoutErrorChecks(selection);
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ /**
+ * Event handler for an event that's fired after the single event sub view has
+ * finished row construction. This hook gives us the opportunity to customize
+ * the rows present in the sub view.
+ */
+ onCustomizeRows_(e) {
+ const event = tr.b.getOnlyElement(this.currentSelection_);
+ const rows = e.rows;
+
+ rows.unshift({
+ name: 'ID',
+ value: event.id
+ });
+ rows.push({
+ name: 'From',
+ value: createAnalysisLinkTo(event.startSlice)
+ });
+ rows.push({
+ name: 'To',
+ value: createAnalysisLinkTo(event.endSlice)
+ });
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-flow-event-sub-view',
+ tr.model.FlowEvent,
+ {
+ multi: false,
+ title: 'Flow Event',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html
new file mode 100644
index 00000000000..31e3eb18f25
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const TestUtils = tr.c.TestUtils;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = TestUtils.newModel(function(model) {
+ model.p1 = model.getOrCreateProcess(1);
+ model.t2 = model.p1.getOrCreateThread(model.p1);
+ model.sA = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ title: 'a', start: 0, end: 2
+ }));
+ model.sB = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ title: 'b', start: 9, end: 11
+ }));
+ model.fe = TestUtils.newFlowEventEx({
+ cat: 'cat',
+ id: 1234,
+ title: 'MyFlow',
+ start: 1,
+ end: 10,
+ startSlice: model.sA,
+ endSlice: model.sB
+ });
+ model.flowEvents.push(model.fe);
+ });
+
+ const selection = new EventSet();
+ selection.push(model.fe);
+ assert.strictEqual(selection.length, 1);
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.isFlow = true;
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html
new file mode 100644
index 00000000000..e89fa2626ef
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+
+<dom-module id='tr-ui-a-single-frame-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #asv {
+ flex: 0 0 auto;
+ align-self: stretch;
+ }
+ </style>
+ <tr-ui-a-alert-sub-view id="asv">
+ </tr-ui-a-alert-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-frame-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.$.asv.selection = tr.b.getOnlyElement(selection).associatedAlerts;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ return tr.b.getOnlyElement(this.currentSelection_).associatedEvents;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-frame-sub-view',
+ tr.model.Frame,
+ {
+ multi: false,
+ title: 'Frame',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html
new file mode 100644
index 00000000000..43b0e8a80cd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-instant-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ <div id='content'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-instant-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ Polymer.dom(this.$.content).textContent = '';
+ const realView = document.createElement('tr-ui-a-single-event-sub-view');
+ realView.setSelectionWithoutErrorChecks(selection);
+
+ Polymer.dom(this.$.content).appendChild(realView);
+
+ this.currentSelection_ = selection;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-instant-event-sub-view',
+ tr.model.InstantEvent,
+ {
+ multi: false,
+ title: 'Instant Event',
+ });
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-instant-event-sub-view',
+ tr.model.InstantEvent,
+ {
+ multi: true,
+ title: 'Instant Events',
+ });
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html
new file mode 100644
index 00000000000..4ad85d2e6db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const Thread = tr.model.Thread;
+ const EventSet = tr.model.EventSet;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = new Model();
+ const p52 = model.getOrCreateProcess(52);
+ const t53 = p52.getOrCreateThread(53);
+
+ const ie = new tr.model.ProcessInstantEvent('cat', 'title', 7, 10, {});
+ ie.duration = 20;
+ p52.instantEvents.push(ie);
+
+
+ const selection = new EventSet();
+ selection.push(ie);
+ assert.strictEqual(selection.length, 1);
+
+ const subView = document.createElement(
+ 'tr-ui-a-single-instant-event-sub-view');
+ subView.selection = selection;
+
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html
new file mode 100644
index 00000000000..49810ab3fbd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-object-instance-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+
+ #snapshots > * {
+ display: block;
+ }
+
+ :host {
+ overflow: auto;
+ display: block;
+ }
+
+ * {
+ -webkit-user-select: text;
+ }
+
+ .title {
+ border-bottom: 1px solid rgb(128, 128, 128);
+ font-size: 110%;
+ font-weight: bold;
+ }
+
+ td, th {
+ font-family: monospace;
+ vertical-align: top;
+ }
+ </style>
+ <div id='content'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-object-instance-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get requiresTallView() {
+ if (this.$.content.children.length === 0) {
+ return false;
+ }
+ if (this.$.content.children[0] instanceof
+ tr.ui.analysis.ObjectInstanceView) {
+ return this.$.content.children[0].requiresTallView;
+ }
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const instance = tr.b.getOnlyElement(selection);
+ if (!(instance instanceof tr.model.ObjectInstance)) {
+ throw new Error('Only supports object instances');
+ }
+
+ Polymer.dom(this.$.content).textContent = '';
+ this.currentSelection_ = selection;
+
+ const typeInfo = tr.ui.analysis.ObjectInstanceView.getTypeInfo(
+ instance.category, instance.typeName);
+ if (typeInfo) {
+ const customView = new typeInfo.constructor();
+ Polymer.dom(this.$.content).appendChild(customView);
+ customView.modelEvent = instance;
+ } else {
+ this.appendGenericAnalysis_(instance);
+ }
+ },
+
+ appendGenericAnalysis_(instance) {
+ let html = '';
+ html += '<div class="title">' +
+ instance.typeName + ' ' +
+ instance.id + '</div>\n';
+ html += '<table>';
+ html += '<tr>';
+ html += '<tr><td>creationTs:</td><td>' +
+ instance.creationTs + '</td></tr>\n';
+ if (instance.deletionTs !== Number.MAX_VALUE) {
+ html += '<tr><td>deletionTs:</td><td>' +
+ instance.deletionTs + '</td></tr>\n';
+ } else {
+ html += '<tr><td>deletionTs:</td><td>not deleted</td></tr>\n';
+ }
+ html += '<tr><td>snapshots:</td><td id="snapshots"></td></tr>\n';
+ html += '</table>';
+ Polymer.dom(this.$.content).innerHTML = html;
+ const snapshotsEl = Polymer.dom(this.$.content).querySelector('#snapshots');
+ instance.snapshots.forEach(function(snapshot) {
+ const snapshotLink = document.createElement('tr-ui-a-analysis-link');
+ snapshotLink.selection = new tr.model.EventSet(snapshot);
+ Polymer.dom(snapshotsEl).appendChild(snapshotLink);
+ });
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-object-instance-sub-view',
+ tr.model.ObjectInstance,
+ {
+ multi: false,
+ title: 'Object Instance',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html
new file mode 100644
index 00000000000..f5414dd957a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_object_instance_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ test('analyzeSelectionWithObjectInstanceUnknownType', function() {
+ const i10 = new ObjectInstance(
+ {}, '0x1000', 'cat', 'someUnhandledName', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+ const s20 = i10.addSnapshot(20, {foo: 2});
+
+ const selection = new tr.model.EventSet();
+ selection.push(i10);
+
+ const view =
+ document.createElement('tr-ui-a-single-object-instance-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html
new file mode 100644
index 00000000000..5565db8d004
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-object-snapshot-sub-view'>
+ <template>
+ <style>
+ #args {
+ white-space: pre;
+ }
+
+ :host {
+ overflow: auto;
+ display: flex;
+ }
+
+ ::content * {
+ -webkit-user-select: text;
+ }
+
+ ::content .title {
+ border-bottom: 1px solid rgb(128, 128, 128);
+ font-size: 110%;
+ font-weight: bold;
+ }
+
+ ::content td, th {
+ font-family: monospace;
+ vertical-align: top;
+ }
+ </style>
+ <slot></slot>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-object-snapshot-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get requiresTallView() {
+ if (this.children.length === 0) {
+ return false;
+ }
+ if (this.children[0] instanceof tr.ui.analysis.ObjectSnapshotView) {
+ return this.children[0].requiresTallView;
+ }
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const snapshot = tr.b.getOnlyElement(selection);
+ if (!(snapshot instanceof tr.model.ObjectSnapshot)) {
+ throw new Error('Only supports object instances');
+ }
+
+ Polymer.dom(this).textContent = '';
+ this.currentSelection_ = selection;
+
+ const typeInfo = tr.ui.analysis.ObjectSnapshotView.getTypeInfo(
+ snapshot.objectInstance.category, snapshot.objectInstance.typeName);
+ if (typeInfo) {
+ const customView = new typeInfo.constructor();
+ Polymer.dom(this).appendChild(customView);
+ customView.modelEvent = snapshot;
+ } else {
+ this.appendGenericAnalysis_(snapshot);
+ }
+ },
+
+ appendGenericAnalysis_(snapshot) {
+ const instance = snapshot.objectInstance;
+
+ Polymer.dom(this).textContent = '';
+
+ const titleEl = document.createElement('div');
+ Polymer.dom(titleEl).classList.add('title');
+ Polymer.dom(titleEl).appendChild(document.createTextNode('Snapshot of '));
+ Polymer.dom(this).appendChild(titleEl);
+
+ const instanceLinkEl = document.createElement('tr-ui-a-analysis-link');
+ instanceLinkEl.selection = new tr.model.EventSet(instance);
+ Polymer.dom(titleEl).appendChild(instanceLinkEl);
+
+ Polymer.dom(titleEl).appendChild(document.createTextNode(' @ '));
+
+ Polymer.dom(titleEl).appendChild(tr.v.ui.createScalarSpan(snapshot.ts, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument,
+ inline: true,
+ }));
+
+ const tableEl = document.createElement('table');
+ Polymer.dom(this).appendChild(tableEl);
+
+ const rowEl = document.createElement('tr');
+ Polymer.dom(tableEl).appendChild(rowEl);
+
+ const labelEl = document.createElement('td');
+ Polymer.dom(labelEl).textContent = 'args:';
+ Polymer.dom(rowEl).appendChild(labelEl);
+
+ const argsEl = document.createElement('td');
+ argsEl.id = 'args';
+ Polymer.dom(rowEl).appendChild(argsEl);
+
+ const objectViewEl = document.createElement('tr-ui-a-generic-object-view');
+ objectViewEl.object = snapshot.args;
+ Polymer.dom(argsEl).appendChild(objectViewEl);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-object-snapshot-sub-view',
+ tr.model.ObjectSnapshot,
+ {
+ multi: false,
+ title: 'Object Snapshot',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html
new file mode 100644
index 00000000000..41fca173931
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_object_snapshot_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_snapshotView', function() {
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+ i10.updateBounds();
+
+ const selection = new tr.model.EventSet();
+ selection.push(s10);
+
+ const view =
+ document.createElement('tr-ui-a-single-object-snapshot-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html
new file mode 100644
index 00000000000..7396cfa3eca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-power-sample-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-power-sample-table',
+
+ ready() {
+ this.$.table.tableColumns = [
+ {
+ title: 'Time',
+ width: '100px',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.start, {
+ unit: tr.b.Unit.byName.timeStampInMs
+ });
+ }
+ },
+ {
+ title: 'Power',
+ width: '100%',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.powerInW, {
+ unit: tr.b.Unit.byName.powerInWatts
+ });
+ }
+ }
+ ];
+ this.sample = undefined;
+ },
+
+ get sample() {
+ return this.sample_;
+ },
+
+ set sample(sample) {
+ this.sample_ = sample;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.sample === undefined) {
+ this.$.table.tableRows = [];
+ } else {
+ this.$.table.tableRows = [this.sample];
+ }
+ this.$.table.rebuild();
+ }
+});
+</script>
+
+<dom-module id='tr-ui-a-single-power-sample-sub-view'>
+ <template>
+ <style>
+ :host { display: block; }
+ </style>
+ <tr-ui-a-power-sample-table id="samplesTable">
+ </tr-ui-a-power-sample-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-power-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.selection.length !== 1) {
+ throw new Error('Cannot pass multiple samples to sample table.');
+ }
+ this.$.samplesTable.sample = tr.b.getOnlyElement(this.selection);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-power-sample-sub-view',
+ tr.model.PowerSample,
+ {
+ multi: false,
+ title: 'Power Sample',
+ });
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html
new file mode 100644
index 00000000000..8ee1dfcf899
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/single_power_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+ series.addPowerSample(1, 1);
+
+ const view = document.createElement('tr-ui-a-single-power-sample-sub-view');
+ view.selection = new tr.model.EventSet(series.samples);
+
+ this.addHTMLOutput(view);
+ });
+
+ test('setSelection', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+ series.addPowerSample(1, 1);
+
+ const view = document.createElement('tr-ui-a-single-power-sample-sub-view');
+ const eventSet = new tr.model.EventSet(series.samples);
+ view.selection = eventSet;
+
+ assert.deepEqual(view.$.samplesTable.sample,
+ tr.b.getOnlyElement(series.samples));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html
new file mode 100644
index 00000000000..851c60952ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/stack_frame.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-sample-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="content"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ ready() {
+ this.$.content.tableColumns = [
+ {
+ title: '',
+ value: row => row.title,
+ width: '100px'
+ },
+ {
+ title: '',
+ value: row => row.value,
+ width: '100%'
+ }
+ ];
+ this.$.content.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+
+ if (this.currentSelection_ === undefined) {
+ this.$.content.tableRows = [];
+ return;
+ }
+
+ const sample = tr.b.getOnlyElement(this.currentSelection_);
+ const table = this.$.content;
+ const rows = [];
+
+ rows.push({
+ title: 'Title',
+ value: sample.title
+ });
+
+ rows.push({
+ title: 'Sample time',
+ value: tr.v.ui.createScalarSpan(sample.start, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ })
+ });
+
+ const callStackTableEl = document.createElement('tr-ui-b-table');
+ callStackTableEl.tableRows = sample.getNodesAsArray().reverse();
+ callStackTableEl.tableColumns = [
+ {
+ title: 'function name',
+ value: row => row.functionName || '(anonymous function)'
+ },
+ {
+ title: 'location',
+ value: row => row.url
+ }
+ ];
+ callStackTableEl.rebuild();
+ rows.push({
+ title: 'Call stack',
+ value: callStackTableEl
+ });
+ table.tableRows = rows;
+ table.rebuild();
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-sample-sub-view',
+ tr.model.Sample,
+ {
+ multi: false,
+ title: 'Sample',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html
new file mode 100644
index 00000000000..7f8c131f82c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const newSampleNamed = tr.c.TestUtils.newSampleNamed;
+
+ test('instantiate_withSingleSample', function() {
+ let t53;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ model.samples.push(newSampleNamed(t53, 'X', 'my-category',
+ ['a', 'b', 'c'], 0.184));
+ }
+ });
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+
+ assert.strictEqual(selection.length, 0);
+ selection.push(t53.samples[0]);
+ assert.strictEqual(selection.length, 1);
+
+ const view = document.createElement('tr-ui-a-single-sample-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ view, 'tr-ui-b-table');
+
+ const rows = table.tableRows;
+ assert.strictEqual(rows.length, 3);
+ assert.strictEqual(rows[0].value, 'X');
+ assert.strictEqual(rows[1].value.value, 0.184);
+ assert.strictEqual(rows[1].value.unit, tr.b.Unit.byName.timeStampInMs);
+
+ const callStackRows = rows[2].value.tableRows;
+ assert.lengthOf(callStackRows, 3);
+ assert.deepEqual(callStackRows.map(x => x.title), ['a', 'b', 'c']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html
new file mode 100644
index 00000000000..720fdfeb65a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-thread-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #events {
+ display: flex;
+ flex-direction: column;
+ }
+
+ </style>
+ <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view>
+ <div id="events">
+ <tr-ui-a-related-events id="relatedEvents">
+ </tr-ui-a-related-events>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ this.$.relatedEvents.setRelatedEvents(selection);
+ if (this.$.relatedEvents.hasRelatedEvents()) {
+ this.$.relatedEvents.style.display = '';
+ } else {
+ this.$.relatedEvents.style.display = 'none';
+ }
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-thread-slice-sub-view',
+ tr.model.ThreadSlice,
+ {
+ multi: false,
+ title: 'Slice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..84bb292384e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/analysis/single_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0.0, duration: 0.5}));
+ t53.sliceGroup.createSubSlices();
+
+ const selection = new tr.model.EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-single-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('instantiateWithFlowEvent', function() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+
+ m.t2 = m.p1.getOrCreateThread(2);
+ m.t3 = m.p1.getOrCreateThread(3);
+ m.t4 = m.p1.getOrCreateThread(4);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5,
+ type: tr.model.ThreadSlice}));
+ m.sB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15,
+ type: tr.model.ThreadSlice}));
+ m.sC = m.t4.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20,
+ type: tr.model.ThreadSlice}));
+
+ m.t2.createSubSlices();
+ m.t3.createSubSlices();
+ m.t4.createSubSlices();
+
+ m.f1 = newFlowEventEx({
+ title: 'flowish', start: 0, end: 10,
+ startSlice: m.sA,
+ endSlice: m.sB
+ });
+ m.f2 = newFlowEventEx({
+ title: 'flowish', start: 15, end: 21,
+ startSlice: m.sB,
+ endSlice: m.sC
+ });
+ });
+
+ const selection = new tr.model.EventSet();
+ selection.push(m.sA);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-single-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html
new file mode 100644
index 00000000000..225b2729769
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-thread-time-slice-sub-view'>
+ <template>
+ <style>
+ table {
+ border-collapse: collapse;
+ border-width: 0;
+ margin-bottom: 25px;
+ width: 100%;
+ }
+
+ table tr > td:first-child {
+ padding-left: 2px;
+ }
+
+ table tr > td {
+ padding: 2px 4px 2px 4px;
+ vertical-align: text-top;
+ width: 150px;
+ }
+
+ table td td {
+ padding: 0 0 0 0;
+ width: auto;
+ }
+ tr {
+ vertical-align: top;
+ }
+
+ tr:nth-child(2n+0) {
+ background-color: #e2e2e2;
+ }
+ </style>
+ <table>
+ <tr>
+ <td>Running process:</td><td id="process-name"></td>
+ </tr>
+ <tr>
+ <td>Running thread:</td><td id="thread-name"></td>
+ </tr>
+ <tr>
+ <td>State:</td>
+ <td><b><span id="state"></span></b></td>
+ </tr>
+ <tr>
+ <td>Start:</td>
+ <td>
+ <tr-v-ui-scalar-span id="start">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+ <tr>
+ <td>Duration:</td>
+ <td>
+ <tr-v-ui-scalar-span id="duration">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+
+ <tr>
+ <td>On CPU:</td><td id="on-cpu"></td>
+ </tr>
+
+ <tr>
+ <td>Running instead:</td><td id="running-instead"></td>
+ </tr>
+
+ <tr>
+ <td>Args:</td><td id="args"></td>
+ </tr>
+ </table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-thread-time-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const timeSlice = tr.b.getOnlyElement(selection);
+
+ if (!(timeSlice instanceof tr.model.ThreadTimeSlice)) {
+ throw new Error('Only supports thread time slices');
+ }
+
+ this.currentSelection_ = selection;
+
+ const thread = timeSlice.thread;
+
+ const root = Polymer.dom(this.root);
+ Polymer.dom(root.querySelector('#state')).textContent =
+ timeSlice.title;
+ const stateColor = tr.b.ColorScheme.colorsAsStrings[timeSlice.colorId];
+ root.querySelector('#state').style.backgroundColor = stateColor;
+
+ Polymer.dom(root.querySelector('#process-name')).textContent =
+ thread.parent.userFriendlyName;
+ Polymer.dom(root.querySelector('#thread-name')).textContent =
+ thread.userFriendlyName;
+
+ root.querySelector('#start').setValueAndUnit(
+ timeSlice.start, tr.b.Unit.byName.timeStampInMs);
+ root.querySelector('#duration').setValueAndUnit(
+ timeSlice.duration, tr.b.Unit.byName.timeDurationInMs);
+
+ const onCpuEl = root.querySelector('#on-cpu');
+ Polymer.dom(onCpuEl).textContent = '';
+ const runningInsteadEl = root.querySelector('#running-instead');
+ if (timeSlice.cpuOnWhichThreadWasRunning) {
+ Polymer.dom(runningInsteadEl.parentElement).removeChild(runningInsteadEl);
+
+ const cpuLink = document.createElement('tr-ui-a-analysis-link');
+ cpuLink.selection = new tr.model.EventSet(
+ timeSlice.getAssociatedCpuSlice());
+ Polymer.dom(cpuLink).textContent =
+ timeSlice.cpuOnWhichThreadWasRunning.userFriendlyName;
+ Polymer.dom(onCpuEl).appendChild(cpuLink);
+ } else {
+ Polymer.dom(onCpuEl.parentElement).removeChild(onCpuEl);
+
+ const cpuSliceThatTookCpu = timeSlice.getCpuSliceThatTookCpu();
+ if (cpuSliceThatTookCpu) {
+ const cpuLink = document.createElement('tr-ui-a-analysis-link');
+ cpuLink.selection = new tr.model.EventSet(cpuSliceThatTookCpu);
+ if (cpuSliceThatTookCpu.thread) {
+ Polymer.dom(cpuLink).textContent =
+ cpuSliceThatTookCpu.thread.userFriendlyName;
+ } else {
+ Polymer.dom(cpuLink).textContent = cpuSliceThatTookCpu.title;
+ }
+ Polymer.dom(runningInsteadEl).appendChild(cpuLink);
+ } else {
+ Polymer.dom(runningInsteadEl.parentElement).removeChild(
+ runningInsteadEl);
+ }
+ }
+
+ const argsEl = root.querySelector('#args');
+ if (Object.keys(timeSlice.args).length > 0) {
+ const argsView =
+ document.createElement('tr-ui-a-generic-object-view');
+ argsView.object = timeSlice.args;
+
+ argsEl.parentElement.style.display = '';
+ Polymer.dom(argsEl).textContent = '';
+ Polymer.dom(argsEl).appendChild(argsView);
+ } else {
+ argsEl.parentElement.style.display = 'none';
+ }
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-thread-time-slice-sub-view',
+ tr.model.ThreadTimeSlice,
+ {
+ multi: false,
+ title: 'Thread Timeslice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html
new file mode 100644
index 00000000000..bfffd41861b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_thread_time_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('runningSlice', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ const binderSlice = cpu.slices[0];
+ assert.strictEqual(binderSlice.title, 'Binder_1');
+ const launcherSlice = cpu.slices[1];
+ assert.strictEqual(launcherSlice.title, 'Android.launcher');
+
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+
+ const view = document.createElement(
+ 'tr-ui-a-single-thread-time-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(thread.timeSlices[0]);
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ // Clicking the analysis link should focus the Binder1's timeslice.
+ let didSelectionChangeHappen = false;
+ view.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(binderSlice)));
+ didSelectionChangeHappen = true;
+ });
+ Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click();
+ assert.isTrue(didSelectionChangeHappen);
+ });
+
+ test('sleepingSlice', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ const binderSlice = cpu.slices[0];
+ assert.strictEqual(binderSlice.title, 'Binder_1');
+ const launcherSlice = cpu.slices[1];
+ assert.strictEqual(launcherSlice.title, 'Android.launcher');
+
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+
+ const view = document.createElement(
+ 'tr-ui-a-single-thread-time-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(thread.timeSlices[1]);
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ // Clicking the analysis link should focus the Android.launcher slice
+ let didSelectionChangeHappen = false;
+ view.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(launcherSlice)));
+ didSelectionChangeHappen = true;
+ });
+ Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click();
+ assert.isTrue(didSelectionChangeHappen);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html
new file mode 100644
index 00000000000..76110b4b468
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/user_expectation_related_samples_table.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-user-expectation-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #events {
+ display: flex;
+ flex-direction: column;
+ }
+ </style>
+ <tr-ui-a-single-event-sub-view id="realView"></tr-ui-a-single-event-sub-view>
+ <div id="events">
+ <tr-ui-a-user-expectation-related-samples-table id="relatedSamples"></tr-ui-a-user-expectation-related-samples-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-user-expectation-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.$.realView.addEventListener('customize-rows',
+ this.onCustomizeRows_.bind(this));
+
+ this.currentSelection_ = selection;
+ this.$.realView.setSelectionWithoutErrorChecks(selection);
+
+ this.$.relatedSamples.selection = selection;
+ if (this.$.relatedSamples.hasRelatedSamples()) {
+ this.$.events.style.display = '';
+ } else {
+ this.$.events.style.display = 'none';
+ }
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ return tr.b.getOnlyElement(this.currentSelection_).associatedEvents;
+ },
+
+ onCustomizeRows_(event) {
+ const ue = tr.b.getOnlyElement(this.selection);
+
+ if (ue.rawCpuMs) {
+ event.rows.push({
+ name: 'Total CPU',
+ value: tr.v.ui.createScalarSpan(ue.totalCpuMs, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-user-expectation-sub-view',
+ tr.model.um.UserExpectation,
+ {
+ multi: false,
+ title: 'User Expectation',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html
new file mode 100644
index 00000000000..92c4594af5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-stack-frame'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-stack-frame',
+
+ ready() {
+ this.stackFrame_ = undefined;
+ this.$.table.tableColumns = [];
+ this.$.table.showHeader = true;
+ },
+
+ get stackFrame() {
+ return this.stackFrame_;
+ },
+
+ set stackFrame(stackFrame) {
+ const table = this.$.table;
+
+ this.stackFrame_ = stackFrame;
+ if (stackFrame === undefined) {
+ table.tableColumns = [];
+ table.tableRows = [];
+ table.rebuild();
+ return;
+ }
+
+ let hasName = false;
+ let hasTitle = false;
+
+ table.tableRows = stackFrame.stackTrace;
+ table.tableRows.forEach(function(row) {
+ hasName |= row.name !== undefined;
+ hasTitle |= row.title !== undefined;
+ });
+
+ const cols = [];
+ if (hasName) {
+ cols.push({
+ title: 'Name',
+ value(row) { return row.name; }
+ });
+ }
+
+ if (hasTitle) {
+ cols.push({
+ title: 'Title',
+ value(row) { return row.title; }
+ });
+ }
+
+ table.tableColumns = cols;
+ table.rebuild();
+ },
+
+ tableForTesting() {
+ return this.$.table;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html
new file mode 100644
index 00000000000..4523906a321
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/stack_frame.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2', 'a3']);
+
+ const stackFrameView = document.createElement('tr-ui-a-stack-frame');
+ stackFrameView.stackFrame = fA;
+ this.addHTMLOutput(stackFrameView);
+ });
+
+ test('clearingStackFrame', function() {
+ const model = new tr.Model();
+ const fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2', 'a3']);
+
+ const stackFrameView = document.createElement('tr-ui-a-stack-frame');
+ stackFrameView.stackFrame = fA;
+ stackFrameView.stackFrame = undefined;
+
+ assert.isUndefined(stackFrameView.stackFrame);
+ assert.lengthOf(stackFrameView.$.table.$.body.children, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html
new file mode 100644
index 00000000000..0e4f633fb00
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+
+<!--
+@fileoverview Analysis view stacked pane. See the stacked pane view element
+(tr-ui-a-stacked-pane-view) documentation for more details.
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const StackedPaneImpl = {
+ /**
+ * Request changing the child pane of this pane in the associated stacked
+ * pane view. If the assigned builder is undefined, request removing the
+ * current child pane.
+ *
+ * Note that setting this property before appended() is called will have no
+ * effect (as there will be no listener attached to the pane).
+ *
+ * This method is intended to be called by subclasses.
+ */
+ set childPaneBuilder(childPaneBuilder) {
+ this.childPaneBuilder_ = childPaneBuilder;
+ this.dispatchEvent(new tr.b.Event('request-child-pane-change'));
+ },
+
+ get childPaneBuilder() {
+ return this.childPaneBuilder_;
+ },
+
+ /**
+ * Called right after the pane is appended to a pane view.
+ *
+ * This method triggers an immediate rebuild by default. Subclasses are
+ * free to change this behavior (e.g. if a pane has lots of data to display,
+ * it might decide to defer rebuilding in order not to cause jank).
+ */
+ appended() {
+ this.rebuild();
+ }
+ };
+
+ const StackedPane = [tr.ui.analysis.RebuildableBehavior, StackedPaneImpl];
+
+ return {
+ StackedPane,
+ };
+});
+
+Polymer({
+ is: 'tr-ui-a-stacked-pane',
+ behaviors: [tr.ui.analysis.StackedPane]
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html
new file mode 100644
index 00000000000..7af70fa3d7a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('changeChildPane', function() {
+ const pane = document.createElement('tr-ui-a-stacked-pane');
+ let didFireEvent;
+ pane.addEventListener('request-child-pane-change', function() {
+ didFireEvent = true;
+ });
+
+ didFireEvent = false;
+ pane.childPaneBuilder = undefined;
+ assert.isTrue(didFireEvent);
+
+ didFireEvent = false;
+ pane.childPaneBuilder = function() {
+ return undefined;
+ };
+ assert.isTrue(didFireEvent);
+
+ didFireEvent = false;
+ pane.childPaneBuilder = function() {
+ return document.createElement('tr-ui-a-stacked-pane');
+ };
+ assert.isTrue(didFireEvent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html
new file mode 100644
index 00000000000..e8bea2dd034
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<!--
+@fileoverview Analysis view container which displays vertically stacked panes.
+The panes represent a hierarchy where a child pane contains the details of the
+current selection in its parent pane. The container provides simple primitives
+for panes to request changing their child pane:
+
+ +=<tr-ui-a-stacked-pane-view>=+ +=<tr-ui-a-stacked-pane-view>=+
+ |+.<tr-ui-a-stacked-pane>....+| |+.<tr-ui-a-stacked-pane>....+|
+ |: Pane 1 +| ===========> |: Pane 1 +|
+ |+...........................+| Pane 1 |+...........................+|
+ |+.<tr-ui-a-stacked-pane>....+| requests |+.<tr-ui-a-stacked-pane>....+|
+ |: Pane 2 (detail of Pane 1) +| child pane |: Pane 4 (detail of Pane 1) +|
+ |+...........................+| change (e.g. |+...........................+|
+ |+.<tr-ui-a-stacked-pane>....+| selection +=============================+
+ |: Pane 3 (detail of Pane 2) +| changed)
+ |+...........................+|
+ +=============================+
+
+Note that the actual UI provided by tr-ui-a-stacked-pane-view and
+tr-ui-a-stacked-pane is merely a wrapper container with flex box vertical
+stacking. No other visual features (such as pane spacing or borders) is
+provided by either element.
+
+The stacked pane element (tr-ui-a-stacked-pane) is defined in a separate file.
+
+Sample use case:
+
+ Create an empty stacked pane view and add it to the DOM:
+
+ const paneView = document.createElement('tr-ui-a-stacked-pane-view');
+ Polymer.dom(someParentView).appendChild(paneView);
+
+ Define one or more pane subclasses:
+
+ TODO(polymer): Write this documentation
+ <polymer-element name="some-pane-1" extends="tr-ui-a-stacked-pane">
+ ...
+ </polymer-element>
+
+ Set the top-level pane (by providing a builder function):
+
+ paneView.setPaneBuilder(function() {
+ const topPane = document.createElement('some-pane-1');
+ pane.someProperty = someValue;
+ return topPane;
+ });
+
+ Show a child pane with details upon user interaction (these methods should be
+ in the definition of the pane subclass Polymer element):
+
+ ready: function() {
+ this.$.table.addEventListener(
+ 'selection-changed', this.changeChildPane_.bind(this));
+ }
+
+ changeChildPane_: function() {
+ this.childPaneBuilder = function() {
+ const selectedRow = this.$.table.selectedTableRow;
+ const detailsPane = document.createElement('some-pane-2');
+ detailsPane.someProperty = selectedRow;
+ return detailsPane;
+ }.bind(this);
+ }
+-->
+<dom-module id='tr-ui-a-stacked-pane-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #pane_container > * {
+ flex: 0 0 auto;
+ }
+ </style>
+ <div id="pane_container">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-stacked-pane-view',
+
+ /**
+ * Add a pane to the stacked pane view. This method performs two operations:
+ *
+ * 1. Remove existing descendant panes
+ * If the optional parent pane is provided, all its current descendant
+ * panes are removed. Otherwise, all panes are removed from the view.
+ *
+ * 2. Build and add new pane
+ * If a pane builder is provided and returns a pane, the new pane is
+ * appended to the view (after the provided parent, or at the top).
+ */
+ setPaneBuilder(paneBuilder, opt_parentPane) {
+ const paneContainer = this.$.pane_container;
+
+ // If the parent pane is provided, it must be an HTML element and a child
+ // of the pane container.
+ if (opt_parentPane) {
+ if (!(opt_parentPane instanceof HTMLElement)) {
+ throw new Error('Parent pane must be an HTML element');
+ }
+ if (opt_parentPane.parentElement !== paneContainer) {
+ throw new Error('Parent pane must be a child of the pane container');
+ }
+ }
+
+ // Remove all descendants of the parent pane (or all panes if no parent
+ // pane was specified) in reverse order.
+ while (Polymer.dom(paneContainer).lastElementChild !== null &&
+ Polymer.dom(paneContainer).lastElementChild !== opt_parentPane) {
+ const removedPane = Polymer.dom(this.$.pane_container).lastElementChild;
+ const listener = this.listeners_.get(removedPane);
+ if (listener === undefined) {
+ throw new Error('No listener associated with pane');
+ }
+ this.listeners_.delete(removedPane);
+ removedPane.removeEventListener(
+ 'request-child-pane-change', listener);
+ Polymer.dom(paneContainer).removeChild(removedPane);
+ }
+
+ if (opt_parentPane && opt_parentPane.parentElement !== paneContainer) {
+ throw new Error('Parent pane was removed from the pane container');
+ }
+
+ // This check is performed here (and not at the beginning of the method)
+ // because undefined pane builder means that the parent pane requested
+ // having no child pane (e.g. when selection is cleared).
+ if (!paneBuilder) return;
+
+ const pane = paneBuilder();
+ if (!pane) return;
+
+ if (!(pane instanceof HTMLElement)) {
+ throw new Error('Pane must be an HTML element');
+ }
+
+ // Listen for child pane change requests from the newly added pane.
+ const listener = function(event) {
+ this.setPaneBuilder(pane.childPaneBuilder, pane);
+ }.bind(this);
+ if (!this.listeners_) {
+ // Instead of initializing the listeners map in a created() callback,
+ // we do it lazily here so that subclasses could provide their own
+ // created() callback (Polymer currently doesn't allow calling overriden
+ // superclass methods in strict mode).
+ this.listeners_ = new WeakMap();
+ }
+ this.listeners_.set(pane, listener);
+ pane.addEventListener('request-child-pane-change', listener);
+
+ Polymer.dom(paneContainer).appendChild(pane);
+ pane.appended();
+ },
+
+ /**
+ * Request rebuilding all panes in the view. The panes are rebuilt from the
+ * top to the bottom (so that parent panes could request changing their
+ * child panes when they're being rebuilt and the newly constructed child
+ * panes would be rebuilt as well).
+ */
+ rebuild() {
+ let currentPane = Polymer.dom(this.$.pane_container).firstElementChild;
+ while (currentPane) {
+ currentPane.rebuild();
+ currentPane = currentPane.nextElementSibling;
+ }
+ },
+
+ // For testing purposes.
+ get panesForTesting() {
+ const panes = [];
+ let currentChild = Polymer.dom(this.$.pane_container).firstElementChild;
+ while (currentChild) {
+ panes.push(currentChild);
+ currentChild = currentChild.nextElementSibling;
+ }
+ return panes;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html
new file mode 100644
index 00000000000..ceae19ab0db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createPaneView() {
+ return document.createElement('tr-ui-a-stacked-pane-view');
+ }
+
+ function createPane(paneId, opt_rebuildPaneCallback, opt_appendedCallback) {
+ const paneEl = document.createElement('tr-ui-a-stacked-pane');
+ paneEl.paneId = paneId;
+
+ const divEl = document.createElement('div');
+ Polymer.dom(divEl).textContent = 'Pane ' + paneId;
+ divEl.style.width = '400px';
+ divEl.style.background = '#ccc';
+ divEl.style.textAlign = 'center';
+ Polymer.dom(paneEl).appendChild(divEl);
+
+ if (opt_rebuildPaneCallback) {
+ paneEl.onRebuild_ = opt_rebuildPaneCallback;
+ }
+
+ if (opt_appendedCallback) {
+ paneEl.appended = opt_appendedCallback;
+ }
+
+ return paneEl;
+ }
+
+ function createPaneBuilder(paneId, opt_rebuildPaneCallback,
+ opt_appendedCallback) {
+ return createPane.bind(
+ undefined, paneId, opt_rebuildPaneCallback, opt_appendedCallback);
+ }
+
+ function assertPanes(paneView, expectedPaneIds) {
+ const actualPaneIds = paneView.panesForTesting.map(function(pane) {
+ return pane.paneId;
+ });
+ assert.deepEqual(actualPaneIds, expectedPaneIds);
+ }
+
+ test('instantiate_empty', function() {
+ const viewEl = createPaneView();
+ viewEl.rebuild();
+ assertPanes(viewEl, []);
+ // Don't add the pane to HTML output because it has zero height.
+ });
+
+ test('instantiate_singlePane', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ viewEl.rebuild();
+
+ assertPanes(viewEl, [1]);
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('instantiate_multiplePanes', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ viewEl.setPaneBuilder(createPaneBuilder(2), viewEl.panesForTesting[0]);
+ viewEl.setPaneBuilder(createPaneBuilder(3), viewEl.panesForTesting[1]);
+
+ assertPanes(viewEl, [1, 2, 3]);
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('changePanes', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ assertPanes(viewEl, [1]);
+
+ viewEl.setPaneBuilder(null);
+ assertPanes(viewEl, []);
+
+ viewEl.setPaneBuilder(createPaneBuilder(2));
+ assertPanes(viewEl, [2]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(3), viewEl.panesForTesting[0]);
+ assertPanes(viewEl, [2, 3]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(4), viewEl.panesForTesting[0]);
+ assertPanes(viewEl, [2, 4]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(5), viewEl.panesForTesting[1]);
+ assertPanes(viewEl, [2, 4, 5]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(6), viewEl.panesForTesting[2]);
+ assertPanes(viewEl, [2, 4, 5, 6]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(7), viewEl.panesForTesting[1]);
+ assertPanes(viewEl, [2, 4, 7]);
+
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('childPanes', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ assertPanes(viewEl, [1]);
+
+ // Pane 1 requests a child pane 2.
+ const pane1 = viewEl.panesForTesting[0];
+ pane1.childPaneBuilder = createPaneBuilder(2);
+ assertPanes(viewEl, [1, 2]);
+
+ // Pane 2 requests removing its child pane (nothing happens).
+ const pane2 = viewEl.panesForTesting[1];
+ pane2.childPaneBuilder = undefined;
+ assertPanes(viewEl, [1, 2]);
+
+ // Pane 2 requests a child pane 3.
+ pane2.childPaneBuilder = createPaneBuilder(3);
+ assertPanes(viewEl, [1, 2, 3]);
+
+ // Pane 2 requests a child pane 4 (its previous child pane 3 is removed).
+ pane2.childPaneBuilder = createPaneBuilder(4);
+ assertPanes(viewEl, [1, 2, 4]);
+
+ // Pane 1 requests removing its child pane (panes 2 and 4 are removed).
+ pane1.childPaneBuilder = undefined;
+ assertPanes(viewEl, [1]);
+
+ // Check that removed panes cannot affect the pane view.
+ pane2.childPaneBuilder = createPaneBuilder(5);
+ assertPanes(viewEl, [1]);
+
+ // Pane 1 requests a child pane 6 (check that everything still works).
+ pane1.childPaneBuilder = createPaneBuilder(6);
+ assertPanes(viewEl, [1, 6]);
+
+ // Change the top pane to pane 7.
+ viewEl.setPaneBuilder(createPaneBuilder(7));
+ assertPanes(viewEl, [7]);
+
+ // Check that removed panes cannot affect the pane view.
+ pane1.childPaneBuilder = createPaneBuilder(5);
+ assertPanes(viewEl, [7]);
+ });
+
+ test('rebuild', function() {
+ const viewEl = createPaneView();
+
+ const rebuiltPaneIds = [];
+ const rebuildPaneCallback = function() {
+ rebuiltPaneIds.push(this.paneId);
+ };
+
+ viewEl.setPaneBuilder(createPaneBuilder(1, rebuildPaneCallback));
+ viewEl.setPaneBuilder(createPaneBuilder(2, rebuildPaneCallback),
+ viewEl.panesForTesting[0]);
+ viewEl.setPaneBuilder(createPaneBuilder(3, rebuildPaneCallback),
+ viewEl.panesForTesting[1]);
+
+ // Rebuild isn't triggered.
+ assert.deepEqual(rebuiltPaneIds, []);
+
+ // Rebuild is triggered, but it isn't necessary (all panes are clean).
+ viewEl.rebuild();
+ assert.deepEqual(rebuiltPaneIds, []);
+
+ // All panes are now marked as dirty, but rebuild isn't triggered (it was
+ // only scheduled).
+ viewEl.panesForTesting.forEach(function(pane) {
+ pane.scheduleRebuild_();
+ });
+ assert.deepEqual(rebuiltPaneIds, []);
+
+ // Finally, rebuild was triggered and the panes are dirty.
+ viewEl.rebuild();
+ assert.deepEqual(rebuiltPaneIds, [1, 2, 3]);
+
+ // Make sure that panes are clean after the previous rebuild.
+ viewEl.rebuild();
+ assert.deepEqual(rebuiltPaneIds, [1, 2, 3]);
+ });
+
+ test('appended', function() {
+ const viewEl = createPaneView();
+ let didFireAppended;
+
+ didFireAppended = false;
+ viewEl.setPaneBuilder(createPaneBuilder(1, undefined, function() {
+ didFireAppended = true;
+ }));
+ assert.isTrue(didFireAppended);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html
new file mode 100644
index 00000000000..b371eac4cf9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ function StubAnalysisTable() {
+ this.ownerDocument_ = document;
+ this.nodes_ = [];
+ }
+
+ StubAnalysisTable.prototype = {
+ __proto__: Object.protoype,
+
+ get ownerDocument() {
+ return this.ownerDocument_;
+ },
+
+ appendChild(node) {
+ if (node.tagName === 'TFOOT' || node.tagName === 'THEAD' ||
+ node.tagName === 'TBODY') {
+ node.__proto__ = StubAnalysisTable.prototype;
+ node.nodes_ = [];
+ node.ownerDocument_ = document;
+ }
+ this.nodes_.push(node);
+ },
+
+ get lastNode() {
+ return this.nodes_.pop();
+ },
+
+ get nodeCount() {
+ return this.nodes_.length;
+ }
+ };
+
+ return {
+ StubAnalysisTable,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html
new file mode 100644
index 00000000000..3c2bfdbd9cf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-user-expectation-related-samples-table'>
+ <template>
+ <style>
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-user-expectation-related-samples-table',
+
+ ready() {
+ this.samples_ = [];
+ this.$.table.tableColumns = [
+ {
+ title: 'Event(s)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.type;
+ if (row.tooltip) {
+ typeEl.title = row.tooltip;
+ }
+ return typeEl;
+ },
+ width: '150px'
+ },
+ {
+ title: 'Link',
+ width: '100%',
+ value(row) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ if (row.name) {
+ linkEl.setSelectionAndContent(row.selection, row.name);
+ } else {
+ linkEl.selection = row.selection;
+ }
+ return linkEl;
+ }
+ }
+ ];
+ },
+
+ hasRelatedSamples() {
+ return (this.samples_ && this.samples_.length > 0);
+ },
+
+ set selection(eventSet) {
+ this.samples_ = [];
+ const samples = new tr.model.EventSet;
+ eventSet.forEach(function(ue) {
+ samples.addEventSet(ue.associatedSamples);
+ }.bind(this));
+
+ if (samples.length > 0) {
+ this.samples_.push({
+ type: 'Overlapping samples',
+ tooltip: 'All samples overlapping the selected user expectation(s).',
+ selection: samples
+ });
+ }
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const table = this.$.table;
+ if (this.samples_ && this.samples_.length > 0) {
+ table.tableRows = this.samples_.slice();
+ } else {
+ table.tableRows = [];
+ }
+ table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html
new file mode 100644
index 00000000000..368f41ce6c6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import"
+ href="/tracing/ui/analysis/user_expectation_related_samples_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+ const node = tr.c.TestUtils.newProfileNodes(m, ['fake']);
+ const s1 = new tr.model.Sample(1, 'a_1', node, m.t2);
+ const s2 = new tr.model.Sample(2, 'a_2', node, m.t2);
+ const s3 = new tr.model.Sample(3, 'a_3', node, m.t2);
+ const s4 = new tr.model.Sample(4, 'a_4', node, m.t2);
+ const s5 = new tr.model.Sample(5, 'a_5', node, m.t2);
+ const s6 = new tr.model.Sample(6, 'a_6', node, m.t2);
+ m.samples.push(s1, s2, s3, s4, s5, s6);
+ m.ve = new tr.c.TestUtils.newSliceEx(
+ {title: 'V8.Execute', start: 0, end: 4, type: tr.model.ThreadSlice});
+ m.t2.sliceGroup.pushSlice(m.ve);
+ m.up = new tr.c.TestUtils.newInteractionRecord(m, 0, 4);
+ m.up.associatedEvents.push(m.ve);
+ m.userModel.expectations.push(m.up);
+ });
+ return m;
+ }
+
+ test('overlappingSamples', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-user-expectation-related-samples-table');
+ viewEl.selection = new tr.model.EventSet([m.up]);
+
+ let overlappingSamples;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Overlapping samples') {
+ assert.isUndefined(overlappingSamples);
+ overlappingSamples = row.selection;
+ }
+ });
+
+ const samplesTitles = overlappingSamples.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(samplesTitles,
+ ['a_1', 'a_2', 'a_3', 'a_4']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html
new file mode 100644
index 00000000000..48ba2ac9fa7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A base class for all annotation views.
+ * @constructor
+ */
+ function AnnotationView(viewport, annotation) {
+ }
+
+ AnnotationView.prototype = {
+ draw(ctx) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ AnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html
new file mode 100644
index 00000000000..3a3b3615f91
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/comment_box_annotation.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/rect_annotation.html">
+<link rel="import" href="/tracing/model/x_marker_annotation.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createPopulatedTimeline() {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(2);
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'a', start: 80, duration: 50}));
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ const vp = new tr.ui.TimelineViewport(timeline);
+ timeline.model = model;
+ timeline.style.maxHeight = '600px';
+
+ return timeline;
+ }
+
+ test('rectAnnotation', function() {
+ const fakeYComponents1 = [{stableId: '1.2', yPercentOffset: 0.3}];
+ const fakeYComponents2 = [{stableId: '1.2', yPercentOffset: 0.9}];
+ const start = new tr.model.Location(50, fakeYComponents1);
+ const end = new tr.model.Location(100, fakeYComponents2);
+ const rectAnnotation = new tr.model.RectAnnotation(start, end);
+
+ const timeline = createPopulatedTimeline();
+ timeline.model.addAnnotation(rectAnnotation);
+ this.addHTMLOutput(timeline);
+ });
+
+ test('xMarkerAnnotation', function() {
+ const xMarkerAnnotation = new tr.model.XMarkerAnnotation(90);
+
+ const timeline = createPopulatedTimeline();
+ const model = timeline.model;
+ timeline.model.addAnnotation(xMarkerAnnotation);
+ this.addHTMLOutput(timeline);
+ });
+
+ test('commentBoxAnnotation', function() {
+ const fakeYComponents = [{stableId: '1.2', yPercentOffset: 0.5}];
+ const location = new tr.model.Location(120, fakeYComponents);
+ const text = 'abc';
+ const commentBoxAnnotation =
+ new tr.model.CommentBoxAnnotation(location, text);
+
+ const timeline = createPopulatedTimeline();
+ timeline.model.addAnnotation(commentBoxAnnotation);
+ this.addHTMLOutput(timeline);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html
new file mode 100644
index 00000000000..5237b9b0f55
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/annotations/annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A view of a comment box consisting of a textarea and a line to the
+ * actual location.
+ * @extends {AnnotationView}
+ * @constructor
+ */
+ function CommentBoxAnnotationView(viewport, annotation) {
+ this.viewport_ = viewport;
+ this.annotation_ = annotation;
+ this.textArea_ = undefined;
+
+ this.styleWidth = 250;
+ this.styleHeight = 50;
+ this.fontSize = 10;
+ this.rightOffset = 50;
+ this.topOffset = 25;
+ }
+
+ CommentBoxAnnotationView.prototype = {
+ __proto__: tr.ui.annotations.AnnotationView.prototype,
+
+ removeTextArea() {
+ Polymer.dom(Polymer.dom(this.textArea_).parentNode).removeChild(
+ this.textArea_);
+ },
+
+ draw(ctx) {
+ const coords = this.annotation_.location.toViewCoordinates(
+ this.viewport_);
+ if (coords.viewX < 0) {
+ if (this.textArea_) {
+ this.textArea_.style.visibility = 'hidden';
+ }
+ return;
+ }
+
+ // Set up textarea element.
+ if (!this.textArea_) {
+ this.textArea_ = document.createElement('textarea');
+ this.textArea_.style.position = 'absolute';
+ this.textArea_.readOnly = true;
+ this.textArea_.value = this.annotation_.text;
+ // Set the z-index so that this is shown on top of canvas.
+ this.textArea_.style.zIndex = 1;
+ Polymer.dom(Polymer.dom(ctx.canvas).parentNode)
+ .appendChild(this.textArea_);
+ }
+
+ this.textArea_.style.width = this.styleWidth + 'px';
+ this.textArea_.style.height = this.styleHeight + 'px';
+ this.textArea_.style.fontSize = this.fontSize + 'px';
+ this.textArea_.style.visibility = 'visible';
+
+ // Update positions to latest coordinate.
+ this.textArea_.style.left =
+ coords.viewX + ctx.canvas.getBoundingClientRect().left +
+ this.rightOffset + 'px';
+ this.textArea_.style.top =
+ coords.viewY - ctx.canvas.getBoundingClientRect().top -
+ this.topOffset + 'px';
+
+ // Draw pointer line from offset to actual location.
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, coords.viewX,
+ coords.viewY - ctx.canvas.getBoundingClientRect().top,
+ coords.viewX + this.rightOffset,
+ coords.viewY - this.topOffset -
+ ctx.canvas.getBoundingClientRect().top);
+ ctx.stroke();
+ }
+ };
+
+ return {
+ CommentBoxAnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html
new file mode 100644
index 00000000000..16e5f920eea
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/annotations/annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A view responsible for drawing a single highlight rectangle box on
+ * the timeline.
+ * @extends {AnnotationView}
+ * @constructor
+ */
+ function RectAnnotationView(viewport, annotation) {
+ this.viewport_ = viewport;
+ this.annotation_ = annotation;
+ }
+
+ RectAnnotationView.prototype = {
+ __proto__: tr.ui.annotations.AnnotationView.prototype,
+
+ draw(ctx) {
+ const dt = this.viewport_.currentDisplayTransform;
+ const startCoords =
+ this.annotation_.startLocation.toViewCoordinates(this.viewport_);
+ const endCoords =
+ this.annotation_.endLocation.toViewCoordinates(this.viewport_);
+
+ // Prevent drawing into the ruler track by clamping the initial Y
+ // point and the rect's Y size.
+ let startY = startCoords.viewY - ctx.canvas.getBoundingClientRect().top;
+ const sizeY = endCoords.viewY - startCoords.viewY;
+ if (startY + sizeY < 0) {
+ // In this case sizeY is negative. If final Y is negative,
+ // overwrite startY so that the rectangle ends at y=0.
+ startY = sizeY;
+ } else if (startY < 0) {
+ startY = 0;
+ }
+
+ ctx.fillStyle = this.annotation_.fillStyle;
+ ctx.fillRect(startCoords.viewX, startY,
+ endCoords.viewX - startCoords.viewX, sizeY);
+ }
+ };
+
+ return {
+ RectAnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html
new file mode 100644
index 00000000000..933ea17ca57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/annotations/annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A view that draws a vertical line on the timeline at a specific timestamp.
+ * @extends {AnnotationView}
+ * @constructor
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html
new file mode 100644
index 00000000000..46c62818fc3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Represents a procedural animation that can be run by an
+ * tr.ui.b.AnimationController.
+ *
+ * @constructor
+ */
+ function Animation() {
+ }
+
+ Animation.prototype = {
+
+ /**
+ * Called when an animation has been queued after a running animation.
+ *
+ * @return {boolean} True if the animation can take on the responsibilities
+ * of the running animation. If true, takeOverFor will be called on the
+ * animation.
+ *
+ * This can be used to build animations that accelerate as pairs of them are
+ * queued.
+ */
+ canTakeOverFor(existingAnimation) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Called to take over responsiblities of an existingAnimation.
+ *
+ * At this point, the existingAnimation has been ticked one last time, then
+ * stopped. This animation will be started after this returns and has the
+ * job of finishing(or transitioning away from) the effect the existing
+ * animation was trying to accomplish.
+ */
+ takeOverFor(existingAnimation, newStartTimestamp, target) {
+ throw new Error('Not implemented');
+ },
+
+ start(timestamp, target) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Called when an animation is stopped before it finishes. The animation can
+ * do what it wants here, usually nothing.
+ *
+ * @param {Number} timestamp When the animation was stopped.
+ * @param {Object} target The object being animated. May be undefined, take
+ * care.
+ * @param {boolean} willBeTakenOverByAnotherAnimation Whether this animation
+ * is going to be handed to another animation's takeOverFor function.
+ */
+ didStopEarly(timestamp, target,
+ willBeTakenOverByAnotherAnimation) {
+ },
+
+ /**
+ * @return {boolean} true if the animation is finished.
+ */
+ tick(timestamp, target) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ Animation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html
new file mode 100644
index 00000000000..a6149cba5fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/animation.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Manages execution, queueing and blending of tr.ui.b.Animations against
+ * a single target.
+ *
+ * Targets must have a cloneAnimationState() method that returns all the
+ * animatable states of that target.
+ *
+ * @constructor
+ * @extends {tr.b.EventTarget}
+ */
+ 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.');
+ }
+ if (target.cloneAnimationState === undefined ||
+ typeof target.cloneAnimationState !== 'function') {
+ throw new Error('target must have a cloneAnimationState function');
+ }
+
+ this.target_ = target;
+ },
+
+ get activeAnimation() {
+ return this.activeAnimation_;
+ },
+
+ get hasActiveAnimation() {
+ return !!this.activeAnimation_;
+ },
+
+ queueAnimation(animation, opt_now) {
+ if (this.target_ === undefined) {
+ throw new Error('Cannot queue animations without a target');
+ }
+
+ let now;
+ if (opt_now !== undefined) {
+ now = opt_now;
+ } else {
+ now = window.performance.now();
+ }
+
+ if (this.activeAnimation_) {
+ // Must tick the animation before stopping it case its about to stop,
+ // and to update the target with its final sets of edits up to this
+ // point.
+ const done = this.activeAnimation_.tick(now, this.target_);
+ if (done) {
+ this.activeAnimation_ = undefined;
+ }
+ }
+
+ if (this.activeAnimation_) {
+ if (animation.canTakeOverFor(this.activeAnimation_)) {
+ this.activeAnimation_.didStopEarly(now, this.target_, true);
+ animation.takeOverFor(this.activeAnimation_, now, this.target_);
+ } else {
+ this.activeAnimation_.didStopEarly(now, this.target_, false);
+ }
+ }
+ this.activeAnimation_ = animation;
+ this.activeAnimation_.start(now, this.target_);
+
+ if (this.tickScheduled_) return;
+ this.tickScheduled_ = true;
+ tr.b.requestAnimationFrame(this.tickActiveAnimation_, this);
+ },
+
+ cancelActiveAnimation(opt_now) {
+ if (!this.activeAnimation_) return;
+ let now;
+ if (opt_now !== undefined) {
+ now = opt_now;
+ } else {
+ now = window.performance.now();
+ }
+ this.activeAnimation_.didStopEarly(now, this.target_, false);
+ this.activeAnimation_ = undefined;
+ },
+
+ tickActiveAnimation_(frameBeginTime) {
+ this.tickScheduled_ = false;
+ if (!this.activeAnimation_) return;
+
+ if (this.target_ === undefined) {
+ this.activeAnimation_.didStopEarly(frameBeginTime, this.target_, false);
+ return;
+ }
+
+ const oldTargetState = this.target_.cloneAnimationState();
+
+ const done = this.activeAnimation_.tick(frameBeginTime, this.target_);
+ if (done) {
+ this.activeAnimation_ = undefined;
+ }
+
+ if (this.activeAnimation_) {
+ this.tickScheduled_ = true;
+ tr.b.requestAnimationFrame(this.tickActiveAnimation_, this);
+ }
+
+ if (oldTargetState) {
+ const e = new tr.b.Event('didtick');
+ e.oldTargetState = oldTargetState;
+ this.dispatchEvent(e, false, false);
+ }
+ }
+ };
+
+ return {
+ AnimationController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html
new file mode 100644
index 00000000000..9366ab4db2b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/animation_controller.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function SimpleAnimation(options) {
+ this.stopTime = options.stopTime;
+
+ this.startCalled = false;
+ this.didStopEarlyCalled = false;
+ this.wasTakenOver = false;
+ this.tickCount = 0;
+ }
+
+ SimpleAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+ canTakeOverFor(existingAnimation) {
+ return false;
+ },
+
+ takeOverFor(existingAnimation, newStartTimestamp, target) {
+ throw new Error('Not implemented');
+ },
+
+ start(timestamp, target) {
+ this.startCalled = true;
+ },
+
+ didStopEarly(timestamp, target, willBeTakenOver) {
+ this.didStopEarlyCalled = true;
+ this.wasTakenOver = willBeTakenOver;
+ },
+
+ /**
+ * @return {boolean} true if the animation is finished.
+ */
+ tick(timestamp, target) {
+ this.tickCount++;
+ return timestamp >= this.stopTime;
+ }
+ };
+
+ test('cancel', function() {
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const animation = new SimpleAnimation({stopTime: 100});
+ controller.queueAnimation(animation);
+
+ tr.b.forcePendingRAFTasksToRun(0);
+ assert.strictEqual(animation.tickCount, 1);
+ controller.cancelActiveAnimation();
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.isTrue(animation.didStopEarlyCalled);
+ });
+
+ test('simple', function() {
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const animation = new SimpleAnimation({stopTime: 100});
+ controller.queueAnimation(animation);
+
+ tr.b.forcePendingRAFTasksToRun(0);
+ assert.strictEqual(animation.tickCount, 1);
+ assert.isTrue(controller.hasActiveAnimation);
+
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.strictEqual(animation.tickCount, 2);
+ assert.isFalse(controller.hasActiveAnimation);
+ });
+
+ test('queueTwo', function() {
+ // Clear all pending rafs so if something is lingering it will blow up here.
+ tr.b.forcePendingRAFTasksToRun(0);
+
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const a1 = new SimpleAnimation({stopTime: 100});
+ const a2 = new SimpleAnimation({stopTime: 100});
+ controller.queueAnimation(a1, 0);
+ assert.isTrue(a1.startCalled);
+ controller.queueAnimation(a2, 50);
+ assert.isTrue(a1.didStopEarlyCalled);
+ assert.isTrue(a2.startCalled);
+
+ tr.b.forcePendingRAFTasksToRun(150);
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.isAbove(a2.tickCount, 0);
+ });
+
+ /**
+ * @constructor
+ */
+ function AnimationThatCanTakeOverForSimpleAnimation() {
+ this.takeOverForAnimation = undefined;
+ }
+
+ AnimationThatCanTakeOverForSimpleAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+
+ canTakeOverFor(existingAnimation) {
+ return existingAnimation instanceof SimpleAnimation;
+ },
+
+ takeOverFor(existingAnimation, newStartTimestamp, target) {
+ this.takeOverForAnimation = existingAnimation;
+ },
+
+ start(timestamp, target) {
+ this.startCalled = true;
+ }
+ };
+
+ test('takeOver', function() {
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const a1 = new SimpleAnimation({stopTime: 100});
+ const a2 = new AnimationThatCanTakeOverForSimpleAnimation();
+ controller.queueAnimation(a1, 0);
+ assert.isTrue(a1.startCalled);
+ assert.strictEqual(a1.tickCount, 0);
+ controller.queueAnimation(a2, 10);
+ assert.isTrue(a1.didStopEarlyCalled);
+ assert.isTrue(a1.wasTakenOver);
+ assert.strictEqual(a1.tickCount, 1);
+
+ assert.strictEqual(a1, a2.takeOverForAnimation);
+ assert.isTrue(a2.startCalled);
+
+ controller.cancelActiveAnimation();
+ assert.isFalse(controller.hasActiveAnimation);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html
new file mode 100644
index 00000000000..1745861cbe4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+
+<script>
+'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.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));
+ 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) {
+ // Flip |rect| around |y=x|.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html
new file mode 100644
index 00000000000..48e5ff778aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/ui/base/bar_chart.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 100},
+ {x: 20, value: 110},
+ {x: 30, value: 100},
+ {x: 40, value: 50}
+ ];
+ });
+
+ test('instantiation_singleDatum', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 100},
+ ];
+ });
+
+ test('instantiation_stacked', function() {
+ const chart = new tr.ui.b.BarChart();
+ chart.isStacked = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, foo: 10, bar: 5, qux: 7},
+ {x: 20, foo: 11, bar: 6, qux: 3},
+ {x: 30, foo: 10, bar: 4, qux: 8},
+ {x: 40, foo: 5, bar: 1, qux: 2}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.BarChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+ chart.brushedRange = tr.b.math.Range.fromExplicitRange(20, 40);
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: undefined},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 25, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: 40},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 20, value: 60},
+ {x: 30, value: 80},
+ {x: 40, value: 20},
+ {x: 50, value: 30},
+ {x: 60, value: 20},
+ {x: 70, value: 15},
+ {x: 80, value: 20}
+ ];
+
+ let mouseDownX = undefined;
+ let curMouseX = undefined;
+
+ function updateBrushedRange() {
+ if (mouseDownX === undefined || (mouseDownX === curMouseX)) {
+ chart.brushedRange = new tr.b.math.Range();
+ return;
+ }
+ const r = new tr.b.math.Range();
+ r.min = Math.min(mouseDownX, curMouseX);
+ r.max = Math.max(mouseDownX, curMouseX);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownX = e.x;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ });
+
+ test('instantiation_overrideDataRange', function() {
+ let chart = new tr.ui.b.BarChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: -20},
+ {x: 1, value: 100},
+ {x: 2, value: -40},
+ {x: 3, value: 100},
+ ];
+
+ chart = new tr.ui.b.BarChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 50},
+ ];
+ });
+
+ test('instantiation_Waterfall', function() {
+ const chart = new tr.ui.b.BarChart();
+ chart.graphWidth = 300;
+ chart.graphHeight = 200;
+ chart.isStacked = true;
+ chart.isGrouped = true;
+ chart.isWaterfall = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, alpha: 40, group: 'group1' },
+ {x: 1, alpha: 30, group: 'group2' },
+ {x: 2},
+ {x: 3, hide: 40, beta: 55, group: 'group1' },
+ {x: 4, hide: 40, beta: 65, group: 'group2' },
+ {x: 5},
+ {x: 6, hide: 95, omega: 10, group: 'group1' },
+ {x: 7, hide: 95, omega: 20, group: 'group2' }
+ ];
+ });
+
+ test('instantiation_showHoverValuesForTransparentData', function() {
+ const chart = new tr.ui.b.BarChart();
+ chart.graphWidth = 300;
+ chart.graphHeight = 200;
+ chart.isStacked = true;
+ chart.isGrouped = true;
+ chart.displayXInHover = true;
+ chart.getDataSeries('alpha').color = 'transparent';
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, alpha: 40, beta: 32, omega: 13, group: 'group1' },
+ {x: 1, alpha: 30, beta: 22, omega: 14, group: 'group2' },
+ {x: 2},
+ {x: 3, alpha: 55, beta: 35, omega: 15, group: 'group1' },
+ {x: 4, alpha: 45, beta: 40, omega: 16, group: 'group2' },
+ {x: 5},
+ {x: 6, alpha: 50, beta: 10, omega: 17, group: 'group1' },
+ {x: 7, alpha: 60, beta: 15, omega: 18, group: 'group2' }
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/base.html b/chromium/third_party/catapult/tracing/tracing/ui/base/base.html
new file mode 100644
index 00000000000..e0ca9e23c6b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/base.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/polymer_preload.html" data-suppress-import-order>
+
+<!--
+Polymer is imported through third-party HTML files, which means that we have to
+manually list all recursive imports.
+-->
+<link rel="import" href="/components/polymer/polymer-micro.html" data-suppress-import-order>
+<link rel="import" href="/components/polymer/polymer-mini.html" data-suppress-import-order>
+<link rel="import" href="/components/polymer/polymer.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/ui/base/polymer_postload.html" data-suppress-import-order>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html
new file mode 100644
index 00000000000..9138d1ea8b0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const BoxChart = tr.ui.b.define('box-chart', tr.ui.b.NameLineChart);
+
+ BoxChart.prototype = {
+ __proto__: tr.ui.b.NameLineChart.prototype,
+
+ get hideLegend() {
+ return true;
+ },
+
+ 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);
+ }
+ },
+
+ 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';
+
+ // Draw a box between percentiles 25 and 75:
+ 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();
+
+ // Draw a horizontal line for percentile_50:
+ 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();
+
+ // Draw two shorter horizontal lines for percentiles 0 and 100:
+ 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();
+
+ // Draw a vertical line between percentiles 0 and 100.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html
new file mode 100644
index 00000000000..da2c7665ace
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/box_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.BoxChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {
+ x: 'a'.repeat(15) + 'A',
+ percentile_0: 30,
+ percentile_25: 60,
+ percentile_50: 110,
+ percentile_75: 160,
+ percentile_100: 210,
+ },
+ {
+ x: 'b'.repeat(10) + 'B',
+ percentile_0: 0,
+ percentile_25: 50,
+ percentile_50: 100,
+ percentile_75: 150,
+ percentile_100: 200,
+ },
+ {
+ x: 'c'.repeat(5) + 'C',
+ percentile_0: 100,
+ percentile_25: 150,
+ percentile_50: 200,
+ percentile_75: 250,
+ percentile_100: 300,
+ },
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html b/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html
new file mode 100644
index 00000000000..540f8e1c7ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html
@@ -0,0 +1,350 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const deg2rad = tr.b.math.deg2rad;
+
+ const constants = {
+ DEFAULT_SCALE: 0.5,
+ DEFAULT_EYE_DISTANCE: 10000,
+ MINIMUM_DISTANCE: 1000,
+ MAXIMUM_DISTANCE: 100000,
+ FOV: 15,
+ RESCALE_TIMEOUT_MS: 200,
+ MAXIMUM_TILT: 80,
+ SETTINGS_NAMESPACE: 'tr.ui_camera'
+ };
+
+ const Camera = tr.ui.b.define('camera');
+
+ Camera.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ decorate(eventSource) {
+ this.eventSource_ = eventSource;
+
+ this.eventSource_.addEventListener('beginpan',
+ this.onPanBegin_.bind(this));
+ this.eventSource_.addEventListener('updatepan',
+ this.onPanUpdate_.bind(this));
+ this.eventSource_.addEventListener('endpan',
+ this.onPanEnd_.bind(this));
+
+ this.eventSource_.addEventListener('beginzoom',
+ this.onZoomBegin_.bind(this));
+ this.eventSource_.addEventListener('updatezoom',
+ this.onZoomUpdate_.bind(this));
+ this.eventSource_.addEventListener('endzoom',
+ this.onZoomEnd_.bind(this));
+
+ this.eventSource_.addEventListener('beginrotate',
+ this.onRotateBegin_.bind(this));
+ this.eventSource_.addEventListener('updaterotate',
+ this.onRotateUpdate_.bind(this));
+ this.eventSource_.addEventListener('endrotate',
+ this.onRotateEnd_.bind(this));
+
+ this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE];
+ this.gazeTarget_ = [0, 0, 0];
+ this.rotation_ = [0, 0];
+
+ this.pixelRatio_ = window.devicePixelRatio || 1;
+ },
+
+
+ get modelViewMatrix() {
+ const mvMatrix = mat4.create();
+
+ mat4.lookAt(mvMatrix, this.eye_, this.gazeTarget_, [0, 1, 0]);
+ return mvMatrix;
+ },
+
+ get projectionMatrix() {
+ const rect =
+ tr.ui.b.windowRectForElement(this.canvas_).
+ scaleSize(this.pixelRatio_);
+
+ const aspectRatio = rect.width / rect.height;
+ const matrix = mat4.create();
+ mat4.perspective(
+ matrix, deg2rad(constants.FOV), aspectRatio, 1, 100000);
+
+ return matrix;
+ },
+
+ set canvas(c) {
+ this.canvas_ = c;
+ },
+
+ set deviceRect(rect) {
+ this.deviceRect_ = rect;
+ },
+
+ get stackingDistanceDampening() {
+ const gazeVector = [
+ this.gazeTarget_[0] - this.eye_[0],
+ this.gazeTarget_[1] - this.eye_[1],
+ this.gazeTarget_[2] - this.eye_[2]];
+ vec3.normalize(gazeVector, gazeVector);
+ return 1 + gazeVector[2];
+ },
+
+ loadCameraFromSettings(settings) {
+ this.eye_ = settings.get(
+ 'eye', this.eye_, constants.SETTINGS_NAMESPACE);
+ this.gazeTarget_ = settings.get(
+ 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE);
+ this.rotation_ = settings.get(
+ 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE);
+
+ this.dispatchRenderEvent_();
+ },
+
+ saveCameraToSettings(settings) {
+ settings.set(
+ 'eye', this.eye_, constants.SETTINGS_NAMESPACE);
+ settings.set(
+ 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE);
+ settings.set(
+ 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE);
+ },
+
+ resetCamera() {
+ this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE];
+ this.gazeTarget_ = [0, 0, 0];
+ this.rotation_ = [0, 0];
+
+ const settings = tr.b.SessionSettings();
+ const keys = settings.keys(constants.SETTINGS_NAMESPACE);
+ if (keys.length !== 0) {
+ this.loadCameraFromSettings(settings);
+ return;
+ }
+
+ if (this.deviceRect_) {
+ const rect = tr.ui.b.windowRectForElement(this.canvas_).
+ scaleSize(this.pixelRatio_);
+
+ this.eye_[0] = this.deviceRect_.width / 2;
+ this.eye_[1] = this.deviceRect_.height / 2;
+
+ this.gazeTarget_[0] = this.deviceRect_.width / 2;
+ this.gazeTarget_[1] = this.deviceRect_.height / 2;
+ }
+
+ this.saveCameraToSettings(settings);
+ this.dispatchRenderEvent_();
+ },
+
+ updatePanByDelta(delta) {
+ const rect =
+ tr.ui.b.windowRectForElement(this.canvas_).
+ scaleSize(this.pixelRatio_);
+
+ // Get the eye vector, since we'll be adjusting gazeTarget.
+ const eyeVector = [
+ this.eye_[0] - this.gazeTarget_[0],
+ this.eye_[1] - this.gazeTarget_[1],
+ this.eye_[2] - this.gazeTarget_[2]];
+ const length = vec3.length(eyeVector);
+ vec3.normalize(eyeVector, eyeVector);
+
+ const halfFov = constants.FOV / 2;
+ const multiplier =
+ 2.0 * length * Math.tan(deg2rad(halfFov)) / rect.height;
+
+ // Get the up and right vectors.
+ const up = [0, 1, 0];
+ const rotMatrix = mat4.create();
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[1]), [0, 1, 0]);
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[0]), [1, 0, 0]);
+ vec3.transformMat4(up, up, rotMatrix);
+
+ const right = [0, 0, 0];
+ vec3.cross(right, eyeVector, up);
+ vec3.normalize(right, right);
+
+ // Update the gaze target.
+ for (let i = 0; i < 3; ++i) {
+ this.gazeTarget_[i] +=
+ delta[0] * multiplier * right[i] - delta[1] * multiplier * up[i];
+
+ this.eye_[i] = this.gazeTarget_[i] + length * eyeVector[i];
+ }
+
+ // If we have some z offset, we need to reposition gazeTarget
+ // to be on the plane z = 0 with normal [0, 0, 1].
+ if (Math.abs(this.gazeTarget_[2]) > 1e-6) {
+ const gazeVector = [-eyeVector[0], -eyeVector[1], -eyeVector[2]];
+ const newLength = tr.b.math.clamp(
+ -this.eye_[2] / gazeVector[2],
+ constants.MINIMUM_DISTANCE,
+ constants.MAXIMUM_DISTANCE);
+
+ for (let i = 0; i < 3; ++i) {
+ this.gazeTarget_[i] = this.eye_[i] + newLength * gazeVector[i];
+ }
+ }
+
+ this.saveCameraToSettings(tr.b.SessionSettings());
+ this.dispatchRenderEvent_();
+ },
+
+ updateZoomByDelta(delta) {
+ let deltaY = delta[1];
+ deltaY = tr.b.math.clamp(deltaY, -50, 50);
+ let scale = 1.0 - deltaY / 100.0;
+
+ const eyeVector = [0, 0, 0];
+ vec3.subtract(eyeVector, this.eye_, this.gazeTarget_);
+
+ const length = vec3.length(eyeVector);
+
+ // Clamp the length to allowed values by changing the scale.
+ if (length * scale < constants.MINIMUM_DISTANCE) {
+ scale = constants.MINIMUM_DISTANCE / length;
+ } else if (length * scale > constants.MAXIMUM_DISTANCE) {
+ scale = constants.MAXIMUM_DISTANCE / length;
+ }
+
+ vec3.scale(eyeVector, eyeVector, scale);
+ vec3.add(this.eye_, this.gazeTarget_, eyeVector);
+
+ this.saveCameraToSettings(tr.b.SessionSettings());
+ this.dispatchRenderEvent_();
+ },
+
+ updateRotateByDelta(delta) {
+ delta[0] *= 0.5;
+ delta[1] *= 0.5;
+
+ if (Math.abs(this.rotation_[0] + delta[1]) > constants.MAXIMUM_TILT) {
+ return;
+ }
+ if (Math.abs(this.rotation_[1] - delta[0]) > constants.MAXIMUM_TILT) {
+ return;
+ }
+
+ const eyeVector = [0, 0, 0, 0];
+ vec3.subtract(eyeVector, this.eye_, this.gazeTarget_);
+
+ // Undo the current rotation.
+ const rotMatrix = mat4.create();
+ mat4.rotate(
+ rotMatrix, rotMatrix, -deg2rad(this.rotation_[0]), [1, 0, 0]);
+ mat4.rotate(
+ rotMatrix, rotMatrix, -deg2rad(this.rotation_[1]), [0, 1, 0]);
+ vec4.transformMat4(eyeVector, eyeVector, rotMatrix);
+
+ // Update rotation values.
+ this.rotation_[0] += delta[1];
+ this.rotation_[1] -= delta[0];
+
+ // Redo the new rotation.
+ mat4.identity(rotMatrix);
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[1]), [0, 1, 0]);
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[0]), [1, 0, 0]);
+ vec4.transformMat4(eyeVector, eyeVector, rotMatrix);
+
+ vec3.add(this.eye_, this.gazeTarget_, eyeVector);
+
+ this.saveCameraToSettings(tr.b.SessionSettings());
+ this.dispatchRenderEvent_();
+ },
+
+
+ // Event callbacks.
+ onPanBegin_(e) {
+ this.panning_ = true;
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ },
+
+ onPanUpdate_(e) {
+ if (!this.panning_) return;
+
+ const delta = this.getMouseDelta_(e, this.lastMousePosition_);
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ this.updatePanByDelta(delta);
+ },
+
+ onPanEnd_(e) {
+ this.panning_ = false;
+ },
+
+ onZoomBegin_(e) {
+ this.zooming_ = true;
+
+ const p = this.getMousePosition_(e);
+
+ this.lastMousePosition_ = p;
+ this.zoomPoint_ = p;
+ },
+
+ onZoomUpdate_(e) {
+ if (!this.zooming_) return;
+
+ const delta = this.getMouseDelta_(e, this.lastMousePosition_);
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ this.updateZoomByDelta(delta);
+ },
+
+ onZoomEnd_(e) {
+ this.zooming_ = false;
+ this.zoomPoint_ = undefined;
+ },
+
+ onRotateBegin_(e) {
+ this.rotating_ = true;
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ },
+
+ onRotateUpdate_(e) {
+ if (!this.rotating_) return;
+
+ const delta = this.getMouseDelta_(e, this.lastMousePosition_);
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ this.updateRotateByDelta(delta);
+ },
+
+ onRotateEnd_(e) {
+ this.rotating_ = false;
+ },
+
+
+ // Misc helper functions.
+ getMousePosition_(e) {
+ const rect = tr.ui.b.windowRectForElement(this.canvas_);
+ return [(e.clientX - rect.x) * this.pixelRatio_,
+ (e.clientY - rect.y) * this.pixelRatio_];
+ },
+
+ getMouseDelta_(e, p) {
+ const newP = this.getMousePosition_(e);
+ return [newP[0] - p[0], newP[1] - p[1]];
+ },
+
+ dispatchRenderEvent_() {
+ tr.b.dispatchSimpleEvent(this, 'renderrequired', false, false);
+ }
+ };
+
+ return {
+ Camera,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html
new file mode 100644
index 00000000000..7083856c539
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/math/bbox2.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/ui/base/quad_stack_view.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createQuads() {
+ const quads = [
+ tr.b.math.Quad.fromXYWH(-500, -500, 30, 30), // 4 corners
+ tr.b.math.Quad.fromXYWH(-500, 470, 30, 30),
+ tr.b.math.Quad.fromXYWH(470, -500, 30, 30),
+ tr.b.math.Quad.fromXYWH(470, 470, 30, 30),
+ tr.b.math.Quad.fromXYWH(-250, -250, 250, 250), // crosshairs
+ tr.b.math.Quad.fromXYWH(0, -250, 250, 250), // crosshairs
+ tr.b.math.Quad.fromXYWH(-250, 0, 250, 250), // crosshairs
+ tr.b.math.Quad.fromXYWH(0, 0, 250, 250) // crosshairs
+ ];
+ quads[0].stackingGroupId = 0;
+ quads[1].stackingGroupId = 0;
+ quads[2].stackingGroupId = 0;
+ quads[3].stackingGroupId = 0;
+ quads[4].stackingGroupId = 1;
+ quads[5].stackingGroupId = 1;
+ quads[6].stackingGroupId = 1;
+ quads[7].stackingGroupId = 1;
+ return quads;
+ }
+
+ function createQuadStackView(testFramework) {
+ const quads = createQuads();
+ const view = new tr.ui.b.QuadStackView();
+ // simulate the constraints of the layer-tree-view
+ view.style.height = '400px';
+ view.style.width = '800px';
+ view.deviceRect = tr.b.math.Rect.fromXYWH(-250, -250, 500, 500);
+ view.quads = quads;
+
+ testFramework.addHTMLOutput(view);
+ return view;
+ }
+
+ test('initialState', function() {
+ const view = createQuadStackView(this);
+
+ const viewRect =
+ view.getBoundingClientRect();
+ assert.strictEqual(viewRect.height, 400);
+ assert.strictEqual(viewRect.width, 800);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html
new file mode 100644
index 00000000000..3d1359fe61b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html
@@ -0,0 +1,453 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/chart_legend_key.html">
+<link rel="import" href="/tracing/ui/base/d3.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<template id="chart-base-template">
+ <svg> <!-- svg tag is dropped by ChartBase.decorate. -->
+ <g xmlns="http://www.w3.org/2000/svg" id="chart-area">
+ <g class="x axis"></g>
+ <g class="y axis"></g>
+ <text id="title"></text>
+ </g>
+ </svg>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const DataSeriesEnableChangeEventType = 'data-series-enabled-change';
+
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const svgNS = 'http://www.w3.org/2000/svg';
+ const ColorScheme = tr.b.ColorScheme;
+
+ function getColorOfKey(key, selected) {
+ let id = ColorScheme.getColorIdForGeneralPurposeString(key);
+ if (selected) {
+ id += ColorScheme.properties.brightenedOffsets[0];
+ }
+ return ColorScheme.colorsAsStrings[id];
+ }
+
+ /**
+ * Returns width and height of SVG text node.
+ *
+ * @param {!Element} parentNode
+ * @param {string} text
+ * @param {function(!Element)=} opt_callback
+ * @param {*=} opt_this
+ * @returns {!Object}
+ */
+ function getSVGTextSize(parentNode, text, opt_callback, opt_this) {
+ const textNode = document.createElementNS(
+ 'http://www.w3.org/2000/svg', 'text');
+ textNode.setAttributeNS(null, 'x', 0);
+ textNode.setAttributeNS(null, 'y', 0);
+ textNode.setAttributeNS(null, 'fill', 'black');
+ textNode.appendChild(document.createTextNode(text));
+ parentNode.appendChild(textNode);
+ if (opt_callback) {
+ opt_callback.call(opt_this || parentNode, textNode);
+ }
+ const width = textNode.getComputedTextLength();
+ const height = textNode.getBBox().height;
+ parentNode.removeChild(textNode);
+ return {width, height};
+ }
+
+ function DataSeries(key) {
+ this.key_ = key;
+ this.target_ = undefined;
+ this.title_ = '';
+ this.optional_ = false;
+ this.enabled_ = true;
+ this.color_ = getColorOfKey(key, false);
+ this.highlightedColor_ = getColorOfKey(key, true);
+ }
+
+ DataSeries.prototype = {
+ get key() {
+ return this.key_;
+ },
+
+ get title() {
+ return this.title_;
+ },
+
+ set title(t) {
+ this.title_ = t;
+ },
+
+ get color() {
+ return this.color_;
+ },
+
+ set color(c) {
+ this.color_ = c;
+ },
+
+ get highlightedColor() {
+ return this.highlightedColor_;
+ },
+
+ set highlightedColor(c) {
+ this.highlightedColor_ = c;
+ },
+
+ get optional() {
+ return this.optional_;
+ },
+
+ set optional(optional) {
+ this.optional_ = optional;
+ },
+
+ get enabled() {
+ return this.enabled_;
+ },
+
+ set enabled(enabled) {
+ // If the caller is disabling a data series, but it wasn't optional, then
+ // force it to be optional.
+ if (!this.optional && !enabled) {
+ this.optional = true;
+ }
+ this.enabled_ = enabled;
+ },
+
+ get target() {
+ return this.target_;
+ },
+
+ set target(t) {
+ this.target_ = t;
+ }
+ };
+
+ /**
+ * A virtual base class for basic charts that provides basic chart
+ * infrastructure such as a title and legend.
+ *
+ * Generally, setting a field on a chart instance will cause it to update its
+ * contents, which assumes that the chart is attached to a document, so
+ * callers should create the chart and immediately attach it to a document
+ * before configuring it. Embedders that are polymer dom-modules can use the
+ * attached() callback to wait to configure the chart until they are attached
+ * to a document.
+ *
+ * TODO(#3058) Use a class for Polymer 2.0.
+ *
+ * @constructor
+ */
+ const ChartBase = tr.ui.b.define('svg', undefined, svgNS);
+
+ ChartBase.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ getDataSeries(key) {
+ if (!this.seriesByKey_.has(key)) {
+ this.seriesByKey_.set(key, new DataSeries(key));
+ }
+ return this.seriesByKey_.get(key);
+ },
+
+ decorate() {
+ Polymer.dom(this).classList.add('chart-base');
+ this.setAttribute('style', 'cursor: default; user-select: none;');
+ this.chartTitle_ = undefined;
+ this.seriesByKey_ = new Map();
+ this.graphWidth_ = undefined;
+ this.graphHeight_ = undefined;
+ this.margin = {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ };
+ this.hideLegend_ = false;
+ this.showTitleInLegend_ = false;
+ this.titleHeight_ = '16pt';
+
+ // This should use tr.ui.b.instantiateTemplate. However, creating
+ // svg-namespaced elements inside a template isn't possible. Thus, this
+ // hack.
+ const template =
+ Polymer.dom(THIS_DOC).querySelector('#chart-base-template');
+ const svgEl = Polymer.dom(template.content).querySelector('svg');
+ for (let i = 0; i < Polymer.dom(svgEl).children.length; i++) {
+ Polymer.dom(this).appendChild(
+ Polymer.dom(svgEl.children[i]).cloneNode(true));
+ }
+
+ 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 !== '').
+ 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');
+ }
+ this.tempHighlightedLegendKey_ = undefined;
+ this.updateHighlight_();
+ },
+
+ updateHighlight_() {
+ // Update label colors.
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ const legendEntriesSel = chartAreaSel.selectAll('.legend');
+ const getDataSeries = chart.getDataSeries.bind(chart);
+ const currentHighlightedLegendKey = chart.currentHighlightedLegendKey;
+ legendEntriesSel.each(function(key) {
+ // NOTE: this = legendEntry
+ const dataSeries = getDataSeries(key);
+ if (key === currentHighlightedLegendKey) {
+ this.style.fill = dataSeries.highlightedColor;
+ this.style.fontWeight = 'bold';
+ } else {
+ this.style.fill = dataSeries.color;
+ this.style.fontWeight = '';
+ }
+ });
+ }
+ };
+
+ return {
+ ChartBase,
+ DataSeriesEnableChangeEventType,
+ getColorOfKey,
+ getSVGTextSize,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html
new file mode 100644
index 00000000000..a17e7b74853
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html
@@ -0,0 +1,571 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/chart_base.html">
+<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ // This does not include the tick labels.
+ const D3_Y_AXIS_WIDTH_PX = 9;
+
+ // This includes the tick labels.
+ const D3_X_AXIS_HEIGHT_PX = 23;
+
+ // For charts with log y-axes, the y-axis tick values may need to be sanitized
+ // if the data is zero or negative.
+ function sanitizePower(x, defaultValue) {
+ if (!isNaN(x) && isFinite(x) && (x !== 0)) return x;
+ return defaultValue;
+ }
+
+ const ChartBase2D = tr.ui.b.define('chart-base-2d', tr.ui.b.ChartBase);
+
+ ChartBase2D.prototype = {
+ __proto__: tr.ui.b.ChartBase.prototype,
+
+ decorate() {
+ super.decorate();
+ Polymer.dom(this).classList.add('chart-base-2d');
+
+ this.xScale_ = d3.scale.linear();
+ this.yScale_ = d3.scale.linear();
+ this.isYLogScale_ = false;
+ this.yLogScaleBase_ = 10;
+ this.yLogScaleMin_ = undefined;
+ this.autoDataRange_ = new tr.b.math.Range();
+ this.overrideDataRange_ = undefined;
+ this.hideXAxis_ = false;
+ this.hideYAxis_ = false;
+ this.data_ = [];
+ this.xAxisLabel_ = '';
+ this.yAxisLabel_ = '';
+ this.textHeightPx_ = 0;
+ this.unit_ = undefined;
+
+ d3.select(this.chartAreaElement)
+ .append('g')
+ .attr('id', 'brushes');
+ d3.select(this.chartAreaElement)
+ .append('g')
+ .attr('id', 'series');
+
+ this.addEventListener('mousedown', this.onMouseDown_.bind(this));
+ },
+
+ get yLogScaleBase() {
+ return this.yLogScaleBase_;
+ },
+
+ set yLogScaleBase(b) {
+ this.yLogScaleBase_ = b;
+ },
+
+ get unit() {
+ return this.unit_;
+ },
+
+ set unit(unit) {
+ this.unit_ = unit;
+ this.updateContents_();
+ },
+
+ get xAxisLabel() {
+ return this.xAxisLabel_;
+ },
+
+ set xAxisLabel(label) {
+ this.xAxisLabel_ = label;
+ },
+
+ get yAxisLabel() {
+ return this.yAxisLabel_;
+ },
+
+ set yAxisLabel(label) {
+ this.yAxisLabel_ = label;
+ },
+
+ get hideXAxis() {
+ return this.hideXAxis_;
+ },
+
+ set hideXAxis(h) {
+ this.hideXAxis_ = h;
+ this.updateContents_();
+ },
+
+ get hideYAxis() {
+ return this.hideYAxis_;
+ },
+
+ set hideYAxis(h) {
+ this.hideYAxis_ = h;
+ this.updateContents_();
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ /**
+ * Sets the data array for the object
+ *
+ * @param {Array} data The data. Each element must be an object, with at
+ * least an x property. All other properties become series names in the
+ * chart. The data can be sparse (i.e. every x value does not have to
+ * contain data for every series).
+ */
+ set data(data) {
+ if (data === undefined) {
+ throw new Error('data must be an Array');
+ }
+
+ this.data_ = data;
+ this.updateSeriesKeys_();
+ this.updateDataRange_();
+ this.updateContents_();
+ },
+
+ set isYLogScale(logScale) {
+ if (logScale) {
+ this.yScale_ = d3.scale.log().base(this.yLogScaleBase);
+ } else {
+ this.yScale_ = d3.scale.linear();
+ }
+ this.isYLogScale_ = logScale;
+ },
+
+ getYScaleMin_() {
+ return this.isYLogScale_ ? this.yLogScaleMin_ : 0;
+ },
+
+ getYScaleDomain_(minValue, maxValue) {
+ if (this.overrideDataRange_ !== undefined) {
+ return [this.dataRange.min, this.dataRange.max];
+ }
+ if (this.isYLogScale_) {
+ return [this.getYScaleMin_(), maxValue];
+ }
+ return [Math.min(minValue, this.getYScaleMin_()), maxValue];
+ },
+
+ getSampleWidth_(data, index, leftSide) {
+ let leftIndex;
+ let rightIndex;
+ if (leftSide) {
+ leftIndex = Math.max(index - 1, 0);
+ rightIndex = index;
+ } else {
+ leftIndex = index;
+ rightIndex = Math.min(index + 1, data.length - 1);
+ }
+ const leftWidth = this.getXForDatum_(data[index], index) -
+ this.getXForDatum_(data[leftIndex], leftIndex);
+ const rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) -
+ this.getXForDatum_(data[index], index);
+ return tr.b.math.Statistics.mean([leftWidth, rightWidth]);
+ },
+
+ updateSeriesKeys_() {
+ // Don't clear seriesByKey_; the caller might have put state in it using
+ // getDataSeries() before setting data.
+ this.data_.forEach(function(datum) {
+ Object.keys(datum).forEach(function(key) {
+ if (this.isDatumFieldSeries_(key)) {
+ this.getDataSeries(key);
+ }
+ }, this);
+ }, this);
+ },
+
+ isDatumFieldSeries_(fieldName) {
+ return fieldName !== 'x';
+ },
+
+ getXForDatum_(datum, index) {
+ return datum.x;
+ },
+
+ updateMargins_() {
+ this.margin.left = this.hideYAxis ? 0 : this.yAxisWidth;
+ this.margin.bottom = this.hideXAxis ? 0 : this.xAxisHeight;
+
+ if (this.hideXAxis && !this.hideYAxis) {
+ this.margin.bottom = 10;
+ }
+ if (this.hideYAxis && !this.hideXAxis) {
+ this.margin.left = 10;
+ }
+ this.margin.top = this.hideYAxis ? 0 : 10;
+
+ if (this.yAxisLabel) {
+ this.margin.top += this.textHeightPx_;
+ }
+ if (this.xAxisLabel) {
+ this.margin.right = Math.max(this.margin.right,
+ 16 + tr.ui.b.getSVGTextSize(this, this.xAxisLabel).width);
+ }
+
+ super.updateMargins_();
+ },
+
+ get xAxisHeight() {
+ return D3_X_AXIS_HEIGHT_PX;
+ },
+
+ computeScaleTickWidth_(scale) {
+ if (this.data.length === 0) return 0;
+
+ let tickValues = scale.ticks();
+ let tickFormat = scale.tickFormat();
+
+ if (this.isYLogScale_) {
+ const enclosingPowers = this.dataRange.enclosingPowers();
+ tickValues = [];
+ const maxPower = sanitizePower(enclosingPowers.max, this.yLogScaleBase);
+ for (let power = sanitizePower(enclosingPowers.min, 1);
+ power <= maxPower;
+ power *= this.yLogScaleBase) {
+ tickValues.push(power);
+ }
+ tickFormat = v => v.toString();
+ }
+
+ if (this.unit) {
+ tickFormat = v => this.unit.format(v);
+ }
+
+ let maxTickWidth = 0;
+ for (const tickValue of tickValues) {
+ maxTickWidth = Math.max(maxTickWidth,
+ tr.ui.b.getSVGTextSize(this, tickFormat(tickValue)).width);
+ }
+
+ return D3_Y_AXIS_WIDTH_PX + maxTickWidth;
+ },
+
+ get yAxisWidth() {
+ return this.computeScaleTickWidth_(this.yScale_);
+ },
+
+ updateScales_() {
+ if (this.data_.length === 0) return;
+
+ this.xScale_.range([0, this.graphWidth]);
+ this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this)));
+
+ this.yScale_.range([this.graphHeight, 0]);
+ this.yScale_.domain([this.dataRange.min, this.dataRange.max]);
+ },
+
+ updateBrushContents_(brushSel) {
+ brushSel.selectAll('*').remove();
+ },
+
+ updateXAxis_(xAxis) {
+ xAxis.selectAll('*').remove();
+ xAxis[0][0].style.opacity = 0;
+ if (this.hideXAxis) return;
+
+ this.drawXAxis_(xAxis);
+
+ const label = xAxis.append('text').attr('class', 'label');
+ this.drawXAxisTicks_(xAxis);
+ this.drawXAxisLabel_(label);
+ xAxis[0][0].style.opacity = 1;
+ },
+
+ drawXAxis_(xAxis) {
+ xAxis.attr('transform', 'translate(0,' + this.graphHeight + ')')
+ .call(d3.svg.axis()
+ .scale(this.xScale_)
+ .orient('bottom'));
+ },
+
+ drawXAxisLabel_(label) {
+ label
+ .attr('x', this.graphWidth + 16)
+ .attr('y', 8)
+ .text(this.xAxisLabel);
+ },
+
+ drawXAxisTicks_(xAxis) {
+ let previousRight = undefined;
+ xAxis.selectAll('.tick')[0].forEach(function(tick) {
+ const currentLeft = tick.transform.baseVal[0].matrix.e;
+ if ((previousRight === undefined) ||
+ (currentLeft > (previousRight + 3))) {
+ const currentWidth = tick.getBBox().width;
+ previousRight = currentLeft + currentWidth;
+ } else {
+ tick.style.opacity = 0;
+ }
+ });
+ },
+
+ set overrideDataRange(range) {
+ this.overrideDataRange_ = range;
+ },
+
+ get dataRange() {
+ if (this.overrideDataRange_ !== undefined) {
+ return this.overrideDataRange_;
+ }
+ return this.autoDataRange_;
+ },
+
+ updateDataRange_() {
+ if (this.overrideDataRange_ !== undefined) return;
+
+ const dataBySeriesKey = this.getDataBySeriesKey_();
+ this.autoDataRange_.reset();
+ for (const [series, values] of Object.entries(dataBySeriesKey)) {
+ for (let i = 0; i < values.length; i++) {
+ this.autoDataRange_.addValue(values[i][series]);
+ }
+ }
+
+ // Choose the closest power of yLogScaleBase, rounded down, as the
+ // smallest tick to display.
+ this.yLogScaleMin_ = undefined;
+ if (this.autoDataRange_.min !== undefined) {
+ let minValue = this.autoDataRange_.min;
+ if (minValue === 0) {
+ minValue = 1;
+ }
+
+ const onePowerLess = tr.b.math.lesserPower(
+ minValue / this.yLogScaleBase);
+ this.yLogScaleMin_ = onePowerLess;
+ }
+ },
+
+ updateYAxis_(yAxis) {
+ yAxis.selectAll('*').remove();
+ yAxis[0][0].style.opacity = 0;
+ if (this.hideYAxis) return;
+
+ this.drawYAxis_(yAxis);
+ this.drawYAxisTicks_(yAxis);
+
+ const label = yAxis.append('text').attr('class', 'label');
+ this.drawYAxisLabel_(label);
+ },
+
+ drawYAxis_(yAxis) {
+ let axisModifier = d3.svg.axis()
+ .scale(this.yScale_)
+ .orient('left');
+
+ let tickFormat;
+
+ if (this.isYLogScale_) {
+ if (this.yLogScaleMin_ === undefined) return;
+ const tickValues = [];
+ const enclosingPowers = this.dataRange.enclosingPowers();
+ const maxPower = sanitizePower(enclosingPowers.max, this.yLogScaleBase);
+ for (let power = sanitizePower(enclosingPowers.min, 1);
+ power <= maxPower;
+ power *= this.yLogScaleBase) {
+ tickValues.push(power);
+ }
+
+ // The default tickFormat() for log scales always uses scientific
+ // notation. Override it to use Number.toString(), which only uses
+ // scientific notation for extreme values, and uses decimal notation for
+ // a broader range of values. Decimal notation is generally slightly
+ // easier to skim than scientific notation in the context of chart axes.
+ axisModifier = axisModifier.tickValues(tickValues);
+ tickFormat = v => v.toString();
+ }
+
+ if (this.unit) {
+ tickFormat = v => this.unit.format(v);
+ }
+
+ if (tickFormat) {
+ axisModifier = axisModifier.tickFormat(tickFormat);
+ }
+
+ yAxis.call(axisModifier);
+ },
+
+ drawYAxisLabel_(label) {
+ const labelWidthPx = Math.ceil(tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, this.yAxisLabel).width);
+ label
+ .attr('x', -labelWidthPx)
+ .attr('y', -8)
+ .text(this.yAxisLabel);
+ },
+
+ drawYAxisTicks_(yAxis) {
+ let previousTop = undefined;
+ yAxis.selectAll('.tick')[0].forEach(function(tick) {
+ const bbox = tick.getBBox();
+ const currentTop = tick.transform.baseVal[0].matrix.f;
+ const currentBottom = currentTop + bbox.height;
+ if ((previousTop === undefined) ||
+ (previousTop > (currentBottom + 3))) {
+ previousTop = currentTop;
+ } else {
+ tick.style.opacity = 0;
+ }
+ });
+ yAxis[0][0].style.opacity = 1;
+ },
+
+ updateContents_() {
+ if (this.textHeightPx_ === 0) {
+ // Measure the height of a string that is as tall as it can be,
+ // with both an ascender and a descender.
+ // https://en.wikipedia.org/wiki/Ascender_(typography)
+ this.textHeightPx_ = tr.ui.b.getSVGTextSize(this, 'Ay').height;
+ // If the chart is not yet rooted in a document, then the height will be
+ // 0. Callers should make sure that updateContents_ is called at least
+ // once after the chart is rooted in a document so that textHeightPx_
+ // can be computed.
+ }
+
+ this.updateScales_();
+ super.updateContents_();
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ this.updateXAxis_(chartAreaSel.select('.x.axis'));
+ this.updateYAxis_(chartAreaSel.select('.y.axis'));
+ for (const child of this.querySelectorAll('.axis path, .axis line')) {
+ child.style.fill = 'none';
+ child.style.shapeRendering = 'crispEdges';
+ child.style.stroke = 'black';
+ }
+ this.updateBrushContents_(chartAreaSel.select('#brushes'));
+ this.updateDataContents_(chartAreaSel.select('#series'));
+ },
+
+ updateDataContents_(seriesSel) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Returns a map of series key to the data for that series.
+ *
+ * Example:
+ * // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]}
+ * this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}];
+ * this.getDataBySeriesKey_();
+ * @return {Object} A map of series data by series key.
+ */
+ getDataBySeriesKey_() {
+ const dataBySeriesKey = {};
+ for (const [key, series] of this.seriesByKey_) {
+ dataBySeriesKey[key] = [];
+ }
+
+ this.data_.forEach(function(multiSeriesDatum, index) {
+ const x = this.getXForDatum_(multiSeriesDatum, index);
+
+ d3.keys(multiSeriesDatum).forEach(function(seriesKey) {
+ // Skip 'x' - it's not a series
+ if (seriesKey === 'x') return;
+
+ if (multiSeriesDatum[seriesKey] === undefined) return;
+
+ if (!this.isDatumFieldSeries_(seriesKey)) return;
+
+ const singleSeriesDatum = {x};
+ singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey];
+ dataBySeriesKey[seriesKey].push(singleSeriesDatum);
+ }, this);
+ }, this);
+
+ return dataBySeriesKey;
+ },
+
+ getChartPointAtClientPoint_(clientPoint) {
+ const rect = this.getBoundingClientRect();
+ return {
+ x: clientPoint.x - rect.left - this.margin.left,
+ y: clientPoint.y - rect.top - this.margin.top
+ };
+ },
+
+ getDataPointAtChartPoint_(chartPoint) {
+ return {
+ x: tr.b.math.clamp(this.xScale_.invert(chartPoint.x),
+ this.xScale_.domain()[0], this.xScale_.domain()[1]),
+ y: tr.b.math.clamp(this.yScale_.invert(chartPoint.y),
+ this.yScale_.domain()[0], this.yScale_.domain()[1])
+ };
+ },
+
+ getDataPointAtClientPoint_(clientX, clientY) {
+ const chartPoint = this.getChartPointAtClientPoint_(
+ {x: clientX, y: clientY});
+ return this.getDataPointAtChartPoint_(chartPoint);
+ },
+
+ prepareDataEvent_(mouseEvent, dataEvent) {
+ const dataPoint = this.getDataPointAtClientPoint_(
+ mouseEvent.clientX, mouseEvent.clientY);
+ dataEvent.x = dataPoint.x;
+ dataEvent.y = dataPoint.y;
+ },
+
+ onMouseDown_(mouseEvent) {
+ tr.ui.b.trackMouseMovesUntilMouseUp(
+ this.onMouseMove_.bind(this, mouseEvent.button),
+ this.onMouseUp_.bind(this, mouseEvent.button));
+ mouseEvent.preventDefault();
+ mouseEvent.stopPropagation();
+ const dataEvent = new tr.b.Event('item-mousedown');
+ dataEvent.button = mouseEvent.button;
+ this.prepareDataEvent_(mouseEvent, dataEvent);
+ this.dispatchEvent(dataEvent);
+ for (const child of this.querySelector('#brushes').children) {
+ child.setAttribute('fill', 'rgb(103, 199, 165)');
+ }
+ },
+
+ onMouseMove_(button, mouseEvent) {
+ if (mouseEvent.buttons !== undefined) {
+ mouseEvent.preventDefault();
+ mouseEvent.stopPropagation();
+ }
+ const dataEvent = new tr.b.Event('item-mousemove');
+ dataEvent.button = button;
+ this.prepareDataEvent_(mouseEvent, dataEvent);
+ this.dispatchEvent(dataEvent);
+ for (const child of this.querySelector('#brushes').children) {
+ child.setAttribute('fill', 'rgb(103, 199, 165)');
+ }
+ },
+
+ onMouseUp_(button, mouseEvent) {
+ mouseEvent.preventDefault();
+ mouseEvent.stopPropagation();
+ const dataEvent = new tr.b.Event('item-mouseup');
+ dataEvent.button = button;
+ this.prepareDataEvent_(mouseEvent, dataEvent);
+ this.dispatchEvent(dataEvent);
+ for (const child of this.querySelector('#brushes').children) {
+ child.setAttribute('fill', 'rgb(213, 236, 229)');
+ }
+ }
+ };
+
+ return {
+ ChartBase2D,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html
new file mode 100644
index 00000000000..cba736ee811
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/chart_base_2d.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const ChartBase2D = tr.ui.b.ChartBase2D;
+ const ChartBase2DBrushX = tr.ui.b.define(
+ 'chart-base-2d-brush-1d', ChartBase2D);
+
+ ChartBase2DBrushX.prototype = {
+ __proto__: ChartBase2D.prototype,
+
+ decorate() {
+ super.decorate();
+ this.brushedRange_ = new tr.b.math.Range();
+ },
+
+ set brushedRange(range) {
+ this.brushedRange_.reset();
+ this.brushedRange_.addRange(range);
+ this.updateContents_();
+ },
+
+ get brushedRange() {
+ return tr.b.math.Range.fromDict(this.brushedRange_.toJSON());
+ },
+
+ computeBrushRangeFromIndices(indexA, indexB) {
+ indexA = tr.b.math.clamp(indexA, 0, this.data_.length - 1);
+ indexB = tr.b.math.clamp(indexB, 0, this.data_.length - 1);
+ const leftIndex = Math.min(indexA, indexB);
+ const rightIndex = Math.max(indexA, indexB);
+
+ const brushRange = new tr.b.math.Range();
+ brushRange.addValue(
+ this.getXForDatum_(this.data_[leftIndex], leftIndex) -
+ this.getSampleWidth_(this.data_, leftIndex, true));
+ brushRange.addValue(
+ this.getXForDatum_(this.data_[rightIndex], rightIndex) +
+ this.getSampleWidth_(this.data_, rightIndex, false));
+ return brushRange;
+ },
+
+ getDataIndex_(dataX) {
+ if (this.data.length === 0) return undefined;
+ const bisect = d3.bisector(this.getXForDatum_.bind(this)).right;
+ return bisect(this.data_, dataX) - 1;
+ },
+
+ prepareDataEvent_(mouseEvent, dataEvent) {
+ ChartBase2D.prototype.prepareDataEvent_.call(
+ this, mouseEvent, dataEvent);
+ dataEvent.index = this.getDataIndex_(dataEvent.x);
+ if (dataEvent.index !== undefined) {
+ dataEvent.data = this.data_[dataEvent.index];
+ }
+ },
+
+ updateBrushContents_(brushSel) {
+ brushSel.selectAll('*').remove();
+ const brushes = this.brushedRange_.isEmpty ? [] : [this.brushedRange_];
+ const brushRectsSel = brushSel.selectAll('rect').data(brushes);
+ brushRectsSel.enter().append('rect');
+ brushRectsSel.exit().remove();
+ this.drawBrush_(brushRectsSel);
+ },
+
+ drawBrush_(brushRectsSel) {
+ brushRectsSel
+ .attr('x', d => this.xScale_(d.min))
+ .attr('y', 0)
+ .attr('width', d => this.xScale_(d.max) - this.xScale_(d.min))
+ .attr('height', this.graphHeight)
+ .attr('fill', 'rgb(213, 236, 229)');
+ }
+ };
+
+ return {
+ ChartBase2DBrushX,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html
new file mode 100644
index 00000000000..1b5b4943898
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id="tr-ui-b-chart-legend-key">
+ <template>
+ <style>
+ #checkbox {
+ margin: 0;
+ visibility: hidden;
+ vertical-align: text-top;
+ }
+ #label, #link {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: inline-block;
+ }
+ </style>
+
+ <input type=checkbox id="checkbox" checked>
+ <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link>
+ <label id="label"></label>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-chart-legend-key',
+
+ ready() {
+ this.$.checkbox.addEventListener(
+ 'change', this.onCheckboxChange_.bind(this));
+ },
+
+ /**
+ * Dispatch an event when the checkbox is toggled.
+ * The checkbox is visible when optional is set to true.
+ */
+ onCheckboxChange_() {
+ tr.b.dispatchSimpleEvent(this, tr.ui.b.DataSeriesEnableChangeEventType,
+ true, false,
+ {key: Polymer.dom(this).textContent, enabled: this.enabled});
+ },
+
+ set textContent(t) {
+ Polymer.dom(this.$.label).textContent = t;
+ Polymer.dom(this.$.link).textContent = t;
+ this.updateContents_();
+ },
+
+ set width(w) {
+ w -= 20; // reserve 20px for the checkbox
+ this.$.link.style.width = w + 'px';
+ this.$.label.style.width = w + 'px';
+ },
+
+ get textContent() {
+ return Polymer.dom(this.$.label).textContent;
+ },
+
+ /**
+ * When a legend-key is "optional", then its checkbox is visible to allow
+ * the user to enable/disable the data series for the key.
+ *
+ * @param {boolean} optional
+ */
+ set optional(optional) {
+ this.$.checkbox.style.visibility = optional ? 'visible' : 'hidden';
+ },
+
+ get optional() {
+ return this.$.checkbox.style.visibility === 'visible';
+ },
+
+ set enabled(enabled) {
+ this.$.checkbox.checked = enabled ? 'checked' : '';
+ },
+
+ get enabled() {
+ return this.$.checkbox.checked;
+ },
+
+ set color(c) {
+ this.$.label.style.color = c;
+ this.$.link.color = c;
+ },
+
+ /**
+ * When target is defined, label is hidden and link is shown.
+ * When the link is clicked, then a RequestSelectionChangeEvent is
+ * dispatched containing the target.
+ * When target is undefined, label is shown and link is hidden, so that the
+ * link is not clickable.
+ */
+ set target(target) {
+ this.$.link.setSelectionAndContent(
+ target, Polymer.dom(this.$.label).textContent);
+ this.updateContents_();
+ },
+
+ get target() {
+ return this.$.link.selection;
+ },
+
+ set title(title) {
+ this.$.link.title = title;
+ },
+
+ updateContents_() {
+ this.$.link.style.display = this.target ? '' : 'none';
+ this.$.label.style.display = this.target ? 'none' : '';
+ this.$.label.htmlFor = this.optional ? 'checkbox' : '';
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html
new file mode 100644
index 00000000000..0de935ed327
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-checkbox'>
+ <template>
+ <style>
+ .inline {
+ display: inline-block;
+ }
+ </style>
+
+ <input type="checkbox" id="checkbox" class="inline"/>
+ <div id="label" class="inline"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-checkbox',
+
+ created() {
+ this.needsInit_ = true;
+ this.defaultCheckedValue_ = undefined;
+ this.settingsKey_ = undefined;
+ this.label_ = undefined;
+ this.checked_ = false;
+ this.is_ready_ = false;
+ },
+
+ ready() {
+ this.is_ready_ = true;
+ this.$.checkbox.addEventListener('click', function() {
+ this.checked = this.$.checkbox.checked;
+ }.bind(this));
+ this.maybeUpdateElements_();
+ },
+
+ maybeUpdateElements_() {
+ if (!this.is_ready_) return;
+ this.$.label.innerText = this.label_;
+ this.$.checkbox.checked = this.checked_;
+ },
+
+ get defaultCheckedValue() {
+ return this.defaultCheckedValue_;
+ },
+
+ set defaultCheckedValue(defaultCheckedValue) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.defaultCheckedValue_ = defaultCheckedValue;
+ this.maybeInit_();
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.settingsKey_ = settingsKey;
+ this.maybeInit_();
+ },
+
+ maybeInit_() {
+ if (!this.needsInit_) return;
+ if (this.settingsKey_ === undefined) return;
+ if (this.defaultCheckedValue_ === undefined) return;
+ this.needsInit_ = false;
+ this.checked = tr.b.Settings.get(
+ this.settingsKey_, this.defaultCheckedValue_);
+ },
+
+ get label() {
+ return this.label_;
+ },
+
+ set label(label) {
+ this.label_ = label;
+ this.maybeUpdateElements_();
+ },
+
+ get checked() {
+ return this.checked_;
+ },
+
+ set checked(checked) {
+ this.checked_ = checked;
+ this.maybeUpdateElements_();
+ tr.b.Settings.set(this.settingsKey_, this.checked_);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html
new file mode 100644
index 00000000000..63073255379
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/checkbox.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-checkbox-picker'>
+ <template>
+ <style>
+ #container {
+ display: flex;
+ flex-direction: column;
+ }
+ </style>
+
+ <div id="container">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-checkbox-picker',
+ created() {
+ this.needsInit_ = true;
+ this.settingsKey_ = undefined;
+ this.is_ready_ = false;
+ this.checkboxes_ = undefined;
+ },
+
+ ready() {
+ this.is_ready_ = true;
+ this.maybeInit_();
+ this.maybeRenderCheckboxes_();
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.settingsKey_ = settingsKey;
+ this.maybeInit_();
+ },
+
+ maybeInit_() {
+ if (!this.needsInit_) return;
+ if (this.settingsKey_ === undefined) return;
+ if (this.checkboxes_ === undefined) return;
+
+ this.needsInit_ = false;
+
+ for (const key in this.checkboxes_) {
+ this.checkboxes_[key].defaultCheckedValue = false;
+ this.checkboxes_[key].settingsKey = this.settingsKey_ + key;
+ }
+ },
+
+ set items(items) {
+ this.checkboxes_ = {};
+ items.forEach(function(e) {
+ if (e.key in this.checkboxes_) {
+ throw new Error(e.key + ' already exists');
+ }
+ const checkboxEl = document.createElement('tr-ui-b-checkbox');
+ checkboxEl.label = e.label;
+ this.checkboxes_[e.key] = checkboxEl;
+ }.bind(this));
+ this.maybeInit_();
+ this.maybeRenderCheckboxes_();
+ },
+
+ maybeRenderCheckboxes_() {
+ if (!this.is_ready_) return;
+ if (this.checkboxes_ === undefined) return;
+ for (const key in this.checkboxes_) {
+ Polymer.dom(this.$.container).appendChild(this.checkboxes_[key]);
+ }
+ },
+
+ selectCheckbox(key) {
+ if (!(key in this.checkboxes_)) {
+ throw new Error(key + ' does not exists');
+ }
+ this.checkboxes_[key].checked = true;
+ },
+
+ unselectCheckbox(key) {
+ if (!(key in this.checkboxes_)) {
+ throw new Error(key + ' does not exists');
+ }
+ this.checkboxes_[key].checked = false;
+ },
+
+ get checkedKeys() {
+ return Object.keys(this.checkboxes_).filter(function(k) {
+ return this.checkboxes_[k].checked;
+ }.bind(this));
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html
new file mode 100644
index 00000000000..ad893d6cc51
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/checkbox_picker.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basicAllCheckboxUnchecked', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'}
+ ];
+ this.addHTMLOutput(cp);
+ assert.deepEqual(cp.checkedKeys, []);
+ });
+
+ test('basicSomeCheckboxChecked', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+
+ cp.selectCheckbox('Toyota');
+ cp.selectCheckbox('Tesla');
+ this.addHTMLOutput(cp);
+ assert.deepEqual(cp.checkedKeys.sort(), ['Tesla', 'Toyota']);
+ cp.unselectCheckbox('Toyota');
+ assert.deepEqual(cp.checkedKeys, ['Tesla']);
+ });
+
+ test('duplicateKeys', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ assert.throws(function() {
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Toyota', label: 'I want to drive electric car'},
+ ];
+ });
+ });
+
+ test('selectAndUnselectNonExistingKey', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ ];
+ assert.throws(function() {
+ cp.selectCheckbox('Lamborghini');
+ });
+ assert.throws(function() {
+ cp.unselectCheckbox('Roll Royce');
+ });
+ });
+
+ test('testPersistentStateOneSetSettingsKeyBeforeSettingItems', function() {
+ const container1 = tr.ui.b.createDiv({textContent: 'Checkbox Picker One'});
+ container1.style.border = 'solid';
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.settingsKey = 'checkbox-picker-test-one';
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ cp.selectCheckbox('Toyota');
+ cp.selectCheckbox('Tesla');
+ Polymer.dom(container1).appendChild(cp);
+ this.addHTMLOutput(container1);
+ cp.unselectCheckbox('Tesla');
+ assert.deepEqual(cp.checkedKeys, ['Toyota']);
+
+ this.addHTMLOutput(document.createElement('br'));
+
+ const container2 = tr.ui.b.createDiv(
+ {textContent:
+ 'Checkbox Picker Two (Same settingsKey as Checkbox Picker One)'});
+ container2.style.border = 'solid #0000FF';
+ const cp2 = document.createElement('tr-ui-b-checkbox-picker');
+ cp2.settingsKey = 'checkbox-picker-test-one';
+ cp2.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ Polymer.dom(container2).appendChild(cp2);
+ this.addHTMLOutput(container2);
+ assert.deepEqual(cp2.checkedKeys, ['Toyota']);
+ });
+
+ test('testPersistentStateTwoSetSettingsKeyAfterSettingItems', function() {
+ const container1 = tr.ui.b.createDiv({textContent: 'Checkbox Picker One'});
+ container1.style.border = 'solid';
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ cp.settingsKey = 'checkbox-picker-test-one';
+ cp.selectCheckbox('Toyota');
+ cp.selectCheckbox('Tesla');
+ Polymer.dom(container1).appendChild(cp);
+ this.addHTMLOutput(container1);
+ assert.deepEqual(cp.checkedKeys.sort(), ['Tesla', 'Toyota']);
+
+ this.addHTMLOutput(document.createElement('br'));
+
+ const container2 = tr.ui.b.createDiv(
+ {textContent:
+ 'Checkbox Picker Two (Same settingsKey as Checkbox Picker One)'});
+ container2.style.border = 'solid #0000FF';
+ const cp2 = document.createElement('tr-ui-b-checkbox-picker');
+ cp2.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ Polymer.dom(container2).appendChild(cp2);
+ this.addHTMLOutput(container2);
+ cp2.settingsKey = 'checkbox-picker-test-one';
+ assert.deepEqual(cp2.checkedKeys.sort(), ['Tesla', 'Toyota']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html
new file mode 100644
index 00000000000..47ac3492ef5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/checkbox.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basicUnchecked', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ checkbox.label = 'Yo like pizza?';
+ this.addHTMLOutput(checkbox);
+ assert.strictEqual(checkbox.label, 'Yo like pizza?');
+ assert.isFalse(checkbox.checked);
+ });
+
+ test('basicChecked', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ checkbox.label = 'Yo like cookie?';
+ checkbox.checked = true;
+ this.addHTMLOutput(checkbox);
+ assert.strictEqual(checkbox.label, 'Yo like cookie?');
+ assert.isTrue(checkbox.checked);
+ });
+
+ test('testPersistentStateOneSetSettingsKeyBeforeAddToDom', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ checkbox.settingsKey = 'checkbox-basic-test-one';
+ checkbox.label = 'I like sushi';
+ checkbox.defaultCheckedValue = false;
+ this.addHTMLOutput(checkbox);
+ assert.isFalse(checkbox.checked);
+ checkbox.checked = true;
+
+ const checkbox2 = document.createElement('tr-ui-b-checkbox');
+ checkbox2.label = 'I like sushi';
+ checkbox2.defaultCheckedValue = false;
+ checkbox2.settingsKey = 'checkbox-basic-test-one';
+ this.addHTMLOutput(checkbox2);
+ assert.isTrue(checkbox2.checked);
+ });
+
+ test('testPersistentStateTwoSetSettingsKeyAfterAddToDom', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ this.addHTMLOutput(checkbox);
+ checkbox.label = 'I like Ramen';
+ checkbox.settingsKey = 'checkbox-basic-test-two';
+ checkbox.defaultCheckedValue = false;
+ assert.isFalse(checkbox.checked);
+ checkbox.checked = true;
+
+ const checkbox2 = document.createElement('tr-ui-b-checkbox');
+ this.addHTMLOutput(checkbox2);
+ checkbox2.label = 'I like Ramen';
+ checkbox2.defaultCheckedValue = false;
+ checkbox2.settingsKey = 'checkbox-basic-test-two';
+ assert.isTrue(checkbox2.checked);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html
new file mode 100644
index 00000000000..c082f12f834
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/model/compound_event_selection_state.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<!--
+@fileoverview A component used to display a label and a color square.
+
+The colored square is typically filled with the color associated with
+that label, using the getColorId* methods from base/color_scheme.
+-->
+<dom-module id='tr-ui-b-color-legend'>
+ <template>
+ <style>
+ :host {
+ display: inline-block;
+ }
+
+ #square {
+ font-size: 150%; /* Make the square bigger. */
+ line-height: 0%; /* Prevent the square from increasing legend height. */
+ }
+ </style>
+ <span id="square"></span>
+ <span id="label"></span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-color-legend',
+
+ ready() {
+ const blackSquareCharCode = 9632;
+ this.$.square.innerText = String.fromCharCode(blackSquareCharCode);
+ this.label_ = undefined;
+
+ this.compoundEventSelectionState_ =
+ tr.model.CompoundEventSelectionState.NOT_SELECTED;
+ },
+
+ set compoundEventSelectionState(compoundEventSelectionState) {
+ this.compoundEventSelectionState_ = compoundEventSelectionState;
+ // TODO(nduca): Adjust appearance based on associated state.
+ },
+
+ get label() {
+ return this.label_;
+ },
+
+ set label(label) {
+ if (label === undefined) {
+ this.setLabelAndColorId(undefined, undefined);
+ return;
+ }
+
+ const colorId = tr.b.ColorScheme.getColorIdForGeneralPurposeString(
+ label);
+ this.setLabelAndColorId(label, colorId);
+ },
+
+ setLabelAndColorId(label, colorId) {
+ this.label_ = label;
+
+ Polymer.dom(this.$.label).textContent = '';
+ Polymer.dom(this.$.label).appendChild(tr.ui.b.asHTMLOrTextNode(label));
+
+ if (colorId === undefined) {
+ this.$.square.style.color = 'initial';
+ } else {
+ this.$.square.style.color = tr.b.ColorScheme.colorsAsStrings[colorId];
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html
new file mode 100644
index 00000000000..b19b3a4b7e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/color_legend.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const CompoundEventSelectionState = tr.model.CompoundEventSelectionState;
+
+ function checkSquareColor(colorLegend, expectedColor) {
+ assert.strictEqual(
+ getComputedStyle(colorLegend.$.square).color, expectedColor);
+ }
+
+ test('noLabelSet', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(0, 0, 0)');
+ });
+
+ test('undefinedLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = undefined;
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(0, 0, 0)');
+ });
+
+ test('emptyLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = '';
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 161, 161)');
+ });
+
+ test('nonEmptyLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Frequency';
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 133, 236)');
+ });
+
+ test('longLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Total memory usage';
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(150, 193, 255)');
+ });
+
+ test('directlySetColorId', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.setLabelAndColorId('hello_world', 7 /* colorId */);
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(152, 220, 149)');
+ });
+
+ test('directlyProvidedLabelElement', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.setLabelAndColorId(
+ tr.ui.b.createSpan({textContent: 'hello',
+ className: 'hello-span'}),
+ 7 /* colorId */);
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(152, 220, 149)');
+ });
+
+ test('cessObjectSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Object selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.EVENT_SELECTED;
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(228, 184, 134)');
+ });
+
+ test('cessSomeAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Some associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.SOME_ASSOCIATED_EVENTS_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 155, 172)');
+ });
+
+ test('cessAllAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'All associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(150, 193, 255)');
+ });
+
+ test('cessObjectAndSomeAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Object and some associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.EVENT_AND_SOME_ASSOCIATED_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(204, 158, 255)');
+ });
+
+ test('cessObjectAndAllAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Object and all associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.EVENT_AND_ALL_ASSOCIATED_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 146, 193)');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html
new file mode 100644
index 00000000000..9d14ce92974
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html
@@ -0,0 +1,427 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/chart_base_2d_brushable_x.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const ColumnChart = tr.ui.b.define('column-chart', tr.ui.b.ChartBase2DBrushX);
+
+ ColumnChart.prototype = {
+ __proto__: tr.ui.b.ChartBase2DBrushX.prototype,
+
+ decorate() {
+ super.decorate();
+
+ // ColumnChart allows bars to have arbitrary, non-uniform widths. Bars
+ // need not all be the same width. The width of each bar is automatically
+ // computed from the bar's x-coordinate and that of the next bar, which
+ // can not define the width of the last bar. This is the width (in the
+ // xScale's domain (as opposed to the xScale's range (which is measured in
+ // pixels))) of the last bar. When there are at least 2 bars, this is
+ // computed as the average width of the bars. When there is a single bar,
+ // this must default to a non-zero number so that the width of the only
+ // bar will not be zero.
+ this.xCushion_ = 1;
+
+ this.isStacked_ = false;
+ this.isGrouped_ = false;
+
+ this.enableHoverBox = true;
+ this.displayXInHover = false;
+ this.enableToolTip = false;
+
+ this.toolTipCallBack_ = () => {};
+ },
+
+ set toolTipCallBack(callback) {
+ this.toolTipCallBack_ = callback;
+ },
+
+ get toolTipCallBack() {
+ return this.toolTipCallBack_;
+ },
+
+ set isGrouped(grouped) {
+ this.isGrouped_ = grouped;
+ if (grouped) {
+ this.getDataSeries('group').color = 'transparent';
+ }
+ this.updateContents_();
+ },
+
+ get isGrouped() {
+ return this.isGrouped_;
+ },
+
+ set isStacked(stacked) {
+ this.isStacked_ = true;
+ this.updateContents_();
+ },
+
+ get isStacked() {
+ return this.isStacked_;
+ },
+
+ get defaultGraphHeight() {
+ return 100;
+ },
+
+ get defaultGraphWidth() {
+ return 10 * this.data_.length;
+ },
+
+ updateScales_() {
+ if (this.data_.length === 0) return;
+
+ let xDifferences = 0;
+ let currentX = undefined;
+ let previousX = undefined;
+ this.data_.forEach(function(datum, index) {
+ previousX = currentX;
+ currentX = this.getXForDatum_(datum, index);
+ if (previousX !== undefined) {
+ xDifferences += currentX - previousX;
+ }
+ }, this);
+
+ // X.
+ // Leave a cushion on the right so that the last rect doesn't
+ // exceed the chart boundaries. The last rect's width is set to the
+ // average width of the rects, which is chart.width / data.length.
+ this.xScale_.range([0, this.graphWidth]);
+ const domain = d3.extent(this.data_, this.getXForDatum_.bind(this));
+ if (this.data_.length > 1) {
+ this.xCushion_ = xDifferences / (this.data_.length - 1);
+ }
+ this.xScale_.domain([domain[0], domain[1] + this.xCushion_]);
+
+ // Y.
+ this.yScale_.range([this.graphHeight, 0]);
+ this.yScale_.domain(this.getYScaleDomain_(
+ this.dataRange.min, this.dataRange.max));
+ },
+
+ updateDataRange_() {
+ if (!this.isStacked) {
+ super.updateDataRange_();
+ return;
+ }
+
+ this.autoDataRange_.reset();
+ this.autoDataRange_.addValue(0);
+ for (const datum of this.data_) {
+ let sum = 0;
+ for (const [key, series] of this.seriesByKey_) {
+ if (datum[key] === undefined) {
+ continue;
+ } else if (this.isGrouped && key === 'group') {
+ continue;
+ }
+ sum += datum[key];
+ }
+ this.autoDataRange_.addValue(sum);
+ }
+ },
+
+ getStackedRectsForDatum_(datum, index) {
+ const stacks = [];
+ let bottom = this.yScale_.range()[0];
+ let sum = 0;
+ for (const [key, series] of this.seriesByKey_) {
+ if (datum[key] === undefined || !this.isSeriesEnabled(key)) {
+ continue;
+ } else if (this.isGrouped && key === 'group') {
+ continue;
+ }
+
+ sum += this.dataRange.clamp(datum[key]);
+ const heightPx = bottom - this.yScale_(sum);
+ bottom -= heightPx;
+ stacks.push({
+ key,
+ value: datum[key],
+ color: this.getDataSeries(key).color,
+ heightPx,
+ topPx: bottom,
+ underflow: sum < this.dataRange.min,
+ overflow: sum > this.dataRange.max,
+ });
+ }
+ return stacks;
+ },
+
+ getRectsForDatum_(datum, index) {
+ if (this.isStacked) {
+ return this.getStackedRectsForDatum_(datum, index);
+ }
+
+ const stacks = [];
+ for (const [key, series] of this.seriesByKey_) {
+ if (datum[key] === undefined || !this.isSeriesEnabled(key)) {
+ continue;
+ }
+
+ const clampedValue = this.dataRange.clamp(datum[key]);
+ const topPx = this.yScale_(Math.max(
+ clampedValue, this.getYScaleMin_()));
+ stacks.push({
+ key,
+ value: datum[key],
+ topPx,
+ heightPx: this.yScale_.range()[0] - topPx,
+ color: this.getDataSeries(key).color,
+ underflow: datum[key] < this.dataRange.min,
+ overflow: datum[key] > this.dataRange.max,
+ });
+ }
+ stacks.sort(function(a, b) {
+ return b.topPx - a.topPx;
+ });
+ return stacks;
+ },
+
+ drawToolTip_(rect) {
+ if (!this.enableToolTip) return;
+
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ chartAreaSel.selectAll('.tooltip').remove();
+
+ const labelText = 'View Breakdown';
+ const labelWidth = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, labelText).width + 5;
+ const labelHeight = this.textHeightPx_;
+
+ const toolTipLeftPx = rect.leftPx + (rect.widthPx / 2);
+ const toolTipTopPx = rect.topPx;
+
+ chartAreaSel
+ .append('rect')
+ .attr('class', 'tooltip')
+ .attr('fill', 'white')
+ .attr('opacity', 0.8)
+ .attr('stroke', 'black')
+ .attr('x', toolTipLeftPx)
+ .attr('y', toolTipTopPx)
+ .attr('width', labelWidth + 5)
+ .attr('height', labelHeight + 10);
+
+ chartAreaSel
+ .append('text')
+ .style('cursor', 'pointer')
+ .attr('class', 'tooltip')
+ .on('mousedown', () => this.toolTipCallBack_(rect))
+ .attr('fill', 'blue')
+ .attr('x', toolTipLeftPx + 4)
+ .attr('y', toolTipTopPx + labelHeight)
+ .attr('text-decoration', 'underline')
+ .text(labelText);
+ },
+
+ drawHoverValueBox_(rect) {
+ const rectHoverEvent = new tr.b.Event('rect-mouseenter');
+ rectHoverEvent.rect = rect;
+ this.dispatchEvent(rectHoverEvent);
+
+ if (!this.enableHoverBox) return;
+
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ chartAreaSel.selectAll('.hover').remove();
+ let keyWidthPx = 0;
+ let keyHeightPx = 0;
+ if (seriesKeys.length > 1 && !this.isGrouped) {
+ keyWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.key).width + 5;
+ keyHeightPx = this.textHeightPx_;
+ }
+
+ let xLabelWidthPx = 0;
+ let xLabelHeightPx = 0;
+ if (this.displayXInHover) {
+ xLabelWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.datum.x).width + 5;
+ xLabelHeightPx = this.textHeightPx_;
+ }
+
+ let groupWidthPx = 0;
+ let groupHeightPx = 0;
+ if (this.isGrouped && rect.datum.group !== undefined) {
+ groupWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.datum.group).width + 5;
+ groupHeightPx = this.textHeightPx_;
+ }
+
+ let value = rect.value;
+ if (this.unit) value = this.unit.format(value);
+ const valueWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, value).width + 5;
+ const valueHeightPx = this.textHeightPx_;
+
+ const hoverWidthPx = Math.max(keyWidthPx, valueWidthPx,
+ xLabelWidthPx, groupWidthPx);
+
+ let hoverLeftPx = rect.leftPx + (rect.widthPx / 2);
+ hoverLeftPx = Math.max(hoverLeftPx - hoverWidthPx, -this.margin.left);
+
+ const hoverHeightPx = keyHeightPx + valueHeightPx +
+ xLabelHeightPx + groupHeightPx + 2;
+
+ const topOffSetPx = this.isGrouped ? 36 : 12;
+ let hoverTopPx = rect.topPx;
+ hoverTopPx = Math.min(
+ hoverTopPx, this.getBoundingClientRect().height -
+ hoverHeightPx - topOffSetPx);
+
+ chartAreaSel
+ .append('rect')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', 'white')
+ .attr('stroke', 'black')
+ .attr('x', hoverLeftPx)
+ .attr('y', hoverTopPx)
+ .attr('width', hoverWidthPx)
+ .attr('height', hoverHeightPx);
+
+ if (seriesKeys.length > 1 && !this.isGrouped) {
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx - 2)
+ .text(rect.key);
+ }
+
+ if (this.displayXInHover) {
+ chartAreaSel.append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx + xLabelHeightPx - 2)
+ .text(rect.datum.x);
+ }
+
+ if (this.isGrouped && rect.datum.group !== undefined) {
+ chartAreaSel.append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx +
+ xLabelHeightPx + groupHeightPx - 2)
+ .text(rect.datum.group);
+ }
+
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + hoverHeightPx - 2)
+ .text(value);
+ },
+
+ clearHoverValueBox_(rect) {
+ const event = window.event;
+ if (event.relatedTarget &&
+ Array.from(event.relatedTarget.classList).includes('hover')) {
+ return;
+ }
+
+ const rectHoverEvent = new tr.b.Event('rect-mouseleave');
+ rectHoverEvent.rect = rect;
+ this.dispatchEvent(rectHoverEvent);
+
+ d3.select(this.chartAreaElement).selectAll('.hover').remove();
+ },
+
+ drawRect_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('rect')
+ .attr('fill', rect.color)
+ .attr('x', rect.leftPx)
+ .attr('y', rect.topPx)
+ .attr('width', rect.widthPx)
+ .attr('height', rect.heightPx)
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .on('mouseenter', this.drawHoverValueBox_.bind(this, rect))
+ .on('mouseleave', this.clearHoverValueBox_.bind(this, rect));
+ sel.exit().remove();
+ },
+
+ drawUnderflow_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('text')
+ .text('*')
+ .attr('fill', rect.color)
+ .attr('x', rect.leftPx + (rect.widthPx / 2))
+ .attr('y', this.graphHeight)
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .on('mouseenter', this.drawHoverValueBox_.bind(this, rect))
+ .on('mouseleave', this.clearHoverValueBox_.bind(this, rect));
+ sel.exit().remove();
+ },
+
+ drawOverflow_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('text')
+ .text('*')
+ .attr('fill', rect.color)
+ .attr('x', rect.leftPx + (rect.widthPx / 2))
+ .attr('y', 0);
+ sel.exit().remove();
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const rectsSel = dataSel.selectAll('path');
+ this.data_.forEach(function(datum, index) {
+ const currentX = this.getXForDatum_(datum, index);
+ let width = undefined;
+ if (index < (this.data_.length - 1)) {
+ const nextX = this.getXForDatum_(this.data_[index + 1], index + 1);
+ width = nextX - currentX;
+ } else {
+ width = this.xCushion_;
+ }
+ for (const rect of this.getRectsForDatum_(datum, index)) {
+ rect.datum = datum;
+ rect.index = index;
+ rect.leftPx = this.xScale_(currentX);
+ rect.rightPx = this.xScale_(currentX + width);
+ rect.widthPx = rect.rightPx - rect.leftPx;
+ this.drawRect_(rect, rectsSel);
+ if (rect.underflow) {
+ this.drawUnderflow_(rect, rectsSel);
+ }
+ if (rect.overflow) {
+ this.drawOverflow_(rect, rectsSel);
+ }
+ }
+ }, this);
+ }
+ };
+
+ return {
+ ColumnChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html
new file mode 100644
index 00000000000..4771b9f60fa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html
@@ -0,0 +1,276 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('chartLegendKey', function() {
+ let key = document.createElement('tr-ui-b-chart-legend-key');
+ key.textContent = 'Lorem ipsum dolor sit amet';
+ key.color = 'red';
+ this.addHTMLOutput(key);
+
+ key = document.createElement('tr-ui-b-chart-legend-key');
+ key.textContent = 'ipsum dolor sit amet';
+ key.target = 'orange ipsum';
+ key.color = 'orange';
+ this.addHTMLOutput(key);
+
+ key = document.createElement('tr-ui-b-chart-legend-key');
+ key.target = 'brown dolor';
+ key.color = 'brown';
+ key.textContent = 'dolor sit amet';
+ this.addHTMLOutput(key);
+ });
+
+ test('instantiation_legendTargets', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.getDataSeries('lorem_ipsum').target = 'lorem_ipsumTarget';
+ chart.getDataSeries('lorem_ipsum').title = 'lorem ipsum';
+ chart.getDataSeries('qux').target = 'quxTarget';
+ chart.getDataSeries('lorem_ipsum').optional = true;
+ chart.getDataSeries('bar').optional = true;
+ chart.isStacked = true;
+ chart.hideXAxis = true;
+ this.addHTMLOutput(chart);
+ chart.data = [{x: 0, foo: 3, lorem_ipsum: 5, bar: 1, qux: 2}];
+
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ chart, function(element) {
+ return element.tagName === 'TR-UI-B-CHART-LEGEND-KEY' &&
+ element.textContent === 'lorem_ipsum' &&
+ element.target === 'lorem_ipsumTarget';
+ }));
+ });
+
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.xAxisLabel = 'ms';
+ chart.yAxisLabel = '#';
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 100},
+ {x: 20, value: 110},
+ {x: 30, value: 100},
+ {x: 40, value: 50}
+ ];
+ });
+
+ test('instantiation_singleDatum', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 100},
+ ];
+ });
+
+ test('instantiation_stacked', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.isStacked = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, foo: 10, bar: 5, qux: 7},
+ {x: 20, foo: 11, bar: 6, qux: 3},
+ {x: 30, foo: 10, bar: 4, qux: 8},
+ {x: 40, foo: 5, bar: 1, qux: 2}
+ ];
+ });
+
+ test('instantiation_singleSeries_yLogScale', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.isYLogScale = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 100},
+ {x: 20, value: 10},
+ {x: 30, value: 1},
+ {x: 40, value: 0.1},
+ {x: 50, value: 0.01},
+ {x: 60, value: 0.001}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSeries_yLogScale', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.isYLogScale = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: undefined},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 25, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: 40},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('brushRangeFromIndices', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ const data = [
+ {x: 10, value: 50},
+ {x: 30, value: 60},
+ {x: 70, value: 70},
+ {x: 80, value: 80},
+ {x: 120, value: 90}
+ ];
+ chart.data = data;
+ let r = new tr.b.math.Range();
+
+ // Range min should be 10.
+ r = chart.computeBrushRangeFromIndices(-2, 1);
+ assert.strictEqual(r.min, 10);
+
+ // Range max should be 120.
+ r = chart.computeBrushRangeFromIndices(3, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [10, 120]
+ r = chart.computeBrushRangeFromIndices(-2, 10);
+ assert.strictEqual(r.min, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [20, 100]
+ r = chart.computeBrushRangeFromIndices(1, 3);
+ assert.strictEqual(r.min, 20);
+ assert.strictEqual(r.max, 100);
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 20, value: 60},
+ {x: 30, value: 80},
+ {x: 40, value: 20},
+ {x: 50, value: 30},
+ {x: 60, value: 20},
+ {x: 70, value: 15},
+ {x: 80, value: 20}
+ ];
+
+ let mouseDownX = undefined;
+ let curMouseX = undefined;
+
+ function updateBrushedRange() {
+ if (mouseDownX === undefined || (mouseDownX === curMouseX)) {
+ chart.brushedRange = new tr.b.math.Range();
+ return;
+ }
+ const r = new tr.b.math.Range();
+ r.min = Math.min(mouseDownX, curMouseX);
+ r.max = Math.max(mouseDownX, curMouseX);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownX = e.x;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ });
+
+ test('overrideDataRange', function() {
+ let chart = new tr.ui.b.ColumnChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 100},
+ ];
+
+ chart = new tr.ui.b.ColumnChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 50},
+ ];
+ });
+
+ test('instantiationGrouped', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.graphWidth = 300;
+ chart.graphHeight = 200;
+ chart.isStacked = true;
+ chart.isGrouped = true;
+ chart.displayXInHover = true;
+ chart.enableToolTip = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50, group: 'group1' },
+ {x: 20, alpha: 110, beta: 75, group: 'group2' },
+ {x: 30 },
+ {x: 40, alpha: 100, beta: 125, group: 'group1' },
+ {x: 50, alpha: 50, beta: 125, group: 'group2' }
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html b/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html
new file mode 100644
index 00000000000..8c38d0bb1b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const constants = {
+ HEADING_WIDTH: 250
+ };
+
+ return {
+ constants,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html
new file mode 100644
index 00000000000..3ce0d3908b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Container that decorates its children.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * @constructor
+ */
+ const ContainerThatDecoratesItsChildren = tr.ui.b.define('div');
+
+ ContainerThatDecoratesItsChildren.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.observer_ = new WebKitMutationObserver(this.didMutate_.bind(this));
+ this.observer_.observe(this, { childList: true });
+
+ // textContent is a variable on regular HTMLElements. However, we want to
+ // hook and prevent writes to it.
+ Object.defineProperty(
+ this, 'textContent',
+ { get: undefined, set: this.onSetTextContent_});
+ },
+
+ appendChild(x) {
+ HTMLDivElement.prototype.appendChild.call(this, x);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ insertBefore(x, y) {
+ HTMLDivElement.prototype.insertBefore.call(this, x, y);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ removeChild(x) {
+ HTMLDivElement.prototype.removeChild.call(this, x);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ replaceChild(x, y) {
+ HTMLDivElement.prototype.replaceChild.call(this, x, y);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ onSetTextContent_(textContent) {
+ if (textContent !== '') {
+ throw new Error('textContent can only be set to \'\'.');
+ }
+ this.clear();
+ },
+
+ clear() {
+ while (Polymer.dom(this).lastChild) {
+ HTMLDivElement.prototype.removeChild.call(
+ this, Polymer.dom(this).lastChild);
+ }
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ didMutate_(records) {
+ this.beginDecorating_();
+ for (let i = 0; i < records.length; i++) {
+ const addedNodes = records[i].addedNodes;
+ if (addedNodes) {
+ for (let j = 0; j < addedNodes.length; j++) {
+ this.decorateChild_(addedNodes[j]);
+ }
+ }
+ const removedNodes = records[i].removedNodes;
+ if (removedNodes) {
+ for (let j = 0; j < removedNodes.length; j++) {
+ this.undecorateChild_(removedNodes[j]);
+ }
+ }
+ }
+ this.doneDecoratingForNow_();
+ },
+
+ decorateChild_(child) {
+ throw new Error('Not implemented');
+ },
+
+ undecorateChild_(child) {
+ throw new Error('Not implemented');
+ },
+
+ beginDecorating_() {
+ },
+
+ doneDecoratingForNow_() {
+ }
+ };
+
+ return {
+ ContainerThatDecoratesItsChildren,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html
new file mode 100644
index 00000000000..54417b3ded6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createChild() {
+ const span = document.createElement('span');
+ span.decorated = false;
+ return span;
+ }
+
+ /**
+ * @constructor
+ */
+ const SimpleContainer = tr.ui.b.define(
+ 'simple-container', tr.ui.b.ContainerThatDecoratesItsChildren);
+
+ SimpleContainer.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorateChild_(child) {
+ assert.isFalse(child.decorated);
+ child.decorated = true;
+ },
+
+ undecorateChild_(child) {
+ assert.isTrue(child.decorated);
+ child.decorated = false;
+ }
+ };
+
+ test('add', function() {
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(createChild());
+ Polymer.dom(container).appendChild(createChild());
+ Polymer.dom(container).appendChild(createChild());
+ assert.isTrue(container.children[0].decorated);
+ assert.isTrue(container.children[1].decorated);
+ assert.isTrue(container.children[2].decorated);
+ });
+
+ test('clearUsingTextContent', function() {
+ const c0 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c0);
+ Polymer.dom(container).textContent = '';
+ assert.isFalse(c0.decorated);
+ });
+
+ test('clear', function() {
+ const c0 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c0);
+ container.clear();
+ assert.isFalse(c0.decorated);
+ });
+
+ test('insertNewBefore', function() {
+ const c0 = createChild();
+ const c1 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c1);
+ Polymer.dom(container).insertBefore(c0, c1);
+ assert.isTrue(c0.decorated);
+ assert.isTrue(c1.decorated);
+ });
+
+ test('insertExistingBefore', function() {
+ const c0 = createChild();
+ const c1 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c1);
+ Polymer.dom(container).appendChild(c0);
+ Polymer.dom(container).insertBefore(c0, c1);
+ assert.isTrue(c0.decorated);
+ assert.isTrue(c1.decorated);
+ });
+
+ test('testReplace', function() {
+ const c0 = createChild();
+ const c1 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c0);
+ Polymer.dom(container).replaceChild(c1, c0);
+ assert.isFalse(c0.decorated);
+ assert.isTrue(c1.decorated);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html b/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html
new file mode 100644
index 00000000000..bce0554c512
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<script src="/tracing/ui/base/d3_preload.js"></script>
+<script src="/d3.min.js"></script>
+<script src="/tracing/ui/base/d3_postload.js"></script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js
new file mode 100644
index 00000000000..94cefdb15f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2014 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. */
+'use strict';
+
+(function(window) {
+ window.define = undefined;
+}).call(this, this);
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js
new file mode 100644
index 00000000000..57548f1d175
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js
@@ -0,0 +1,11 @@
+/* Copyright (c) 2014 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. */
+'use strict';
+
+(function(window) {
+ window.define = function(x) {
+ window.d3 = x;
+ };
+ window.define.amd = true;
+})(this);
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html
new file mode 100644
index 00000000000..2a1ad88d6cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function iterateElementDeeplyImpl(element, cb, thisArg, includeElement) {
+ if (includeElement && cb.call(thisArg, element)) return true;
+
+ if (element.root &&
+ element.root !== element &&
+ iterateElementDeeplyImpl(element.root, cb, thisArg, false)) {
+ // Some elements, most notably Polymer template dom-repeat='...'
+ // elements, are their own shadow root. Make sure that we avoid infinite
+ // recursion by avoiding these elements.
+ return true;
+ }
+ const children = Polymer.dom(element).children;
+ for (let i = 0; i < children.length; i++) {
+ if (iterateElementDeeplyImpl(children[i], cb, thisArg, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function iterateElementDeeply(element, cb, thisArg) {
+ iterateElementDeeplyImpl(element, cb, thisArg, false);
+ }
+
+ function findDeepElementMatchingPredicate(element, predicate) {
+ let foundElement = undefined;
+ function matches(element) {
+ const match = predicate(element);
+ if (!match) {
+ return false;
+ }
+ foundElement = element;
+ return true;
+ }
+ iterateElementDeeply(element, matches);
+ return foundElement;
+ }
+
+ function findDeepElementsMatchingPredicate(element, predicate) {
+ const foundElements = [];
+ function matches(element) {
+ const match = predicate(element);
+ if (match) {
+ foundElements.push(element);
+ }
+ return false;
+ }
+ iterateElementDeeply(element, matches);
+ return foundElements;
+ }
+
+ function findDeepElementMatching(element, selector) {
+ return findDeepElementMatchingPredicate(element, function(element) {
+ return element.matches(selector);
+ });
+ }
+ function findDeepElementsMatching(element, selector) {
+ return findDeepElementsMatchingPredicate(element, function(element) {
+ return element.matches(selector);
+ });
+ }
+ function findDeepElementWithTextContent(element, re) {
+ return findDeepElementMatchingPredicate(element, function(element) {
+ if (element.children.length !== 0) return false;
+ return re.test(Polymer.dom(element).textContent);
+ });
+ }
+
+ return {
+ findDeepElementMatching,
+ findDeepElementsMatching,
+ findDeepElementMatchingPredicate,
+ findDeepElementsMatchingPredicate,
+ findDeepElementWithTextContent,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html
new file mode 100644
index 00000000000..d9e1d49f556
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<dom-module id='tr-ui-b-deep-utils-test-a'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<dom-module id='tr-ui-b-deep-utils-test-b'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<dom-module id='tr-ui-b-deep-utils-test-c'>
+ <template>
+ <tr-ui-b-deep-utils-test-b class='x'></tr-ui-b-deep-utils-test-b>
+ <tr-ui-b-deep-utils-test-a class='x'></tr-ui-b-deep-utils-test-a>
+ <tr-ui-b-deep-utils-test-a class='x'></tr-ui-b-deep-utils-test-a>
+ </template>
+</dom-module>
+<dom-module id='tr-ui-b-deep-utils-test-d'>
+ <template>
+ <tr-ui-b-deep-utils-test-c></tr-ui-b-deep-utils-test-c>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-a'
+});
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-b'
+});
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-c'
+});
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-d'
+});
+
+tr.b.unittest.testSuite(function() {
+ test('testFindDeepElementMatching', function() {
+ const d = document.createElement('tr-ui-b-deep-utils-test-d');
+
+ const b = tr.ui.b.findDeepElementMatching(d, 'tr-ui-b-deep-utils-test-b.x');
+ assert.isDefined(b);
+ assert.strictEqual(b.tagName, 'TR-UI-B-DEEP-UTILS-TEST-B');
+ });
+
+ test('testFindDeepElementsMatching', function() {
+ const d = document.createElement('tr-ui-b-deep-utils-test-d');
+
+ const a = tr.ui.b.findDeepElementsMatching(
+ d, 'tr-ui-b-deep-utils-test-a.x');
+ assert.isDefined(a);
+ assert.strictEqual(a[0].tagName, 'TR-UI-B-DEEP-UTILS-TEST-A');
+ assert.strictEqual(a[1].tagName, 'TR-UI-B-DEEP-UTILS-TEST-A');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html
new file mode 100644
index 00000000000..adf7f10b34d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html
@@ -0,0 +1,390 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function createSpan(opt_dictionary) {
+ let ownerDocument = document;
+ if (opt_dictionary && opt_dictionary.ownerDocument) {
+ ownerDocument = opt_dictionary.ownerDocument;
+ }
+ const spanEl = ownerDocument.createElement('span');
+ if (opt_dictionary) {
+ if (opt_dictionary.className) {
+ spanEl.className = opt_dictionary.className;
+ }
+ if (opt_dictionary.textContent) {
+ Polymer.dom(spanEl).textContent =
+ opt_dictionary.textContent;
+ }
+ if (opt_dictionary.tooltip) {
+ spanEl.title = opt_dictionary.tooltip;
+ }
+ if (opt_dictionary.parent) {
+ Polymer.dom(opt_dictionary.parent).appendChild(spanEl);
+ }
+ if (opt_dictionary.bold) {
+ spanEl.style.fontWeight = 'bold';
+ }
+ if (opt_dictionary.italic) {
+ spanEl.style.fontStyle = 'italic';
+ }
+ if (opt_dictionary.marginLeft) {
+ spanEl.style.marginLeft = opt_dictionary.marginLeft;
+ }
+ if (opt_dictionary.marginRight) {
+ spanEl.style.marginRight = opt_dictionary.marginRight;
+ }
+ if (opt_dictionary.backgroundColor) {
+ spanEl.style.backgroundColor = opt_dictionary.backgroundColor;
+ }
+ if (opt_dictionary.color) {
+ spanEl.style.color = opt_dictionary.color;
+ }
+ }
+ return spanEl;
+ }
+
+ function createLink(opt_args) {
+ let ownerDocument = document;
+ if (opt_args && opt_args.ownerDocument) {
+ ownerDocument = opt_args.ownerDocument;
+ }
+ const linkEl = ownerDocument.createElement('a');
+ if (opt_args) {
+ if (opt_args.href) linkEl.href = opt_args.href;
+ if (opt_args.tooltip) linkEl.title = opt_args.tooltip;
+ if (opt_args.color) linkEl.style.color = opt_args.color;
+ if (opt_args.bold) linkEl.style.fontWeight = 'bold';
+ if (opt_args.italic) linkEl.style.fontStyle = 'italic';
+ if (opt_args.className) linkEl.className = opt_args.className;
+ if (opt_args.parent) Polymer.dom(opt_args.parent).appendChild(linkEl);
+ if (opt_args.marginLeft) linkEl.style.marginLeft = opt_args.marginLeft;
+ if (opt_args.marginRight) linkEl.style.marginRight = opt_args.marginRight;
+ if (opt_args.backgroundColor) {
+ linkEl.style.backgroundColor = opt_args.backgroundColor;
+ }
+ if (opt_args.textContent) {
+ Polymer.dom(linkEl).textContent = opt_args.textContent;
+ }
+ }
+ return linkEl;
+ }
+
+ function createDiv(opt_dictionary) {
+ const divEl = document.createElement('div');
+ if (opt_dictionary) {
+ if (opt_dictionary.className) {
+ divEl.className = opt_dictionary.className;
+ }
+ if (opt_dictionary.parent) {
+ Polymer.dom(opt_dictionary.parent).appendChild(divEl);
+ }
+ if (opt_dictionary.textContent) {
+ Polymer.dom(divEl).textContent =
+ opt_dictionary.textContent;
+ }
+ if (opt_dictionary.maxWidth) {
+ divEl.style.maxWidth = opt_dictionary.maxWidth;
+ }
+ }
+ return divEl;
+ }
+
+ function createScopedStyle(styleContent) {
+ const styleEl = document.createElement('style');
+ styleEl.scoped = true;
+ Polymer.dom(styleEl).innerHTML = styleContent;
+ return styleEl;
+ }
+
+ function valuesEqual(a, b) {
+ if (a instanceof Array && b instanceof Array) {
+ return a.length === b.length && JSON.stringify(a) === JSON.stringify(b);
+ }
+ return a === b;
+ }
+
+ function createSelector(
+ targetEl, targetElProperty,
+ settingsKey, defaultValue,
+ items, opt_namespace) {
+ let defaultValueIndex;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (valuesEqual(item.value, defaultValue)) {
+ defaultValueIndex = i;
+ break;
+ }
+ }
+ if (defaultValueIndex === undefined) {
+ throw new Error('defaultValue must be in the items list');
+ }
+
+ const selectorEl = document.createElement('select');
+ selectorEl.addEventListener('change', onChange);
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ const optionEl = document.createElement('option');
+ Polymer.dom(optionEl).textContent = item.label;
+ optionEl.targetPropertyValue = item.value;
+ optionEl.item = item;
+ Polymer.dom(selectorEl).appendChild(optionEl);
+ }
+ function onChange(e) {
+ const value = selectorEl.selectedOptions[0].targetPropertyValue;
+ tr.b.Settings.set(settingsKey, value, opt_namespace);
+ targetEl[targetElProperty] = value;
+ }
+ const oldSetter = targetEl.__lookupSetter__('selectedIndex');
+ selectorEl.__defineGetter__('selectedValue', function(v) {
+ return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue;
+ });
+ selectorEl.__defineGetter__('selectedItem', function(v) {
+ return selectorEl.children[selectorEl.selectedIndex].item;
+ });
+ selectorEl.__defineSetter__('selectedValue', function(v) {
+ for (let i = 0; i < selectorEl.children.length; i++) {
+ const value = selectorEl.children[i].targetPropertyValue;
+ if (valuesEqual(value, v)) {
+ const changed = selectorEl.selectedIndex !== i;
+ if (changed) {
+ selectorEl.selectedIndex = i;
+ onChange();
+ }
+ return;
+ }
+ }
+ throw new Error('Not a valid value');
+ });
+
+ const initialValue = tr.b.Settings.get(
+ settingsKey, defaultValue, opt_namespace);
+ let didSet = false;
+ for (let i = 0; i < selectorEl.children.length; i++) {
+ if (valuesEqual(selectorEl.children[i].targetPropertyValue,
+ initialValue)) {
+ didSet = true;
+ targetEl[targetElProperty] = initialValue;
+ selectorEl.selectedIndex = i;
+ break;
+ }
+ }
+ if (!didSet) {
+ selectorEl.selectedIndex = defaultValueIndex;
+ targetEl[targetElProperty] = defaultValue;
+ }
+
+ return selectorEl;
+ }
+
+ function createEditCategorySpan(optionGroupEl, targetEl) {
+ const spanEl = createSpan({className: 'edit-categories'});
+ Polymer.dom(spanEl).textContent = 'Edit categories';
+ Polymer.dom(spanEl).classList.add('labeled-option');
+
+ spanEl.addEventListener('click', function() {
+ targetEl.onClickEditCategories();
+ });
+ return spanEl;
+ }
+
+ function createOptionGroup(targetEl, targetElProperty,
+ settingsKey, defaultValue,
+ items) {
+ function onChange() {
+ let value = [];
+ if (this.value.length) {
+ value = this.value.split(',');
+ }
+ tr.b.Settings.set(settingsKey, value);
+ targetEl[targetElProperty] = value;
+ }
+
+ const optionGroupEl = createSpan({className: 'labeled-option-group'});
+ const initialValue = tr.b.Settings.get(settingsKey, defaultValue);
+ for (let i = 0; i < items.length; ++i) {
+ const item = items[i];
+ const id = 'category-preset-' + item.label.replace(/ /g, '-');
+
+ const radioEl = document.createElement('input');
+ radioEl.type = 'radio';
+ Polymer.dom(radioEl).setAttribute('id', id);
+ Polymer.dom(radioEl).setAttribute('name', 'category-presets-group');
+ Polymer.dom(radioEl).setAttribute('value', item.value);
+ radioEl.addEventListener('change', onChange.bind(radioEl, targetEl,
+ targetElProperty,
+ settingsKey));
+ if (valuesEqual(initialValue, item.value)) {
+ radioEl.checked = true;
+ }
+
+ const labelEl = document.createElement('label');
+ Polymer.dom(labelEl).textContent = item.label;
+ Polymer.dom(labelEl).setAttribute('for', id);
+
+ const spanEl = createSpan({className: 'labeled-option'});
+ Polymer.dom(spanEl).appendChild(radioEl);
+ Polymer.dom(spanEl).appendChild(labelEl);
+
+ spanEl.__defineSetter__('checked', function(opt_bool) {
+ const changed = radioEl.checked !== (!!opt_bool);
+ if (!changed) return;
+
+ radioEl.checked = !!opt_bool;
+ onChange();
+ });
+ spanEl.__defineGetter__('checked', function() {
+ return radioEl.checked;
+ });
+
+ Polymer.dom(optionGroupEl).appendChild(spanEl);
+ }
+ Polymer.dom(optionGroupEl).appendChild(
+ createEditCategorySpan(optionGroupEl, targetEl));
+ // Since this option group element is not yet added to the tree,
+ // querySelector will fail during updateEditCategoriesStatus_ call.
+ // Hence, creating the element with the 'expanded' classlist category
+ // added, if last selected value was 'Manual' selection.
+ if (!initialValue.length) {
+ Polymer.dom(optionGroupEl).classList.add('categories-expanded');
+ }
+ targetEl[targetElProperty] = initialValue;
+
+ return optionGroupEl;
+ }
+
+ let nextCheckboxId = 1;
+ function createCheckBox(targetEl, targetElProperty,
+ settingsKey, defaultValue,
+ label, opt_changeCb) {
+ const buttonEl = document.createElement('input');
+ buttonEl.type = 'checkbox';
+
+ let initialValue = defaultValue;
+ if (settingsKey !== undefined) {
+ initialValue = tr.b.Settings.get(settingsKey, defaultValue);
+ buttonEl.checked = !!initialValue;
+ }
+ if (targetEl) {
+ targetEl[targetElProperty] = initialValue;
+ }
+
+ function onChange() {
+ if (settingsKey !== undefined) {
+ tr.b.Settings.set(settingsKey, buttonEl.checked);
+ }
+ if (targetEl) {
+ targetEl[targetElProperty] = buttonEl.checked;
+ }
+ if (opt_changeCb) {
+ opt_changeCb.call();
+ }
+ }
+
+ buttonEl.addEventListener('change', onChange);
+
+ const id = '#checkbox-' + nextCheckboxId++;
+
+ const spanEl = createSpan();
+ spanEl.style.display = 'flex';
+ spanEl.style.whiteSpace = 'nowrap';
+ Polymer.dom(buttonEl).setAttribute('id', id);
+
+ const labelEl = document.createElement('label');
+ Polymer.dom(labelEl).textContent = label;
+ Polymer.dom(labelEl).setAttribute('for', id);
+ Polymer.dom(spanEl).appendChild(buttonEl);
+ Polymer.dom(spanEl).appendChild(labelEl);
+
+ spanEl.__defineSetter__('checked', function(opt_bool) {
+ const changed = buttonEl.checked !== (!!opt_bool);
+ if (!changed) return;
+
+ buttonEl.checked = !!opt_bool;
+ onChange();
+ });
+ spanEl.__defineGetter__('checked', function() {
+ return buttonEl.checked;
+ });
+
+ return spanEl;
+ }
+
+ /**
+ * @param {!string} label
+ * @param {function()=} opt_callback
+ * @param {*=} opt_this
+ */
+ function createButton(label, opt_callback, opt_this) {
+ const buttonEl = document.createElement('input');
+ buttonEl.type = 'button';
+ buttonEl.value = label;
+
+ function onClick() {
+ opt_callback.call(opt_this || buttonEl);
+ }
+
+ if (opt_callback) {
+ buttonEl.addEventListener('click', onClick);
+ }
+
+ return buttonEl;
+ }
+
+ function createTextInput(
+ targetEl, targetElProperty, settingsKey, defaultValue) {
+ const initialValue = tr.b.Settings.get(settingsKey, defaultValue);
+ const el = document.createElement('input');
+ el.type = 'text';
+ function onChange(e) {
+ tr.b.Settings.set(settingsKey, el.value);
+ targetEl[targetElProperty] = el.value;
+ }
+ el.addEventListener('input', onChange);
+ el.value = initialValue;
+ targetEl[targetElProperty] = initialValue;
+
+ return el;
+ }
+
+ function isElementAttachedToDocument(el) {
+ let cur = el;
+ while (Polymer.dom(cur).parentNode) {
+ cur = Polymer.dom(cur).parentNode;
+ }
+ return (cur === el.ownerDocument || cur.nodeName === '#document-fragment');
+ }
+
+ function asHTMLOrTextNode(value, opt_ownerDocument) {
+ if (value instanceof Node) {
+ return value;
+ }
+ const ownerDocument = opt_ownerDocument || document;
+ return ownerDocument.createTextNode(value);
+ }
+
+ return {
+ createSpan,
+ createLink,
+ createDiv,
+ createScopedStyle,
+ createSelector,
+ createOptionGroup,
+ createCheckBox,
+ createButton,
+ createTextInput,
+ isElementAttachedToDocument,
+ asHTMLOrTextNode,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html
new file mode 100644
index 00000000000..46313143710
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ test('simpleSpanAndDiv', function() {
+ const divEl = tr.ui.b.createDiv({
+ className: 'a-div-class', parent: document.body
+ });
+ const testText = 'some span text';
+ const spanEl = tr.ui.b.createSpan({
+ className: 'a-span-class',
+ textContent: testText,
+ parent: divEl
+ });
+ const eltInDocument = Polymer.dom(document)
+ .querySelector('.a-div-class>.a-span-class');
+ assert.strictEqual(Polymer.dom(eltInDocument).textContent, testText);
+ Polymer.dom(eltInDocument.parentElement).removeChild(eltInDocument);
+ });
+
+ test('createSpan_ownerDocument', function() {
+ const spanEl = tr.ui.b.createSpan({
+ className: 'a-span-class',
+ bold: true,
+ ownerDocument: THIS_DOC
+ });
+ assert.strictEqual(spanEl.ownerDocument, THIS_DOC);
+ });
+
+ test('createLink', function() {
+ const linkEl = tr.ui.b.createLink({
+ parent: document.body,
+ className: 'a-link-class',
+ textContent: 'Google',
+ href: 'http://www.google.com/'
+ });
+ const eltInDocument = Polymer.dom(document)
+ .querySelector('.a-link-class');
+ assert.strictEqual(Polymer.dom(eltInDocument).textContent, 'Google');
+ assert.strictEqual(eltInDocument.href, 'http://www.google.com/');
+ Polymer.dom(eltInDocument.parentElement).removeChild(eltInDocument);
+ });
+
+ test('checkboxFromDefaults', function() {
+ const target = {foo: undefined};
+ const cb = tr.ui.b.createCheckBox(
+ target, 'foo', 'myCheckBox', false, 'Foo');
+ assert.isFalse(target.foo);
+ });
+
+ test('checkboxFromSettings', function() {
+ tr.b.Settings.set('myCheckBox', true);
+ const target = {foo: undefined};
+ const cb = tr.ui.b.createCheckBox(
+ target, 'foo', 'myCheckBox', false, 'Foo');
+ assert.isTrue(target.foo);
+ });
+
+ test('checkboxChanged', function() {
+ const target = {foo: undefined};
+ const cb = tr.ui.b.createCheckBox(
+ target, 'foo', 'myCheckBox', false, 'Foo');
+ cb.checked = true;
+
+ assert.isTrue(tr.b.Settings.get('myCheckBox', undefined));
+ assert.isTrue(target.foo);
+ });
+
+ test('selectorSettingsAlreaySet', function() {
+ tr.b.Settings.set('myScale', 0.25);
+
+ const target = {
+ scale: 314
+ };
+ const sel = tr.ui.b.createSelector(
+ target, 'scale',
+ 'myScale', 0.375,
+ [{label: '6.25%', value: 0.0625},
+ {label: '12.5%', value: 0.125},
+ {label: '25%', value: 0.25},
+ {label: '37.5%', value: 0.375},
+ {label: '50%', value: 0.5},
+ {label: '75%', value: 0.75},
+ {label: '100%', value: 1},
+ {label: '200%', value: 2}
+ ]);
+ assert.strictEqual(target.scale, 0.25);
+ assert.strictEqual(sel.selectedIndex, 2);
+ });
+
+ test('selectorSettingsDefault', function() {
+ const target = {
+ scale: 314
+ };
+ const sel = tr.ui.b.createSelector(
+ target, 'scale',
+ 'myScale', 0.375,
+ [{label: '6.25%', value: 0.0625},
+ {label: '12.5%', value: 0.125},
+ {label: '25%', value: 0.25},
+ {label: '37.5%', value: 0.375},
+ {label: '50%', value: 0.5},
+ {label: '75%', value: 0.75},
+ {label: '100%', value: 1},
+ {label: '200%', value: 2}
+ ]);
+ assert.strictEqual(target.scale, 0.375);
+ assert.strictEqual(sel.selectedIndex, 3);
+ });
+
+ test('selectorSettingsChanged', function() {
+ const target = {
+ scale: 314
+ };
+ const sel = tr.ui.b.createSelector(
+ target, 'scale',
+ 'myScale', 0.375,
+ [{label: '6.25%', value: 0.0625},
+ {label: '12.5%', value: 0.125},
+ {label: '25%', value: 0.25},
+ {label: '37.5%', value: 0.375},
+ {label: '50%', value: 0.5},
+ {label: '75%', value: 0.75},
+ {label: '100%', value: 1},
+ {label: '200%', value: 2}
+ ]);
+ assert.strictEqual(sel.selectedValue, 0.375);
+ sel.selectedValue = 0.75;
+ assert.strictEqual(target.scale, 0.75);
+ assert.strictEqual(sel.selectedValue, 0.75);
+ assert.strictEqual(undefined), 0.75, tr.b.Settings.get('myScale');
+ });
+
+ test('asHTMLOrTextNode_string', function() {
+ // Default owner document.
+ let node = tr.ui.b.asHTMLOrTextNode('Hello, World!');
+ assert.instanceOf(node, Node);
+ assert.strictEqual(Polymer.dom(node).textContent, 'Hello, World!');
+ assert.strictEqual(node.ownerDocument, document);
+
+ // Custom owner document.
+ node = tr.ui.b.asHTMLOrTextNode('Bye, World!', THIS_DOC);
+ assert.instanceOf(node, Node);
+ assert.strictEqual(Polymer.dom(node).textContent, 'Bye, World!');
+ assert.strictEqual(node.ownerDocument, THIS_DOC);
+ });
+
+ test('asHTMLOrTextNode_node', function() {
+ // Node object. Owner document should NOT be modified.
+ let node = document.createTextNode('Hi', THIS_DOC);
+ assert.strictEqual(tr.ui.b.asHTMLOrTextNode(node), node);
+ assert.strictEqual(node.ownerDocument, document);
+
+ // HTMLElement object. Owner document should NOT be modified.
+ node = THIS_DOC.createElement('div');
+ assert.strictEqual(tr.ui.b.asHTMLOrTextNode(node), node);
+ assert.strictEqual(node.ownerDocument, THIS_DOC);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html
new file mode 100644
index 00000000000..614c3c2fb8f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id="tr-ui-b-drag-handle">
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ box-sizing: border-box;
+ display: block;
+ }
+
+ :host(.horizontal-drag-handle) {
+ background-image: -webkit-gradient(linear,
+ 0 0, 0 100%,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ cursor: ns-resize;
+ flex: 0 0 auto;
+ height: 7px;
+ position: relative;
+ }
+
+ :host(.vertical-drag-handle) {
+ background-image: -webkit-gradient(linear,
+ 0 0, 100% 0,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-left: 1px solid white;
+ border-right: 1px solid #8e8e8e;
+ cursor: ew-resize;
+ flex: 0 0 auto;
+ position: relative;
+ width: 7px;
+ }
+ </style>
+ <div></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-drag-handle',
+
+ created() {
+ this.lastMousePos_ = 0;
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onMouseUp_ = this.onMouseUp_.bind(this);
+ this.addEventListener('mousedown', this.onMouseDown_);
+ this.target_ = undefined;
+ this.horizontal = true;
+ this.observer_ = new WebKitMutationObserver(
+ this.didTargetMutate_.bind(this));
+ this.targetSizesByModeKey_ = {};
+ this.currentDraggingSize_ = undefined;
+ },
+
+ get modeKey_() {
+ return this.target_.className === '' ? '.' : this.target_.className;
+ },
+
+ get target() {
+ return this.target_;
+ },
+
+ set target(target) {
+ this.observer_.disconnect();
+ this.target_ = target;
+ if (!this.target_) return;
+ this.observer_.observe(this.target_, {
+ attributes: true,
+ attributeFilter: ['class']
+ });
+ },
+
+ get horizontal() {
+ return this.horizontal_;
+ },
+
+ set horizontal(h) {
+ this.horizontal_ = h;
+ if (this.horizontal_) {
+ this.className = 'horizontal-drag-handle';
+ } else {
+ this.className = 'vertical-drag-handle';
+ }
+ },
+
+ get vertical() {
+ return !this.horizontal_;
+ },
+
+ set vertical(v) {
+ this.horizontal = !v;
+ },
+
+ forceMutationObserverFlush_() {
+ const records = this.observer_.takeRecords();
+ if (records.length) {
+ this.didTargetMutate_(records);
+ }
+ },
+
+ didTargetMutate_(e) {
+ const modeSize = this.targetSizesByModeKey_[this.modeKey_];
+ if (modeSize !== undefined) {
+ this.setTargetSize_(modeSize);
+ return;
+ }
+
+ // If we hadn't previously sized the target, then just remove any manual
+ // sizing that we applied.
+ this.target_.style[this.targetStyleKey_] = '';
+ },
+
+ get targetStyleKey_() {
+ return this.horizontal_ ? 'height' : 'width';
+ },
+
+ getTargetSize_() {
+ // Get the actual size, which may be different from the expected size
+ // because of size constraints (e.g. min-width) etc.
+ const size =
+ parseInt(window.getComputedStyle(this.target_)[this.targetStyleKey_]);
+ this.targetSizesByModeKey_[this.modeKey_] = size;
+ return size;
+ },
+
+ setTargetSize_(s) {
+ this.target_.style[this.targetStyleKey_] = s + 'px';
+ this.targetSizesByModeKey_[this.modeKey_] = this.getTargetSize_();
+ tr.b.dispatchSimpleEvent(this, 'drag-handle-resize', true, false);
+ },
+
+ applyDelta_(delta) {
+ // Apply new size to the target.
+ if (this.target_ === this.nextElementSibling) {
+ this.currentDraggingSize_ += delta;
+ } else {
+ this.currentDraggingSize_ -= delta;
+ }
+ this.setTargetSize_(this.currentDraggingSize_);
+ },
+
+ onMouseMove_(e) {
+ // Compute the difference in height position.
+ const curMousePos = this.horizontal_ ? e.clientY : e.clientX;
+ const delta = this.lastMousePos_ - curMousePos;
+
+ this.applyDelta_(delta);
+
+ this.lastMousePos_ = curMousePos;
+ e.preventDefault();
+ return true;
+ },
+
+ onMouseDown_(e) {
+ if (!this.target_) return;
+ this.forceMutationObserverFlush_();
+ // Start with the current actual size.
+ this.currentDraggingSize_ = this.getTargetSize_();
+ this.lastMousePos_ = this.horizontal_ ? e.clientY : e.clientX;
+ document.addEventListener('mousemove', this.onMouseMove_);
+ document.addEventListener('mouseup', this.onMouseUp_);
+ e.preventDefault();
+ return true;
+ },
+
+ onMouseUp_(e) {
+ document.removeEventListener('mousemove', this.onMouseMove_);
+ document.removeEventListener('mouseup', this.onMouseUp_);
+ e.preventDefault();
+ this.currentDraggingSize_ = undefined;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html
new file mode 100644
index 00000000000..9faa91fce81
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const createDragHandle = function() {
+ const el = document.createElement('div');
+ el.style.border = '1px solid black';
+ el.style.width = '200px';
+ el.style.height = '200px';
+ el.style.display = 'flex';
+ el.style.flexDirection = 'column';
+
+ const upperEl = document.createElement('div');
+ upperEl.style.flex = '1 1 auto';
+ upperEl.style.minHeight = '0';
+
+ const lowerEl = document.createElement('div');
+ lowerEl.style.height = '100px';
+ lowerEl.style.minHeight = '50px';
+
+ const dragHandle = document.createElement('tr-ui-b-drag-handle');
+ dragHandle.target = lowerEl;
+
+ Polymer.dom(el).appendChild(upperEl);
+ Polymer.dom(el).appendChild(dragHandle);
+ Polymer.dom(el).appendChild(lowerEl);
+ el.upperEl = upperEl;
+ el.dragHandle = dragHandle;
+ el.lowerEl = lowerEl;
+
+ el.getLowerElHeight = function() {
+ return parseInt(getComputedStyle(this.lowerEl).height);
+ };
+ return el;
+ };
+
+ test('instantiate', function() {
+ this.addHTMLOutput(createDragHandle());
+ });
+
+ test('dragWithoutConstraint', function() {
+ const el = createDragHandle();
+ this.addHTMLOutput(el);
+
+ const dragHandle = el.dragHandle;
+ assert.strictEqual(el.getLowerElHeight(), 100);
+ dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}});
+ dragHandle.onMouseMove_({clientX: 0, clientY: -10, preventDefault() {}});
+ assert.strictEqual(el.getLowerElHeight(), 110);
+ dragHandle.onMouseUp_({preventDefault() {}});
+ });
+
+ test('dragWithConstraint', function() {
+ const el = createDragHandle();
+ this.addHTMLOutput(el);
+
+ const dragHandle = el.dragHandle;
+ assert.strictEqual(el.getLowerElHeight(), 100);
+ dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}});
+ dragHandle.onMouseMove_({clientX: 0, clientY: 60, preventDefault() {}});
+ // The actual size is constrained by minHeight.
+ assert.strictEqual(el.getLowerElHeight(), 50);
+ dragHandle.onMouseUp_({preventDefault() {}});
+
+ // Drag again. Should based on the actual size.
+ dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}});
+ dragHandle.onMouseMove_({clientX: 0, clientY: -10, preventDefault() {}});
+ assert.strictEqual(el.getLowerElHeight(), 60);
+ dragHandle.onMouseUp_({preventDefault() {}});
+ });
+
+ test('classNameMutation', function() {
+ const el = createDragHandle();
+
+ const styleEl = document.createElement('style');
+ Polymer.dom(styleEl).textContent =
+ '.mode-a { height: 100px; } .mode-b { height: 50px; }';
+ Polymer.dom(document.head).appendChild(styleEl);
+
+ this.addHTMLOutput(el);
+
+ try {
+ const dragHandle = el.dragHandle;
+ const mouseDown = {clientX: 0, clientY: 0, preventDefault() {}};
+ const mouseMove = {clientX: 0, clientY: -10, preventDefault() {}};
+ const mouseUp = {preventDefault() {}};
+
+ el.lowerEl.className = 'mode-a';
+ assert.strictEqual(el.getLowerElHeight(), 100);
+ dragHandle.onMouseDown_(mouseDown);
+ dragHandle.onMouseMove_(mouseMove);
+ assert.strictEqual(el.getLowerElHeight(), 110);
+ dragHandle.onMouseUp_(mouseUp);
+
+ // Change the class, which should restore the layout
+ // to the default sizing for mode-b
+ el.lowerEl.className = 'mode-b';
+ dragHandle.forceMutationObserverFlush_();
+ assert.strictEqual(el.getLowerElHeight(), 50);
+
+ dragHandle.onMouseDown_(mouseDown);
+ dragHandle.onMouseMove_(mouseMove);
+ assert.strictEqual(el.getLowerElHeight(), 60);
+ dragHandle.onMouseUp_(mouseUp);
+
+ // Restore the class-a, which should restore the layout
+ // to sizing when we were changed.
+ el.lowerEl.className = 'mode-a';
+ dragHandle.forceMutationObserverFlush_();
+ assert.strictEqual(el.getLowerElHeight(), 110);
+
+ dragHandle.onMouseDown_(mouseDown);
+ dragHandle.onMouseMove_(mouseMove);
+ assert.strictEqual(el.getLowerElHeight(), 120);
+ dragHandle.onMouseUp_(mouseUp);
+ } finally {
+ Polymer.dom(document.head).removeChild(styleEl);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html b/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html
new file mode 100644
index 00000000000..449f3576274
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html
@@ -0,0 +1,415 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/elided_cache.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides various helper methods for drawing to a provided
+ * canvas.
+ */
+tr.exportTo('tr.ui.b', function() {
+ const elidedTitleCache = new tr.ui.b.ElidedTitleCache();
+ const ColorScheme = tr.b.ColorScheme;
+ const colorsAsStrings = ColorScheme.colorsAsStrings;
+
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const blackColorId = ColorScheme.getColorIdForReservedName('black');
+
+ /**
+ * This value is used to allow for consistent style UI elements.
+ * Thread time visualisation uses a smaller rectangle that has this height.
+ * @const
+ */
+ const THIN_SLICE_HEIGHT = 4;
+
+ /**
+ * This value is used to for performance considerations when drawing large
+ * zoomed out traces that feature cpu time in the slices. If the waiting
+ * width is less than the threshold, we only draw the rectangle as a solid.
+ * @const
+ */
+ const SLICE_WAITING_WIDTH_DRAW_THRESHOLD = 3;
+
+ /**
+ * If the slice has mostly been waiting to be scheduled on the cpu, the
+ * wall clock will be far greater than the cpu clock. Draw the slice
+ * only as an idle slice, if the active width is not thicker than the
+ * threshold.
+ * @const
+ */
+ const SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD = 1;
+
+ /**
+ * Should we elide text on trace labels?
+ * Without eliding, text that is too wide isn't drawn at all.
+ * Disable if you feel this causes a performance problem.
+ * This is a default value that can be overridden in tracks for testing.
+ * @const
+ */
+ const SHOULD_ELIDE_TEXT = true;
+
+ /**
+ * Draw the define line into |ctx|.
+ *
+ * @param {Context} ctx The context to draw into.
+ * @param {float} x1 The start x position of the line.
+ * @param {float} y1 The start y position of the line.
+ * @param {float} x2 The end x position of the line.
+ * @param {float} y2 The end y position of the line.
+ */
+ function drawLine(ctx, x1, y1, x2, y2) {
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ }
+
+ /**
+ * Draw the defined triangle into |ctx|.
+ *
+ * @param {Context} ctx The context to draw into.
+ * @param {float} x1 The first corner x.
+ * @param {float} y1 The first corner y.
+ * @param {float} x2 The second corner x.
+ * @param {float} y2 The second corner y.
+ * @param {float} x3 The third corner x.
+ * @param {float} y3 The third corner y.
+ */
+ function drawTriangle(ctx, x1, y1, x2, y2, x3, y3) {
+ ctx.beginPath();
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.lineTo(x3, y3);
+ ctx.closePath();
+ }
+
+ /**
+ * Draw an arrow into |ctx|.
+ *
+ * @param {Context} ctx The context to draw into.
+ * @param {float} x1 The shaft x.
+ * @param {float} y1 The shaft y.
+ * @param {float} x2 The head x.
+ * @param {float} y2 The head y.
+ * @param {float} arrowLength The length of the head.
+ * @param {float} arrowWidth The width of the head.
+ */
+ function drawArrow(ctx, x1, y1, x2, y2, arrowLength, arrowWidth) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const len = Math.sqrt(dx * dx + dy * dy);
+ const perc = (len - arrowLength) / len;
+ const bx = x1 + perc * dx;
+ const by = y1 + perc * dy;
+ const ux = dx / len;
+ const uy = dy / len;
+ const ax = uy * arrowWidth;
+ const ay = -ux * arrowWidth;
+
+ ctx.beginPath();
+ drawLine(ctx, x1, y1, x2, y2);
+ ctx.stroke();
+
+ drawTriangle(ctx,
+ bx + ax, by + ay,
+ x2, y2,
+ bx - ax, by - ay);
+ ctx.fill();
+ }
+
+ /**
+ * Draw the provided slices to the screen.
+ *
+ * Each of the elements in |slices| must provide the follow methods:
+ * * start
+ * * duration
+ * * colorId
+ * * selected
+ *
+ * @param {Context} ctx The canvas context.
+ * @param {TimelineDrawTransform} dt The draw transform.
+ * @param {float} viewLWorld The left most point of the world viewport.
+ * @param {float} viewRWorld The right most point of the world viewport.
+ * @param {float} viewHeight The height of the viewport.
+ * @param {Array} slices The slices to draw.
+ * @param {bool} async Whether the slices are drawn with async style.
+ */
+ function drawSlices(ctx, dt, viewLWorld, viewRWorld, viewHeight, slices,
+ async) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const height = viewHeight * pixelRatio;
+ const viewL = dt.xWorldToView(viewLWorld);
+ const viewR = dt.xWorldToView(viewRWorld);
+
+ let darkRectHeight = THIN_SLICE_HEIGHT * pixelRatio;
+
+ // Not enough space for both colors, use light color only.
+ if (height < darkRectHeight) {
+ darkRectHeight = 0;
+ }
+
+ const lightRectHeight = height - darkRectHeight;
+
+ ctx.save();
+ const rect = new tr.ui.b.FastRectRenderer(
+ ctx, viewL, viewR, 2, 2, colorsAsStrings);
+ rect.setYandH(0, height);
+
+ const lowSlice = tr.b.findLowIndexInSortedArray(
+ slices,
+ function(slice) { return slice.start + slice.duration; },
+ viewLWorld);
+
+ let hadTopLevel = false;
+
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ const x = slice.start;
+ if (x > viewRWorld) break;
+
+ const xView = dt.xWorldToView(x);
+ let wView = 1;
+ if (slice.duration > 0) {
+ const w = Math.max(slice.duration, 0.000001);
+ wView = Math.max(dt.xWorldVectorToView(w), 1);
+ }
+
+ const colorId = EventPresenter.getSliceColorId(slice);
+ const alpha = EventPresenter.getSliceAlpha(slice, async);
+ const lightAlpha = alpha * 0.70;
+
+ if (async && slice.isTopLevel) {
+ rect.setYandH(3, height - 3);
+ hadTopLevel = true;
+ } else {
+ rect.setYandH(0, height);
+ }
+
+ // If cpuDuration is available, draw rectangles proportional to the
+ // amount of cpu time taken.
+ if (!slice.cpuDuration) {
+ // No cpuDuration available, draw using only one alpha.
+ rect.fillRect(xView, wView, colorId, alpha);
+ continue;
+ }
+
+ let activeWidth = wView * (slice.cpuDuration / slice.duration);
+ let waitingWidth = wView - activeWidth;
+
+ // Check if we have enough screen space to draw the whole slice, with
+ // both color tones.
+ //
+ // Truncate the activeWidth to 0 if it is less than 'threshold' pixels.
+ if (activeWidth < SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD) {
+ activeWidth = 0;
+ waitingWidth = wView;
+ }
+
+ // Truncate the waitingWidth to 0 if it is less than 'threshold' pixels.
+ if (waitingWidth < SLICE_WAITING_WIDTH_DRAW_THRESHOLD) {
+ activeWidth = wView;
+ waitingWidth = 0;
+ }
+
+ // We now draw the two rectangles making up the event slice.
+ // NOTE: The if statements are necessary for performance considerations.
+ // We do not want to force draws, if the width of the rectangle is 0.
+ //
+ // First draw the solid color, representing the 'active' part.
+ if (activeWidth > 0) {
+ rect.fillRect(xView, activeWidth, colorId, alpha);
+ }
+
+ // Next draw the two toned 'idle' part.
+ // NOTE: We subtract 1 from the left-hand edge and draw one extra pixel to
+ // prevent drawing artifacts. Without this, the two parts of the slice
+ // ('active' and 'idle') may appear split apart.
+ if (waitingWidth > 0) {
+ // First draw the light toned top part.
+ rect.setYandH(0, lightRectHeight);
+ rect.fillRect(xView + activeWidth - 1,
+ waitingWidth + 1, colorId, lightAlpha);
+ // Then the solid bottom half.
+ rect.setYandH(lightRectHeight, darkRectHeight);
+ rect.fillRect(xView + activeWidth - 1,
+ waitingWidth + 1, colorId, alpha);
+ // Reset for the next slice.
+ rect.setYandH(0, height);
+ }
+ }
+ rect.flush();
+
+ if (async && hadTopLevel) {
+ // Draw a top border over async slices in order to visually separate
+ // them from events above it.
+ // See https://github.com/google/trace-viewer/issues/725.
+ rect.setYandH(2, 1);
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ const x = slice.start;
+ if (x > viewRWorld) break;
+
+ if (!slice.isTopLevel) continue;
+
+ const xView = dt.xWorldToView(x);
+ let wView = 1;
+ if (slice.duration > 0) {
+ const w = Math.max(slice.duration, 0.000001);
+ wView = Math.max(dt.xWorldVectorToView(w), 1);
+ }
+
+ rect.fillRect(xView, wView, blackColorId, 0.7);
+ }
+ rect.flush();
+ }
+
+ ctx.restore();
+ }
+
+ /**
+ * Draw the provided instant slices as lines to the screen.
+ *
+ * Each of the elements in |slices| must provide the follow methods:
+ * * start
+ * * duration with value of 0.
+ * * colorId
+ * * selected
+ *
+ * @param {Context} ctx The canvas context.
+ * @param {TimelineDrawTransform} dt The draw transform.
+ * @param {float} viewLWorld The left most point of the world viewport.
+ * @param {float} viewRWorld The right most point of the world viewport.
+ * @param {float} viewHeight The height of the viewport.
+ * @param {Array} slices The slices to draw.
+ * @param {Numer} lineWidthInPixels The width of the lines.
+ */
+ function drawInstantSlicesAsLines(
+ ctx, dt, viewLWorld, viewRWorld, viewHeight, slices, lineWidthInPixels) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const height = viewHeight * pixelRatio;
+
+ ctx.save();
+ ctx.lineWidth = lineWidthInPixels * pixelRatio;
+
+ const lowSlice = tr.b.findLowIndexInSortedArray(
+ slices,
+ function(slice) { return slice.start; },
+ viewLWorld);
+
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ const x = slice.start;
+ if (x > viewRWorld) break;
+
+ ctx.strokeStyle = EventPresenter.getInstantSliceColor(slice);
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.beginPath();
+ ctx.moveTo(xView, 0);
+ ctx.lineTo(xView, height);
+ ctx.stroke();
+ }
+ ctx.restore();
+ }
+
+ /**
+ * Draws the labels for the given slices.
+ *
+ * The |slices| array must contain objects with the following API:
+ * * start
+ * * duration
+ * * title
+ * * didNotFinish (optional)
+ *
+ * @param {Context} ctx The graphics context.
+ * @param {TimelineDrawTransform} dt The draw transform.
+ * @param {float} viewLWorld The left most point of the world viewport.
+ * @param {float} viewRWorld The right most point of the world viewport.
+ * @param {Array} slices The slices to label.
+ * @param {bool} async Whether the slice labels are drawn with async style.
+ * @param {float} fontSize The font size.
+ * @param {float} yOffset The font offset.
+ */
+ function drawLabels(ctx, dt, viewLWorld, viewRWorld, slices, async,
+ fontSize, yOffset) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ ctx.save();
+
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ ctx.font = (fontSize * pixelRatio) + 'px sans-serif';
+
+ if (async) {
+ ctx.font = 'italic ' + ctx.font;
+ }
+
+ const cY = yOffset * pixelRatio;
+
+ const lowSlice = tr.b.findLowIndexInSortedArray(
+ slices,
+ function(slice) { return slice.start + slice.duration; },
+ viewLWorld);
+
+ // Don't render text until it is 20px wide
+ const quickDiscardThreshold = pixWidth * 20;
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ if (slice.start > viewRWorld) break;
+
+ if (slice.duration <= quickDiscardThreshold) continue;
+
+ // Clip slice boundaries to viewport.
+ const xLeftClipped = Math.max(slice.start, viewLWorld);
+ const xRightClipped = Math.min(slice.start + slice.duration, viewRWorld);
+ const visibleWidth = xRightClipped - xLeftClipped;
+
+ const title = slice.title +
+ (slice.didNotFinish ? ' (Did Not Finish)' : '');
+
+ let drawnTitle = title;
+ let drawnWidth = elidedTitleCache.labelWidth(ctx, drawnTitle);
+ const fullLabelWidth = elidedTitleCache.labelWidthWorld(
+ ctx, drawnTitle, pixWidth);
+ if (SHOULD_ELIDE_TEXT && fullLabelWidth > visibleWidth) {
+ const elidedValues = elidedTitleCache.get(
+ ctx, pixWidth,
+ drawnTitle, drawnWidth,
+ visibleWidth);
+ drawnTitle = elidedValues.string;
+ drawnWidth = elidedValues.width;
+ }
+
+ if (drawnWidth * pixWidth < visibleWidth) {
+ ctx.fillStyle = EventPresenter.getTextColor(slice);
+ const cX = dt.xWorldToView((xLeftClipped + xRightClipped) / 2);
+ ctx.fillText(drawnTitle, cX, cY, drawnWidth);
+ }
+ }
+ ctx.restore();
+ }
+
+ return {
+ drawSlices,
+ drawInstantSlicesAsLines,
+ drawLabels,
+
+ drawLine,
+ drawTriangle,
+ drawArrow,
+
+ elidedTitleCache_: elidedTitleCache,
+
+ THIN_SLICE_HEIGHT,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html
new file mode 100644
index 00000000000..0c37e9910b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<dom-module id='tr-ui-b-dropdown'>
+ <template>
+ <style>
+ button {
+ @apply --dropdown-button;
+ }
+ button.open {
+ @apply --dropdown-button-open;
+ }
+ dialog {
+ position: absolute;
+ margin: 0;
+ padding: 1em;
+ border: 1px solid darkgrey;
+ @apply --dropdown-dialog;
+ }
+ </style>
+
+ <button id="button" on-tap="open">[[label]]</button>
+
+ <dialog id="dialog" on-tap="onDialogTap_" on-cancel="close">
+ <slot></slot>
+ </dialog>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.b', function() {
+ Polymer({
+ is: 'tr-ui-b-dropdown',
+
+ properties: {
+ label: {
+ type: String,
+ value: '',
+ },
+ },
+
+ open() {
+ if (this.isOpen) return;
+
+ Polymer.dom(this.$.button).classList.add('open');
+ const buttonRect = this.$.button.getBoundingClientRect();
+ this.$.dialog.style.top = buttonRect.bottom - 1 + 'px';
+ this.$.dialog.style.left = buttonRect.left + 'px';
+ this.$.dialog.showModal();
+
+ const dialogRect = this.$.dialog.getBoundingClientRect();
+ if (dialogRect.right > window.innerWidth) {
+ // If the dialog's right edge falls past the right edge of the window,
+ // then move the dialog to the left so that its right edge lines up with
+ // the button's right edge, but not so far left that its left edge falls
+ // past the left edge of the window.
+ this.$.dialog.style.left = Math.max(0, buttonRect.right -
+ dialogRect.width) + 'px';
+ }
+ },
+
+ onDialogTap_(event) {
+ // Clicking on elements inside the dialog should never close it.
+ if (event.detail.sourceEvent.srcElement !== this.$.dialog) return;
+
+ // Close the dialog when the user clicks on the backdrop outside the
+ // dialog, which sends click events to the dialog even though the
+ // coordinates are outside the dialog.
+ const dialogRect = this.$.dialog.getBoundingClientRect();
+ let inside = true;
+ inside &= event.detail.x >= dialogRect.left;
+ inside &= event.detail.x < dialogRect.right;
+ inside &= event.detail.y >= dialogRect.top;
+ inside &= event.detail.y < dialogRect.bottom;
+ if (inside) return;
+
+ event.preventDefault();
+ this.close();
+ },
+
+ close() {
+ if (!this.isOpen) return;
+ this.$.dialog.close();
+ Polymer.dom(this.$.button).classList.remove('open');
+ this.$.button.focus();
+ },
+
+ get isOpen() {
+ return this.$.button.classList.contains('open');
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html
new file mode 100644
index 00000000000..bdc118a4d09
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function dispatchClick(elem, x, y) {
+ const clickEvent = document.createEvent('MouseEvents');
+ const bubbles = true;
+ const cancelable = false;
+ const button = 0;
+ const ctrlKey = false;
+ const altKey = false;
+ const shiftKey = false;
+ const metaKey = false;
+ clickEvent.initMouseEvent(
+ 'click', bubbles, cancelable, document.defaultView, button, x, y, x,
+ y, ctrlKey, altKey, shiftKey, metaKey, button, elem);
+ elem.dispatchEvent(clickEvent);
+ }
+
+ test('basic', function() {
+ const dd = document.createElement('tr-ui-b-dropdown');
+ dd.style.marginLeft = '50px';
+ dd.style.width = '50px';
+ dd.label = 'Settings';
+
+ const textDiv = tr.ui.b.createDiv({textContent: 'text'});
+ Polymer.dom(dd).appendChild(textDiv);
+ const target = {};
+ const checkbox = tr.ui.b.createCheckBox(
+ target, 'enabled', undefined, true, 'checkbox');
+ const actualCheckbox = checkbox.querySelector('input');
+ Polymer.dom(dd).appendChild(checkbox);
+
+ const container = tr.ui.b.createDiv();
+ container.style.height = '100px';
+ Polymer.dom(container).appendChild(dd);
+ Polymer.dom(container).appendChild(
+ tr.ui.b.createDiv({textContent: 'some text'}));
+ this.addHTMLOutput(container);
+
+ dd.open();
+ assert.isTrue(dd.isOpen);
+
+ dd.close();
+ assert.isFalse(dd.isOpen);
+
+ dd.open();
+ assert.isTrue(dd.isOpen);
+
+ // Dispatching a click event at contents of the dropdown should never close
+ // it, even if it is outside of the dropdown, which can happen if the user
+ // presses the spacebar while the checkbox is focused.
+ const actualCheckboxRect = actualCheckbox.getBoundingClientRect();
+ dispatchClick(
+ actualCheckbox, actualCheckboxRect.left, actualCheckboxRect.top);
+ assert.isTrue(dd.isOpen);
+ dispatchClick(actualCheckbox, 0, 0);
+ assert.isTrue(dd.isOpen);
+
+ const textDivRect = textDiv.getBoundingClientRect();
+ dispatchClick(textDiv, textDivRect.left, textDivRect.top);
+ assert.isTrue(dd.isOpen);
+ dispatchClick(textDiv, 0, 0);
+ assert.isTrue(dd.isOpen);
+
+ // Clicking outside the dropdown should close it.
+ dispatchClick(dd.$.dialog, 0, 0);
+ assert.isFalse(dd.isOpen);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html b/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html
new file mode 100644
index 00000000000..85e6cf681f6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides a caching layer for elided text values.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Cache for elided strings.
+ * Moved from the ElidedTitleCache protoype to a "global" for speed
+ * (variable reference is 100x faster).
+ * key: String we wish to elide.
+ * value: Another dict whose key is width
+ * and value is an ElidedStringWidthPair.
+ */
+ const elidedTitleCacheDict = new Map();
+ const elidedTitleCache = new ElidedTitleCache();
+
+ /**
+ * A cache for elided strings.
+ * @constructor
+ */
+ function ElidedTitleCache() {
+ // TODO(jrg): possibly obsoleted with the elided string cache.
+ // Consider removing.
+ this.textWidthMap = new Map();
+ }
+
+ ElidedTitleCache.prototype = {
+ /**
+ * Return elided text.
+ *
+ * @param {ctx} Context The graphics context.
+ * @param {pixWidth} Pixel width.
+ * @param {title} Original title text.
+ * @param {width} Drawn width in world coords.
+ * @param {sliceDuration} Where the title must fit (in world coords).
+ * @return {ElidedStringWidthPair} Elided string and width.
+ */
+ get(ctx, pixWidth, title, width, sliceDuration) {
+ let elidedDict = elidedTitleCacheDict.get(title);
+ if (!elidedDict) {
+ elidedDict = new Map();
+ elidedTitleCacheDict.set(title, elidedDict);
+ }
+
+ let elidedDictForPixWidth = elidedDict.get(pixWidth);
+ if (!elidedDictForPixWidth) {
+ elidedDict.set(pixWidth, new Map());
+ elidedDictForPixWidth = elidedDict.get(pixWidth);
+ }
+
+ let stringWidthPair = elidedDictForPixWidth.get(sliceDuration);
+ if (stringWidthPair === undefined) {
+ let newtitle = title;
+ let elided = false;
+ while (this.labelWidthWorld(ctx, newtitle, pixWidth) > sliceDuration) {
+ if (newtitle.length * 0.75 < 1) break;
+ newtitle = newtitle.substring(0, newtitle.length * 0.75);
+ elided = true;
+ }
+
+ if (elided && newtitle.length > 3) {
+ newtitle = newtitle.substring(0, newtitle.length - 3) + '...';
+ }
+
+ stringWidthPair = new ElidedStringWidthPair(
+ newtitle, this.labelWidth(ctx, newtitle));
+ elidedDictForPixWidth.set(sliceDuration, stringWidthPair);
+ }
+ return stringWidthPair;
+ },
+
+ quickMeasureText_(ctx, text) {
+ let w = this.textWidthMap.get(text);
+ if (!w) {
+ w = ctx.measureText(text).width;
+ this.textWidthMap.set(text, w);
+ }
+ return w;
+ },
+
+ labelWidth(ctx, title) {
+ return this.quickMeasureText_(ctx, title) + 2;
+ },
+
+ labelWidthWorld(ctx, title, pixWidth) {
+ return this.labelWidth(ctx, title) * pixWidth;
+ }
+ };
+
+ /**
+ * A pair representing an elided string and world-coordinate width
+ * to draw it.
+ * @constructor
+ */
+ function ElidedStringWidthPair(string, width) {
+ this.string = string;
+ this.width = width;
+ }
+
+ return {
+ ElidedTitleCache,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html
new file mode 100644
index 00000000000..977561e2787
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides color scheme related functions.
+ */
+tr.exportTo('tr.ui.b', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const colors = ColorScheme.colors;
+ const colorsAsStrings = ColorScheme.colorsAsStrings;
+
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * Provides methods to get view values for events.
+ */
+ const EventPresenter = {
+ getSelectableItemColorAsString(item) {
+ const offset = this.getColorIdOffset_(item);
+ const colorId = ColorScheme.getVariantColorId(item.colorId, offset);
+ return colorsAsStrings[colorId];
+ },
+
+ getColorIdOffset_(event) {
+ return event.selectionState;
+ },
+
+ getTextColor(event) {
+ if (event.selectionState === SelectionState.DIMMED) {
+ return 'rgb(60,60,60)';
+ }
+ return 'rgb(0,0,0)';
+ },
+
+ getSliceColorId(slice) {
+ const offset = this.getColorIdOffset_(slice);
+ return ColorScheme.getVariantColorId(slice.colorId, offset);
+ },
+
+ getSliceAlpha(slice, async) {
+ let alpha = 1;
+ if (async) {
+ alpha *= 0.3;
+ }
+ return alpha;
+ },
+
+ getInstantSliceColor(instant) {
+ const offset = this.getColorIdOffset_(instant);
+ const colorId = ColorScheme.getVariantColorId(instant.colorId, offset);
+ return colors[colorId].toStringWithAlphaOverride(1.0);
+ },
+
+ getObjectInstanceColor(instance) {
+ const offset = this.getColorIdOffset_(instance);
+ const colorId = ColorScheme.getVariantColorId(instance.colorId, offset);
+ return colors[colorId].toStringWithAlphaOverride(0.25);
+ },
+
+ getObjectSnapshotColor(snapshot) {
+ const offset = this.getColorIdOffset_(snapshot);
+ let colorId = snapshot.objectInstance.colorId;
+ colorId = ColorScheme.getVariantColorId(colorId, offset);
+ return colors[colorId];
+ },
+
+ getCounterSeriesColor(colorId, selectionState,
+ opt_alphaMultiplier) {
+ const event = {selectionState};
+ const offset = this.getColorIdOffset_(event);
+ const c = colors[ColorScheme.getVariantColorId(colorId, offset)];
+ return c.toStringWithAlphaOverride(
+ opt_alphaMultiplier !== undefined ? opt_alphaMultiplier : 1.0);
+ },
+
+ getBarSnapshotColor(snapshot, offset) {
+ const snapshotOffset = this.getColorIdOffset_(snapshot);
+ let colorId = snapshot.objectInstance.colorId;
+ colorId = ColorScheme.getAnotherColorId(colorId, offset);
+ colorId = ColorScheme.getVariantColorId(colorId, snapshotOffset);
+ return colors[colorId].toStringWithAlphaOverride(1.0);
+ }
+ };
+
+ return {
+ EventPresenter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html
new file mode 100644
index 00000000000..f9531aa9cdf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ function mockEvent(colorId, selectionState) {
+ return { colorId, selectionState };
+ }
+
+ function mockSnapshot(colorId, selectionState) {
+ return { objectInstance: { colorId }, selectionState };
+ }
+
+ function isColor(color) {
+ return color.toString().startsWith('rgb');
+ }
+
+ test('instantSliceColor', function() {
+ const color = EventPresenter.getInstantSliceColor(mockEvent(1, 0));
+ const variant = EventPresenter.getInstantSliceColor(mockEvent(1, 1));
+ assert.isTrue(isColor(color));
+ assert.notStrictEqual(color, variant);
+ });
+
+ test('objectInstanceColor', function() {
+ const color = EventPresenter.getObjectInstanceColor(mockEvent(2, 0));
+ const variant = EventPresenter.getInstantSliceColor(mockEvent(2, 2));
+ assert.isTrue(isColor(color));
+ assert.notStrictEqual(color, variant);
+ });
+
+ test('objectSnapshotColor', function() {
+ const color = EventPresenter.getObjectSnapshotColor(mockSnapshot(3, 0));
+ const variant = EventPresenter.getObjectSnapshotColor(mockSnapshot(3, 3));
+ assert.isTrue(isColor(color));
+ assert.notStrictEqual(color, variant);
+ });
+
+ test('counterSeriesColor', function() {
+ const color = EventPresenter.getCounterSeriesColor(1, 0);
+ const variant = EventPresenter.getCounterSeriesColor(1, 1);
+ const transparent = EventPresenter.getCounterSeriesColor(1, 0, 0.0);
+ assert.isTrue(isColor(color));
+ assert.isTrue(isColor(transparent));
+ assert.notStrictEqual(color, variant);
+ assert.notStrictEqual(variant, transparent);
+ assert.notStrictEqual(transparent, color);
+ });
+
+ test('barSnapshotColor', function() {
+ const color = EventPresenter.getBarSnapshotColor(mockSnapshot(1, 2), 1000);
+ assert.isTrue(isColor(color));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html b/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html
new file mode 100644
index 00000000000..750140bc8f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides a mechanism for drawing massive numbers of
+ * colored rectangles into a canvas in an efficient manner, provided
+ * they are drawn left to right with fixed y and height throughout.
+ *
+ * The basic idea used here is to fuse subpixel rectangles together so that
+ * we never issue a canvas fillRect for them. It turns out Javascript can
+ * do this quite efficiently, compared to asking Canvas2D to do the same.
+ *
+ * Rather than expending compute cycles trying to figure out an average
+ * color for fused rectangles from css strings, you instead draw using
+ * palettized colors. The fused rect color is chosen from the rectangle with
+ * the higher alpha value, if equal the max palette index encountered.
+ *
+ * Make sure to flush the trackRenderer before finishing drawing in order
+ * to commit any queued drawing operations.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Creates a fast rect renderer with a specific set of culling rules
+ * and color palette.
+ *
+ * Rectangles that are drawn will be clipped horizontally to the range
+ * [xMin, xMax]; this is done because CanvasRenderingContext2D does not draw
+ * rectangles with coordinates of very large magnitude correctly.
+ *
+ * @param {GraphicsContext2D} ctx Canvas2D drawing context.
+ * @param {number} xMin Left border of the viewport (pre-transformation).
+ * @param {number} xMax Right border of the viewport (pre-transformation).
+ * @param {number} minRectSize Only rectangles with width < minRectSize are
+ * considered for merging.
+ * @param {number} maxMergeDist Only rectangles that are at most this far
+ apart are considered for merging.
+ * @param {Array} palette The color palette for drawing. Palette slots
+ * should map to valid Canvas fillStyle strings.
+ *
+ * @constructor
+ */
+ function FastRectRenderer(
+ ctx, xMin, xMax, minRectSize, maxMergeDist, palette) {
+ this.ctx_ = ctx;
+ this.xMin_ = xMin;
+ this.xMax_ = xMax;
+ this.minRectSize_ = minRectSize;
+ this.maxMergeDist_ = maxMergeDist;
+ this.palette_ = palette;
+ }
+
+ FastRectRenderer.prototype = {
+ y_: 0,
+ h_: 0,
+ merging_: false,
+ mergeStartX_: 0,
+ mergeCurRight_: 0,
+ mergedColorId_: 0,
+ mergedAlpha_: 0,
+
+ /**
+ * Changes the y position and height for subsequent fillRect
+ * calls. x and width are specified on the fillRect calls.
+ */
+ setYandH(y, h) {
+ if (this.y_ === y &&
+ this.h_ === h) {
+ return;
+ }
+ this.flush();
+ this.y_ = y;
+ this.h_ = h;
+ },
+
+ /**
+ * Fills rectangle at the specified location, if visible. If the
+ * rectangle is subpixel, it will be merged with adjacent rectangles.
+ * The drawing operation may not take effect until flush is called.
+ * @param {number} colorId The color of this rectangle, as an index
+ * in the renderer's color palette.
+ * @param {number} alpha The opacity of the rectangle as 0.0-1.0 number.
+ */
+ fillRect(x, w, colorId, alpha) {
+ const r = x + w;
+ if (w < this.minRectSize_) {
+ if (r - this.mergeStartX_ > this.maxMergeDist_) {
+ this.flush();
+ }
+ if (!this.merging_) {
+ this.merging_ = true;
+ this.mergeStartX_ = x;
+ this.mergeCurRight_ = r;
+ this.mergedColorId_ = colorId;
+ this.mergedAlpha_ = alpha;
+ } else {
+ this.mergeCurRight_ = r;
+
+ if (this.mergedAlpha_ < alpha ||
+ (this.mergedAlpha_ === alpha && this.mergedColorId_ < colorId)) {
+ this.mergedAlpha_ = alpha;
+ this.mergedColorId_ = colorId;
+ }
+ }
+ } else {
+ if (this.merging_) {
+ this.flush();
+ }
+ this.ctx_.fillStyle = this.palette_[colorId];
+ this.ctx_.globalAlpha = alpha;
+ const xLeft = Math.max(x, this.xMin_);
+ const xRight = Math.min(r, this.xMax_);
+ if (xLeft < xRight) {
+ this.ctx_.fillRect(xLeft, this.y_, xRight - xLeft, this.h_);
+ }
+ }
+ },
+
+ /**
+ * Commits any pending fillRect operations to the underlying graphics
+ * context.
+ */
+ flush() {
+ if (this.merging_) {
+ this.ctx_.fillStyle = this.palette_[this.mergedColorId_];
+ this.ctx_.globalAlpha = this.mergedAlpha_;
+ const xLeft = Math.max(this.mergeStartX_, this.xMin_);
+ const xRight = Math.min(this.mergeCurRight_, this.xMax_);
+ if (xLeft < xRight) {
+ this.ctx_.fillRect(xLeft, this.y_, xRight - xLeft, this.h_);
+ }
+ this.merging_ = false;
+ }
+ }
+ };
+
+ return {
+ FastRectRenderer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html b/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html
new file mode 100644
index 00000000000..0182602631d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.b', function() {
+ const FaviconsByHue = {
+ blue: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAlrklEQVR4Ae2dCXwdVb3H5265yc3SpEk3ukEXCqVUBLT4Wm19oFKtaN0fKijy9CMguPBarIJsIiA8qsjTh7SllAoFeVBaEARkLV1ooXtL0yRdkqZp9u3uy/v/5uY/OZm75y659+acdnLOnP385zv/+58zZ2YMinTplIAhzsoDceaT2RKUQLwHIMFqh0V2ll0kn4XA6byv9/Vw834kX19e7keRQCzhRyk6bJJYRvD1YTXuhRdeqDj77LPPtNls400mU7HRaCzFFggEVJ/iSqhsicFgKIXUKL6bvB6fz9fj9/u7Kb4bPjaK67Xb7Q0HDhw49IUvfKEd2XUb7WpxHIYvXRgJ8AELkzRso1gmKrwkBfjG7373u5Zly5ZNKS8vn2G1Ws80m83YphPI0wnQUemQFp0IzQR9tdfrxXbI5XId6ujo+PCuu+6qXbNmjYfa9NMmngDoBmt+hIe944M53AUhwqwCvXTp0qJrr732opKSkk8XFhZ+imC+gIAryAZB0QnlJuB3OJ3Ot3p6el5/6KGHttxzzz0O6pse+GEP+3AGnKE2EhgG0tAFt99++4WkoT9tsVgW0DaH4guzAeg4+uD0eDxbaXuDNPzrt9xyy3bS8G4qB8BF6OOoKr+yDDfAB0B91VVXFf72t7+9lLT05QUFBZfQoYWtnA+ux+12v0ra/W+/+tWvXlq5cqWTBjUsYR8OgDPU8KGtjR9++OHHx4wZ8+2ioqKv0X4lbfnsWh0Ox9+bmprWzpgxYxsNFBpd1Op5bcbkM+AMtgr11q1bTz/zzDP/gy4Qv02zGtPzmehIY6MZmmq6UF176NChJ+bMmXOkD3QR9khFczY+HwEXwTbV1NTMI229FCYIXSTm43gTho8uUgMwYUir3zN16tR3qAIfbXkJej4dcIxF1dbkm44ePfqZqqqqpTT7MZf2pYsgAZqN2dTS0nLP5MmTX6EsDDrDHqFU7kTnA+Aa2BMmTDBv2bLliyNHjlxCZsgFuXMYhr6nZL7saGtru/eiiy7aUF9f76UeAfKcBz2XAUffVbgJbAuB/Y3KysoldONl5tDjkrs9oBtL+1tbWwH6UwS6/mZSzg0sVwHXTJG9e/deOGXKlOWksS/MOelncYdJo2+vra396axZs7ZTN0XTJYt7Hdq1XANc1dg0DNOqVatGLl68+DZa/3E1XTwCeOn6JLCly6ncU9+mNLnBZRLOYPAHHI5H2l5/8TdHbl3SRjUx6DkztZgrgKOfDLf5xIkT36moqLiLzJG0rAFJAomsKDp1W51S74IZnSIX8DcrXV3LlK/Oe5xqZPsckGc96LkAOPpowrZ79+5ZNK31BzkzQtKI4qxvV0dJTSLJ592kHKu7QfnPxXupFmhzbFkNeTb/tGsae/bs2Va6wr/lrLPO2izhTgLQZIuaaMp1yvTNyvNbb1HomFB1ZtrAUNYqymztGMNt2rhx44T58+evohs1n0r2+AyX8mnT4KIAvZ63lA82f1/55TX1FJ21tnk2As4zJObq6urP0BTgCmlri2TFDmcEcHQDtnlz4w+Uyz+Hm0Rsm2PuPGtcNpkomtZesGBBYXNz8210d+05CXfWsBLaEQNd5I+e8JyyYettCh0zyoBrpawyWbJFg2twv/jiixPnzZu3mhZFzQ2VqIyJRwIZ0+BiZzyeTcqebVcqS350nKKzxmTJBsDRB3WWZN++fXPpps060tpVouxkODEJDAng6GIg0KI0Hv+mcsXnN9FeVsyyDLWJwnCbadXfomnTpm2UcCcGc1blNhiqlNMmblT+9soi6hdmWKC4hlSJDiXgaBsCsNDKvysnTpz4JIWLaJMupyVgKFLGjHtSefrNK2kYFtpwjIeMs6FqWIOb7kr+Yty4cX+m2+0446XLBwkESHuPrPqz8uymX9BwhhTyoQBchZseQiigdcj30grAO+SDCPlAtW4MeLikdMQdyvqt9yp0rCl1SDR5pgFX4V64cGERvdhmRWlp6XU6scjdfJNAcfF1ysqNK5Q5C2F+ZhzyTF4AqHCPGjXKSjdwHqUHfr+ab8cyW8YzZLMo0QTgcj2jfO/S7ynNzS7KxtOI0UqkJC1TGlyFm3pccPDgwfsk3Ck5drlVidX6VWXFxvvAAG0Z0+SZAJzhtjQ2Ni6ld5D8KLeOjOxtyiRgK/6R8uy7S6m+jF14phtwmEBow3L8+PGr6FnJm1MmLFlRbkqgtOxm5am3rgITtIGNtJrJ6QQcHcdPkYUuKL9MsybLKSydlICijKxcrjz+0pdJFKzJ0wZ5ugBnuM27du2aT7ffV9JUIGCXTkqAJEAsjJ2wQlm1fj7tpPWOZzoAB9yo1/zSSy/NoLdJraMwFsdLJyUgSqBQGX/GOuX+FTMoEpCDmZRr8nQBbqIHgovnzp27mtaWlImjkmEpAU0CYGPmR1crF19cTHH4hU854KmuECcMOmo9derUAyNGjLiawtJlWAJZOQ8eTQb27keUyz7xM8qS8jnyVGpwNk0s+/fv/4qEO9oRlWkDJGArvVpZ89JXKC7lMyupApzhNm/YsGH6GWec8eCAAcgdKYFYEhhz2oPK3X+ZTtlSao+nEnDzxWRL0eNmj0q7O9bRlOkhEoA9ft6cR5WPq/Y4IE+J+ZyKSjS7m56jvK+srEzeqQw5epmNyDkbXBRPT8//Kl++6EaKSok9nqwG10yTHTt2fJpWB0q4xYMlw4lLoJhu5z/y3KepYEpMlWQBV7U3mSXFNN99H71YPfEByRJSAqIEwND4yfcpFyzgqcOkGE2mMGtvy2OPPXY9vZjnTLGfMiwlMGgJWCxnKktv/QmVT3pWZbCAM9zmxx9//IzRo0fj0STppARSJ4HykTcqN//3GVRhUqZKMoCrC6no6Zy7yTSxpW5ksiYpAZKA0WhTPj73dxRKakHWYABn7W3Zs2cPvjH5eXlApATSIoGi4i8oK56/tA9ysAr2EnKDARxlzJdddlkJ3dC5N6HWZGYpgUQlMH7SvbRWpYSKsamSUA2JAs7a2/ynP/3pOvrc9eSEWpOZpQQSlYDZPFn54a/xcDoDnpAWTxRw5DfRJ7DL6HUPP060rzK/lMCgJFA+8sfKZd/CqlRc9yXEbCKZWXtbli1b9gN6EX3loDorC0kJJCoBk6lS+ebVP6BiCU8bJgI48ppxU2fs2LHXJNpHmV9KICkJVFZdo3zsY7j5w6ZKXNXFCzhrb/PDDz/8HbK9x8ZVu8wkJZAqCZjNY5Wf3vkdqo4Bj8sWjxdw5DPRt3KKTjvtNNxhkk5KIPMSqBz1E2Xq7ITekBUP4Ky9LevWrfsGae9JmR+ZbFFKgCRgLpik3HL3NygUty0eD+Cq9h4/fnwBbTdIQUsJDKkERo+9QSkr47ubMfmNlQHaG5v56aef/ndaUDVtSAcnG5cSMFumKXc/fDGYpI35jCiXeADH3KOZ7lp+Sy6HjShHmZApCWA57dgJ3wKTtIFNQB7RxQIc6abLL7+cniEesTBiLTJBSiCTEiguWah8/isjqEkAHpXhaIk4M5BuXrp06ZfoOUtcvUonJTD0EjCaipSvff9L1JGYU4bRAEeaCjh9P+fr0jwZ+uMqe9AnAZgpo0Z/nfYY8IgcR0qA9sZmeuCBBybZbLZ/66taelIC2SEBKzF5zTJMWbMdDl5DXDTAVe29aNGib5D2jpQvpEIZISWQEQkYicm5C0QtnjDg6uwJPY72tYx0WDYiJZCoBMorGXDW4iE1hNPMOBMQb1qzZs0MmvueHlJKRmS1BCZYYZoOA2exTFd+dT/eTsuzKSFaPJwkNMDPO++8+fLiMvdA+Z8JJcqPN+9RGnocoZ0PBELjFF2cbjdYIEykvq4wWehd4APb05dBari4gaWCe/p8AT+uFOdT4j7aoJTB7oAGowFurqqqmicBV5QPmgLKX3b7lVbHANmRLLPVVSjnGT6hzFRa44dHHEqIHhQThXC8+YQiqQ66K9rnvakoD1O9DPiAJvSAo8vYjMXFxWZ6U9VFA3IP052fv+5VGntzBW4+SCYl4KtQ/L3tpCBJ0+WpC/hKLgKrvb29DDj41Q4WIvUOcaZHH310lslkGqlPHI77uQd38CgZTBbSVBVKXk+CGYwjS758/ywwS1sIz/oI1uCmmTNnflKaJ7l/OmuQG3migQ9xnvg0W2gaN/2TfYDzoLQDFw5wVYOT/T1XAq7JKacDKuS2csVg1B/unB6W2nkwaiiumEs7rMEBueZEG5zpN9Gt+QKyv+douWQg5yXAkPvtHYO78MxiCZisJXNsVRML7C3HndRN5li1w/WnNPaNDz744Ll0ZpRm8Zhk1wYhAYacjPJBlM7eIgHFUFryxZvPpR6q/Io9DavBJ0yYcJY0T0Qx5U84CDnNrtjb82dQZHqZysefRQPaRltEDc4JRlr7PS1/Ri9HopeAwWRWjLYKQiF/NLnBWgpmocGZY3XYoomCBOybaPXgNKnBVfnk7R8V8qLyvIAcrBoLiqaCXdoYcvXYMeB8KmPfSIBPUVPln7yWQD/kjEEOD7fABsBVfvtGoTIdYoOPHDnSXFhYODmHhyq7noAEgpCPUPyOTiql3QBMoIbsyGo0F04uInYdbW3RTZRbb711AnXZmh3dlr3IhAQYcpooz0RzaWmDTk1r0YLrwS4GwRaJuoMGmXrjOeecI5fHQiLDzKmQF9ILXFXIGYfc8q2jZ4JdBlyFnE9ZHolx1KhR8gJzmMHNw9Ugz8U7nrijWToyZCZFtMEBu7GoqGgiD1j6w08CKuTWUsXv6s65O56GApVdlWM+cnoNbqB3D+JzEdINYwkw5DlnkxvNYJetEdVEETU4Ioy0RLZEzoEPY7r7hh6EvIQ0eQ/FZP/sCpilPgNwKG0VbgyFdzTqCXC8ZFw6KQEAoxgLS3NoPbkR7GosIyxqcBxSgwQcYpCOJWDAOnIrKUbS5AH9M5GcKUt8OiEZcK1HbIMjQiVfAq7JRgb6JADIDQR5tpuuAaMGuGaisAbXIiTgkutwEujX5L2UnJ02uSEIOHdfZVpqcBaH9GNKIKjJQ6yAmOUylYHsa+6cprBZg3MfpA3OkpB+WAkENXmxEnDbs2+e3KABrvU9RINTih56LbMMSAlAAqomL7BRQFOU2SGYgMouOqV1jGHWIrxer50+8iofV8uOQ5a1vVA1OUEecOPtWdlhkxsUH/2saE5lmufBtVifz4erCOmkBGJKIKjJ8V0ETT/GLJPODAG/X8+uOg+O0087BaHB09kJWXd+SSCoyYuUgIceaB/qeXL/AA2uci3a4JB8QGrw/AIwE6NRNbmlcMht8oBftT40ZY2xsw2OsJogAYcopEtUAqomt5Am9w6dJg8ENPNagzysBs/2W7KJCl/mz4wE8OYsg3loNLnKbNAG1+DGqFmDI1LdpA2eGRjytRX19XAEecDr6kMqcyM1BNTrR41ltCxqcAYc6yOlkxIYtASCmhyP9WZ2doVmUXhtL1hWHWtw3lccDkcb1H22L6zROiwDWSmBoCa39mnyDHSRmPV7nG36lliDs1r3t7e31+kzyX0pgcFIQNPkGbrj6be3gV287Z95Vk0U7MCpkdXV1bXyIjMoEPk3eQmokJsKglOIAD1tm6J4Wo7UMsd9PQ+wBse+CvgzzzwjAe+TjvRSIwGGnB4qS02F4WohE8W58zk94CGzKP6XX3652+VyNdN6lFHh6pFxUgKDkQAgDygWxeDzDKZ47DJeV3PvvtfpVQChJgoKs80C+8Xf09NzRJopEIt0qZQAIFfou0GpXoUIVv0uxxHqq8ov+cxzyDShmsFut9elcmCyLikBloAKuZEm71Jsi/vdKrMi4GqTbIMz8cjgw0yK1OB8SKSfagkMgDwVlZMGDzg6oJR9tIFh5lmzwdEMR/pPnjxZiwjppATSJQHVJg/QRaffm3wT9Gvg624GswPgRsXhNLh//fr1u2nRFYCXTkogbRJQbybCXEl2diXgCzh2bthNFQHwAZAz4BgEgEaijz4C29zZ2VkjzRSIRbp0SiAIOT7MgCnExDeyThS/s7uma+vaZqpANFHUbusBZ8i9ra2tWyXgqozknzRLQIMcF56JOiLc19O6lYrB1hmgvVGVCDj2VQ1Ovq+mpmaLBBwikS4TElAhx7vJE55dIWhb6rZQH6G9WYNrXRYBh/ZmDe5buXLlVj85LacMSAmkWQIa5Im0Q4x2bXkUGpzhZo7VWsIBrp4JGzZsaCc7/KDU4olIW+ZNVgL9kMe2x4P2d+dB+86X8NFP1uARAUffWIPDnvHSdOE2CTjEIl0mJRCEPA57nAj3dzXj468qr+SzDa51V9TgiGTAcTZ4yQ7fLAHXZCUDGZSABnlUm5wgba3dDFZpE00Uraf6Bx5YveNM8C5fvnzbJZdc4iwuLqYH7Yavq+ytURq70rRIKIvEGlmZAYswDjZCRBchLUJ0ULeGqYzaQL8AfEj/PA5nz8u/Zw3O2ntAC+EAR0bVnnn33Xe7Gxsb35gyZcqlxhR9mGj/oU7liWfrlPZOd5jRZGfUbK9bmUnPGIYIeEB3B8i1PyUKBHTo+vPFEYrcfpR6orYfR6NZmiUQ8Cs9XU1vbDiyEysI2f5myLVe6wFHAqSlanDyPTt37nz+9NNPTxngv/3DHqW5lV4tkGPO67ErPi+9pgw/mYAGfjyO8zJo+vL6dH2dmc6vb1/fP31/9Pn1+7HK69P15fXt9eUP+LxKR/OB5yk7flrFOfABNehtcCSKgHuvu+66t2n5bGtk7TGgvpg7uQg3BmW22BSTGa8pIwehx+s4L3wxzOXFOM4j+sjHecSwmEcMi3nEsJhHDIt5ENY75IXjMhxWI+P4E6u8Pp3bYV/fHsWDRb/f1Vq3b9XblBzxAhNFowEOte+hlYWO+vr6f6QKcDSaq06F3FQYdeUEow9fDGfLmMU+ieFI/RPziOFU5Y9UT/T4gOJ2tP/D7e7Bmz+hwcNeYKKOcIAjHiaKZqa8+uqr6+l9KYgf9g6QG/sgxwHXbxAQgyCG9fmGal/skxiO1B8xjxhOVf5I9USLV8j+7mjd/Rz1RzRPwGuIiwQ4zBScFaDas3Tp0r0dHR2HpRYPyo8hD+7Jv5mUABj0eeyHjx58Yh+1y4CDVTAb4qIBzpCjEjfNiW+Qd+775dcPeTRdI9NCf+OSlQl98M3RvAFM0sbmCVhNCHAcSah8TYuvXr16PT2MjAql65OAapPjXXzRnP4iCnk5Llw5ToMvhsPlzYU4cQxiOFLfxTxiuC+/3+/xNB9/cz3tito7rHmCIpE0ONJwRrAd7l61alXjkSNHXpBaHKLpd5hZMfELJ3FA9Buy8oESw/p8vC/mEcOcnmu+OAYxHGkcYh4xTPlx38DtaHnhZP3rjZQEDR5xehBF4eIFXDVT1q5d+whp8YhnS7DK4fdXhdyEd/FJl04J+ANef3PDpkeoDTZPkgIcfR2gxe+7776aY8eO/VNq8dDD2A95sjamLE8/eSTggRsuLj2Otn821D5fQ4lxaW8cpWgaHOnQ1pqZQmHXU0899VePxxPWoEeB4ewYchwadhzmw4V4jhPDnJ6oL9YhhuOtRywjhuMtr88n1iGGOZ8YJ4Y5PZKv+H2BthOb/0pl8F5mEfCoFkUswNEHVICLTdVMufPOOw+QFn9TanGIJtTBHjeSucIHCjkQZsfhSOmcL14/2fqSLa/vZ6z6YqXr68M+1p24nK1vHq3++wHaZfMETEaFG2XjARzaWgOcwq4XX3zxYdLiKC9dGAkw5Pqf2czso0OMkRhGXG5u9N5vpb3p/YdpAKy9AR+YjGlJxAs4a3GcPa4lS5bsOnHixGapxUkaEVwQcnqrasYdw80wowMcl/HOJN0gtLfb1bH5yMHHd1FlDDhr75QAjk6yFsdVKyB3bty48UE5owLRRHYa5JgSY8dhniZDPMeJYU5P1BfrEMOR6hHzIBzLcV8j1aePR31cRgxzPjFODPel+xWvv6N5x4OUhCWoYA8MxqW9KV9cJgryAXBocQbcdeONN+6kd4k/J9eoQDyRnQq5se+Fk3yg2UcxDvcdULUmjotcbeQULhtvffr8XC6Sj5a5TORe9KdwXq5PXz5KOn0WUHH2nnyudt/qnVSMtXfMqcH+xuMHHGVYi6sXm2jwpptuWk4PJrfLNSqiSEPDGuShSTImggTUNSdee/uxA2uXUxaGO27bm6uNxwbnvKzF8fOABp2vvfZa89atW/8oLzhZRJF9zVyJnEWmCBKgb14qPZ01f2xv3o03VsE8AXNx295cVSKAo4yoxVXIFy9e/Aw91rZLXnCySCP7Jpo+NNLnPMQvHXAYfjz/UDuXEcNcVowTw5yeal9sQwxHakfMI4bF/HhiyuPq2LV/293PUB6GO2HtjfoHA7g4o4LGnWvWrLnL6XT6pKkCkUZ3gNxAL4HnA4rcCMfrOG+k8rHS420n3nyJthcrPxgK+D2+5oa37qI+qHyRj4vLhLU3xpAo4CjDgOOMUrX4HXfcse/AgQPr6I20SJcuhgQYcvVijS++pN938RpQHD0n1h378Cms99Zrb7CXkBsM4GiAIVenDGnfccMNNzzU0tLSKE2V+OSvmiuYXZFOkwDmvD2e7saa/X99iCLxOBoAF7W3ljfewGABZ1ucpw2d7733XusTTzxxE33+xCNNlfjED3vcqELON2WGr0+WCS03cXtaTmy6qbutppUkyHAnNO+tl/xgAUc9DLmmxWnacAeB/hDdANK3I/cjSCAIebi3d0QokKfRZHcrvZ01D9XtW72DhqjX3mBtUC5ZwGGqaFqcws5LL7109dGjR9+WN4DiPx7DHXLc0HE5Wt7es/m21WCob4PiTOimTjiJJwM46gPg2PiCE2ee/Wc/+9lvyB5vkvY4SSNO12+uxFkgT7LB7vZ6uptq9678DQ3JThsYggkAppgvCg7OJQs4WkUnMH2CMw6dc9ANoJNPPvnkL8ke90p7nCQSpzPS9CFscryHbzhsEEvA7/a2NLzzy46WXSdpV+WH/KQuLFEvu1QAzrY4mypqJ+lVE9u3bdv2Z9jjEnIWd2wfkBsM+W+T9813093K6j/X7l+9nSQjwp3UhaUo5VQAjvoY8gGmysKFC1fSgqxX3G6ckNLFKwEVcu3rY/k5swK729Hb9Mqed29fSXLRmyawCAZ9YSnKOVWAo06GHDTjQgGdti9atOjXdNH5noScpJGAU00VI74+ln+OXv2gOJ0t7x3cduevaXQqJ+TztGDK4IbkUg24aI+rkNNXIrquuOKKG+kBiYNyURZEHr/LR8j99OFXt6v94KH377/R4WjtImkAcBFuMJQS7Q1Jp0NFoHNiBw0Eube2tnbT/PnzFzz3UtMIA76mJV1cEjAYcIhInLgTkuMOZonH3XW8dvdff9zZur+JhtNLGwMO8zal2hviSgfgqBduAOhki7u6u7u3NHWO+yxNidkk5EEhxfM3CHmfSHN0zQq98Fjxunta6w+v+9GphneO0Wj0cKdUc7Nc0wW4qG608AcffNBrMlvfLx0x5XMGo7lAQs6HIbbfLytNnLELZUkOrO2mF2b2nDz64rX1hzccpG7p4YbmBuApd+kCHB3lI8G+2vnOlr0dBYVV+4tKxl1MswWW/gOX8rHlXYUsq+C8ChaeZv8/vOqYvo5hb2l48+d1+9fiNrwId8rmuyMd7HQCLrYJyDXQ20/tOGUxF+6wlU1aYDQWFPGBEwvIcHgJ9MtKE2f4jFkQq9rcnu72xrp//OTIgccx181wY8477XBDBJkGXAO9o2VPm+JzbioZMXWewVRQ2n/g0C3poklgoKyyc57cTxeUXnfHCVrXfU1D7fr9NJ4e2gA4w530OpNoMuK0TAGO9ljlaJB3tVd3u1yNb5ZVzPy40Wyt7L+Y4u5JP5IE+iFnsUbKmfl4zHN7nG3VdXtWXNvU8GYd9QBgZxxujDyTgKM9OAZc9e1dDY6ejoOvl1fNnm0yFY1TaApR/QhoMK/8G0UCGuQGEmUWKHK83jhA89z0gvoPDu1cfn1b864T1H29WZIRzc1iyzTgA+CmTqj7Lkeru6156xsVoy+cQk+fn44DJyHnQxTd1yBXRRk9bzpTsSrQ7/MoLvvJN/a/d9uSno5jLdQew40bOVghmFG4Md5MA4424UJA97rtvub6f71VPupcq9lSNttgNBLj8oZQUFzR/w6UU+ZVOeD2eV2B3u7ax/a9e/PvXI7OTuqxCDcuKDMON6Q2VICjbYacJ/jpHYte/8mjr35gtVUdLCwaPYfmyunDlFKbQ1ixXBByiDRzTl0RGPBiPXd7S8Pbyw68d+/TdAz5YlK8QzkkcEMSQwk42mfI4Wugt53c3uB0nHyttHz6THo4dywOnjRZIK7ojiHPxOw4lg4EYJI4mnfW7V95ff3h9bupd9DarLlhkohTgZk9+/pElS2AA27eVOjt3fW9p4699kr5qFkmc0HZR6TJ0nfEYngDzZUYmQeZrN6ZhEnSeXj1nk2/vr2nsw5vn4LGZrj1i6cG2VLyxYYacIyAz2zW4hro9HPnO3nstZ2FhZX7Cm1j5tCDAEWkyqU2j3HctV+7FJvjWE+CWRKvt6utpeGtX+7f/vv/6zNJGG7McfPFZNpuv8cY/oDkbAAcHRIhF0FXw21N2084HfWv2UonjaHPhEwJaikJ+oAjqdvRINfFD2ZXfSILF5I+Fz2kUP/akT0rlhyv2bCX6mKNDcD1N3CgqIbc4RzPJof+YOoEJx7eioNPl+FDlHSxqdgQnj77h5+oGPeJXxQUlE3Cg7qZ+EmmdnPWYYYjGRec/nMrbnfnsbaT2+6v2f3wZqoPJghDzVOAvNwVDbLCSqbplJTNNsAxKP5hBeR4OBGfSQDkDHpRYWFFyYzzf/Gd4oqpV5JGt+IZxlRqLGorr1zwmdjEmOMZEp/X4erpqFld/f4Djzud7ZghgabGBrDZ1sYsCa/lTqwhKphOly0min6MLCT42KAV2Kbzeb1Ob9Pxf+32utteLSqZOJ4++jRJmi16Efbv95/8rDsi++pzFX3mCM1kvXPkw7X/Vbd31eskc3H6D9pbhBvHJ7mfiv7upjSUjRpcHCD6xyYLa3PW6DBbVM0+4/yffKq88iPXmq0jJuOdf/J2vyjC/nBQk/fviyHRzva6u462N+96qHrng29RHtbUrLUx9cc3bljpsEISq8yKcLYDzkIC5Aw6bHNAzva5CrnZbC6c/pHrLykbefYVZmv5NAk6iy66PwBsV8fhrrYDj1Xv+uOr9GYyBpt9ntcWbe2s1NriiHMFcPSZtTlAhzZn0AE4ww7fOuP86z45ovLcKyzWkecEL0RN0kYnwYguaGP78MJLetl8277O1j2Pffj+n96mPAAZG8BmHxobYPMdSYCdtVqb+qa5XAKcO40+49qBQYc2Z42uAk77qj919tUfqxh1wZXWosrz6cEKslxQbPhOLwZNFKz4I7D9broL2fp+e/OO1TW7H3mPBMNgi75ojgBqvpCkYG64XAQckkW/sYlmCzQ6Ty2KoBeccc53Z5eP/uiXrIWjFpjNRTaD+no0FM1/2DWo6cIRb3D1eh12l7P5jY5TH6yv27cGt9cBsQg1wtDWvIl2dk5obeq75nIVcB4AQ86gs+nCoLNmV7V8YcnY4ikzvr3ANuKMz1mLqi4k0E3q+7nVu6OoIn+cOv9NUyJ4+ACfBKG3t263d9a9XPvh2jecPSdxg4a1M4BmyBlqnvaD1s4ZcyTc0ct1wHlMetBhi7CNziYM+6qmrzrtwtHjJi/6rK1k/OfoiblpAJ1hz0XNzpoai6AANTafu/uwvafh5cajG//ZcmL7KZIJA8xwiz7SoK1ZY+c02DQO1eUL4OJ4grZH0E6HRmetDsAZetE3T5q6eHr5mPPmWQurzjcXls8i0K20VFcx4iWYeA9JFpoyA4CmJatYI0JQu7zOjr0uZ8v7HU073zlW82w1dR7aGPAC5nA+0llj8z2HnDNFaAxhXb4BzoMMUtlvo0Ojs1bXA69qdEqHby4sLLeOm7p4Vln5tAsshRXnFxSMOJseirbgAhXPjAZvmrDYgn7/jRRuPjV+EGLUxbzRBSKWqdJ7RnChGKBPftAt9AMeZ/v7XR2HdzTWPLvX6eyAycFQA2jeGHBOY23NGhuNcEMUzA/HRyo/RhM6ChF0aHbRVhe1O0POceybiovH28ZNW/SR4pJJ55oLiieZzLZJJottPFY2BoHHWnWAT1Wr0owkUn18JJYoHv9xUQiQNd/roJfnNPi89mNed++x3p5jexoPb9zV29uAu4qAlDUx+ww2fI6Dz0CL9nWkzlD23HZ6qef2aKL3HmNl84VBZ83OQEfyOR98lDWOnjB3dFnFOZOttjGTLIWlk81m20RaMlBpUEw2Ay2QoRPARg1SffQXF7F9vtpFaOEgxbSrhuhDAV57gBZ+BBSf3e9ztXq99uMeZ/dRl73pWFf7vqOn6jfBhmYoRe0rwhsuLOZlu5p9tTv5/Gc4Ac7HEWMWN4ZW9AE6Q83Q8z6fHKKvQq+r10DmjrmoZEKx1Ta6yGItK7aYy7AiUvF4u+weV1evy37K4eip7yWzAmBCi4obwwyfta7oI8xAM8TYF/NwWbHevNXWNPYQNxwBF4Uggo4wg8q+CL0IuAg350Ec18H1oi0xjH3RMXiI4zBrVwZcDyxDy1DzPudnn+tjX2x32IQhfOmCEmBZMJDwGXQxLMYBbqSxz5AjDg4+b7wPH9DBMXz6fUCKOEAs+gwv+0gTw9jHBsd+cG+Y/uUDMUyHH3XYLBsGNJIvQq3PgwbEesQGGUDRR1i/Mez6eHEf9WJfOp0EWPi6aLkbQQIsLwYZ2aLFiekRqhwAJkPK8KJMtLhIdcr4PgnwwZECSU4Cejnq91G7Po7BFVvWx+n3xbwyHIcE/h9VLWRYHWXC/QAAAABJRU5ErkJggg==', // @suppress longLineCheck
+
+ green: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAltklEQVR4Ae2dCXQcxZnHR3NoNDp8SD7kU7bxFXCchBhMYoLNmhCcOBBykGw2gYTkPV6AhGXD2sTZJQcJG3jsgw3hscuCsTEsOAQW1sbY+MAHxpYtHzI+5EOy5UMStnWPZkZzab9/j75WTWt6NKO5Z6r82lVdXV1d9e/ffPq6uro7zyBDIhXIi7DyngjLyWJRKhDpCYiy2pwoztrpxSwCb+d1bayFm9f1Yu3+cj2MAgOJH2bXnNnEGiHWppW8d999d/inPvWp6YWFheNMJlOR0WgswdLT06PElFdM+xbn5eWVQDXK76TI7vP57H6/v5PyOxFjobwuh8Nx4dixYye+9rWvtaK4ZqFVNY/TiGUIoQCfsBCbcjaLNVHgJRUQG3/4wx9ali1bNmXYsGEzrFbrdLPZjGUagTyNAB2ZCLXoh3CJoD/p9XqxnOju7j7R1tZ2/LHHHqtbtWqVh47pp0X8AaAZbPmRzvnAJzPXhRBhVoBeunSp7b777ruuuLj4xoKCghsI5s8TcPnpIBT9oNwE/D6Xy7Xdbrd/8Oyzz+5+/PHHndQ2LfA5D3suA85QGwmMPLLQ+b///e/nkIW+0WKxLKBlLuUXpAPQEbTB5fF4KmnZShb+g0ceeaSKLLyb9gPgIvQRVJVdRXIN8CCo77777oI//vGPt5CV/n5+fv5NdGrhK2dDsLvd7k1k3f/n17/+9frly5e7qFM5CXsuAM5QI4a1Nh4/fvza0aNH/4PNZvs2rZfRks2h2el0/u2TTz55dcaMGXuoo7DoolXPajcmmwFnsBWoKysrJ02fPv3v6QLxH2hUY1o2E63XNxqhOUkXqq+eOHHitblz557pBV2EXW/XjM3PRsBFsE21tbXXk7VeCheELhKzsb9Rw0cXqT1wYciqP37FFVd8SBX4aMlK0LPphKMvirWm2FRfX//lESNGLKXRj3m0LoOOAjQas/Py5cuPV1RUbKQiDDrDrrNX5mRnA+Aq2OPHjzfv3r3766WlpUvIDfl85pyG1LeU3Jd9LS0tT1x33XVrzp8/76UWAfKMBz2TAUfbFbgJbAuBfUdZWdkSuvFyZepxydwW0I2lo83NzQD9rwS69mZSxnUsUwFXXZHDhw/PmTJlytNksedknPpp3GCy6FV1dXX/OGvWrCpqpui6pHGr+zct0wBXLDZ1w/TSSy+V3n777b+j+R8/pYtHAC9DrwIO9xHD5c5XDF5fS0ya0MWo3+nwvrBx47nfLLlvKypj0DNmaDFTAEc7GW5zQ0PDD4YPH/4YuSMJmQMSExVpsPPxhjsMHt/FuLWkp8dwqb3dt2zhnD2vUKXsnwPytAc9EwBHG01YDh06NIuGtf5DjoyQGmHC4XMLwmwd/Caft2fnmXr3A3d8Zf9hqgXWHEtaQ57Of9pViz179mwrXeE/MnPmzF0S7sEDGuueJnPevCuusO76sPq6R2bPHm2l+sy0gKG0NZTp2jCG27R27drx8+fPf4lu1NwQ6wnKlf0TZcFF/bwe//Z9uxw/vvfuj89Tftr65ukIOI+QmE+ePPllGgJ8UfraIloDp5MBOFoB37zxQvdPvr5gP24SsW+OsfO0CenkoqhWe8GCBQWXLl36Hd1de1vCnTas9GtIXp5h5LgJ1re3H7z2dwsWjMTUYlwrpZXLki4WXIV73bp1E66//vqVNClK3mLvh1RkGcmy4GJr3B7/zkOVXXfd86PD5yg/bVyWdAAcbVBGSY4cOTKPbtqsJqs9QhRPpqNTIBWAo4U0l+1yw1nXd29duH8nrabFKEuqXRSG20yz/hZPnTp1rYQ7OpjTqTRNUhwxtsK69t3tcxZTuzDCAsOVUiOaSsBxbAhgoZl/d02YMOF1SttokSGDFSCabeVj819/v3LOXdQNCy04xynjLFUHVuGmu5K/HDNmzHN0ux2/eBmyQoEe84gRluc2V13zS+pOSiFPBeAK3PQQQj7NQ36CZgA+Kh9EyAqqgzpBQ4h5w4aZH6URlidwrmljSix5sgFX4F60aJGNXmzzYklJyf1BqsiVrFOgqNh0/5ubJr24aFEp3M+kQ57MCwAF7pEjR1rpBs4KeuD3W1l3NtOkQ6kaRQnXfZfL/+Y3bqz7Ed3f6KZyPIwYbpe4bEuWBVfgphbn19TUPCnhjsu5y6hKCgqM33pr4+QnwQAtSbPkyQCc4bY0NjYupXeQ3JNRZ0Y2Nm4KFBab7tlSdc1SqjBpF56JBhwuEI5hOXfu3N30rOS/xk0tWVFGKjB0mPlfN1bOuRtM0AI2EuomJxJwNBx/iix0QfkNupJ+mtIySAUMpSPyn16z5fPfICnYkicM8kQBznCbq6ur59Pt9+U0FAjYZZAK4J6+aczE/BffWn/1fJIjoXc8EwE44Ea95vXr18+gt0mtpjQmx8sgFVAVIEgKJkzJX/2fq66aQZmAHMzE3ZInCnATPRBcNG/evJU0t2SI2iuZkAoIChiNeUM+O6d45cLbxxVRNv7Cxx3weFeIHwwaar148eJTQ4cO/SmlZUiyAuk4Dh5Ogs5O3wsLPrfnQSoT9zHyeFpw/FhQn+Xo0aPflHCHO6Vym6hASYnpp29v+dw3wQ4tYChuhjdegDPc5jVr1kybPHnyM2IHZFoqMJAC48Zbn/nzi1dNo3Jx9cfjCbh54cKFRfS42Qrpdw90OuV2rQLwx6/9QvGKhQsVfxyQx8WKx6MS/EgUv5vmGTw5ZMgQeadSe/aSvJ5pPrgoj73D91/zr97zEOXFxR+P1YKrrsm+fftupNmBEm7xbMl01AoUlRjvWb1u9o20Y1xclVgBV6w3uSVFNN79JL3LLuoOyR2kAqICYKhisu3JBQvG8tBhTIzGsjNbb8vLL7/8C3oxz3SxoTItFRisAhaLcfqyP435Oe0f86jKYAFnuM2vvPLK5FGjRuHRJBmkAnFToLTM8tCfnpk5mSqMyVWJBXBcWFro6Zw/0Z+Vwrj1TFYkFSAFwNQX5w/5N0rGNCFrMICz9bZ8/PHH+MbkV+UZkQokQoGiQtPX/rb+M7f0Qg5WwV5UYTCAYx/zrbfeWkw3dJ6I6miysFQgSgXGV9ieWHjrqGLajV2VqGqIFnC23ua//OUv99PnriuiOposLBWIUgGLJa9iya8q8HA6Ax6VFY8WcJQ30Sewh9DrHn4WZVtlcanAoBQYXmr62fe+NwGzUnHdFxWz0RRm621ZtmzZT+hF9GWDaq3cSSoQpQImU17ZnfeO+gntFvWwYTSAo6wZN3XKy8vvjbKNsrhUICYFykZa7r1mwUjc/GFXJaL6IgWcrbf5+eef/wH53uUR1S4LSQXipIDZklf+m99N/AFVx4BH5ItHCjjKmehbObaxY8fiDpMMUoGkK0BW/OezZxdH9YasSABn621ZvXr1HWS9Jya9Z/KAUgFSID8/b+KjT02/g5IR++KRAK5Y73HjxuXT8oBUWiqQSgVGlVseoCnZfHdzQH4HKgDrjcX8xhtv/B1NqJqays7JY0sFLPl5U59bVbEQTNLCfOoKEwngGHs0013L78npsLo6yg1JUgAMjhlb8D0wSQvYBOS6YSDAsd30/e9/n54hHrpItxa5QSqQRAWKh5gWffWbY4bSIQF4WIbDbcQvA9vNS5cuvY2es8TVqwxSgZQrYDQabHffU34bNWTAIcNwgGObAjh9P+c70j1J+XmVDehVACyOLs//Dq0y4Loc621g59301FNPTSwsLPyiVFcqkE4K2ArzvvjPv52GIWv2w0P64uEAV6z34sWL76BfjF65dOqzbEsOKQAm5/9diWjFowZcGT2hx9G+nUO6ya5mkAL0WBsDzla8X+tDWWa+uDStWrVqBo19T+u3l8xIawUsplFp3b54NY7mik/703/MxNtpeTSlnxWHk64NKuCf/exn58uLS6086b8+3Pqg4WDNHw0O5yf9Gkuf9+sX6N3twXmaVWwMkUWv+Q7eLVShHk1mv310Kg9Vrt/h/PStQoN/PlVxhBYYa7AbVCwc4PQxzxHXS8ANhkZ7jaGq8W8Gh6ed9MuM4C2ebrD7Jhp6CIJsDr481/UGw4nnqY8MeFB3tYDjF6BY8KKiIjO9qeq6oNI5urL+1L8bOt2XM673PrPf4OjwZDXk/p6e68BqV1cXAx5kxUP54MgzrVixYpbJZCrNuLOagAZnItyQwWQ2GgppXlKeEec8OwON75V+/YErZlHv2A8P6qieBTddeeWVX5LuSZBWGbnCkDs7PQa/PyO7EL7RZI5HTCj+EhXaTwt7IKpfprXgintCBU3kf8+TgIfXNlO2AnJbicVAt7izLoBR2xDLPOoYW/CgP1eiBWf6TXRrPp/877lZp0YOd4ghhyUPNUKRydJYbaa5IyYU5l8+53BRP5hjxYprf9NYNz7zzDOfpl9GSSZ3Wra9vwIMORm9rArUn5Kbfzzt09QphV+xcyEt+Pjx42dK90SUKXvSDDksedYEwnrYyIKZ1J89tOhacN5gpLnfU7Om87Ij/RRgyLPJiFlsZjALC84cK/1GBgdswLqJZg9OzabOcwdl3KcAIC8oNuMtrn2ZGZpCHyxW0xXU/H4Xmgw49xLrRgJ8Sob2VTY7CgVUyLNgnLwXcIXfXgkUpvv54KWlpeaCgoKKKHSSRTNYAQXyIrPB1eXVzOLIrE5ZrcaK0lKbuaXFCbDZYCsuCfcEmcbf/va34ym2cqaMs18BhjyTZ/3TmKB17ncngV1Y8X6AM/XGq65SPsaZ/WdV9jBIAUBuLSSfnPFgIjIoHj2pCFO7xR6oFpy7YRw5cqS8wAw69bmz0gc5cMiwQE0uKrH0G0kRfXDFQbfZbBMyrGuyuXFUQIGc3p/Q7fSRT65O6YjjERJXVX6hCewqHPNRsILAFjyP3j2Iz0XIkMMKBCA3ZdwQosloBLsqyziFogXHBiNNkS3OhrFRdE6GwSsAyPPJkrvJkmeCHVeYNeUBcPbBlc7ziko9AY6XjMsgFVDmkysXnqAjAwIN54NdlWWkRQuOLuRJwCGDDKyA0ZRnsNrM5JOn/zi5yZzHgHPz1VEUZCjkS8BVbWSiVwGGXCEkjVUxGlXA1b85bMHVDAl4Gp/BFDaNIXe7vGk7uEL+iOheK0zzKAqkkxY8hQBlwqEBeX4BJmilZ2uNRuX6UeGYW8gWnNelD85KyDikAgy5uzv9xslNRvUiU217PwtOW7TQq4VlQioABRTIrTQzNc1MeU9eD9gNacHVPzper9dBH3mVj6tJlsMqwJB7yJKnyzg5vTXAITRaYZrHwdV8n8/Xpa7IhFQgjAKAnOZhp83gSo/foGVXGQfHD1D9EcKCh+mT3CQVCFKAIfe6yZKrFAUVSdqK39cjsqtwLfrgaEiPtOBJOx9ZcyBAbs7H3JUUd8mnWPCgn5l4QalskICn+CRl6OEVyMld8brp9VkpMuU9fj+7KCrkIS14v9fpZqjostnJVYDuJJIlJ6RSYMrBrK9HAVyFG71nC45MZZE+eHKhyLajMeQ+jz/phtzvy4MPrrIMbUULzoDbs0102Z/kKgDITRZj0g253+8Huwy40mm24KoCTqezBeZezglXJZGJQSgAyA0EOSx5MgLcfp+7p0V7LLbgTL2/tbX1tLaQXJcKDEYBtuSD2Xcw+zg6u8EuflHMs+KiYAVByTx58mSdvMgMCCL/j12BpEFO9Laed9Yxx70t72ELjnUF8DfffFMC3quOjOKjAEOeyMEVfOyqevtFLeD9RlH8GzZs6Ozu7r5E81FGxqd7shapAI1mwCen5zz93sT45H5Pz6UTey52ktb9XBTor1jv3o1+u91+RropkEWGeCoAyI0EebyHV8Bqt8t7htoKuEMCjn4AcqWAw+E4jQwZpALxVkCBnG7tK5DDZ4nT4nb5wawIuNJ09sFFC+7DSIq04PE+tbI+VoAhj5dPjiHCbrsXgNNTGMEWXBwHVyFvamqq48bIWCqQCAUAeQ8ZcJoBGHP1+KF0NHvALCw4c6zUG8qC+995551DNOkq9iPH3HRZQTYrgJuJmKQVa6CvOffUfNhwiOoRXRSFXwYcx0AGCvjoI7CX2tvba6WbAllkSKQCsUKuXGB2eWsr37twidopuihKs7WAM+Te5ubmSgl4Ik+trJsVYMgHMz0E/ndXm6eS6qI3E+m7KHwsxYLTiq+2tna3BJxlkXGiFQDceDe5EiMd6UIPzLU0OneD2d4FDKtBz4L7li9fXkmzs4IKq3vJhFQgAQow5NFUTYT696w5DwsuuieK/416QgGu/BLWrFnTSn54jbTi0cgty8aqgAo5rj0HWHB7vtvhqTnyUVMrlWYLDrhDAo62YQOsNvwZLw0X7pGAkxIyJFWBgHsy8CHhf9tb3Pj4q8IrxWBXhRs1iBYc6ww4fg1e8sN3ScAhiwzJVoAhJ1dc/2YnNaq5oWsXRQBcdFHU5oo3epAJwBly79NPP73npptuchUVFRWoe+RgwnXRZmh3YBQqu4OuMQuyiX0a6GQHCuhs1D1GX7VBKVhp7APgtfvSS4dcm1bUsQVn6x105FCAo6Diz3z00UedjY2NW6dMmXKL0ag19kHtiHil9nyj4b2dVYaOLvEVFhHvnpKCXs9XDUa3m44dpF1QW7TiB23UWdHdR+cw8DlDBlCgE/S30A5h9tOpLubsaG/r6JWnJ+gNrtbmrRdO7sYMQva/GXK1nVrAsQGaoCDMvufgwYP/N2nSpLgB/sJb6w0tHWhTZgV3t4teidBNjYbkkEhPem2/uCyjpt1fu127f7LLa4+vbZ+2Pdry2vWB9tdu1+6vPV6gvN/vMzTUHv8/Ku2hBaz2gxs1hTLLqIEB995///07aPpss661QS1RhEyEG93LtxbQKxH4+7gQPdLAZRGLad5fzOMyYoxyXEZMi2XEtFhGTItlxLRYBmltQFkE3ofTSmYE/w20v3Y7H4dj7fECrorP42mu2rZhB23VvcDEnuEAh9n30MxC5/nz59+LF+A4aKaGAOT5wbxpO6M9X9jOedqyqVjntujxo21Tostrjxfheldnx3tuu91JxWHBQ15goqpQgCMfFpytuGfTpk3v0PtSkJ/zAZBbLL2QMyRiDIUYCjEtlkllWmyTmNZrk1hGTMervF49YfL99JbNpvrat6k5onsCXvsFPcDhpuBXofjhS5cuPdzW1nZKWvGAfhaGvJ+cMiPRCoBBj8t16tCOTUfoWAw4WAWz/UKoi0wUQmGGHJW4aUx8TVlZ2YP0DR9sz/kAyBG8HsgjQ7IUAOD2jvY1dDwMa0F8hjsk4HoWHO2FyVet+MqVK9+hh5Hl2YQyvQGQm/PJXQkXcKcCge9YcFrJDPFftOVDVJFWWdH2Z4Dy9PpjT92R/e9QH8EiPAwwGtI9oXxdHxzb8ItgP9z90ksvNZ45c+ZdOf8K0vQFC42sKJAzwNoYRfmkiWltOV4Xy4hp3p5psdgHMa3XD7GMmKbyALKrs/3dMx8faKQkLDgAB6MhrTflRww4fi3uV1999QWy4rq/FlSYi0GB3GLJxa4ntc9+r9d/5tjHL9BB2T2JCXA0PsiKP/nkk7Vnz559X1rx/ueVIQ9z8a+OJMsygYGmaHTAXVdnZ+f7x/bsqO0FfEC4cZbC+eDYDmutuimU7v7rX//63x6PR/dPAnbK1QDITcoQYq/fDSHwp5hjMR3I7b9d70+3Xj7XPdj6Yt1f266B6htou7a+3nW6c9lTf/Lwf9PuuJ0suidhPYqBAEdzUAEcecVN+cMf/nCMrPg2acUhTf9goYtOk5ncFT5RKII0B07rbedykcax1hfr/tp2DlTfQNu19dE6Rk4c9o5th3d+cIxW2T0Je3HJ1UQCOKy1Cjilu9etW/c8WXGuQ8YaBVTINflydXAK+H007+TUyedpb7begA9MDuhJRAo4W3H8erqXLFlS3dDQsEtacVJDJ0jIdYSJMhvW29nVuevAtvXVtCsDztY7LoCjSWzF4dgDctfatWufkSMqkEY/AHIzja5gLjMHTgcm9AfyOQ9lOM3bo43FOsS0Xj1iGaQHCtG2D/XxPmKa2yPmiWne3uP3+Zvqjj9D21y0gD0wGJH1pnIDXmSiDAIAD7rYfOihhw7Su8TflnNUFH10/zPTRafJbFZOMp9ojrETp/mEinm6lYbZEG192vLcDr042vZp69fuH247psR2tDS/XbVl/UHaj613RKMnLFEkLgqXZSuuXGzigA8//PDT9GByK/6MyKCvAEOuX0Ju0SoAprzd3a3VO9Y/TdsY7oh9b64vWsDZF8cBXZs3b75UWVn5Z3nByXLqxwHI5c0gfYWCt8B6Nzde+HPj6dN4VhDuCZiL2Pfm2qIBHPuIVlyB/Pbbb3+THmurlhecLKl+DH9cHULkYuyfI45kwX68j5jmfcU8Mc3b4x2LxxDTescRy4hpoTwezXN1dVVvfeuVN6kIwx219Ub1gwGcrbhysYkGrFq16jGXy+WTrgokDR8UyE00iZNPKIojHWngsnr7D7Q90uNEWi7a4w1QHgz5vF5f3ZEDj1ETADdfXEZtvdGFaAHHPgw4flGKFX/00UePHDt2bDW9kRbbZRhAAYYcWMslWAMDPcxgb768mm7qYL631nqDvajCYADHARhytuLOBx544NnLly83SlclMv0BuZFGV2ToU6CH4HY7nI37Nr/3LOXicTSt9e4rHGFqsICzL66Oi+/du7f5tddee5g+f+KRrkpk6pvplr4CObsbORwDKBpy9pyuqX74YkN9M60y3FGNe2uVHyzgqIchV604DRvuI9CfpRtA2uPIdR0FFMjlU1L0pQcvjZo0PHvggw37SCqt9QZrgwqxAg5XRbXilHbdcsstK+vr63fIG0CRnw+GPFf9cbpbaejqaNuxZfXylWCod+G7lmAsJYDjDOLgWPiCE788x4MPPvgb8sc/kf44qRFhCECeez45/O5up/OTqo3v/oakwuvOwBBcADDFfFFycCEWC85HRCMwfIJfHBrnpBtATa+//vqvyB/3Sn+cFIkw4Ja+URxCzHKfHGaZ/tJ76SmdX9FrIJpoVeGHYrDEw4KUHHyIB+BoJxrDrorSSHrVRNWePXuegz8uIY/8BCmQG7P/zQVgAn735aYLz+3fsq6KFBLhjunCUlQ7HoCjPoY8yFVZtGjRcpqQtdGtvLhSPKxMh1MgYMkBefZ65TRJ0NDZ1rpxy2vLl1NHta4JDCaYijnEC3A0hCHnURU02rF48eJ/oYvOvRLy6M6ViVwVoymepye64yeytI8sd1dH+94tb6z4FzDSu/CwYNzgRh/iqSAAF/1xNNhBX4nouPPOOx+iByRq5KQsSB55YMizyRXHiEm3vbNm99o3HnJ2dHSQGgBchBsMxcV6Q+lEOHtonNjAPILcW1dXt3P+/PkLPth/eGgePqclQ0QK4L3synvBs2BKMmYIuhz2c/s2rf1ZY33tJyRAFy0MONzbuFpvCJwIwFEvQhDo5It3d3Z27naYCm6mGXWFPNE9UFT+H04B/vhAgPHM9Mv9fvpglNPZfGjnpntOHzl0lvqrhTuulpv1TBTgogVX0wcOHOiix7j2Dx899is0HJYvIefTMHCc1/uFjUwckcL9EHphpv34gY/uq9nzUU0IuGG5AXjcQ6IAR0MZbI6VxjfV17UVlQw5OqR0xEKah2GRkEd+TlXIIW2GGHK86tjtcjnqjx74pwNb38dteNFyx228W0/FRAIuHhOQq6BfqD1+0WIp2Dds1KgFNCRmkz65KFX4tAp5Bvjk8LndDkfriQN7fn5g6waMdTPcGPNOONxQMtmAq6DTnasWn8e1s7R8wvVkyEv4xKFRMoRXQDUIiiGnz16n4b8eGud2d9kbqnd+cC+9bu0o9chOCwBnuHEzJyF+N9WrhmQBjgOyBVchv9xwobOro3XbqPGTrjVZLGV8MaW2TiZ0FQhATlKyqrolk78B49z0HsGT+zatua/uyMHT1AKAnXS40fNkAo7jITDgStx++aKz+cLZD8onTZ1NryEeA59c+uUBoQb6X4UcBdPAJ8dwJt5CRTMDD+xY88YvGs+caqCWad2SpFhu1i7ZgAfBTY1Q1umdz+7zp45uHXfFjCn0AstJeUYJOZ+ggWLVXUmxKcesQHqWkm6/t2zd+saKJW0Xmy5T2xlu3MjBDMGkwg3tkg04jonQD3S60vbVVh/cPmbyFGu+rXA2+eRkyGGWZBhIAdYpYMST75H30Bg3fcqlp62p4eWNry7/N3rVWju1WYQbF5RJhxu6pQpwHJsh5wsN+nit13+quupA4ZChNSVDh881mkw0wiKtOcQaKEAnCJrMoMwIpJESj6Orlaa8Ltv2v6++QeeQLybFO5QpgRtapBJwHJ8hR6yCfuFUzQX6U7d5RPn4K8kvL5cuC6QaOKiQJ8EfJ2/bgItJR3vbwb1b1v3iaOX2Q9RCWG223HBJxKHAZP/+FMHSBXDAzYsCPV18dp06eGBjecVkk7Ww6DPSZVHO14D/sbsyYMEYCuDOpNfj7mlpOL9yw6oXf996sQFvn4LFZri1k6diOFpsu6YacLSef9lsxVXQ6c+d79ShqoN05/NI0TByWYxwWWjAQPrmYc96nz7xNeWBhxRofNvpbDl7rPpX2/73f97qdUkYboxx88Vkwm6/h+28ZmM6AI4miZCLoCvp86eON9ibWzYPHVk+mlyWKXBZMC7WdyI1vZKrvdqwrLEJArAxSoJvgna0XNpctXntkqOVHx6mWtliA3DtDRwYqpQHkJJOAe3BXFr88PCmSist+OKqjZZCpK+55bYvVEy78pcFRcUT8eRL3zAZbZWhnwIAM5bAw3/dXfaz9SeO/vve99fsovrggjDUPATI011xwPj8smJpeO++6QY4mhUwzwHI8Zg5vrQKyBl0W0FJSfENt/39D0pHj73LYrVayXWR1pwE0guBGYjRMaeOkNBDtc1NDSs/XLP6FVdnJ0ZIYKmxAGz2tTFKwnO5ozsQ7ZjIkC4uiraPLBJiLLAK7NP5vG63t/bQvkMOR8emoWWjx9Fr0CZKt0UrYd96nyvHtkM/xhwudkfsra0fHtz6/j/v2/zuB6S5OPwH6y3CjfMT25+KvubGNZWOFlzsINrHLgtbc7bocFsUyz7vq9+6oXzK9PsKCgsraE6L4rb0nVSxutxOByx5aA3Yz/aRn+1yOOobT598dte6N7dTabbUbLUx9Mc3btjosEEKXXkKc9MdcJYGkDPo8M0BOfvnCuRms7lg7uJv31Q+ruJOa1HxVLzcEv65BJ0lDB0z2LiAJD/7VNOF+pcr1/5tE72uhMHmmMe1RV87La222NNMARxtZmsO0GHNGXQAzrAjtn5x0Te/VD556p0FxSVX4Y1RmIorQSdlhKCAjfFsL1lse+eRptOnXv7ovbd2UBGAjAVgcwyLDbD5jiTATlurTW1TQyYBzo1Gm3HtwKDDmrNFVwCndSW+5uavXzNu8oy7CocMuRpfVgi8hiF3hxcDLgpm/GFilMfg6OjYf+H08ZU0MrKXNGOwxVh0RwA1X0hSMjNCJgIOZdFuLKLbAovOQ4si6PlXz7959tipM28rKhm2wGzNL8TrGHLlopShxoQo3Fr3drsdXZ1tWxtO1byzf9v7uL0OiEWokYa15kX0szPCalPb1ZCpgHMHGHIGnV0XBp0tu2Lli4eNKPrc/C8vKC0v/0phybA5NI5uogldivuSbePpGAkB3JifjU+CODrbqlqamjYc2LZxq73tMm7QsHUG0Aw5Q83DfrDaGeOOMBRinOmAc1+0oPONInZfxFix9BOmXjVq+py5Nw8rG/kVmp47lV+XFvDVM8+NUS11H9R0S91xqq350oYTVZXvnzt15CKJxQAz3GKMbbDWbLEzGmwRDE5nQ8ygIwbksOhs1QE54NbG5qu+cMO0cZOmXW8bMvTqgsLiWQS7FW95hc+ersAHA+1XXmRJlrqbXqxz2NnRvv/CmZMfHtm1/ST1F9YY8ALmUDG2s8WGC5IVYFM/lJAtFpz7wzH6xbADdF4AuBZ4xaJTvrKtoLjYOuvaL80qGzPx8wVDSq622Yo/ZTSbLLhbqjwzqsxPp9JKCMiXqBGaAMQ4UMD1xU0Y8jsMmM2HJ9ZpLprH6bQfc3V07m9uPLvv8J4dh112O1wOhhpA88KA8za21myxldqpfFaFbAWcT5IIOvx00VcXrTtDznkcm4aWlRXOuGbeZ4aXjfm0xVYwMT/fOtFsLRhnwsMYyvCjUQG/76KVD62NtVIHoNWWUiAmoHFRCJAVX5pi+oKdk+zzBbe7+6zH6Trb2tz48fG9O6vbm5txVxGQsiXmmMFGzHmIGWjRvw7dGCqc6UGreqb3J1z70VcAzjFbddGVYbC1sVhW+aFUzPzMqNETJ1YUDyubaLUVVeRbrRNMFnOZyWguzAvAj9fToZ6AmwPLjxUKCk1EMltoir30OJOjhyD2+b0On8fb7O7uPtft7Kq3tzWf/eTs2fr6mmr40AylaH1FeEOlxbLsfnCstCeb/2PNs7mP2r6hz+ICeNmycwwwGWqGnde5jBgjjUWsN4/cHfPQ0lFF9PidzVpUWFRgK8KMSIPL2eXo7qLRuvZWZ3vLxS5yKwAmuwgcM8yI2eqKMdIMNEOMdbEM78t1ckzFciPkIuDimQ0CkjYwqByL8IuAY7u4jcujPqS5XkoGpbEuBhE4TrN1ZcC1wDK0DDWvc3mOuT6OxePmTDrXARdPNGvBcCLWgsvrDDEgRzmOOT9UXTgW5wM6BIZPuw5IkQeIxZjh5RjbxDTWsSBwHFjL0f9Z8BztfthuszaIwy0i1NpyOIBYj3hABlCMkdYuDLs2X1xHvViXQaMAi6/Jlqs6CrBeDDKKhcsTt+tUGQQmQ8rwYp9weXp1yvxeBfjkSEFiU0Cro3YdtWvzGFzxyNo87bpYVqYjUOD/AZrbm7Ts1rpFAAAAAElFTkSuQmCC', // @suppress longLineCheck
+
+ red: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAk/0lEQVR4Ae2dCZxUxZ3Hq8/pnhkGmOEQuQS5VCTxWHEDBlyNkciakMMkxujGuOvHO24IKCae0UQlKwmyroocoqtozGpA4oFiVAQU5IaRcchwDsPczNF39/5/b+bfVL/p7ume6bur+DyqXt31r2//5//q1XvPIJRLpgQMMVYeiDGfyhanBGKdgDirzYvsLLtIPguB0/lc7+vh5vNIvr68Oo8ige6EH6Vo3iSxjODrw1rcm2++2f+MM84YV1hYONRkMhUZjcY+OAKBgOZTXDGVLTYYDH0gNYpvIa/V5/O1+v3+FopvgY+D4tra29uP7N27d98VV1zRiOy6g06DcRyGr1wYCfCEhUnK2yiWiQYvSQG+8ac//all3rx5o/v16ze+oKBgnNlsxjGWQB5LgA5MhrToh1BL0Fd4vV4c+1wu176mpqYvHnnkkf0rVqzwUJt+OuQfALrBmh/hvHc8mfkuCBlmDei5c+fab7nllguLi4svttlsXyeYzyPgrJkgKPpBuQn4LU6n88PW1tZ1ixYt2vjoo486qG964PMe9nwGnKE2EhgG0tDWBx988HzS0BdbLJbpdEymeFsmAB1DH5wej2cTHR+Qhl937733biYN76ZyAFyGPoaqcitLvgEeAvX1119ve/jhhy8nLX211Wq9lKYWtnIuuFa3272WtPv/3nPPPW8tWbLESYPKS9jzAXCGGj60tfGLL764YPDgwT+x2+3fp/MyOnLZ1Tscjj/X1NS8OH78+E9poNDoslbPaTMmlwFnsDWoN23adNq4ceN+TBeIP6FVjbG5THSksdEKTQVdqL64b9++lyZPnlzVCboMe6SiWRufi4DLYJsqKyunkraeCxOELhJzcbxxw0cXqQGYMKTVHz399NM/pgp8dOQk6Lk04RiLpq3JNx04cOAbAwYMmEurH1PoXLkIEqDVmPV1dXWPjhw58l3KwqAz7BFKZU90LgAeBHvYsGHmjRs3/mtpaekcMkPOy55pSH9PyXzZ0tDQ8NiFF1646vDhw17qESDPetCzGXD0XYObwLYQ2FeVlZXNoRsvZ6Yfl+ztAd1Y2lNfXw/QXyHQ9TeTsm5g2Qp40BTZtWvX+aNHj15AGvv8rJN+BneYNPrm/fv3/2LixImbqZuy6ZLBve7atWwDXNPYNAzT0qVLS2fNmvUA7f+4gS4eAbxynRIwHN8ozDseFQZHTW9l4m/3BBa/8nnDfT97vKqBKmPQs2ZpMVsARz8ZbvPRo0ev6d+//yNkjiRlD0hvqUh3ecsrpwtD2+GEdcMfELUNbWLewNniBaqU7XNAnvGgZwPg6KMJx44dOybSstYf1coISSOKsy4tiJLa8ySPX6wvrxN3TLpX7KJaoM1xZDTkmfynPaixJ02aVEBX+PdOmDBhg4K754D2tqTFKKacPVhsOPEnce+kSQK/IjMdYChjFWWmdozhNq1evXrYtGnTltKNmq/3doLypXyyNLgsP49XfPhOpfjZzCcEbKGMtc0zEXBeITFXVFR8g5YAn1O2toxW9+FUAI5ewDY/UC9+PvrXAjeJ2DbH2nnGuEwyUYJae/r06bba2toH6O7a6wrujGGlS0eMBjFw1ADxetMT4oHpZ2lbi3GtlFEmS6Zo8CDca9asGT516tTltClK3WLvglRsEanS4HJvXF6x/v0vxHXfWigOUXzGmCyZADj6oK2S7N69ewrdtFlJWnuALDwVjk8C6QAcPSSTpa6iTvxwwm/EejrNiFWWdJsoDLeZdv3NHDNmzGoFd3wwZ1JuMlkGjBsoVlf9TsykfmGFBYorrUo0nYCjbQjAQjv/rhs+fPjLFLbToVwWS4Boto/sL14++ri4joZhoQNznDbO0tVwEG66K/nLIUOGPEW32/GLVy43JGAeUiKeqvsv8UsaTlohTwfgGtz0EIKV9iE/RjsAH1IPIuQG1SGjCAhDWaF4qHmBeGzwYIG3EaRFk6cacA3uGTNm2OnFNs/16dPn1hChqJOck0CJTdxaeY94bsZkzfxMOeSpvADQ4B44cGAB3cBZRg/8fi/nZjNDBpSuVZRow3d4xGsjHxT/VlsrXJSPlxGjFUlIWqo0uAY39dhaXl4+X8GdkLnLqkrsFvE90uTzwQAdKdPkqQCc4bZUV1fPpXeQ3JhVM6M6mzAJ9LGJG+v/IOZShSm78Ew24DCB0Ibl0KFD19Ozkr9JmLRURVkpgdIi8Zvqx8X1YIIOsJFUMzmZgKPj+FNkoQvK79CqyQIKK6ckIE4pEQsqHxbfIVGwJk8a5MkCnOE2b9++fRrdfl9CS4GAXTklATwiYRpVJp7bfb+YRuJI6h3PZAAOuFGv+a233hpPb5NaSeHkPGJCFSuXnRIgSGwTBomVb/2nGE8jAORgJuGaPFmAm+iB4KIpU6Ysp70lJdk5BarXyZaA0ShKLh4tls+6QBRRW/gLn3DAE10hfjDoaMHx48ef6Nu37w0UVi7FEsjEdfBoImh2iMX97hR3Up6Er5EnUoPjx4L6LHv27PmugjvalKo0WQJ97eKGLx8U3wU7dIChhCneRAHOcJtXrVo1dtSoUQvlAaiwkkB3EqAngxauuk2MpXwJtccTCbj5kksuKaLHzZYpu7u76VTpegnAHr9svFh2yQTNHgfkCdHiiagEPxLN7qbnKOeXlJSoO5X62UvxebbZ4LJ4yB5/muzx2RSXEHu8txo8aJps2bLlYtodqOCWZ0uF45YA7T68cfu94mIqmBBTpbeAa9qbzJIiWu+eTy9Wj3tAqoCSgCwBIETr4/OnjwsuHfaK0d4UZu1tef7552+nF/OMkzuqwkoCPZWA1SzGvXS9uI3K93pVpaeAM9zmF154YdSgQYPwaJJySgIJk8DgvmL2C/8hRlGFvTJVegM4Liwt9HTO78k0KUzYyFRFSgIkATJVCq88S/yOgr3akNUTwFl7W3bu3IlvTH5LzYiSQDIk0KdAXEEbsi7vhBysgr24XE8ARxnzlVdeWUw3dB6LqzWVWUkgTgmMHSgeu3Ky9oFeNlXiqiFewFl7m5988slb6XPXI+NqTWVWEohTAhaTGPnMLIGH0xnwuLR4vIAjv4k+gV1Cr3u4Kc6+quxKAj2SwIA+4qbrpwjsSsV1X1zMxpOZtbdl3rx5P6cX0Zf1qLeqkJJAnBIwmUTZ/TPFz6lY3MuG8QCOvGbc1DnllFNujrOPKruSQK8kQG/Kuple0Yx942yqxFRfrICz9jY/88wz15DtfUpMtatMSgIJkoDZJE5Z9mNxDVXHgMdki8cKOPKZ6Fs59lNPPRV3mJRTEki5BIb0FbdNOj2+N2TFAjhrb8vKlSuvIu09IuUjUw0qCZAErBYx4i/XiasoGLMtHgvgmvYeOnSolY47lKSVBNIpgWH9xR0lJcG7m93y210GaG8c5ldfffVfaEPVmHQOTrWtJEAbsca8f7O4BEzSwXxGFEwsgGPt0Ux3LX+ktsNGlKNKSJEEsJ121CDxIzBJB9gE5BFdd4Aj3XT11VfTM8R9Z0SsRSUoCaRQAn0LxIyrvyb6UpMAPCrD0RLxy0C6ee7cud+m5yzV50VIGMqlXwL0/Kb9nsvEt6kn3S4ZRgMcaRrg9P2cHyjzJP0Tq3rQIQGYKSP6iR/QGQMekeNICdDeOExPPPHEiMLCwq91VK3+VxLIDAkUWcXXnrhaYMma7XDw2sVFA1zT3jNnzryKtHekfF0qVBFKAqmQABFpnDUxRIvHDbi2ekKPo30/FR1WbSgJxCuBwcVBwFmLd6kinGbGLwHxphUrVoynte+xXUqpiIyWQKBoWEb3L1GdozXxsS/9u/Z2Wl5N6aLFYaTrXRDwr371q9PUxaVePJl/3nzef4uaN28S7hNHunQ2EOgSRa/r1rkuEXild1enr6unecJVHktd9OlwaOJp1LPddEApg92QotEANw8YMGCqApwktmen8K9cIURTI8kv810BdXGI72JR73LR9+ND5jvzOx9nD80u11QhVj1DxRjwkBr0gOMXoGnwoqIiM72p6sKQ3Hl64nv0fhGoPZ5Vo8ff7P5+v2jw+Eil5S7kfQKBC8FqW1sbAx6ixRGpd4gzLVu2bKLJZCrVJ+bjebbBzXNkoTsipfRQo0HTWRybWz7BWvqHkYMn0qjYDg8ZoB5w1uCmM8888yJlnoTIKitPGHIj3R3hyc0lHwCPLbRdRB4A56EF5yoc4Igzkf09RQEelFNWBwB5f3okJhfnE2MqNZumgFk6wC4gDzoZcKbfRLfmrWR/Tw7mUoGsl0Ao5DzVueEXmUyThxcW8heUeVDanMmAIwLnxoULF55Nv4w+Wg71X85IgCE3AoEccjScPr8ZderZNCSNX3lo8ioKk28aNmzYhFz8cyYPPF/DHZAbRKPXmzNrK6B6qM0ygbxP6WCOtaUjWYNzgpH2fo/JVwDyYdxmUuH9zWZN3eXKePuYjGAWPDPH2tD0GhwZTLR7cIzS4Jp8cvY/QN6PIG/KAU0OVouMxtPBLh0MuTZ3rMFBPRzOjQT4aO1M/ZfTEmDIc8Emt5s0wDV+OydNY5oBR5ym2ktLS802m21kTs+sGlxQAoC8r4nMFZp9DQAGIct8m9EwstRuh0XCw9DGqAfceP/992MrGrYzKJcnEjgJOdjIUhcQBbcPHQx2wXRwIGyDM/XGs846S22PzdI57k23AXkJmbAnfNm5dwUAn1mkbe3+ohNwRAVYgwcBHzhwoLrA7A0pWVxWg5xe5Wo8qQCzZjQAuNRs7rKSwhocAwHsRrvdPhwnyuWnBAB5H9LkLZomzy4ZFJmNYFfjmHuu1+AGevdgMScqPz8loEGuafLsGr/ZYAC7bI3A1x6751EgwkhbZIvVGjiLJH99QF5Mmrw1SzQ5mKVFcAAOpa3BjdnjkyD1BDheMq6ckoDQNDntQsQSYjY4ghzsBllGWLbBMQaDAhxiUI4lYCLNWEzmiqbJM/zBIKvByIBz9zUNzica+QpwFofyWQIMObGe0c4kAgx4sKeswYMRCvCMnsO0dY4hb/P5M/YZT7NJ0+AsI41pXkVBJCKUicLiUX4XCQDyIhNWyYP6sEuedEZE0+DcLwU4S0L5YSXAkLdrmjxslrRFGmOxwal3bLakraOq4cyWACAv1DR5ZvWTVlHArmaJcM/YRAn+zfF6ve2cqHwlgUgSYMi7rDNTASYs1b7PH5DZ1Zjm/gXH4fP52oInKqAkEEUCgJz2YWeMRU6Xv3p2NZWO1c3gCqfS4FFmVCV1kQBD7qS3aKX7LXE+v1/W4BrXbKJwxwNKg7MolB+rBAC5jd69Ql5anS8goMGDyhqdkS8otQQFeFrnKGsb1zQ5Qa5p8jSNwm8ImihByMNq8EC6/9akSUCq2d5JAK+H0zR576rpUWkwSyuXETW4Zq9QzQFlg/dIvqpQpwQYche9vDvVb7X1BgRs8CDL6JKswbUEAry1s6/KUxLokQQAeQFtQUz1HU96FzrYZcC1vss2uBbhcDgaoO7VnvAeza0q1CmBDsiFcPlTIxJQ7aTXoetbYw3O1PsbGxv/oc+kzpUEeiKBk5q8J6XjL9Pk9YBd/KSYZ81EwQmcFllRUbFfXWR2CET933sJAHKrZq4k9w4nelrldOwnLwg3wqzBka4lvPbaawpwSEO5hEkgCHkS18kB72v1zXrAg+vgTL3/7bffbnG5XLVms3lgwkaoKsp7CQByC0nBo+nRxIvD7ffXrjve1EI1dzFR0FoQcGRobW2tUmYKxKJcIiWgQU6gJ1qRg9U2X6CK+gq4wwKOcQByLUN7e/s/EKGckkCiJQDI6fUOCd9x2O7zgVkZcK3rbIPLGtyHlRSlwRM9tao+loAMOcf1xge8TT4vAPfREaLB5XXwIOTHjh3b35sGVVklge4kAMhhqngTsC0E9dR6fGA2BG70IZwG97/xxhs7aNMVgFdOSSBpEsDNxA5zpXdWuY/MjVW1zTuoowA8BHIGHIMA0Ej00Udga5ubmyuVmQKxKJdMCQByE/ENfd6Tf6C2xR+ofPFITS31UzZRtG7rAWfIvfX19ZsU4MmcWlU3SyAIeQ8UOYCt93g3keelI0R7o34ZcJxrGpx8X2Vl5UYFOESiXCokAMgBI3lxHTDkqxyujVQU2ps1eLDLMuD4MbAG9y1ZsmSTn1wwpwooCSRZAgx5PM3Qg3L+JTX10OAMN3OsVRMOcO2XsGrVqkayw8uVFo9H3CpvbyXAkMNa6e7AQ6DNXl/5W8fqGyk7a/CIgKNvrMFhz3hpufBTBTjEolwqJQDIAXd3DrDWuj34+KvGK/lsgweLyhockQw4fg1essM3KMCDslKBFEqAIY+mxdGdynbPBvIAuGyiIElz8o0eRLB6xy/Bu2DBgk8vvfRSZ1FRkU3Lnaf/VRaVCM/xmpwffSRlBijCuUjxyBsxLUJCpMfbkB39AvD6/jn8fufjh46wBmftHdJCOMCRUbNnPvnkk5bq6uoPRo8efbmRnphOhGvbWiGO/c9fhaeuORHVpaQOt+8rwlmCb7uHyC6k7UgpUctEKBStTEijnSf6iZfzRGhCyxJvO3K96Q7T42mi2nnig21N5dhByPY3Qx7snh5wJEAmmgYn37Nt27a/nnbaaQkDfP+dTwp3dT3aySrn9HtEu9+r2YYQUCw2IgbIeRk0lOO4cOmIk12q88ttI8x9jdR/fX79eXfl9en68pHG7w34xW5nzV8pv4cOeQ08pIpwahltMuDeW2+99SPaPlsfTUuE1NjNSTbCjSEVGS2i0NihD2KFG+U4L3w5jDQ4OY7zyL6cRw7LeeSwnEcOy3nksJwHYb1DXjguw2EtMob/uiuvT+d22Ne3h3iw6Az46he37PyITiNeYKJsNMCh9j20s9Bx+PDhvyUKcDSarQ6Q2wnyaNf4nMa3nTFWjsuEcXNfYu1fsvP3RCbQwLU+598a3W4HBaHBw15gou5wgCMeGpy1uGft2rVv0OskEJ/3DpDbjCYNWoZE9iEghkIOy3nSGZb7JIcj9UnOI4cTlT9SPdHiAeZ2Z93r5MnmCaK7uEiA40eCXwWo9sydO3dXU1PTl0qLd8iPIe84U/+nUgJgsC3g+XJJ8+7d1C4DDlbBbBcX7iITmZCZIUclbloTX1VWVnYnfcMH6XnvADmcKwDZKpcqCUBN13jbVpHnpoPNE+a1SzciaXBkRF1BLb58+fI36GFkVKhcpwQ0Td7lS4xKPMmUgFv4PG+3HXmD2pC1d1jzBP2IBjh+FSgIM8W9dOnS6qqqqjfV/iuShuSwsmJTkEsSSV4Qa9+1Pseb77ZWVVMr0OBgE4yC1bAuVsA1M+XFF19cTFo84q8lbAt5EKkgT80kuwMB/7q2I4upNTZPegU4eh2ixefPn1958ODBd5QW7zqhDHm0q3+V1nMJkPIW9f72d149UVHZCXi3cGOWomlwpENbB80UCrteeeWVZz0eT8Q/CSiUr64DciwhnnQcjnbjArk5PV6fy3KL+va6q6+35fX1d1dfd+n6+vjcL/yBjx3Vz1J5Fx2yeRLVougOcPQHFeBiUzNTfvvb3+4lLf53pcUhmq4ON4IKDB2QY3Lg2JfDPHFyHMLxOq67p/X1try+v93V1126vj6cd9jezr+vaCrfS6dsnoDJqHCjbCyAQ1sHAaewa82aNc+QFkd55cJIQA85w5cKH91hiORwKtpOVhs+4nij89gzNB7W3oAPTHZrScQKOGtx/Hpcc+bM2X706NENSouTNCI4QG4lTZ5qx3AzbGif41Ldl0S0B+1d73dtWNy4ezvVx4Cz9k4I4Ogna3EY9oDcuXr16oVqRQWiiexOavKTiOEyC44vtzisRXbGcxznicfnsrHWp8/P5SL5+v531zd9/fry3aV7aOVkk+P4QsrnpAPsgcGYtDfli8lEQT4ADi3OgLtmz569jd4l/rraowLxRHY2TZPjY6kd/5CTJ1kOR0qPXHP4FK471vr0+blcJF/uc/gehMbq69eXj5buoy2xR31trz/duGMblWPtHdPqCfciFhOF87IW1y420eBdd921gB5MblR7VFhE4X2GPHyqig0ngY49J97GxU27FlA6wx2z7c11xgs42+Jo0Pnee+/Vbtq06U/qgpPFGdkH5FhdUS42CeD5qb2exj997qzFG6tgnoC5mG1vbiUewFFG1uIa5LNmzXqNHmvbri44WaSR/QLaZstLiJyLrXP4sRwox2XkMJeV4+Qwpyfal9uQw5HakfPIYTk/tHej37X9vuMbX6M8DHfc2hv19wRw1uLaxSY6sGLFikecTifegYg6lYsiAUCO1RWeUGRFOFbHeSOV7y491nZizRdve93lB0Nu+qD8O22HH6E+AG6+uIxbe2MM8QKOMgw4flGaFn/ooYd27927dyW9kRbpynUjgSDkeP+HOkJkEKBfwCF/68oXmvdgv7dee4O9uFxPAEcDDDlrcccdd9yxqK6urlqZKrHJH5BbeqRfYqs/G3NhzftEwF39ZNPORdR/PI6m195xD6ungLMtzsuGzs8++6z+pZdeuos+f0JLl8pUiWUmGHL82c73A69hcwm/5/3WQ3eVOxrw2gWGO651b73cewo46mHIg1qclg23EOiL6AaQvh11HkECgNysNDltdPKLfe6GRU837d5CotJr7x5rzN4CDlMlqMUp7Lz88suXHzhw4CN1AygC0WGi8x1y3NCp8To++lXN+uVgqPPgu5ZgLC2AY6rQOA6+4MQvr/3OO++8j+zxGmWPkzRidJq5YuiNvomxoQzLBru72e+pWdS46z7qWjsdYAgmAJhivijYM5cIiaITWD7BLw6dc9ANoGMvv/zy3WSP0zeGevzjo6ryy2H50EKQR7pNnmvx0MvugN/7vuPw3Vucx47RbGv8kA+WeFmwVxAkAnAQjM6wqaJ1kl41sfnTTz99Cva4gjz2OQLk+DBTrjswAbt7r6fhqacbdm6m8cpw9+rCUpZdIgBHfQx5iKkyY8aMJbQh6123Gz9I5WKVwElNnrurK16C+4i39d05NeuXkFz0pgkUZkL+9CcKcMwdQw6acaGATrfPnDnz13TR+ZmCnKQRh4OpYs5Rm9yjXVS2f3ZX3YZfk0g0TsjnZcGEwQ1xJxpw2R7XIKevRJy49tprZ9MDEuVqUxZEHrtjyHNpjRwrJvU+R/nDjZtn13scJ0gaAFyGGwwlRHtD0snY3obOyR00EOTe/fv3r582bdr0pmXv9MVXbpWLTQImklWHQGWRxlY203IB7kaf69CC5p037XDU4osCbXQw4DBvE6q9Mf5kAI564UJAJ1vc1dLSsnFUZctltKOuUEHeIaRY/gfkcBBotq6k+KnzJwKe+mUnym9c13roIA1FD3dCNTfkBZcswGV1Ewxv3bq1rcBk+Xycpd836c+vVUHeMQmx/M+yCgozlkIZkoe2mYrWgKf19ROVt/y55cty6pYebmhuAJ5wlyzA0VGeC/a1zm9z1jaVme17hluKL6HVAgtPXMJHloMVsqxCBJrh4+yA292+tv3Ifz7btAu34WW4E7beHUkMyQRcbhNzEpyXTY5jx+kJly2jrSXTSZPbeeLkAiocXgIsq6Aww2fLiFjY3Cf8nsbX2/bf9mzjLqx1M9xY80463BBCqgEPgr7VWdvQbvCuH28tnUo2eR+eOHRKuegSCMqKTHOY55l44F0mDQHn0eXNX9z8yomKPTSiVjoAOMONmzlJsbup3qBLFeBokJVOEPJyV2PLUW/738+2DbjAZjCV8cVUsHcqEFECgDwoyIi50pOAde46n6NiYePuW9a2HfgH9QJgpxxujD6VgKM9OJ4XzT/gOeHY7W5Yd65t0CS70TRE24nRuWrQkV39H0kCDHmmrK1gZnH7/ZjXsfWRhs23b3HUHKW+682SlGhullmqAQ+Bmzqhndd6He5PHDUfTC48ZXShwXyagpynp3ufzRUIMp0OuwLpWUq6/d72wd21G+fsdzfVUX8YbtzIwQ7BlMINeaQacLQJ1wX0Fr/b9zfnwQ/PKxhUUGKyTjIJo4Enr6OI+j+SBGQ5YcU81Qfgdga8gQpP0/O/qP/4d41eB77yK8ONC8qUww15pQtwtM2Q84VGwEsbyN9srdo60FRYPsRin2wxmOzYS4AHc5WLLoGT5kr0fIlMxY5AvL+k2e9ufK/98Lz7aje9SnPIF5PyHcq0wI2xphNwtM+Qww+CvsFRfaTa2/beuILSM+0G0ynKZIGounephJxNkhpf+7aFjTtvp5WSHdRDaG3W3DBJ5KVAzHHKXaYADrj50KCv8rS0rXFUvXtOwSBTX5P1K8pkiY0NNleSSRNu3sAkKfc0L7+j9sMH97ua8fYpaGyGW795KrbOJyFXugHHkHgu4DPkmjanP3e+Na1V2waa7buHmAsn0/ZRu7YXQ5ksUVE4adIlducKcU0mCW7euBvWOo7c/UDtxr90miQMN9a4+WIyabffow5el5gJgKNLMuQy6Fp4g+PY0cNksoyylgymz4SM7nioS9nmurkMOT0JeUh0j05ga/toiuj78OKQr/W9RY3b57x64stdVBlrbACuv4EDJZV2l2lXb+gPrivxw8OXVgvosNFhp6MQ4TvKzvnni+yn/rLUaB2BJ1/4TzKlKRdGArCVe+PY1m70uw9+7Kz+wx/rt26g+mCCMNS8BMjbXbW/vr1pM5FlMw1wjA19wgHI8SVmKx2AnEG39zfbiu8vu+CasdZ+19HHWAvM2ESqzBYSUXgHDRwv5rxC0ub3uCrI1n6w/tMXGr1OrJBAU+MA2GxrY5WE93LH2xQVTZ7LFBNFP0IWEnwc0Aps0/mcfq/3rbYDO+r9zrUjLMVDaePWCGW26EV48px//Kw5ovl4wxSbI/Ty+Y+fa97zq0WNO9aRzOXlP2hvGW7MT0aYJCdH3RHCWDPZoX9ssrA2Z40Os0XT7HMGnP/1C2yDbulrtI7E64nx7lae1EweXKr7Bq0cybGd7SI7m9a1D3zmqln0WN3nH1J+1tSstbH0xzduWOlErjhSgymKz3TAWQyAnEGHbQ7I2T7XIDebzba7+p1z6STbgGv7GwvGKNBZdNF9GWx6J/eXO5x1z/++aetaejMZg80+r2vLtnZGam15xNkCOPrM2hygQ5sz6ACcYYdf8Kuy8y86zz7g2jKj7SwFOkkkjJPBJlNv9xZH3fOP12/+iLICZBwAm31obIDNdyQBdsZqbepb0GUT4Nxp9BnXDgw6tDlrdA1wOtf828rO+afJtkHXDTLZz7XiNQxUBIXz1XwB1KASa9n0Rilx3Of4fJPz+PKF9Vs/o2gGW/ZlcwRQ84UkBbPDZSPgkKzGKfmy2QKNzkuLMujWG0rPmnRhwZBvDzbbp9sN5kLAni8XpQy1n9AG1I6At51edPnBRlf1G4sbduP2OiCWoUYY2poP2c7OCq1NfQ+6bAWcB4D+A3IGnU0XBp01u6blh5qLi27od8b0Mdb+3xxosp9PoJvwch3Anmvr6Vi/BtRegprA9tX6HJu/dDe+vbhp7wf0RincoGHtDKAZcoaal/2gtbPGHKG+dnHZDjgPSA86TBi20dmEYV/T9FMKTx00q3j0ZSOsfb5ZYrCO0UyYLNbssqbuhBpfS/jyoLvl7f9r3f/O+vajx0kmDDDDLftIg7ZmjZ3VYNM4NJcrgMvjgTbHuAA5NDprdQDO0Mu++Yf9xo2dXDB4Kmn1c/uZCibShWkBPi+CR+gy1ZSRgcbmJzxJQ0t8riafaxdp6883uWo+Xtm0r4LGDG0MeAFzOB/prLFhguQE2DQOzeUa4PK4WKsDdD4Ath54TaNTvJbWz2wr+FHfsRMnWErPG2iyndvfVHAGwW7BBSqA7/jX0QwLL1kXrYAYjg1f+LhMBNC4UCSoPfSmqL21Pufn5Z6GLS83V+xq8jphcjDUAJoPBpzTWFuzxu6ongrkkuM5yqUxyWPB+Bh0va0ua3eGnOPYNw21FRX+oHDcV06zlpxdQvtfCg2mEYVGy1CrMNpZw7Mvwy93AmG9oBlafT6GGPYzQGbfLfyOdr/nSHvAd5B28x2scp/Y+Wr7vu1HnG24qwhIWROzz2DD5zj4DLRsX0fqDmXPbqeXe3aPJnrvGXT2WavLpgyDrfflvNoP5eLiYYMmWctGDjEVj+hrtowsMliG01cayugppEK6k2qnbWCFlNGMxhh81vRsXkAbgywizUuvWWinW+QOT8DX7vL76tsCnkPNXs+Bal/rwR3u+gPrWg/DhmYoZe0rwxsuLOdl84P96BLLgdR8Apynq4O5DqWKMOAFtLIPwBlqhp3P9Xk14DvrCKmbzB3zSGtx0RBjob2fuaCoj8GKHZGiJeBub/K62qr97Y4D7tY2MisAZofyPukzzPBZ68o+wgw0Q4xzOQ+X1ddN2fLD5SPg8syGAEkJMqx6kGXA9WlcDvUhzPWiLTmMc9kxeIjjMGtXBlwPLEPLUPM552ef62NfbjdvwhC+ch0SYFkwkPD14PI5QwzokY99jg9XF1rheEAHx/DpzwEp4gCx7DO87CNNDuMcBxz7HWd5+j8LPE+HH3XYLBv40Q4Zan0+NCDXIzfIAMo+wvqDYdfHy+eoF+fK6STAwtdFq9MIEmB5McjIFi1OTo9QZQiYDCnDizLR4iLVqeI7JcCTowTSOwno5ag/R+36OAZXblkfpz+X86pwDBL4fwN/IZwMBwH5AAAAAElFTkSuQmCC', // @suppress longLineCheck
+
+ yellow: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALcAAAC4CAYAAAChOH1KAAAlaElEQVR4Ae2dCZhUxbXHTy+zL8ywDDsSVhEVJQoCkoSIIr4kvohLxO2ZfC8an0mQrCQm+uJ7qHkv5hE/xSQaNokBogkxigaUuLDIpsiOMA4MOwyz7zPd7/yLOZfqnu7p7umeXut83+2qW7du3apTv3v63Lr31rWRkUhowOZViPc6Nutpbq/8WPVO8173sYtJ6kgDusI7yme2nYdTdIZQj0NH1vrgwYNtc+bMyb344otzCgsL87KysnLT0tKym5ub6+rr62vKy8urd+7cWfv444/XlJSUAGSB2VfoKw3HM9KBBqQzOsiSspsEXgmhCMTtDGT2TTfdNDwvL28kQzvC6XSOcDgcQ2w2Wzfenme323M5nuN2uwPql/O5OF+dy+Wq4X2rOV7Z2tpa3NLSsp9Pgv3V1dX7XnnllU/4RKnj7S5edNARl4WjRnQNBFS+njnJ4wKxHtp37NgxpG/fvlPY6l7CAI/kZTgv/YMBN1L64hPAzcAf5eUTXvax9d9x/PjxtZdcckkxH0OAF8gljNThE7acVIdbQLZzDyJuX7du3YChQ4dOycnJ+QJb5M+zFR4Qr73L1v4IW/h3amtr/3nw4MG1kyZNOsJ1FdglBOwpKakItwfQTz31VN4dd9wxnd2LL7J1/hzDPCxRSWDYD7BVf5fdmbeXLl26avbs2dXcFsCdkqCnCtwCNEL7gAEDnBs2bPh8QUHBnenp6TdyWi4vySY1TU1NKysqKl6cMGHCO0eOHGnhBuqQJ71FT3a4FczcqQgdu3btGtWvX787MzMzv8YWun+y0eyvPWzRjzY0NPzp2LFjL44ePXoP52vlRbfo/nZN6PRkhdvyoX/7299245GNe9iHvoMvBC9P6N6KQOX5gvRD9tGX8gjMovvuu6+SixRrjjCpJJngRluwAGz78uXLu0+dOvXbDPW3eL2QFyOeGihnyOevWbPm6VtvvfUsbwLcAnpSuCzJALcH1KtXr+4zduzYWbm5uf/OnZWMvrQnouGv1bD8ftu2bf937bXXnmgDPCkgT3S4lZXmDrGvX79+0EUXXTSbRz3u5fXM8Ps85Upo4FGWBbt3735q4sSJh7n1YskRJqQkKtyoN8B2vPzyyz2uueaaX2RnZ9/NN1bSE7IX4qjSfMOoqa6ubvFbb7318xkzZpRx1XDxKZY8jmoauCqJBjfqi8WB4bzNmzf/W/fu3R/j9R6Bm5o6OWyuErK1fMiKwuhfJ8VNZWUVtT/77MTHFrYNI8oIS8L444kEt7ggju3bt182bNiweXwHcXwnuy5pdwPYzrofMdiR8SaaW9wffFLc+N3RE/7xEStNrHhkCu/iXkgEuFFHBfb8+fMLb7vttkf4YvGb7II4u1g3CVm8vXEpORtfiGzdbbaWmpqW3724ou4/v/WDj8u5cMAd965KvMMt1tp56NCh24qKip7gmy99IttzyVWao3ERYekKcbnpxKmy5h/3HbVpGZcvdzzj1ooDnngUnHQOLI8++mhBZWXlC3369FlowI5tV9lt1KdPz7SFdaUTXnj00REF0kccxqWRjMdKWWBv2rTpUn7YfwnfWRwZ225NnKN3peXWtdDion0799TedfkXPvqY0+GLywWnni2m8Xiz3KgPLLbz6NGj3xgzZsw7BuyY8uH34E47jRxzUc47J/eN/wb6ixf0W1zxFC+VEWvtnDt3biE/ybagZ8+ez7CysngxEqcasNkoq6i785m6w1ctmDt3GB5xEMjjwiOIh0qgDjjJMG49hp9ae5Gt9fA47c+4r1a03BJvRbS43J/s3FN3R5ubIhebMR0Tj7XlFoudtm/fvmsuvfTSNQZsb2wSY91ptw0fMzrnrYObr7iGa5zGC9yUmBrPWMItYGOY7xZ+W/wvrIw8XowkqAa4Q/M+MzjjL0d2jL+FmxBzFyVWcOO4OLPT+AH6b/ELuAs5bp4LYSUkujDg6f37Ohee3ncVHjUWCx4TzmJxUAE7/eTJk4/06NHjKb7bGIt6JDpH8Vt/N9l7dnc8dfaTcY9wJWG0YjKSEm2oFNh80ZhRVlb2NL/D+KP47SFTs3A1UFiQ9qOakglPjx7dKyMWgEcTbgX2+PHjs3j6hCX8fMjXw1We2T/+NZCTY//65jeGLRk/vjuGdaNqwaN1NavA5salnz179jl+9evO+O+WxKxhrIYCA2mrtq71xdwLNt7P+Zp4kacLA+0W1vZoWG4BO4197McM2GH1V8LunJPtuLP84FWPcQOidpHZ1XCjfCxppaWl32Ef+6GE7R1T8bA1UJDveOjUnvHfAQ+8CBthl+uvgK6EGy6PAru4uPj23r17z/VXCZOeOhro1cs5t3T7uNu5xQJ4l7nGXQU3KqzGsfmF0+v79+8/P5oTR6YOKgnYUjfZ+vdLm7/vg7HXtwEOTroE8K6AWyy2kx9ZHTdkyJAlbY1IwJ4wVe4KDTAgacMHZy3Z9vbl47h83MkEhxEHvCvgRpnOefPmFfF49lLMU83rRowGPDRgs1POpRdlLZ33xJAi3iCAe+QJdyXSZ4sCmyuVwY+truA5RKaFW0Gzf2gaiNehQH+tqKt3vZkzaAOeRWnkRZ4m9Jc9pPRIWm6cKMrPPnz48HcN2CH1Q8pmzs6yTzux+8rvsgJkiDBiBjdScAvY8LOv4hd58UyBEaOBoDRQ1DPtEfa/r+LMEX2SMFJwoxzHE0880Yv97AXsZ6OSRowGgtIAeLl0VNaCJx4d0ot3wL9/RLiMxF8AKgKY4WcvY3dkOseNxEgDieZz62qqq29dlTNo422cFhH/O9wzBCeHgptfOHjAgK13lYmHqoHsLMf0ozvHPcD7yehJWMY3HLgtsBcvXnwB34F8ONTGmPxGA94a6FuU9vDiZy6+gNPDBjxcuNXoyA033PA4+01mLmzvnjLrIWuA36jP/dcb8h7nHcMePeks3JbV3rp167X8sVF8NMmI0UBENJCXa7/xo3fGXsuFhWW9Ows39nNed911uRdeeOEvI9IiU4jRgKaBi0Zk/vK663rDGxDAta3BRTsDt2W1n3/++Vk8jfDQ4A5lchkNBK+BNKdt6OJfD5rFewjcIV9chgq3BTZ/UGkY36wxz2cH318mZ4ga4Js7Dy1fMHpYZwHvDNzqIpI/1fEkX0Sab8+E2GEme/AasNltmdO/kP8k79Gpi8tQ4Las9rvvvjuBXxe7LvhqmpxGA53TQE6O7bp1r4+RW/PgNWj3JFS4ldXmW+zfY6vdudqavYwGQtAAOONb89/nXUK23sHCbVnt119//TKelmFqCPUzWY0GwtJAbq596j9eueQyLiSki8tQ4IbVdl555ZWzOTRmO6zuMjuHpgGbbfxlOeAOcIPDoPgLBm7LavM3H0fl5+d/KbSKmdxGA+FrID/P/qWXXxw1iksK2noHC7ey2pMnT8bQXzD7hN8aU4LRgIcGbPYpV3UDf0Fb70CgitV2LFq0aAhb7RkexzMrRgNR1ADPezJj0fyLhvAhYWzBbofuSbBwO6dOnfogX7miUCNGAzHRAA+cOP5lSt6DfPCgXJNg4HawO5LDs0XdFJMWmYMaDWgaKChw3DR5ck/MqBDwwrIjuMUlwTQN0/lzHvjuoBGjgZhqwG6ngmfnDsTbXgGtd0dwY5u6kBwwYMCt5qZNTPvUHLxNA+BwYP/0W3lVLiz9MuxvA6w2FsecOXN68fPaX2wr2wRGAzHXQF6O44tzZlsvEwur7erVEdzY5rz77rtv5s9S49anEaOBuNCA3W5L+/rt3W/mynTomnQEt3JJ+JvrmA3IiNFAXGmgX1FawC+m+YJbzLxjyZIlI/mNdtzTN2I0EFcayMqyXfbS7y4cyZWSURNw6yH+4MYOjokTJ95iLiQ99BX3K271Tx331Qy7guBy0vg8WG/FKoft4IbP4i3IpPztwsLCz3tvNOvxrYEW23iqKPs9VxKfnUlc4fncPSrvtUpYb2lygU+/frc33JZLcs899xSwS3KpxxFSdMVWv4dsle+Qzd0c9xqAGevm/AJVVBSTy+ViCDwhQQN8JLVLc1P7/bz39VXOuTye+/rK5zvNcz+U1ZG4XO5Lb5teWbBs1QHMUCXsWoV4w42yYLUd99133yQ2/dBVSoutbhc5997MmkscS4hOK2hxU1mlb7h9daj3f7r3uq99Yp3GJ67jnqktk5atopVcF3Dr0UnecKNNCu5+/fpNNv42m4PyN8jWdDTW/Rjy8TF22yPLTWeriFyWLQu5mLjeAbD26eaezMHfeQG3SLJaiwRdsK7g5icAJ+kbUjVuc+OziYkpPD0Cdc/nDk0EM9xJFedn2ybyrvizEnatknS4oQIsjlmzZvXMzs6+0MplIgmrAQHcwT0tHZxMYXaGe9QDX03vCW55kaap/vIFt33mzJlXt2VUmcxPYmsAgBfmsWkD4Nz9SbbYvnq1G7yC5Q7hRgYH35W82vjbiQ20d+11wL23JfI6OO1TSIBbXBMArsTbciu4eU4Sc1dSNJREoQKcZ9+DBY+U4F8AIv8GEleJUfrJyiTw2g5uGS0R2hXcPL79mSjVyxwmyhpwwkXJdVNFTeRGUQRwNEXiEkajedkZBF4FblUN/nHr5zDi9p/85Cd92NSzh2YkWTUAwAtgwcWkJXhD+UTKm3VLWh9uhmJYmiNwo5lY7Pw8yXDZaMLk1YAArkZRuOdhaRN5mXSxDdyCZ2FZrUgPKrj55s1QczEpKknuEIB347cRYcGFiEQMUf++3V1DubcEbtVx7Sw3v3UzLLm71LRO14AADqudyJKbaQO3ArdqjQ434naeB3CIsdyJ3M2h110Aj+QoSui16Pwe4DUnm4ZwCYphDhXcGC2Rcxahg0dKkMlIimkAgOdnu6mqzvdTg/Gujqx0G7jFiInFM0iHIME+atSo9MzMzAEqxfyknAbOAc4gMBWJdnGZke4eMOozmengmBcFuA63bdq0aYV4jDDletU02NIAAM/LOge3lZgYEceUMa2FXFWAbcEtKzaen4RHP42kugYE8M6Mg8uFqVh+6FLSfOlVtnU2v+yHcFCRG/xaPOt3KG29evUyN2989UAKpgHwXH4evKZee0A6SD0IsMgucQl9FaFvk7iEgfLr27vnucGvwK38E9lu42FAY7lFGyYkZcGz2YkFLgkg+TkOsdyqtjJaomjnZ7gN3AnQidGsosOhWXDrHZdo1iC4YwHgzEzfbglKsBm4g1NkquUSwGsb4neYEG5MTjp5WG6P0RIeBswxN3BSDd3g2gvAc/irox35wsGV1DW5UK/0DDemNlZeCI5ijQkikT91jY1GjAZ8asAX4AI7Qj3us4BOJOpl6nFfRaU77AI3Ntv00RK+gDBw+1KaSTuvgXOAu6mOZwqRuUcEOuSSuITn9+x8TC9L4hLqpTqdynIjCdbbc+6t1tbWFiQaMRroSAMAnF/MpXoA3lHGKG9rddk8+BWfG9WwNTU11fqaoSjKdTSHSwANAPCsjDYTGQf1xb9IYzPVclWU1UaVdLipoaEBG40YDQSlAR1wuYrzDlGQRZuPUmWb937+1r3L0/fnuQM9+NXhdhu4fWjfJHWoAQtwocwrtyQHC6vX7u1WvctDBkmrb7YBbstTErhVQl1dHb82asRoIDQNAHA8j+frIi+0ksLLzRe5wq/iGaMlQrq7oqLCw6yHdyizdypp4JwFd1MDzz4noyjRbD9OrMpaD8vtlqFAAO4uLy8X8qNZL3OsJNEAf6uGLfg5wKPdJMBdXuMCv4plHF/cElWX06dPV5vREqUK89NJDZwDPPouCv4tTpVTtV5tgVvRvnv37hoDt64eE++MBgB4Bs+hDGvqvaA8pIlIXPIhXdIkjx7KNskvIa4q9xyyA24Py40ViHvlypXVPNbNMzobMRoITwMW4F7FeMOJzZKmxwVa71DPg7hIczNVvba+SdwSJFszTgntbh4xKTHWW1RmwnA0AMDTYcG5kK5ccAXLIyUlfBiLY9Rb3BLEscFVW1tbghUjRgOR0IAFuOaKRKJc7zJqG2wlnObiRTwRBbfQjg0uHg781Fhu1oSRiGkAgKfxuJy3ixGpdVS0qtb9KQeKYQ4V02K5BXA3j5gUI7MRo4FIakAAj2SZelmnKuggr1scYxvg1hNaecTkoLHcUI2RSGtAAI+UxZZyUM89h1wwyviamcWzWG5sVyZ94cKFn/L3CxE3YjQQcQ0AcCfPjAMwIyVMq2vhasenXJ5iWMoVuIV2165duxp4xOSYZDCh0UCkNaADLtY3nLC+yXZs14EmfsPTuqAEz9ZoiQU3p7XW1NQY1wTaMdJlGsC7urDg4Qpc6JoGN/xtuCSw3MKyB9xi0l0nTpzYbPzucNVu9g+kAQE8lDFwlOnh0TDKJ8tsmznZ4pfjHpYb+yABGVq3bNmywbjdUImRrtYAAHeE4IML2HJC4OvIW/e3buB66pZbVbudz41MP/3pT3fziwvmNnxX96wpX2kAgHd2ZtnGFqr68QuO3eCWF59uCQ5iWe7q6uqms2fPbjWuCdRiJBoaEMBDORb4LKugLYwrvmGuw62KEcuNFQtujrccO3bsAwO30pH5iZIGBPBgR05QrWNltk0c4K33gHADcGRq2bBhw3rjd7MmjERVAwAccAcj8LfX7Wxdz3kFbvCLRYleDOIYnOEX9tWca93OnDmznmd+7aFypuiP48jjhMVIdDUQjNdQVecuK7iheSLXrJIXPO7KM6ko46wAl9fMOE0Rj0Q1YsJhC8O9mT+Vfb09Ub8EhFaFKc2taVReYRmDMEszu4eigY4Ad7HZPnyKMAQoVtvjYhLH0eHGusCNHVr27du3euDAgRGF21axm2zH1pLN3Yzjxb3YG89Qel02PzIM3bUXf9jjtSdf4i8def3s4veFW39l+StHHaODjaGW5zd/R8fw08pQy2ppddOuva2rGVPFKrdN4EYzlehuCRKwDuB5Pk/KGzlyZM/169e/z5/vi8gXFwC28x9fZrDh1ieOVNW7cBcscSqcAjWtbXRXf/l/K6/ed6rpDDcXr5fh9jtAtzrKl+XGRtDXzJa77siRI6tHjBhxUyRcE9vhV8lWe4SLTizJR3XZLtRiwMlIzDWAx/qOn6HV+04Rf1iQ4AKAV3Brgc1x6/Y74iIw71hwFjTxqMlKniBTtoUV2lyJS0c+f+GLJzc3EgcaYI+EthyilVwVAAVOhVmP2unj3LJBLLfyZe6///5NVVVVRzty7mXHZA8BeC6PJcF3M0tsdADbzF94OPq9P5OMb4NTsdweCPqDWwCHyW8uKSl5zcB9Tm95fDWSg8FSIzHRAC48SyvoNT64YpNDARvMeogvuJEBZh474axo5ikf/trM784bOacBATzYO2kmH1t5/quLxNLCCK/aSX8Fl7yI1Qav7cQf3DgLLL/7ySefLC4rK9turPd5/QHwbOODn1dIFGKw2uW1tP3/3qZiPpzub7ez2qhOMHDj7GjasWPHSy1qSBG7GYEGlAVnwI3/HR0dtLK53XWCXmLVC9hgE0Y4JLg5v9oBO8L8N82cOfM1nvah1FhvqOa85BoLfl4ZXRiD1a6sp9L7lil/G3CDS79goyr+LDe24Wyw/G5+9axh+/btf4jUsCAOkCwigEfCpzRl+PbN8ZDUzhP0h5oadbNG97d9Wm2wFQhuAVxZ729+85t/raysPGmsd/vTEoBn8dRhRiKvAWW1G+jk7OXqQlKstt9REqlBR3AjD8w+CgHcjUePHq3duXPnImO9WRs+xFhw31Y33H8jWO29J2jR0Qr1QSc8+QcewSX49CuB4IblRgHqopLDxm9/+9sr+E2dMmO9fesUY+DGgvvWTWdSYbVrmqjsxytpBfjjRS4mO/S3caxAcCOPWG8FOD9vUrVnz54XjfWGanyLAG5GUcIfRcFzJPtP0ov7jhPe6RWwA1pt9EwwcIv1Vn4379M4Z86cl9h6VxrrDRX6FgW4GQf3rZwgU2G1qxup8ud/V8N/YrXBYUCrjUMEAzfyifVWvvfGjRvLN2/e/LS5awnV+Bfc5MnCOHiE7s6lWjktTN22Unp6awmVs5aD9rWlR4KF29t6N8yYMWMFT96z07xnKar0HQLwTDOK4ls5HaTCHTlVTTvvWqR8bTyrLaMkQVltFB0s3MjrYb358yL1y5Ytm8vzm7iMewL1+BcB3PjgwfnguMPC85G4Xt5Gc3nShnrWbMhWG70RCtztrPfDDz/88f79+/9sLi79gy1bBHBZN6F/DeA2+4Ez9OdfvE4fc65OWW2UHgrcyC/WG38ROJsavv/97/+Gb8ufNdYb6ulY4H/DRUk13zmU9kKDlY109sd/pd9wFGDLhWRQIyTYXyRUuGG9cRAMC+Kg9e+9914ZX2D+mt0UXjUSSAMAPMP75b5AO6XQ9iama0sJ/XrjQSrjZotLAt7AHfgLWkKFGwUL4GrkhNfr+eJyJd+93Gbck+D0LoCHYtFSIS/uRJ6oom23v6BeIROwwVnIYKMnOgs33BPrriXHG+bNm/cIv45WY0ZPoNbAIoAHznk+By5IIXJhKnGV2JYuaXpe2R4o1PfR4/720/PocX/5O0rH6EhlA9U88096hPPp7gg4A28hWW0cqzNwYz/xvS3r/dxzzx1cvXr1L3j0hOfZCLkeKDPlBP43XBSAEcwCBQlEelz21dP0uGwPFOr76HF/++l59Li//P7S20ZHaO0++sXv31cfbvK22uAtZOks3DgQDijWG2da/V133fXm3r17l5ubO1BPcALA01PcB29mp4OnaVj+jcX0JmsNYMsIiVjt4JTplSscuGGeAbhYb1So7pZbbvnV8ePH9xr/20vTHaxaFpxNWyr41nob4WefrKG9dy6kX4EfXsQlAVfgq9NuQAS+SsKHPy829rt5gquWDydMmPCVjIyMdMzaKWI/8S5hMdJeA+r7MNyN6GwR0Zy/v/NIp+O4ckw9LsfR0/S4bA81xAx1VQ1U+8s19K1/7qVjXGYtL7Dc8oCUpg0cMTQJx3LjSDi4t3tSN3/+fPjfjxn/O7TOyGAXJQ2f0ODdsEAkPLfWtb9yLH/HD7Q9lNrhsqyBnY639tJjv39X+dlitQXssKw26hIu3ChDABf3BGdeHfvfq/jR2BVm/BsqCl4E8OD3SMycGM/ec4JW3LuYVnELADa4wb2TsN0RLkNJJOBGQTjLMBbpAfj06dOfLC4uft8ADhUFLwAcF5m6b5pMcVxAlpyl9298jp5krXiDDY7AU9gSKbhREVhwffSkjt+3rLn55pt/WFpa+rEZQQmtrwA3XJRkE4B9pJw+/trz9MPKOjVhvLc7Ao4iIpGGW/xv/L2o0ZMDBw5U8HyDD/HjscUteEDXSNAaEMCTxWrjgaiT1VT84Ev00IFTVMGKELDBiwz7RQzurrYNqqKHDh1q4tvzGyZfmHVNbsUHufz5byNBasDRZn7kvpiuOokj9LXgEJJHj/vKK/kk7Ex+7KOLlIUQdyBP19CJOa/Q/W/soSOchJERwB1RP5vLs6Qr4PZ55vHFZb2r1b1lXNHRa/muXKYB3OqDgBFfgOvg+CtAz6PHuyo/jqEvOA7WYbF5GrSKX71NDyzaQAc4SYb88O+O67SI+dlcliVdATcKB+CyyMFsH+w6WsnTAO+8pD9dx4CnGcBFNYFDAVwfBw+8V+xzAGyeKar+D+vpu798k7ZzjWCtxR3Rh/0iXtmugluvqECO0MbPD5zpmUs7RvamKQx4Rgp/S0rXUVBxAVwpkk1ivPviAPtsPVUv2Uizfvaq+jgToBarDbBhsdGcLpFowo0GqIas3kOnud0fjBlAk/nWc450Wpe0MMkKFV3BB9ddgHiLtzC27GOfmvcWPTj3DfqIuwFQY4ErolvshIab22KdnWiIasyGYqrkZwreu/ICmsivYBVIpyGzkY41IP92cpHZce7ob23icY/jVXToZ3+jB55fR59wDfCNSN1iR3xkxFcro2G55bhyhlqAf3yEanccp7WfG05j+XszRQAcf7VGAmsgHgHHyQawedqzXQ8up/949WNrVEQHO2J3IANpKZpwS10EbhV+eoYaV++lt68dRaPyM2mAAVzUFDgUwJEz1v436oBb6p+W0cZbX6CHNn1KpzkJUMPP1h+GYo/U+ifnaNdJtOHWwZZGus/UUMufPqS114+mfvkZNNxpLHjQPS6Ax9JFwRh2Hdtjnqzy9WnP0k9Ky9QNmpiCDQVGG27pNB1yxF31jeT63Xu07ooLqLx3N7oy3UFO6TjZyYS+NaAPqUbbgquhvkZq5FGwX13/ND3L/YgPnsLH9jXch76OmsQKbjRQQd0WWrAv30r7bXbaOLIPXZHppG7GTQmOBQ/AeZeuHj1B7/HEOXSikg4/vZZmzV5Ba/mwsNbiX8uoiNygQR9HVWIJtzQUjYaLYrkp6w5QOfvhb04aSn3YDx9m3BRRVcehAN7VFCk3hAfz9p6kN29fQD9Y+REd4poJ2GKx9TuPXV0ln4qJNdxotPeiQD9dTc3sprw39gI60zufxsFNkb9cny0xiUoDMtrUFTTBr1e30huo8e399D/TfkPPcj9V8oF1/1qeFRGLHbOeiTXcaLj0g1hvPXSv2EqfuG20bngRXc43fAod/H8rHRgzrcX5gS39tOlKjEI4IcDGmzPHKql43ts0i7/g+y6rQe44yoiIgC19GFNNwTWLF0FdcLJh4cf1Cd/p5Q9SqyW7ex7lLL2X7vjsILq3WyZ/vIBzWZ3ImYy01wDch3AFUOMZbJ5TpH7rYVrAs64uPVvtYan1N2hgrbGIwQr38GHtHw+WW28AlCKLnP0IW+ubyLX0A9pxqIzWXNib+vLk7oPlYtNArqvwfDwcvYgLUsu2eP9pemfOSvrBwyvpHe4HuCAyGgKwvZ/siwuwoYV4styojwieYsaCGT14dj1lxfl7YZYlz3j2drr6Xy6m2T3zqD+PqpAZNmTt+BGAiiVYgcWHC8L3H46+toOeenAZvc/7wuUAzAI01vVnRCLwP8ElRlDiFW40EXUTwAVyAG4tA3tR3oI76J4xA+mOvHTKgKtiIIfq2kswcANquCDVTdTIj0YsvXcpLSo9rcatYZ31RaDmU8Aa5Wp/0BinxDPcUA3qhwXuEwCHLw5LbgGO+MyraNCDk+nuEb1pOrsr6TyyYiBnxXiLP8ABNW6dswvStP8UrXrmPVr8x410mPfXgUYcUGOID1CLbx3CfwLvFUWJd7hFFagnrLhALq4KLjoF9IyvXkb9Zk+lmSOK6Cv8XfZMfl7cQC4a9BECatyIqW6gBob6b0+toT/+5SM1OQ5cDgEbcd0FAdRwQeIWaq6bkkSBG5VFXQVyWHFxVQC4QK7iU4ZTr4e/RLeN7kdfzcugXAU57xnOBRYfIykE1htv8yioG6lm1zH6y3/9nZat/UQ96CQgA2yJ+3JB4h5sdFYiwS1wCeDe/jjAFosOa57+2c9Q4X9/mW4Z3Zdm8J3OQszJJ3c7Uwl0AI0Fkw80sFPBU5iV7zpOL//0VVqx9VP1pTAALEAjrltq8asTwlpz3S1JRLil8gI5XBUs8MdlfFwgV8AX5lHW4zfSxCsG0vUDu9NEfnY8Xax5Ml+Awu0QK13bRE2lZ2n9llJ6g4f11pdXW4+h6hYacfjUcus8YVwQrnM7SWS40RjUXyAXn1wgB+ACucTTxw2mbj+cRlNH9aZp/QroYobcpmZ3QkFcUiJbdLHQ8Bnw0gC7Hu5jFbRzDz8Dwi/nrtlUom6VwzLLIhYa6zrUsNJiqRPCBeH6tpNEh1sa5AtyfXQFcAN6gVydAHdNoAF3j6PpQ3rQlIIcGoxRFoCubg5x5niHXYcZz3wAaIx6VNRSSXEZrV28iVYt2UBHuCkCrkCNdT0O10OsdMJDzW1Rkixw6+0R0MVd0V0WuQgV0MXKO798CRV9bRxdMbQHje3TjS7n0Za+cF0wdq7DjgPFwroDZIgCmkPAjDHpttGO4/zo6YcHy2jbnzbRlld30CnOAmB1qAVoPR1Ay4IjyMLRxJdkg1vvEbQNroq4KwI7ABeovUNsQz7nzHHU78YxdMWQnjSWn0q8LDuNemIObVyQAnbrwpQzQ3Tg9fi5rYF/BV7klLgijX9wIQiYEeKtcn7r5czJKvqo+AxtW7mdtvxxkxq+E+urwytwSyh5BGgu0XI/AlcywXIkM9zSFWLJBXSEFsQcB+BYF+glriBvS7dPGU6FUy+iwcOKaFBRPg3qlkUD89NpIFv4fmzdnQAez1OLK6POLK91bBPLq0IGFxd8AjHSsN5mlVt4/PlYVROV8qQ2paeq6DDPr3d4zW4q4WE7fAsdYAJWAVbiANk7DpiRJjAjVIflMGkFfZAqgrbqC+AF6Ahl8QU20mS7vo86WXIzyfm1K6jfZQNpQEEm5fP0w1lZaZTNw47Z7L9ns4XPZl8+i61+NsOdwQA3svWtY9+4ni1xHfvJdTw8V1fPS2Mz1Vc0UNVHpXTkT1voWE2DB5AAFFCK1RVgBWR9Xc8j+wjMEnJRyS2pBLfekzrkiCtQOdThFaB9wS0nhYRShne5so5jIy4CwCACmncolhWQCpwIJS7wAmyJ+8rrXS5nTx3RFZ46rfZsqQCohwK7Hgr4HaXpZUgcR9PjWBfo9LikIRSQBWZ93V8a0vUyJI5jpKRA6UY8NSAgeoeAGmmBQu/9UDrSvAXwQQRCPRRQA4X6PhI/V6r59al0oxZPDQisSPUVlzQ9lLx6iLi3AEiIHgqk3qHk886rCjA/7TWADjESugZ0vUncO5RSJV3W9VBAlTRZ9w6xXdIkrwkDaKAjxQfY1WwOoIFQdGvADaDMzmz+f6SMYEX4z7hMAAAAAElFTkSuQmCC' // @suppress longLineCheck
+ };
+
+ return {
+ FaviconsByHue,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/file.html b/chromium/third_party/catapult/tracing/tracing/ui/base/file.html
new file mode 100644
index 00000000000..0c4945933f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/file.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function readFile(fileBlob) {
+ return new Promise(function(resolve, reject) {
+ const reader = new FileReader();
+ const filename = fileBlob.name;
+ reader.onload = function(data) {
+ resolve(data.target.result);
+ };
+ reader.onerror = function(err) {
+ reject(err);
+ };
+
+ const isBinary = filename.endsWith('.gz') || filename.endsWith('.zip');
+ if (isBinary) {
+ reader.readAsArrayBuffer(fileBlob);
+ } else {
+ reader.readAsText(fileBlob);
+ }
+ });
+ }
+ return {
+ readFile,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html
new file mode 100644
index 00000000000..942d83f9542
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-b-grouping-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #table {
+ flex: 1 1 auto;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function Row(title, data, groupingKeyFuncs, rowStatsConstructor) {
+ this.title = title;
+ this.data_ = data;
+ if (groupingKeyFuncs === undefined) {
+ groupingKeyFuncs = [];
+ }
+ this.groupingKeyFuncs_ = groupingKeyFuncs;
+ this.rowStatsConstructor_ = rowStatsConstructor;
+
+ this.subRowsBuilt_ = false;
+ this.subRows_ = undefined;
+
+ this.rowStats_ = undefined;
+ }
+
+ Row.prototype = {
+ getCurrentGroupingKeyFunc_() {
+ if (this.groupingKeyFuncs_.length === 0) return undefined;
+ return this.groupingKeyFuncs_[0];
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ get rowStats() {
+ if (this.rowStats_ === undefined) {
+ this.rowStats_ = new this.rowStatsConstructor_(this);
+ }
+ return this.rowStats_;
+ },
+
+ rebuildSubRowsIfNeeded_() {
+ if (this.subRowsBuilt_) return;
+ this.subRowsBuilt_ = true;
+
+ const groupingKeyFunc = this.getCurrentGroupingKeyFunc_();
+ if (groupingKeyFunc === undefined) {
+ this.subRows_ = undefined;
+ return;
+ }
+
+ const dataByKey = {};
+ let hasValues = false;
+ this.data_.forEach(function(datum) {
+ const key = groupingKeyFunc(datum);
+ hasValues = hasValues || (key !== undefined);
+ if (dataByKey[key] === undefined) {
+ dataByKey[key] = [];
+ }
+ dataByKey[key].push(datum);
+ });
+ if (!hasValues) {
+ this.subRows_ = undefined;
+ return;
+ }
+
+ this.subRows_ = [];
+ for (const key in dataByKey) {
+ const row = new Row(key,
+ dataByKey[key],
+ this.groupingKeyFuncs_.slice(1),
+ this.rowStatsConstructor_);
+ this.subRows_.push(row);
+ }
+ },
+
+ get isExpanded() {
+ return (this.subRows &&
+ (this.subRows.length > 0) &&
+ (this.subRows.length < 5));
+ },
+
+ get subRows() {
+ this.rebuildSubRowsIfNeeded_();
+ return this.subRows_;
+ }
+ };
+
+ Polymer({
+ is: 'tr-ui-b-grouping-table',
+
+ created() {
+ this.dataToGroup_ = undefined;
+ this.groupBy_ = undefined;
+ this.rowStatsConstructor_ = undefined;
+ },
+
+ get tableColumns() {
+ return this.$.table.tableColumns;
+ },
+
+ set tableColumns(tableColumns) {
+ this.$.table.tableColumns = tableColumns;
+ },
+
+ get tableRows() {
+ return this.$.table.tableRows;
+ },
+
+ get sortColumnIndex() {
+ return this.$.table.sortColumnIndex;
+ },
+
+ set sortColumnIndex(sortColumnIndex) {
+ this.$.table.sortColumnIndex = sortColumnIndex;
+ },
+
+ get sortDescending() {
+ return this.$.table.sortDescending;
+ },
+
+ set sortDescending(sortDescending) {
+ this.$.table.sortDescending = sortDescending;
+ },
+
+ get selectionMode() {
+ return this.$.table.selectionMode;
+ },
+
+ set selectionMode(selectionMode) {
+ this.$.table.selectionMode = selectionMode;
+ },
+
+ get rowHighlightStyle() {
+ return this.$.table.rowHighlightStyle;
+ },
+
+ set rowHighlightStyle(rowHighlightStyle) {
+ this.$.table.rowHighlightStyle = rowHighlightStyle;
+ },
+
+ get cellHighlightStyle() {
+ return this.$.table.cellHighlightStyle;
+ },
+
+ set cellHighlightStyle(cellHighlightStyle) {
+ this.$.table.cellHighlightStyle = cellHighlightStyle;
+ },
+
+ get selectedColumnIndex() {
+ return this.$.table.selectedColumnIndex;
+ },
+
+ set selectedColumnIndex(selectedColumnIndex) {
+ this.$.table.selectedColumnIndex = selectedColumnIndex;
+ },
+
+ get selectedTableRow() {
+ return this.$.table.selectedTableRow;
+ },
+
+ set selectedTableRow(selectedTableRow) {
+ this.$.table.selectedTableRow = selectedTableRow;
+ },
+
+ get groupBy() {
+ return this.groupBy_;
+ },
+
+ set groupBy(groupBy) {
+ this.groupBy_ = groupBy;
+ this.updateContents_();
+ },
+
+ get dataToGroup() {
+ return this.dataToGroup_;
+ },
+
+ set dataToGroup(dataToGroup) {
+ this.dataToGroup_ = dataToGroup;
+ this.updateContents_();
+ },
+
+ get rowStatsConstructor() {
+ return this.rowStatsConstructor_;
+ },
+
+ set rowStatsConstructor(rowStatsConstructor) {
+ this.rowStatsConstructor_ = rowStatsConstructor;
+ this.updateContents_();
+ },
+
+ rebuild() {
+ this.$.table.rebuild();
+ },
+
+ updateContents_() {
+ const groupBy = this.groupBy_ || [];
+ const dataToGroup = this.dataToGroup_ || [];
+ const rowStatsConstructor = this.rowStatsConstructor_ || function() {};
+
+ const superRow = new Row('', dataToGroup, groupBy,
+ rowStatsConstructor);
+ this.$.table.tableRows = superRow.subRows || [];
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html
new file mode 100644
index 00000000000..6d2f1b917e6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+
+<dom-module id='tr-ui-b-grouping-table-groupby-picker'>
+ <template>
+ <style>
+ #container {
+ display: flex;
+ }
+ #container *:not(:first-child) {
+ padding-left: 3px;
+ border-left: 1px solid black;
+ margin-left: 3px;
+ }
+ </style>
+
+ <div id="container"></div>
+ </template>
+</dom-module>
+
+<dom-module id="tr-ui-b-grouping-table-groupby-picker-group">
+ <template>
+ <style>
+ :host {
+ white-space: nowrap;
+ }
+ #left, #right {
+ user-select: none;
+ cursor: pointer;
+ }
+ </style>
+
+ <span id="left" on-click="moveLeft_">&#9664;</span>
+ <input type="checkbox" id="enabled" on-change="onEnableChanged_">
+ <label for="enabled" id="label"></label>
+ <span id="right" on-click="moveRight_">&#9654;</span>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ Polymer({
+ is: 'tr-ui-b-grouping-table-groupby-picker-group',
+
+ created() {
+ this.picker_ = undefined;
+ this.group_ = undefined;
+ },
+
+ get picker() {
+ return this.picker_;
+ },
+
+ set picker(picker) {
+ this.picker_ = picker;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(g) {
+ this.group_ = g;
+ this.$.label.textContent = g.label;
+ },
+
+ get enabled() {
+ return this.$.enabled.checked;
+ },
+
+ set enabled(enabled) {
+ this.$.enabled.checked = enabled;
+ if (!this.enabled) {
+ this.$.left.style.display = 'none';
+ this.$.right.style.display = 'none';
+ }
+ },
+
+ set isFirst(isFirst) {
+ this.$.left.style.display = (!this.enabled || isFirst) ? 'none' :
+ 'inline';
+ },
+
+ set isLast(isLast) {
+ this.$.right.style.display = (!this.enabled || isLast) ? 'none' :
+ 'inline';
+ },
+
+ moveLeft_() {
+ this.picker.moveLeft_(this);
+ },
+
+ moveRight_() {
+ this.picker.moveRight_(this);
+ },
+
+ onEnableChanged_() {
+ if (!this.enabled) {
+ this.$.left.style.display = 'none';
+ this.$.right.style.display = 'none';
+ }
+ this.picker.onEnableChanged_(this);
+ }
+ });
+
+ Polymer({
+ is: 'tr-ui-b-grouping-table-groupby-picker',
+
+ created() {
+ this.settingsKey_ = undefined;
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ this.settingsKey_ = settingsKey;
+ if (this.$.container.children.length) {
+ this.restoreSetting_();
+ }
+ },
+
+ restoreSetting_() {
+ if (this.settingsKey_ === undefined) return;
+ this.currentGroupKeys = tr.b.Settings.get(this.settingsKey_,
+ this.currentGroupKeys);
+ },
+
+ get possibleGroups() {
+ return [...this.$.container.children].map(groupEl => groupEl.group);
+ },
+
+ set possibleGroups(possibleGroups) {
+ Polymer.dom(this.$.container).textContent = '';
+ for (let i = 0; i < possibleGroups.length; ++i) {
+ const groupEl = document.createElement(
+ 'tr-ui-b-grouping-table-groupby-picker-group');
+ groupEl.picker = this;
+ groupEl.group = possibleGroups[i];
+ Polymer.dom(this.$.container).appendChild(groupEl);
+ }
+ this.restoreSetting_();
+ this.updateFirstLast_();
+ },
+
+ updateFirstLast_() {
+ const groupEls = this.$.container.children;
+ const enabledGroupEls = [...groupEls].filter(el => el.enabled);
+ for (let i = 0; i < enabledGroupEls.length; ++i) {
+ enabledGroupEls[i].isFirst = i === 0;
+ enabledGroupEls[i].isLast = i === enabledGroupEls.length - 1;
+ }
+ },
+
+ get currentGroupKeys() {
+ return this.currentGroups.map(group => group.key);
+ },
+
+ get currentGroups() {
+ const groups = [];
+ for (const groupEl of this.$.container.children) {
+ if (groupEl.enabled) {
+ groups.push(groupEl.group);
+ }
+ }
+ return groups;
+ },
+
+ set currentGroupKeys(newKeys) {
+ if (!tr.b.compareArrays(this.currentGroupKeys, newKeys,
+ (x, y) => x.localeCompare(y))) {
+ return;
+ }
+
+ const possibleGroups = new Map();
+ for (const group of this.possibleGroups) {
+ possibleGroups.set(group.key, group);
+ }
+
+ const groupEls = this.$.container.children;
+
+ let i = 0;
+ for (i = 0; i < newKeys.length; ++i) {
+ const group = possibleGroups.get(newKeys[i]);
+ if (group === undefined) {
+ newKeys.splice(i, 1);
+ --i;
+ continue;
+ }
+ groupEls[i].group = group;
+ groupEls[i].enabled = true;
+ possibleGroups.delete(newKeys[i]);
+ }
+
+ for (const group of possibleGroups.values()) {
+ groupEls[i].group = group;
+ groupEls[i].enabled = false;
+ ++i;
+ }
+
+ this.updateFirstLast_();
+ this.onCurrentGroupsChanged_();
+ },
+
+ moveLeft_(groupEl) {
+ const reference = groupEl.previousSibling;
+ Polymer.dom(this.$.container).removeChild(groupEl);
+ Polymer.dom(this.$.container).insertBefore(groupEl, reference);
+ this.updateFirstLast_();
+
+ if (groupEl.enabled) {
+ this.onCurrentGroupsChanged_();
+ }
+ },
+
+ moveRight_(groupEl) {
+ const reference = groupEl.nextSibling.nextSibling;
+ Polymer.dom(this.$.container).removeChild(groupEl);
+ if (reference) {
+ Polymer.dom(this.$.container).insertBefore(groupEl, reference);
+ } else {
+ Polymer.dom(this.$.container).appendChild(groupEl);
+ }
+ this.updateFirstLast_();
+
+ if (groupEl.enabled) {
+ this.onCurrentGroupsChanged_();
+ }
+ },
+
+ onCurrentGroupsChanged_() {
+ this.dispatchEvent(new tr.b.Event('current-groups-changed'));
+ tr.b.Settings.set(this.settingsKey_, this.currentGroupKeys);
+ },
+
+ onEnableChanged_(groupEl) {
+ this.updateFirstLast_();
+ this.onCurrentGroupsChanged_();
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html
new file mode 100644
index 00000000000..727d430224f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('groupby-picker', function() {
+ const settingsKey = 'tr-ui-b-grouping-table-groupby-picker-test';
+ const picker = document.createElement(
+ 'tr-ui-b-grouping-table-groupby-picker');
+ tr.b.Settings.set(settingsKey, []);
+ picker.settingsKey = settingsKey;
+ picker.possibleGroups = [
+ {key: 'a', label: 'A'},
+ {key: 'b', label: 'B'},
+ {key: 'c', label: 'C'},
+ {key: 'd', label: 'D'},
+ {key: 'e', label: 'E'}
+ ];
+ assert.deepEqual([], picker.currentGroupKeys);
+ this.addHTMLOutput(picker);
+
+ let keys = ['a', 'b', 'c', 'd', 'e'];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ keys = ['e', 'd', 'c', 'b', 'a'];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ keys = [];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ keys = ['a', 'b', 'd'];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ tr.b.Settings.set(settingsKey, ['foo']);
+ picker.settingsKey = settingsKey;
+ assert.deepEqual([], picker.currentGroupKeys);
+
+ tr.b.Settings.set(settingsKey, ['e']);
+ picker.settingsKey = settingsKey;
+ assert.deepEqual(['e'], picker.currentGroupKeys);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html b/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html
new file mode 100644
index 00000000000..9b625c61b03
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel='import' href='/tracing/ui/base/constants.html'>
+
+<dom-module id='tr-ui-b-heading'>
+ <template>
+ <style>
+ :host {
+ background-color: rgb(243, 245, 247);
+ border-right: 1px solid #8e8e8e;
+ display: block;
+ height: 100%;
+ margin: 0;
+ padding: 0 5px 0 0;
+ }
+
+ heading {
+ display: block;
+ overflow-x: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ #arrow {
+ flex: 0 0 auto;
+ font-family: sans-serif;
+ margin-left: 5px;
+ margin-right: 5px;
+ width: 8px;
+ }
+
+ #link, #heading_content {
+ display: none;
+ }
+ </style>
+ <heading id='heading' on-click='onHeadingDivClicked_'>
+ <span id='arrow'></span>
+ <span id='heading_content'></span>
+ <tr-ui-a-analysis-link id='link'></tr-ui-a-analysis-link>
+ </heading>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-b-heading',
+
+ DOWN_ARROW: String.fromCharCode(0x25BE),
+ RIGHT_ARROW: String.fromCharCode(0x25B8),
+
+ ready(viewport) {
+ // Minus 6 === 1px border + 5px padding right.
+ this.style.width = (tr.ui.b.constants.HEADING_WIDTH - 6) + 'px';
+
+ this.heading_ = '';
+ this.expanded_ = true;
+ this.arrowVisible_ = false;
+ this.selectionGenerator_ = undefined;
+
+ this.updateContents_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(text) {
+ if (this.heading_ === text) return;
+
+ this.heading_ = text;
+ this.updateContents_();
+ },
+
+ set arrowVisible(val) {
+ if (this.arrowVisible_ === val) return;
+
+ this.arrowVisible_ = !!val;
+ this.updateContents_();
+ },
+
+ set tooltip(text) {
+ this.$.heading.title = text;
+ },
+
+ set selectionGenerator(generator) {
+ if (this.selectionGenerator_ === generator) return;
+
+ this.selectionGenerator_ = generator;
+ this.updateContents_();
+ },
+
+ get expanded() {
+ return this.expanded_;
+ },
+
+ set expanded(expanded) {
+ if (this.expanded_ === expanded) return;
+
+ this.expanded_ = !!expanded;
+ this.updateContents_();
+ },
+
+ onHeadingDivClicked_() {
+ this.dispatchEvent(new tr.b.Event('heading-clicked', true));
+ },
+
+ updateContents_() {
+ if (this.arrowVisible_) {
+ this.$.arrow.style.display = '';
+ } else {
+ this.$.arrow.style.display = 'none';
+ this.$.heading.style.display = this.expanded_ ? '' : 'none';
+ }
+
+ if (this.arrowVisible_) {
+ Polymer.dom(this.$.arrow).textContent =
+ this.expanded_ ? this.DOWN_ARROW : this.RIGHT_ARROW;
+ }
+
+ this.$.link.style.display = 'none';
+ this.$.heading_content.style.display = 'none';
+
+ if (this.selectionGenerator_) {
+ this.$.link.style.display = 'inline-block';
+ this.$.link.selection = this.selectionGenerator_;
+ Polymer.dom(this.$.link).textContent = this.heading_;
+ } else {
+ this.$.heading_content.style.display = 'inline-block';
+ Polymer.dom(this.$.heading_content).textContent = this.heading_;
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html
new file mode 100644
index 00000000000..3ffd96bfbe3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function HotKey(dict) {
+ if (dict.eventType === undefined) {
+ throw new Error('eventType must be given');
+ }
+ if (dict.keyCode === undefined && dict.keyCodes === undefined) {
+ throw new Error('keyCode or keyCodes must be given');
+ }
+ if (dict.keyCode !== undefined && dict.keyCodes !== undefined) {
+ throw new Error('Only keyCode or keyCodes can be given');
+ }
+ if (dict.callback === undefined) {
+ throw new Error('callback must be given');
+ }
+
+ this.eventType_ = dict.eventType;
+ this.keyCodes_ = [];
+
+ if (dict.keyCode) {
+ this.pushKeyCode_(dict.keyCode);
+ } else if (dict.keyCodes) {
+ dict.keyCodes.forEach(this.pushKeyCode_, this);
+ }
+
+ this.useCapture_ = !!dict.useCapture;
+ this.callback_ = dict.callback;
+ this.thisArg_ = dict.thisArg !== undefined ? dict.thisArg : undefined;
+
+ this.helpText_ = dict.helpText !== undefined ? dict.helpText : undefined;
+ }
+
+ HotKey.prototype = {
+ get eventType() {
+ return this.eventType_;
+ },
+
+ get keyCodes() {
+ return this.keyCodes_;
+ },
+
+ get helpText() {
+ return this.helpText_;
+ },
+
+ call(e) {
+ this.callback_.call(this.thisArg_, e);
+ },
+
+ pushKeyCode_(keyCode) {
+ this.keyCodes_.push(keyCode);
+ }
+ };
+
+ return {
+ HotKey,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html
new file mode 100644
index 00000000000..f56631dc350
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html
@@ -0,0 +1,310 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/ui/base/hot_key.html">
+
+<dom-module id='tv-ui-b-hotkey-controller'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tv-ui-b-hotkey-controller',
+
+ created() {
+ this.isAttached_ = false;
+ this.globalMode_ = false;
+ this.slavedToParentController_ = undefined;
+ this.curHost_ = undefined;
+ this.childControllers_ = [];
+
+ this.bubblingKeyDownHotKeys_ = {};
+ this.capturingKeyDownHotKeys_ = {};
+ this.bubblingKeyPressHotKeys_ = {};
+ this.capturingKeyPressHotKeys_ = {};
+
+ this.onBubblingKeyDown_ = this.onKey_.bind(this, false);
+ this.onCapturingKeyDown_ = this.onKey_.bind(this, true);
+ this.onBubblingKeyPress_ = this.onKey_.bind(this, false);
+ this.onCapturingKeyPress_ = this.onKey_.bind(this, true);
+ },
+
+ attached() {
+ this.isAttached_ = true;
+
+ const host = this.findHost_();
+ if (host.__hotkeyController) {
+ throw new Error('Multiple hotkey controllers attached to this host');
+ }
+
+ host.__hotkeyController = this;
+ this.curHost_ = host;
+
+ let parentElement;
+ if (host.parentElement) {
+ parentElement = host.parentElement;
+ } else {
+ parentElement = Polymer.dom(host).parentNode.host;
+ }
+ const parentController = tr.b.getHotkeyControllerForElement(
+ parentElement);
+
+ if (parentController) {
+ this.slavedToParentController_ = parentController;
+ parentController.addChildController_(this);
+ return;
+ }
+
+ host.addEventListener('keydown', this.onBubblingKeyDown_, false);
+ host.addEventListener('keydown', this.onCapturingKeyDown_, true);
+ host.addEventListener('keypress', this.onBubblingKeyPress_, false);
+ host.addEventListener('keypress', this.onCapturingKeyPress_, true);
+ },
+
+ detached() {
+ this.isAttached_ = false;
+
+ const host = this.curHost_;
+ if (!host) return;
+
+ delete host.__hotkeyController;
+ this.curHost_ = undefined;
+
+ if (this.slavedToParentController_) {
+ this.slavedToParentController_.removeChildController_(this);
+ this.slavedToParentController_ = undefined;
+ return;
+ }
+
+ host.removeEventListener('keydown', this.onBubblingKeyDown_, false);
+ host.removeEventListener('keydown', this.onCapturingKeyDown_, true);
+ host.removeEventListener('keypress', this.onBubblingKeyPress_, false);
+ host.removeEventListener('keypress', this.onCapturingKeyPress_, true);
+ },
+
+ addChildController_(controller) {
+ const i = this.childControllers_.indexOf(controller);
+ if (i !== -1) {
+ throw new Error('Controller already registered');
+ }
+ this.childControllers_.push(controller);
+ },
+
+ removeChildController_(controller) {
+ const i = this.childControllers_.indexOf(controller);
+ if (i === -1) {
+ throw new Error('Controller not registered');
+ }
+ this.childControllers_.splice(i, 1);
+ return controller;
+ },
+
+ getKeyMapForEventType_(eventType, useCapture) {
+ if (eventType === 'keydown') {
+ if (!useCapture) {
+ return this.bubblingKeyDownHotKeys_;
+ }
+ return this.capturingKeyDownHotKeys_;
+ }
+ if (eventType === 'keypress') {
+ if (!useCapture) {
+ return this.bubblingKeyPressHotKeys_;
+ }
+ return this.capturingKeyPressHotKeys_;
+ }
+
+ throw new Error('Unsupported key event');
+ },
+
+ addHotKey(hotKey) {
+ if (!(hotKey instanceof tr.ui.b.HotKey)) {
+ throw new Error('hotKey must be a tr.ui.b.HotKey');
+ }
+
+ const keyMap = this.getKeyMapForEventType_(
+ hotKey.eventType, hotKey.useCapture);
+
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ if (keyMap[keyCode]) {
+ throw new Error('Key is already bound for keyCode=' + keyCode);
+ }
+ }
+
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ keyMap[keyCode] = hotKey;
+ }
+ return hotKey;
+ },
+
+ removeHotKey(hotKey) {
+ if (!(hotKey instanceof tr.ui.b.HotKey)) {
+ throw new Error('hotKey must be a tr.ui.b.HotKey');
+ }
+
+ const keyMap = this.getKeyMapForEventType_(
+ hotKey.eventType, hotKey.useCapture);
+
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ if (!keyMap[keyCode]) {
+ throw new Error('Key is not bound for keyCode=' + keyCode);
+ }
+ keyMap[keyCode] = hotKey;
+ }
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ delete keyMap[keyCode];
+ }
+ return hotKey;
+ },
+
+ get globalMode() {
+ return this.globalMode_;
+ },
+
+ set globalMode(globalMode) {
+ const wasAttached = this.isAttached_;
+ if (wasAttached) {
+ this.detached();
+ }
+ this.globalMode_ = !!globalMode;
+ if (wasAttached) {
+ this.attached();
+ }
+ },
+
+ get topmostConroller_() {
+ if (this.slavedToParentController_) {
+ return this.slavedToParentController_.topmostConroller_;
+ }
+ return this;
+ },
+
+ childRequestsGeneralFocus(child) {
+ const topmost = this.topmostConroller_;
+ if (topmost.curHost_) {
+ if (topmost.curHost_.hasAttribute('tabIndex')) {
+ topmost.curHost_.focus();
+ } else {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+ }
+ } else {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+ }
+ },
+
+ childRequestsBlur(child) {
+ child.blur();
+
+ const topmost = this.topmostConroller_;
+ if (topmost.curHost_) {
+ topmost.curHost_.focus();
+ }
+ },
+
+ findHost_() {
+ if (this.globalMode_) return document.body;
+ if (this.parentElement) return this.parentElement;
+ if (!Polymer.dom(this).parentNode) return this.host;
+
+ let node = this.parentNode;
+ while (Polymer.dom(node).parentNode) node = Polymer.dom(node).parentNode;
+ return node.host;
+ },
+
+ appendMatchingHotKeysTo_(matchedHotKeys,
+ useCapture, e) {
+ const localKeyMap = this.getKeyMapForEventType_(e.type, useCapture);
+ const localHotKey = localKeyMap[e.keyCode];
+ if (localHotKey) {
+ matchedHotKeys.push(localHotKey);
+ }
+
+ for (let i = 0; i < this.childControllers_.length; i++) {
+ const controller = this.childControllers_[i];
+ controller.appendMatchingHotKeysTo_(matchedHotKeys,
+ useCapture, e);
+ }
+ },
+
+ onKey_(useCapture, e) {
+ // Keys dispatched to INPUT elements still bubble, even when they're
+ // handled. So, skip any events that targeted the input element.
+ if (!useCapture && e.path[0].tagName === 'INPUT') return;
+
+ let sortedControllers;
+
+ const matchedHotKeys = [];
+ this.appendMatchingHotKeysTo_(matchedHotKeys, useCapture, e);
+
+ if (matchedHotKeys.length === 0) return false;
+
+ if (matchedHotKeys.length > 1) {
+ // TODO(nduca): To do support for coddling hotKeys, we need to
+ // sort the listeners by their capturing/bubbling order and then pick
+ // the one that would topologically win the tie, per DOM dispatch rules.
+ throw new Error('More than one hotKey is currently unsupported');
+ }
+
+
+ const hotKey = matchedHotKeys[0];
+
+ let prevented = 0;
+ prevented |= hotKey.call(e);
+
+ // We want to return false if preventDefaulted, or one of the handlers
+ // return false. But otherwise, we want to return undefiend.
+ return !prevented && e.defaultPrevented;
+ }
+});
+</script>
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ function getHotkeyControllerForElement(refElement) {
+ let curElement = refElement;
+ while (curElement) {
+ if (curElement.tagName === 'tv-ui-b-hotkey-controller') {
+ return curElement;
+ }
+
+ if (curElement.__hotkeyController) {
+ return curElement.__hotkeyController;
+ }
+
+ if (curElement.parentElement) {
+ curElement = curElement.parentElement;
+ continue;
+ }
+
+ // Probably inside a shadow
+ curElement = findHost(curElement);
+ }
+ return undefined;
+ }
+
+ function findHost(initialNode) {
+ let node = initialNode;
+ while (Polymer.dom(node).parentNode) {
+ node = Polymer.dom(node).parentNode;
+ }
+ return node.host;
+ }
+
+ return {
+ getHotkeyControllerForElement,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html
new file mode 100644
index 00000000000..cff10cb775c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const KeyEventManager = tr.b.KeyEventManager;
+
+ function newKeyEvent(eventType, dict) {
+ const e = new tr.b.Event(eventType, true, true);
+ if (dict.keyCode === undefined) {
+ throw new Error('keyCode required');
+ }
+ e.keyCode = dict.keyCode;
+ return e;
+ }
+
+ test('simpleHotkeyManager', function() {
+ const rootElement = document.createElement('div');
+ Polymer.dom(document.body).appendChild(rootElement);
+ try {
+ const elementShadow = rootElement.createShadowRoot();
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(elementShadow).appendChild(hkc);
+
+ const subElement = document.createElement('div');
+ Polymer.dom(elementShadow).appendChild(subElement);
+
+ assert.strictEqual(tr.b.getHotkeyControllerForElement(subElement), hkc);
+
+ let didGetCalled = false;
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keydown',
+ keyCode: 73, useCapture: true,
+ callback() {
+ didGetCalled = true;
+ }
+ }));
+
+ // Ensure it is called when events target the root element.
+ let e = newKeyEvent('keydown', {keyCode: 73});
+ rootElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+
+ // Ensure it is still called when we target the sub element.
+ didGetCalled = false;
+ e = newKeyEvent('keydown', {keyCode: 73});
+ subElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+ } finally {
+ Polymer.dom(document.body).removeChild(rootElement);
+ }
+ });
+
+ test('nestedHotkeyController', function() {
+ const rootElement = document.createElement('div');
+ Polymer.dom(document.body).appendChild(rootElement);
+ try {
+ const elementShadow = rootElement.createShadowRoot();
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(elementShadow).appendChild(hkc);
+
+ const subElement = document.createElement('div');
+ Polymer.dom(elementShadow).appendChild(subElement);
+ assert.strictEqual(
+ tr.b.getHotkeyControllerForElement(elementShadow), hkc);
+
+ const subHKC = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(subElement).appendChild(subHKC);
+
+ assert.strictEqual(
+ tr.b.getHotkeyControllerForElement(subElement), subHKC);
+
+ let didGetCalled = false;
+ subHKC.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keydown',
+ keyCode: 73, useCapture: true,
+ callback() {
+ didGetCalled = true;
+ }
+ }));
+
+ // Ensure it is called when events target the root element.
+ const e = newKeyEvent('keydown', {keyCode: 73});
+ rootElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+ } finally {
+ Polymer.dom(document.body).removeChild(rootElement);
+ }
+ });
+
+ test('inputInsideHKC', function() {
+ const rootElement = document.createElement('div');
+ Polymer.dom(document.body).appendChild(rootElement);
+ try {
+ const elementShadow = rootElement.createShadowRoot();
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(elementShadow).appendChild(hkc);
+
+ const inputEl = document.createElement('input');
+ Polymer.dom(elementShadow).appendChild(inputEl);
+
+ let didGetCalled = false;
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: 'a'.charCodeAt(0), useCapture: false,
+ callback() {
+ didGetCalled = true;
+ }
+ }));
+
+ // Ensure it is called when events target the root element.
+ didGetCalled = false;
+ let e = newKeyEvent('keypress', {keyCode: 'a'.charCodeAt(0)});
+ rootElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+
+ // Handler should NOT be called when events target the input element.
+ didGetCalled = false;
+ e = newKeyEvent('keypress', {keyCode: 'a'.charCodeAt(0)});
+ inputEl.dispatchEvent(e);
+ assert.isFalse(didGetCalled);
+ } finally {
+ Polymer.dom(document.body).removeChild(rootElement);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html
new file mode 100644
index 00000000000..3a4894278be
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-info-bar'>
+ <template>
+ <style>
+ :host {
+ align-items: center;
+ flex: 0 0 auto;
+ background-color: rgb(252, 235, 162);
+ border-bottom: 1px solid #A3A3A3;
+ border-left: 1px solid white;
+ border-right: 1px solid #A3A3A3;
+ border-top: 1px solid white;
+ display: flex;
+ height: 26px;
+ padding: 0 3px 0 3px;
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ #message { flex: 1 1 auto; }
+ </style>
+
+ <span id='message'></span>
+ <span id='buttons'></span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-info-bar',
+
+ ready() {
+ this.messageEl_ = this.$.message;
+ this.buttonsEl_ = this.$.buttons;
+
+ this.message = '';
+ },
+
+ get message() {
+ return Polymer.dom(this.messageEl_).textContent;
+ },
+
+ set message(message) {
+ Polymer.dom(this.messageEl_).textContent = message;
+ },
+
+ get visible() {
+ return !this.hidden;
+ },
+
+ set visible(visible) {
+ this.hidden = !visible;
+ },
+
+ removeAllButtons() {
+ Polymer.dom(this.buttonsEl_).textContent = '';
+ },
+
+ addButton(text, clickCallback) {
+ const button = document.createElement('button');
+ Polymer.dom(button).textContent = text;
+ button.addEventListener('click', event => clickCallback(event, this));
+ Polymer.dom(this.buttonsEl_).appendChild(button);
+ return button;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html
new file mode 100644
index 00000000000..d095e4d99f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel='import' href='/tracing/ui/base/info_bar.html'>
+
+<dom-module id='tr-ui-b-info-bar-group'>
+ <template>
+ <style>
+ :host {
+ flex: 0 0 auto;
+ flex-direction: column;
+ display: flex;
+ }
+ </style>
+ <div id='messages'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-b-info-bar-group',
+
+ ready() {
+ this.messages_ = [];
+ },
+
+ clearMessages() {
+ this.messages_ = [];
+ this.updateContents_();
+ },
+
+ addMessage(text, opt_buttons) {
+ opt_buttons = opt_buttons || [];
+ for (let i = 0; i < opt_buttons.length; i++) {
+ if (opt_buttons[i].buttonText === undefined) {
+ throw new Error('buttonText must be provided');
+ }
+ if (opt_buttons[i].onClick === undefined) {
+ throw new Error('onClick must be provided');
+ }
+ }
+
+ this.messages_.push({
+ text,
+ buttons: opt_buttons || []
+ });
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.messages).textContent = '';
+ this.messages_.forEach(function(message) {
+ const bar = document.createElement('tr-ui-b-info-bar');
+ bar.message = message.text;
+ bar.visible = true;
+
+ message.buttons.forEach(function(button) {
+ bar.addButton(button.buttonText, button.onClick);
+ }, this);
+
+ Polymer.dom(this.$.messages).appendChild(bar);
+ }, this);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html
new file mode 100644
index 00000000000..304d48ede37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('group-instantiate', function() {
+ const infoBarGroup = document.createElement('tr-ui-b-info-bar-group');
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ infoBarGroup.addMessage(
+ 'Message 2',
+ [{buttonText: 'button 2', onClick() {}}]);
+ this.addHTMLOutput(infoBarGroup);
+ });
+
+ test('group-populate-then-clear', function() {
+ const infoBarGroup = document.createElement('tr-ui-b-info-bar-group');
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ infoBarGroup.addMessage(
+ 'Message 2',
+ [{buttonText: 'button 2', onClick() {}}]);
+ infoBarGroup.clearMessages();
+ assert.strictEqual(infoBarGroup.children.length, 0);
+ });
+
+ test('group-populate-clear-repopulate', function() {
+ const infoBarGroup = document.createElement('tr-ui-b-info-bar-group');
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ infoBarGroup.addMessage(
+ 'Message 2',
+ [{buttonText: 'button 2', onClick() {}}]);
+ infoBarGroup.clearMessages();
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ this.addHTMLOutput(infoBarGroup);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html
new file mode 100644
index 00000000000..94b19482ec8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const infoBar = document.createElement('tr-ui-b-info-bar');
+ infoBar.message = 'This is an info';
+ infoBar.visible = true;
+ this.addHTMLOutput(infoBar);
+ });
+
+ test('buttons', function() {
+ const infoBar = document.createElement('tr-ui-b-info-bar');
+ infoBar.visible = true;
+ infoBar.message = 'This is an info bar with buttons';
+ let didClick = false;
+ const button = infoBar.addButton('More info...', function() {
+ didClick = true;
+ });
+ button.click();
+ assert.isTrue(didClick);
+ this.addHTMLOutput(infoBar);
+ });
+
+ test('hiding', function() {
+ const infoBar = document.createElement('tr-ui-b-info-bar');
+ infoBar.message = 'This is an info bar';
+ infoBar.visible = true;
+ this.addHTMLOutput(infoBar);
+
+ assert.strictEqual(getComputedStyle(infoBar).display, 'flex');
+
+ infoBar.visible = false;
+ assert.strictEqual(getComputedStyle(infoBar).display, 'none');
+
+ infoBar.visible = true;
+ assert.strictEqual(getComputedStyle(infoBar).display, 'flex');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html
new file mode 100644
index 00000000000..e02f4413bb1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/chart_base_2d_brushable_x.html">
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const LineChart = tr.ui.b.define('line-chart', tr.ui.b.ChartBase2DBrushX);
+
+ LineChart.prototype = {
+ __proto__: tr.ui.b.ChartBase2DBrushX.prototype,
+
+ decorate() {
+ super.decorate();
+ this.enableHoverBox = true;
+ this.displayXInHover = false;
+ },
+
+ get defaultGraphWidth() {
+ return 20 * this.data_.length;
+ },
+
+ get defaultGraphHeight() {
+ return 100;
+ },
+
+ drawHoverValueBox_(circle) {
+ tr.ui.b.ColumnChart.prototype.drawHoverValueBox_.call(this, circle);
+ },
+
+ clearHoverValueBox_(circle) {
+ tr.ui.b.ColumnChart.prototype.clearHoverValueBox_.call(this, circle);
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ const dataBySeriesKey = this.getDataBySeriesKey_();
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const pathsSel = dataSel.selectAll('path').data(seriesKeys);
+ pathsSel.enter()
+ .append('path')
+ .style('fill', 'none')
+ .style('stroke-width', '1.5px')
+ .style('stroke', key => this.getDataSeries(key).color)
+ .attr('d', key => {
+ const line = d3.svg.line()
+ .x(d => this.xScale_(d.x))
+ .y(d => this.yScale_(this.dataRange.clamp(d[key])));
+ return line(dataBySeriesKey[key]);
+ });
+ pathsSel.exit().remove();
+
+ if (this.enableHoverBox) {
+ for (let index = 0; index < this.data_.length; ++index) {
+ const datum = this.data_[index];
+ const x = this.getXForDatum_(datum, index);
+ for (const [key, value] of Object.entries(datum)) {
+ if (key === 'x') continue;
+ if (value === undefined) continue;
+ const color = this.getDataSeries(key).color;
+ const circle = document.createElementNS(
+ 'http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', this.xScale_(x));
+ circle.setAttribute('cy',
+ this.yScale_(this.dataRange.clamp(value)));
+ circle.setAttribute('r', 5);
+ circle.style.fill = color;
+ circle.datum = datum;
+ circle.key = key;
+ circle.value = datum[key];
+ circle.leftPx = this.xScale_(x);
+ circle.widthPx = 0;
+ circle.color = color;
+ circle.topPx = this.yScale_(this.dataRange.clamp(value));
+ circle.heightPx = 0;
+ circle.addEventListener(
+ 'mouseenter', () => this.drawHoverValueBox_(circle));
+ circle.addEventListener(
+ 'mouseleave', () => this.clearHoverValueBox_(circle));
+ dataSel[0][0].appendChild(circle);
+ }
+ }
+ }
+ }
+ };
+
+ return {
+ LineChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html
new file mode 100644
index 00000000000..411e46fb5dc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, y: 100},
+ {x: 20, y: 110},
+ {x: 30, y: 100},
+ {x: 40, y: 50}
+ ];
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: undefined},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: 40},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('brushRangeFromIndices', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 30, value: 60},
+ {x: 70, value: 70},
+ {x: 80, value: 80},
+ {x: 120, value: 90}
+ ];
+ let r = new tr.b.math.Range();
+
+ // Range min should be 10.
+ r = chart.computeBrushRangeFromIndices(-2, 1);
+ assert.strictEqual(r.min, 10);
+
+ // Range max should be 120.
+ r = chart.computeBrushRangeFromIndices(3, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [10, 120]
+ r = chart.computeBrushRangeFromIndices(-2, 10);
+ assert.strictEqual(r.min, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [20, 100]
+ r = chart.computeBrushRangeFromIndices(1, 3);
+ assert.strictEqual(r.min, 20);
+ assert.strictEqual(r.max, 100);
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 20, value: 60},
+ {x: 30, value: 80},
+ {x: 40, value: 20},
+ {x: 50, value: 30},
+ {x: 60, value: 20},
+ {x: 70, value: 15},
+ {x: 80, value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let curMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ if (mouseDownIndex === undefined) {
+ chart.brushedRange = new tr.b.math.Range();
+ return;
+ }
+ chart.brushedRange = chart.computeBrushRangeFromIndices(
+ mouseDownIndex, curMouseIndex);
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ curMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ curMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ curMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+
+ test('overrideDataRange', function() {
+ let chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 100},
+ ];
+
+ chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 50},
+ ];
+ });
+
+ test('sizeInBytes', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.unit = tr.b.Unit.byName.sizeInBytes;
+ chart.yLogScaleBase = 2;
+ chart.graphHeight = 400;
+ chart.isYLogScale = true;
+ chart.hideLegend = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 1},
+ {x: 1, value: 1 << 10},
+ {x: 2, value: 1 << 20},
+ {x: 3, value: 1 << 30},
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html
new file mode 100644
index 00000000000..6e2d1652016
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Simple list view.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * @constructor
+ */
+ const ListView = tr.ui.b.define(
+ 'x-list-view', tr.ui.b.ContainerThatDecoratesItsChildren);
+
+ ListView.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorate() {
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+
+ Polymer.dom(this).classList.add('x-list-view');
+ this.style.display = 'block';
+ this.style.userSelect = 'none';
+ this.style.outline = 'none';
+ this.onItemClicked_ = this.onItemClicked_.bind(this);
+ this.onKeyDown_ = this.onKeyDown_.bind(this);
+ this.tabIndex = 0;
+ this.addEventListener('keydown', this.onKeyDown_);
+
+ this.selectionChanged_ = false;
+ },
+
+ decorateChild_(item) {
+ Polymer.dom(item).classList.add('list-item');
+ item.style.paddingTop = '2px';
+ item.style.paddingRight = '4px';
+ item.style.paddingBottom = '2px';
+ item.style.paddingLeft = '4px';
+ item.addEventListener('click', this.onItemClicked_, true);
+
+ Object.defineProperty(
+ item,
+ 'selected', {
+ configurable: true,
+ get: () => item.hasAttribute('selected'),
+ set: value => {
+ // |this| is the ListView.
+ const oldSelection = this.selectedElement;
+ if (oldSelection && oldSelection !== item && value) {
+ Polymer.dom(this.selectedElement).removeAttribute('selected');
+ }
+ if (value) {
+ Polymer.dom(item).setAttribute('selected', 'selected');
+ item.style.backgroundColor = 'rgb(171, 217, 202)';
+ item.style.outline = '1px dotted rgba(0,0,0,0.1)';
+ item.style.outlineOffset = 0;
+ } else {
+ Polymer.dom(item).removeAttribute('selected');
+ item.style.backgroundColor = '';
+ }
+ const newSelection = this.selectedElement;
+ if (newSelection !== oldSelection) {
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ }
+ },
+ });
+ },
+
+ undecorateChild_(item) {
+ this.selectionChanged_ |= item.selected;
+
+ Polymer.dom(item).classList.remove('list-item');
+ item.removeEventListener('click', this.onItemClicked_);
+ delete item.selected;
+ },
+
+ beginDecorating_() {
+ this.selectionChanged_ = false;
+ },
+
+ doneDecoratingForNow_() {
+ if (this.selectionChanged_) {
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ }
+ },
+
+ get selectedElement() {
+ const el = Polymer.dom(this).querySelector('.list-item[selected]');
+ if (!el) return undefined;
+ return el;
+ },
+
+ set selectedElement(el) {
+ if (!el) {
+ if (this.selectedElement) {
+ this.selectedElement.selected = false;
+ }
+ return;
+ }
+
+ if (el.parentElement !== this) {
+ throw new Error(
+ 'Can only select elements that are children of this list view');
+ }
+ el.selected = true;
+ },
+
+ getElementByIndex(index) {
+ return Polymer.dom(this)
+ .querySelector('.list-item:nth-child(' + index + ')');
+ },
+
+ clear() {
+ const changed = this.selectedElement !== undefined;
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.clear.call(this);
+ if (changed) {
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ }
+ },
+
+ onItemClicked_(e) {
+ const currentSelectedElement = this.selectedElement;
+ if (currentSelectedElement) {
+ Polymer.dom(currentSelectedElement).removeAttribute('selected');
+ }
+ let element = e.target;
+ while (element.parentElement !== this) {
+ element = element.parentElement;
+ }
+ if (element !== currentSelectedElement) {
+ Polymer.dom(element).setAttribute('selected', 'selected');
+ }
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ },
+
+ onKeyDown_(e) {
+ if (this.selectedElement === undefined) return;
+
+ if (e.keyCode === 38) { // Up arrow.
+ const prev = Polymer.dom(this.selectedElement).previousSibling;
+ if (prev) {
+ prev.selected = true;
+ tr.ui.b.scrollIntoViewIfNeeded(prev);
+ e.preventDefault();
+ return true;
+ }
+ } else if (e.keyCode === 40) { // Down arrow.
+ const next = Polymer.dom(this.selectedElement).nextSibling;
+ if (next) {
+ next.selected = true;
+ tr.ui.b.scrollIntoViewIfNeeded(next);
+ e.preventDefault();
+ return true;
+ }
+ }
+ },
+
+ addItem(textContent) {
+ const item = document.createElement('div');
+ Polymer.dom(item).textContent = textContent;
+ Polymer.dom(this).appendChild(item);
+ item.style.userSelect = 'none';
+ return item;
+ }
+
+ };
+
+ return {
+ ListView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html
new file mode 100644
index 00000000000..685eefc9472
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ListView = tr.ui.b.ListView;
+
+ test('instantiate', function() {
+ const view = new ListView();
+ const i1 = view.addItem('item 1');
+ const i2 = view.addItem('item 2');
+ const i3 = view.addItem('item 3');
+ this.addHTMLOutput(view);
+ });
+
+ test('programmaticSelection', function() {
+ const view = new ListView();
+ const i1 = view.addItem('item 1');
+ const i2 = view.addItem('item 2');
+ const i3 = view.addItem('item 3');
+
+ i2.selected = true;
+ assert.isTrue(i2.hasAttribute('selected'));
+ i3.selected = true;
+ assert.isFalse(i2.hasAttribute('selected'));
+ assert.isTrue(i3.hasAttribute('selected'));
+ });
+
+ test('clickSelection', function() {
+ const view = new ListView();
+ let didFireSelectionChange = false;
+ view.addEventListener('selection-changed', function() {
+ didFireSelectionChange = true;
+ });
+ const i1 = view.addItem('item 1');
+ const i2 = view.addItem('item 2');
+ const i3 = view.addItem('item 3');
+
+ didFireSelectionChange = false;
+ i2.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.strictEqual(view.selectedElement, i2);
+
+ didFireSelectionChange = false;
+ i3.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.strictEqual(view.selectedElement, i3);
+
+ // Click the same target again.
+ didFireSelectionChange = false;
+ i3.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.isUndefined(view.selectedElement);
+
+ didFireSelectionChange = false;
+ i1.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.strictEqual(view.selectedElement, i1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html
new file mode 100644
index 00000000000..bf32a7f25fe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/mouse_modes.html">
+
+<dom-module id='tr-ui-b-mouse-mode-icon'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ background-image: url(../images/ui-states.png);
+ width: 27px;
+ height: 30px;
+ }
+ :host.active {
+ cursor: auto;
+ }
+ </style>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-mouse-mode-icon',
+
+ properties: {
+ modeName: {
+ type: String,
+ reflectToAttribute: true,
+ observer: 'modeNameChanged'
+ },
+ },
+
+ created() {
+ this.active_ = false;
+ this.acceleratorKey_ = undefined;
+ },
+
+ ready() {
+ this.updateContents_();
+ },
+
+ get mode() {
+ return tr.ui.b.MOUSE_SELECTOR_MODE[this.modeName];
+ },
+
+ set mode(mode) {
+ const modeInfo = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS[mode];
+ if (modeInfo === undefined) {
+ throw new Error('Unknown mode');
+ }
+ this.modeName = modeInfo.name;
+ },
+
+ modeNameChanged() {
+ this.updateContents_();
+ },
+
+ get active() {
+ return this.active_;
+ },
+
+ set active(active) {
+ this.active_ = !!active;
+ if (this.active_) {
+ Polymer.dom(this).classList.add('active');
+ } else {
+ Polymer.dom(this).classList.remove('active');
+ }
+ this.updateContents_();
+ },
+
+ get acceleratorKey() {
+ return this.acceleratorKey_;
+ },
+
+ set acceleratorKey(acceleratorKey) {
+ this.acceleratorKey_ = acceleratorKey;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.modeName === undefined) return;
+
+ const mode = this.mode;
+ if (mode === undefined) {
+ throw new Error('Invalid mode');
+ }
+
+ const modeInfo = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS[mode];
+ if (!modeInfo) {
+ throw new Error('Invalid mode');
+ }
+
+ let title = modeInfo.title;
+ if (this.acceleratorKey_) {
+ title = title + ' (' + this.acceleratorKey_ + ')';
+ }
+ this.title = title;
+
+ let bp;
+ if (this.active_) {
+ bp = modeInfo.activeBackgroundPosition;
+ } else {
+ bp = modeInfo.defaultBackgroundPosition;
+ }
+ this.style.backgroundPosition = bp;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html
new file mode 100644
index 00000000000..047d5af22b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE;
+
+ test('inactive', function() {
+ const icon = document.createElement('tr-ui-b-mouse-mode-icon');
+ icon.mode = MOUSE_SELECTOR_MODE.SELECTION;
+ assert.strictEqual(icon.modeName, 'SELECTION');
+ icon.acceleratorKey = 'a';
+ this.addHTMLOutput(icon);
+ });
+
+ test('active', function() {
+ const icon = document.createElement('tr-ui-b-mouse-mode-icon');
+ icon.mode = MOUSE_SELECTOR_MODE.SELECTION;
+ assert.strictEqual(icon.modeName, 'SELECTION');
+ icon.active = true;
+ this.addHTMLOutput(icon);
+ });
+
+ test('modeNameSetter', function() {
+ const icon = document.createElement('tr-ui-b-mouse-mode-icon');
+ Polymer.dom(icon).setAttribute('mode-name', 'SELECTION');
+ this.addHTMLOutput(icon);
+
+ return Promise.resolve().then(function() {
+ assert.strictEqual(icon.mode, 1);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html
new file mode 100644
index 00000000000..4ee7348833f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html
@@ -0,0 +1,577 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<link rel="import" href="/tracing/ui/base/mouse_modes.html">
+<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<dom-module id='tr-ui-b-mouse-mode-selector'>
+ <template>
+ <style>
+ :host {
+
+ -webkit-user-drag: element;
+ -webkit-user-select: none;
+
+ background: #DDD;
+ border: 1px solid #BBB;
+ border-radius: 4px;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+ left: calc(100% - 120px);
+ position: absolute;
+ top: 100px;
+ user-select: none;
+ width: 29px;
+ z-index: 20;
+ }
+
+ .drag-handle {
+ background: url(../images/ui-states.png) 2px 3px no-repeat;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid #BCBCBC;
+ cursor: move;
+ display: block;
+ height: 13px;
+ width: 27px;
+ }
+
+ .tool-button {
+ background-position: center center;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid #BCBCBC;
+ border-top: 1px solid #F1F1F1;
+ cursor: pointer;
+ }
+
+ .buttons > .tool-button:last-child {
+ border-bottom: none;
+ }
+
+ </style>
+ <div class="drag-handle"></div>
+ <div class="buttons">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE;
+ const MOUSE_SELECTOR_MODE_INFOS = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS;
+
+
+ const MIN_MOUSE_SELECTION_DISTANCE = 4;
+
+ const MODIFIER = {
+ SHIFT: 0x1,
+ SPACE: 0x2,
+ CMD_OR_CTRL: 0x4
+ };
+
+ function isCmdOrCtrlPressed(event) {
+ if (tr.isMac) return event.metaKey;
+ return event.ctrlKey;
+ }
+
+ /**
+ * Provides a panel for switching the interaction mode of the mouse.
+ * It handles the user interaction and dispatches events for the various
+ * modes.
+ */
+ Polymer({
+ is: 'tr-ui-b-mouse-mode-selector',
+
+ created() {
+ this.supportedModeMask_ = MOUSE_SELECTOR_MODE.ALL_MODES;
+
+ this.initialRelativeMouseDownPos_ = {x: 0, y: 0};
+
+ this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN;
+ this.settingsKey_ = undefined;
+ this.mousePos_ = {x: 0, y: 0};
+ this.mouseDownPos_ = {x: 0, y: 0};
+
+ this.onMouseDown_ = this.onMouseDown_.bind(this);
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onMouseUp_ = this.onMouseUp_.bind(this);
+
+ this.onKeyDown_ = this.onKeyDown_.bind(this);
+ this.onKeyUp_ = this.onKeyUp_.bind(this);
+
+ this.mode_ = undefined;
+ this.modeToKeyCodeMap_ = {};
+ this.modifierToModeMap_ = {};
+
+ this.targetElement_ = undefined;
+ this.modeBeforeAlternativeModeActivated_ = null;
+
+ this.isInteracting_ = false;
+ this.isClick_ = false;
+ },
+
+ ready() {
+ this.buttonsEl_ = Polymer.dom(this.root).querySelector('.buttons');
+ this.dragHandleEl_ = Polymer.dom(this.root).querySelector(
+ '.drag-handle');
+ this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES;
+
+ this.dragHandleEl_.addEventListener('mousedown',
+ this.onDragHandleMouseDown_.bind(this));
+
+ this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_);
+ this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_);
+ this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this));
+ },
+
+ attached() {
+ document.addEventListener('keydown', this.onKeyDown_);
+ document.addEventListener('keyup', this.onKeyUp_);
+ },
+
+ detached() {
+ document.removeEventListener('keydown', this.onKeyDown_);
+ document.removeEventListener('keyup', this.onKeyUp_);
+ },
+
+ get targetElement() {
+ return this.targetElement_;
+ },
+
+ set targetElement(target) {
+ if (this.targetElement_) {
+ this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
+ }
+ this.targetElement_ = target;
+ if (this.targetElement_) {
+ this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
+ }
+ },
+
+ get defaultMode() {
+ return this.defaultMode_;
+ },
+
+ set defaultMode(defaultMode) {
+ this.defaultMode_ = defaultMode;
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ this.settingsKey_ = settingsKey;
+ if (!this.settingsKey_) return;
+
+ let mode = tr.b.Settings.get(this.settingsKey_ + '.mode', undefined);
+ // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray
+ // settings to the best of our abilities.
+ if (MOUSE_SELECTOR_MODE_INFOS[mode] === undefined) {
+ mode = undefined;
+ }
+
+ // Restoring settings against unsupported modes should just go back to the
+ // default mode.
+ if ((mode & this.supportedModeMask_) === 0) {
+ mode = undefined;
+ }
+
+ if (!mode) mode = this.defaultMode_;
+ this.mode = mode;
+
+ const pos = tr.b.Settings.get(this.settingsKey_ + '.pos', undefined);
+ if (pos) this.pos = pos;
+ },
+
+ get supportedModeMask() {
+ return this.supportedModeMask_;
+ },
+
+ /**
+ * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE
+ * values.
+ */
+ set supportedModeMask(supportedModeMask) {
+ if (this.mode && (supportedModeMask & this.mode) === 0) {
+ throw new Error('supportedModeMask must include current mode.');
+ }
+
+ function createButtonForMode(mode) {
+ return button;
+ }
+
+ this.supportedModeMask_ = supportedModeMask;
+ Polymer.dom(this.buttonsEl_).textContent = '';
+ for (const modeName in MOUSE_SELECTOR_MODE) {
+ if (modeName === 'ALL_MODES') continue;
+
+ const mode = MOUSE_SELECTOR_MODE[modeName];
+ if ((this.supportedModeMask_ & mode) === 0) continue;
+
+ const button = document.createElement('tr-ui-b-mouse-mode-icon');
+ button.mode = mode;
+ Polymer.dom(button).classList.add('tool-button');
+
+ Polymer.dom(this.buttonsEl_).appendChild(button);
+ }
+ },
+
+ getButtonForMode_(mode) {
+ for (let i = 0; i < this.buttonsEl_.children.length; i++) {
+ const buttonEl = this.buttonsEl_.children[i];
+ if (buttonEl.mode === mode) {
+ return buttonEl;
+ }
+ }
+ return undefined;
+ },
+
+ get mode() {
+ return this.currentMode_;
+ },
+
+ set mode(newMode) {
+ if (newMode !== undefined) {
+ if (typeof newMode !== 'number') {
+ throw new Error('Mode must be a number');
+ }
+ if ((newMode & this.supportedModeMask_) === 0) {
+ throw new Error('Cannot switch to this mode, it is not supported');
+ }
+ if (MOUSE_SELECTOR_MODE_INFOS[newMode] === undefined) {
+ throw new Error('Unrecognized mode');
+ }
+ }
+
+ let modeInfo;
+
+ if (this.currentMode_ === newMode) return;
+
+ if (this.currentMode_) {
+ const buttonEl = this.getButtonForMode_(this.currentMode_);
+ if (buttonEl) buttonEl.active = false;
+
+ // End event.
+ if (this.isInteracting_) {
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end);
+ this.dispatchEvent(mouseEvent);
+ }
+
+ // Exit event.
+ modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_];
+ tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true);
+ }
+
+ this.currentMode_ = newMode;
+
+ if (this.currentMode_) {
+ const buttonEl = this.getButtonForMode_(this.currentMode_);
+ if (buttonEl) buttonEl.active = true;
+
+ // Entering a new mode resets mouse down pos.
+ this.mouseDownPos_.x = this.mousePos_.x;
+ this.mouseDownPos_.y = this.mousePos_.y;
+
+ // Enter event.
+ modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_];
+ if (!this.isInAlternativeMode_) {
+ tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true);
+ }
+
+ // Begin event.
+ if (this.isInteracting_) {
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin);
+ this.dispatchEvent(mouseEvent);
+ }
+ }
+
+ if (this.settingsKey_ && !this.isInAlternativeMode_) {
+ tr.b.Settings.set(this.settingsKey_ + '.mode', this.mode);
+ }
+ },
+
+ setKeyCodeForMode(mode, keyCode) {
+ if ((mode & this.supportedModeMask_) === 0) {
+ throw new Error('Mode not supported');
+ }
+ this.modeToKeyCodeMap_[mode] = keyCode;
+
+ if (!this.buttonsEl_) return;
+
+ const buttonEl = this.getButtonForMode_(mode);
+ if (buttonEl) {
+ buttonEl.acceleratorKey = String.fromCharCode(keyCode);
+ }
+ },
+
+ setCurrentMousePosFromEvent_(e) {
+ this.mousePos_.x = e.clientX;
+ this.mousePos_.y = e.clientY;
+ },
+
+ createEvent_(eventName, sourceEvent) {
+ const event = new tr.b.Event(eventName, true);
+ event.clientX = this.mousePos_.x;
+ event.clientY = this.mousePos_.y;
+ event.deltaX = this.mousePos_.x - this.mouseDownPos_.x;
+ event.deltaY = this.mousePos_.y - this.mouseDownPos_.y;
+ event.mouseDownX = this.mouseDownPos_.x;
+ event.mouseDownY = this.mouseDownPos_.y;
+ event.didPreventDefault = false;
+ event.preventDefault = function() {
+ event.didPreventDefault = true;
+ if (sourceEvent) {
+ sourceEvent.preventDefault();
+ }
+ };
+ event.stopPropagation = function() {
+ sourceEvent.stopPropagation();
+ };
+ event.stopImmediatePropagation = function() {
+ throw new Error('Not implemented');
+ };
+ return event;
+ },
+
+ onMouseDown_(e) {
+ if (e.button !== 0) return;
+ this.setCurrentMousePosFromEvent_(e);
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin, e);
+ if (this.mode === MOUSE_SELECTOR_MODE.SELECTION) {
+ mouseEvent.appendSelection = isCmdOrCtrlPressed(e);
+ }
+ this.dispatchEvent(mouseEvent);
+ this.isInteracting_ = true;
+ this.isClick_ = true;
+ tr.ui.b.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_);
+ },
+
+ onMouseMove_(e) {
+ this.setCurrentMousePosFromEvent_(e);
+
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.update, e);
+ this.dispatchEvent(mouseEvent);
+
+ if (this.isInteracting_) {
+ this.checkIsClick_(e);
+ }
+ },
+
+ onMouseUp_(e) {
+ if (e.button !== 0) return;
+
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end, e);
+ mouseEvent.isClick = this.isClick_;
+ this.dispatchEvent(mouseEvent);
+
+ if (this.isClick_ && !mouseEvent.didPreventDefault) {
+ this.dispatchClickEvents_(e);
+ }
+
+ this.isInteracting_ = false;
+ this.updateAlternativeModeState_(e);
+ },
+
+ onButtonMouseDown_(e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ },
+
+ onButtonMouseUp_(e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ },
+
+ onButtonPress_(e) {
+ this.modeBeforeAlternativeModeActivated_ = undefined;
+ this.mode = e.target.mode;
+ e.preventDefault();
+ },
+
+ onKeyDown_(e) {
+ // Keys dispatched to INPUT elements still bubble, even when they're
+ // handled. So, skip any events that targeted the input element.
+ if (e.path[0].tagName === 'INPUT') return;
+
+ if (e.keyCode === ' '.charCodeAt(0)) {
+ this.spacePressed_ = true;
+ }
+ this.updateAlternativeModeState_(e);
+ },
+
+ onKeyUp_(e) {
+ // Keys dispatched to INPUT elements still bubble, even when they're
+ // handled. So, skip any events that targeted the input element.
+ if (e.path[0].tagName === 'INPUT') return;
+
+ if (e.keyCode === ' '.charCodeAt(0)) {
+ this.spacePressed_ = false;
+ }
+
+ let didHandleKey = false;
+ for (const [modeStr, keyCode] of Object.entries(this.modeToKeyCodeMap_)) {
+ if (e.keyCode === keyCode) {
+ this.modeBeforeAlternativeModeActivated_ = undefined;
+ const mode = parseInt(modeStr);
+ this.mode = mode;
+ didHandleKey = true;
+ }
+ }
+
+ if (didHandleKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
+ this.updateAlternativeModeState_(e);
+ },
+
+ updateAlternativeModeState_(e) {
+ const shiftPressed = e.shiftKey;
+ const spacePressed = this.spacePressed_;
+ const cmdOrCtrlPressed = isCmdOrCtrlPressed(e);
+
+ // Figure out the new mode
+ const smm = this.supportedModeMask_;
+ let newMode;
+ let isNewModeAnAlternativeMode = false;
+ if (shiftPressed &&
+ (this.modifierToModeMap_[MODIFIER.SHIFT] & smm) !== 0) {
+ newMode = this.modifierToModeMap_[MODIFIER.SHIFT];
+ isNewModeAnAlternativeMode = true;
+ } else if (spacePressed &&
+ (this.modifierToModeMap_[MODIFIER.SPACE] & smm) !== 0) {
+ newMode = this.modifierToModeMap_[MODIFIER.SPACE];
+ isNewModeAnAlternativeMode = true;
+ } else if (cmdOrCtrlPressed &&
+ (this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL] & smm) !== 0) {
+ newMode = this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL];
+ isNewModeAnAlternativeMode = true;
+ } else {
+ // Go to the old mode, if there is one.
+ if (this.isInAlternativeMode_) {
+ newMode = this.modeBeforeAlternativeModeActivated_;
+ isNewModeAnAlternativeMode = false;
+ } else {
+ newMode = undefined;
+ }
+ }
+
+ // Maybe a mode change isn't needed.
+ if (this.mode === newMode || newMode === undefined) return;
+
+ // Okay, we're changing.
+ if (isNewModeAnAlternativeMode) {
+ this.modeBeforeAlternativeModeActivated_ = this.mode;
+ }
+ this.mode = newMode;
+ },
+
+ get isInAlternativeMode_() {
+ return !!this.modeBeforeAlternativeModeActivated_;
+ },
+
+ setModifierForAlternateMode(mode, modifier) {
+ this.modifierToModeMap_[modifier] = mode;
+ },
+
+ get pos() {
+ return {
+ x: parseInt(this.style.left),
+ y: parseInt(this.style.top)
+ };
+ },
+
+ set pos(pos) {
+ pos = this.constrainPositionToBounds_(pos);
+
+ this.style.left = pos.x + 'px';
+ this.style.top = pos.y + 'px';
+
+ if (this.settingsKey_) {
+ tr.b.Settings.set(this.settingsKey_ + '.pos', this.pos);
+ }
+ },
+
+ constrainPositionToBounds_(pos) {
+ const parent = this.offsetParent || document.body;
+ const parentRect = tr.ui.b.windowRectForElement(parent);
+
+ const top = 0;
+ const bottom = parentRect.height - this.offsetHeight;
+ const left = 0;
+ const right = parentRect.width - this.offsetWidth;
+
+ const res = {};
+ res.x = Math.max(pos.x, left);
+ res.x = Math.min(res.x, right);
+
+ res.y = Math.max(pos.y, top);
+ res.y = Math.min(res.y, bottom);
+ return res;
+ },
+
+ onDragHandleMouseDown_(e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ const mouseDownPos = {
+ x: e.clientX - this.offsetLeft,
+ y: e.clientY - this.offsetTop
+ };
+ tr.ui.b.trackMouseMovesUntilMouseUp(function(e) {
+ const pos = {};
+ pos.x = e.clientX - mouseDownPos.x;
+ pos.y = e.clientY - mouseDownPos.y;
+ this.pos = pos;
+ }.bind(this));
+ },
+
+ checkIsClick_(e) {
+ if (!this.isInteracting_ || !this.isClick_) return;
+
+ const deltaX = this.mousePos_.x - this.mouseDownPos_.x;
+ const deltaY = this.mousePos_.y - this.mouseDownPos_.y;
+ const minDist = MIN_MOUSE_SELECTION_DISTANCE;
+
+ if (deltaX * deltaX + deltaY * deltaY > minDist * minDist) {
+ this.isClick_ = false;
+ }
+ },
+
+ dispatchClickEvents_(e) {
+ if (!this.isClick_) return;
+
+ const modeInfo = MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION];
+ const eventNames = modeInfo.eventNames;
+
+ let mouseEvent = this.createEvent_(eventNames.begin);
+ mouseEvent.appendSelection = isCmdOrCtrlPressed(e);
+ this.dispatchEvent(mouseEvent);
+
+ mouseEvent = this.createEvent_(eventNames.end);
+ this.dispatchEvent(mouseEvent);
+ }
+ });
+
+ return {
+ MIN_MOUSE_SELECTION_DISTANCE,
+ MODIFIER,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html
new file mode 100644
index 00000000000..577d40dbae3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE;
+ test('instantiate', function() {
+ const sel = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel.supportedModeMask =
+ MOUSE_SELECTOR_MODE.SELECTION |
+ MOUSE_SELECTOR_MODE.PANSCAN;
+ this.addHTMLOutput(sel);
+ });
+
+ test('changeMaskWithUnsupportedMode', function() {
+ const sel = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel.mode = MOUSE_SELECTOR_MODE.SELECTION;
+ assert.throw(function() {
+ sel.supportedModeMask = MOUSE_SELECTOR_MODE.ZOOM;
+ });
+ });
+
+ test('modePersists', function() {
+ const sel1 = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel1.defaultMode_ = MOUSE_SELECTOR_MODE.ZOOM;
+ sel1.settingsKey = 'foo';
+ assert.strictEqual(sel1.mode, MOUSE_SELECTOR_MODE.ZOOM);
+
+ sel1.mode = MOUSE_SELECTOR_MODE.PANSCAN;
+
+ const sel2 = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel2.settingsKey = 'foo';
+ assert.strictEqual(sel2.mode, MOUSE_SELECTOR_MODE.PANSCAN);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html
new file mode 100644
index 00000000000..8d68f0279cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const MOUSE_SELECTOR_MODE = {};
+ MOUSE_SELECTOR_MODE.SELECTION = 0x1;
+ MOUSE_SELECTOR_MODE.PANSCAN = 0x2;
+ MOUSE_SELECTOR_MODE.ZOOM = 0x4;
+ MOUSE_SELECTOR_MODE.TIMING = 0x8;
+ MOUSE_SELECTOR_MODE.ROTATE = 0x10;
+ MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F;
+
+ const MOUSE_SELECTOR_MODE_INFOS = {};
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.PANSCAN] = {
+ name: 'PANSCAN',
+ mode: MOUSE_SELECTOR_MODE.PANSCAN,
+ title: 'pan',
+ eventNames: {
+ enter: 'enterpan',
+ begin: 'beginpan',
+ update: 'updatepan',
+ end: 'endpan',
+ exit: 'exitpan'
+ },
+ activeBackgroundPosition: '-30px -10px',
+ defaultBackgroundPosition: '0 -10px'
+ };
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION] = {
+ name: 'SELECTION',
+ mode: MOUSE_SELECTOR_MODE.SELECTION,
+ title: 'selection',
+ eventNames: {
+ enter: 'enterselection',
+ begin: 'beginselection',
+ update: 'updateselection',
+ end: 'endselection',
+ exit: 'exitselection'
+ },
+ activeBackgroundPosition: '-30px -40px',
+ defaultBackgroundPosition: '0 -40px'
+ };
+
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.ZOOM] = {
+ name: 'ZOOM',
+ mode: MOUSE_SELECTOR_MODE.ZOOM,
+ title: 'zoom',
+ eventNames: {
+ enter: 'enterzoom',
+ begin: 'beginzoom',
+ update: 'updatezoom',
+ end: 'endzoom',
+ exit: 'exitzoom'
+ },
+ activeBackgroundPosition: '-30px -70px',
+ defaultBackgroundPosition: '0 -70px'
+ };
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.TIMING] = {
+ name: 'TIMING',
+ mode: MOUSE_SELECTOR_MODE.TIMING,
+ title: 'timing',
+ eventNames: {
+ enter: 'entertiming',
+ begin: 'begintiming',
+ update: 'updatetiming',
+ end: 'endtiming',
+ exit: 'exittiming'
+ },
+ activeBackgroundPosition: '-30px -100px',
+ defaultBackgroundPosition: '0 -100px'
+ };
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.ROTATE] = {
+ name: 'ROTATE',
+ mode: MOUSE_SELECTOR_MODE.ROTATE,
+ title: 'rotate',
+ eventNames: {
+ enter: 'enterrotate',
+ begin: 'beginrotate',
+ update: 'updaterotate',
+ end: 'endrotate',
+ exit: 'exitrotate'
+ },
+ activeBackgroundPosition: '-30px -130px',
+ defaultBackgroundPosition: '0 -130px'
+ };
+
+ return {
+ MOUSE_SELECTOR_MODE_INFOS,
+ MOUSE_SELECTOR_MODE,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html
new file mode 100644
index 00000000000..f71e0ba2dea
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview A Mouse-event abtraction that waits for
+ * mousedown, then watches for subsequent mousemove events
+ * until the next mouseup event, then waits again.
+ * State changes are signaled with
+ * 'mouse-tracker-start' : mousedown and tracking
+ * 'mouse-tracker-move' : mouse move
+ * 'mouse-tracker-end' : mouseup and not tracking.
+ */
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * @constructor
+ * @param {HTMLElement} targetElement will recv events 'mouse-tracker-start',
+ * 'mouse-tracker-move', 'mouse-tracker-end'.
+ */
+ function MouseTracker(opt_targetElement) {
+ this.onMouseDown_ = this.onMouseDown_.bind(this);
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onMouseUp_ = this.onMouseUp_.bind(this);
+
+ this.targetElement = opt_targetElement;
+ }
+
+ MouseTracker.prototype = {
+
+ get targetElement() {
+ return this.targetElement_;
+ },
+
+ set targetElement(targetElement) {
+ if (this.targetElement_) {
+ this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
+ }
+ this.targetElement_ = targetElement;
+ if (this.targetElement_) {
+ this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
+ }
+ },
+
+ onMouseDown_(e) {
+ if (e.button !== 0) return true;
+
+ e = this.remakeEvent_(e, 'mouse-tracker-start');
+ this.targetElement_.dispatchEvent(e);
+ document.addEventListener('mousemove', this.onMouseMove_);
+ document.addEventListener('mouseup', this.onMouseUp_);
+ this.targetElement_.addEventListener('blur', this.onMouseUp_);
+ this.savePreviousUserSelect_ = document.body.style['-webkit-user-select'];
+ document.body.style['-webkit-user-select'] = 'none';
+ e.preventDefault();
+ return true;
+ },
+
+ onMouseMove_(e) {
+ e = this.remakeEvent_(e, 'mouse-tracker-move');
+ this.targetElement_.dispatchEvent(e);
+ },
+
+ onMouseUp_(e) {
+ document.removeEventListener('mousemove', this.onMouseMove_);
+ document.removeEventListener('mouseup', this.onMouseUp_);
+ this.targetElement_.removeEventListener('blur', this.onMouseUp_);
+ document.body.style['-webkit-user-select'] =
+ this.savePreviousUserSelect_;
+ e = this.remakeEvent_(e, 'mouse-tracker-end');
+ this.targetElement_.dispatchEvent(e);
+ },
+
+ remakeEvent_(e, newType) {
+ const remade = new tr.b.Event(newType, true, true);
+ remade.x = e.x;
+ remade.y = e.y;
+ remade.offsetX = e.offsetX;
+ remade.offsetY = e.offsetY;
+ remade.clientX = e.clientX;
+ remade.clientY = e.clientY;
+ return remade;
+ }
+
+ };
+
+ function trackMouseMovesUntilMouseUp(mouseMoveHandler,
+ opt_mouseUpHandler, opt_keyUpHandler) {
+ function cleanupAndDispatchToMouseUp(e) {
+ document.removeEventListener('mousemove', mouseMoveHandler);
+ if (opt_keyUpHandler) {
+ document.removeEventListener('keyup', opt_keyUpHandler);
+ }
+ document.removeEventListener('mouseup', cleanupAndDispatchToMouseUp);
+ if (opt_mouseUpHandler) {
+ opt_mouseUpHandler(e);
+ }
+ }
+ document.addEventListener('mousemove', mouseMoveHandler);
+ if (opt_keyUpHandler) {
+ document.addEventListener('keyup', opt_keyUpHandler);
+ }
+ document.addEventListener('mouseup', cleanupAndDispatchToMouseUp);
+ }
+
+ return {
+ MouseTracker,
+ trackMouseMovesUntilMouseUp,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html
new file mode 100644
index 00000000000..19bda4bcec8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/bar_chart.html">
+<link rel="import" href="/tracing/ui/base/d3.html">
+
+<script>
+'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) {
+ // Building the y-axis requires measuring text.
+ // If necessary, wait for this element to be displayed.
+ if (tr.ui.b.getSVGTextSize(this, 'test').width === 0) {
+ tr.b.requestAnimationFrame(() => this.updateYAxis_(yAxis));
+ return;
+ }
+
+ // When we can measure text, we're ready to build the y-axis.
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html
new file mode 100644
index 00000000000..771022a86d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_bar_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 100},
+ {x: 'ball', value: 110},
+ {x: 'cat', value: 100},
+ {x: 'dog', value: 50}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 100, beta: 50},
+ {x: 'ball', alpha: 110, beta: 75},
+ {x: 'cat', alpha: 100, beta: 125},
+ {x: 'dog', alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: undefined},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: 40},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 50},
+ {x: 'ball', value: 60},
+ {x: 'cat', value: 80},
+ {x: 'dog', value: 20},
+ {x: 'echo', value: 30},
+ {x: 'fortune', value: 20},
+ {x: 'gpu', value: 15},
+ {x: 'happy', value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let currentMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ const r = new tr.b.math.Range();
+ r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex));
+ r.max = Math.min(chart.data.length, Math.max(mouseDownIndex,
+ currentMouseIndex) + 1);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+
+ test('instantiation_hideXandYAxis', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ chart.hideXAxis = true;
+ chart.hideYAxis = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 100},
+ {x: 'ball', value: 110},
+ {x: 'cat', value: 100},
+ {x: 'dog', value: 50}
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html
new file mode 100644
index 00000000000..6c0144da7ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const MIN_GUIDELINE_HEIGHT_PX = 3;
+
+ const CHECKBOX_WIDTH_PX = 18;
+
+ const NameColumnChart = tr.ui.b.define(
+ 'name-column-chart', tr.ui.b.ColumnChart);
+
+ NameColumnChart.prototype = {
+ __proto__: tr.ui.b.ColumnChart.prototype,
+
+ get xAxisHeight() {
+ // Add 5px for descenders because SVG draws text baselines at the
+ // specified y-coordinate.
+ return 5 + (this.textHeightPx_ * this.data_.length);
+ },
+
+ updateMargins_() {
+ super.updateMargins_();
+ let xAxisTickOverhangPx = 0;
+ for (let i = 0; i < this.data_.length; ++i) {
+ const datum = this.data_[i];
+ xAxisTickOverhangPx = Math.max(xAxisTickOverhangPx,
+ this.xScale_(i) + tr.ui.b.getSVGTextSize(this, datum.x).width -
+ this.graphWidth);
+ }
+ this.margin.right = Math.max(this.margin.right, xAxisTickOverhangPx);
+ },
+
+ getXForDatum_(datum, index) {
+ return index;
+ },
+
+ get xAxisTickOffset() {
+ return 0.5;
+ },
+
+ updateXAxis_(xAxis) {
+ xAxis.selectAll('*').remove();
+ if (this.hideXAxis) return;
+
+ // Draw the tick labels from |this.data_[*].x|.
+ // Lay them out so that the text doesn't overlap.
+ // They may overhang into |this.margin.right|.
+ const nameTexts = xAxis.selectAll('text')
+ .data(this.data_);
+ nameTexts
+ .enter()
+ .append('text')
+ .attr('transform', (d, index) => 'translate(0, ' +
+ this.textHeightPx_ * (this.data_.length - index) + ')')
+ .attr('x', (d, index) => this.xScale_(index))
+ .attr('y', d => this.graphHeight)
+ .text(d => d.x);
+ nameTexts.exit().remove();
+
+ // Draw lines to guide the eye from bottom center of the column to the
+ // tick label.
+ const guideLines = xAxis.selectAll('line.guide').data(this.data_);
+ guideLines.enter()
+ .append('line')
+ .attr('x1', (d, index) => this.xScale_(index + this.xAxisTickOffset))
+ .attr('x2', (d, index) => this.xScale_(index + this.xAxisTickOffset))
+ .attr('y1', () => this.graphHeight)
+ .attr('y2', (d, index) => this.graphHeight + Math.max(
+ MIN_GUIDELINE_HEIGHT_PX,
+ (this.textHeightPx_ * (this.data_.length - index - 1))));
+ }
+ };
+
+ return {
+ NameColumnChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html
new file mode 100644
index 00000000000..5e78dd6aa76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ this.addHTMLOutput(document.createTextNode(
+ 'There should be a capital "A" at the end of the string of "a"s.'));
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ // Make a x-axis tick label long enough that it would overhang past the
+ // right edge of the legend in order to test that updateMargins_ extends the
+ // right margin.
+ chart.data = [
+ {x: 'a'.repeat(20) + 'A', value: 100},
+ {x: 'b', value: 110},
+ {x: 'c', value: 100},
+ {x: 'd', value: 50}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 100, beta: 50},
+ {x: 'ball', alpha: 110, beta: 75},
+ {x: 'cat', alpha: 100, beta: 125},
+ {x: 'dog', alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: undefined},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: 40},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 50},
+ {x: 'ball', value: 60},
+ {x: 'cat', value: 80},
+ {x: 'dog', value: 20},
+ {x: 'echo', value: 30},
+ {x: 'fortune', value: 20},
+ {x: 'gpu', value: 15},
+ {x: 'happy', value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let currentMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ const r = new tr.b.math.Range();
+ r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex));
+ r.max = Math.min(chart.data.length,
+ Math.max(mouseDownIndex, currentMouseIndex) + 1);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html
new file mode 100644
index 00000000000..572ba36e533
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const LineChart = tr.ui.b.LineChart;
+
+ // @constructor
+ const NameLineChart = tr.ui.b.define('name-line-chart', LineChart);
+
+ NameLineChart.prototype = {
+ __proto__: LineChart.prototype,
+
+ getXForDatum_(datum, index) {
+ return index;
+ },
+
+ get xAxisHeight() {
+ // Add 5px for descenders because SVG draws text baselines at the
+ // specified y-coordinate.
+ return 5 + (this.textHeightPx_ * this.data_.length);
+ },
+
+ get xAxisTickOffset() {
+ return 0;
+ },
+
+ updateMargins_() {
+ tr.ui.b.NameColumnChart.prototype.updateMargins_.call(this);
+ },
+
+ 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 - 1))
+ .attr('y1', this.graphHeight)
+ .attr('y2', this.graphHeight);
+ baseline.exit().remove();
+ }
+ };
+
+ return {
+ NameLineChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html
new file mode 100644
index 00000000000..fa7388103ae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 100},
+ {x: 'ball', value: 110},
+ {x: 'cat', value: 100},
+ {x: 'dog', value: 50}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 100, beta: 50},
+ {x: 'ball', alpha: 110, beta: 75},
+ {x: 'cat', alpha: 100, beta: 125},
+ {x: 'dog', alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: undefined},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: 40},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 50},
+ {x: 'ball', value: 60},
+ {x: 'cat', value: 80},
+ {x: 'dog', value: 20},
+ {x: 'echo', value: 30},
+ {x: 'fortune', value: 20},
+ {x: 'gpu', value: 15},
+ {x: 'happy', value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let currentMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ const r = new tr.b.math.Range();
+ r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex));
+ r.max = Math.min(chart.data.length, Math.max(mouseDownIndex,
+ currentMouseIndex) + 1);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html
new file mode 100644
index 00000000000..9b1014d63b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id="overlay-template">
+ <style>
+ overlay-mask {
+ left: 0;
+ padding: 8px;
+ position: absolute;
+ top: 0;
+ z-index: 1000;
+ font-family: sans-serif;
+ -webkit-justify-content: center;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ height: 100%;
+ left: 0;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ }
+ overlay-mask:focus {
+ outline: none;
+ }
+ overlay-vertical-centering-container {
+ -webkit-justify-content: center;
+ flex-direction: column;
+ display: flex;
+ }
+ overlay-frame {
+ z-index: 1100;
+ background: rgb(255, 255, 255);
+ border: 1px solid #ccc;
+ margin: 75px;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+ title-bar {
+ -webkit-align-items: center;
+ flex-direction: row;
+ border-bottom: 1px solid #ccc;
+ background-color: #ddd;
+ display: flex;
+ padding: 5px;
+ flex: 0 0 auto;
+ }
+ title {
+ display: inline;
+ font-weight: bold;
+ flex: 1 1 auto;
+ }
+ close-button {
+ -webkit-align-self: flex-end;
+ border: 1px solid #eee;
+ background-color: #999;
+ font-size: 10pt;
+ font-weight: bold;
+ padding: 2px;
+ text-align: center;
+ width: 16px;
+ }
+ close-button:hover {
+ background-color: #ddd;
+ border-color: black;
+ cursor: pointer;
+ }
+ overlay-content {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ overflow-y: auto;
+ padding: 10px;
+ min-width: 300px;
+ min-height: 0;
+ }
+ button-bar {
+ -webkit-align-items: baseline;
+ border-top: 1px solid #ccc;
+ display: flex;
+ flex: 0 0 auto;
+ flex-direction: row-reverse;
+ padding: 4px;
+ }
+ </style>
+
+ <overlay-mask>
+ <overlay-vertical-centering-container>
+ <overlay-frame>
+ <title-bar>
+ <title></title>
+ <close-button>&#x2715</close-button>
+ </title-bar>
+ <overlay-content>
+ <content></content>
+ </overlay-content>
+ <button-bar></button-bar>
+ </overlay-frame>
+ </overlay-vertical-centering-container>
+ </overlay-mask>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Implements an element that is hidden by default, but
+ * when shown, dims and (attempts to) disable the main document.
+ *
+ * You can turn any div into an overlay. Note that while an
+ * overlay element is shown, its parent is changed. Hiding the overlay
+ * restores its original parentage.
+ *
+ */
+tr.exportTo('tr.ui.b', function() {
+ if (tr.isHeadless) return {};
+
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ /**
+ * Creates a new overlay element. It will not be visible until shown.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ const Overlay = tr.ui.b.define('overlay');
+
+ Overlay.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ /**
+ * Initializes the overlay element.
+ */
+ decorate() {
+ Polymer.dom(this).classList.add('overlay');
+
+ this.parentEl_ = this.ownerDocument.body;
+
+ this.visible_ = false;
+ this.userCanClose_ = true;
+
+ this.onKeyDown_ = this.onKeyDown_.bind(this);
+ this.onClick_ = this.onClick_.bind(this);
+ this.onFocusIn_ = this.onFocusIn_.bind(this);
+ this.onDocumentClick_ = this.onDocumentClick_.bind(this);
+ this.onClose_ = this.onClose_.bind(this);
+
+ this.addEventListener('visible-change',
+ tr.ui.b.Overlay.prototype.onVisibleChange_.bind(this), true);
+
+ // Setup the shadow root
+ const createShadowRoot = this.createShadowRoot ||
+ this.webkitCreateShadowRoot;
+ this.shadow_ = createShadowRoot.call(this);
+ Polymer.dom(this.shadow_).appendChild(
+ tr.ui.b.instantiateTemplate('#overlay-template', THIS_DOC));
+
+ this.closeBtn_ = Polymer.dom(this.shadow_).querySelector('close-button');
+ this.closeBtn_.addEventListener('click', this.onClose_);
+
+ Polymer.dom(this.shadow_)
+ .querySelector('overlay-frame')
+ .addEventListener('click', this.onClick_);
+
+ this.observer_ = new WebKitMutationObserver(
+ this.didButtonBarMutate_.bind(this));
+ this.observer_.observe(
+ Polymer.dom(this.shadow_).querySelector('button-bar'),
+ { childList: true });
+
+ // title is a variable on regular HTMLElements. However, we want to
+ // use it for something more useful.
+ Object.defineProperty(
+ this, 'title', {
+ get() {
+ return Polymer.dom(Polymer.dom(this.shadow_)
+ .querySelector('title')).textContent;
+ },
+ set(title) {
+ Polymer.dom(Polymer.dom(this.shadow_).querySelector('title'))
+ .textContent = title;
+ }
+ });
+ },
+
+ set userCanClose(userCanClose) {
+ this.userCanClose_ = userCanClose;
+ this.closeBtn_.style.display =
+ userCanClose ? 'block' : 'none';
+ },
+
+ get buttons() {
+ return Polymer.dom(this.shadow_).querySelector('button-bar');
+ },
+
+ get visible() {
+ return this.visible_;
+ },
+
+ set visible(newValue) {
+ if (this.visible_ === newValue) return;
+
+ this.visible_ = newValue;
+ const e = new tr.b.Event('visible-change');
+ this.dispatchEvent(e);
+ },
+
+ onVisibleChange_() {
+ this.visible_ ? this.show_() : this.hide_();
+ },
+
+ show_() {
+ Polymer.dom(this.parentEl_).appendChild(this);
+
+ if (this.userCanClose_) {
+ this.addEventListener('keydown', this.onKeyDown_.bind(this));
+ this.addEventListener('click', this.onDocumentClick_.bind(this));
+ this.closeBtn_.addEventListener('click', this.onClose_);
+ }
+
+ this.parentEl_.addEventListener('focusin', this.onFocusIn_);
+ this.tabIndex = 0;
+
+ // Focus the first thing we find that makes sense. (Skip the close button
+ // as it doesn't make sense as the first thing to focus.)
+ const elList =
+ Polymer.dom(this).querySelectorAll('button, input, list, select, a');
+ if (elList.length > 0) {
+ if (elList[0] === this.closeBtn_) {
+ if (elList.length > 1) return elList[1].focus();
+ } else {
+ return elList[0].focus();
+ }
+ }
+ this.focus();
+ },
+
+ hide_() {
+ Polymer.dom(this.parentEl_).removeChild(this);
+
+ this.parentEl_.removeEventListener('focusin', this.onFocusIn_);
+
+ if (this.closeBtn_) {
+ this.closeBtn_.removeEventListener('click', this.onClose_);
+ }
+
+ document.removeEventListener('keydown', this.onKeyDown_);
+ document.removeEventListener('click', this.onDocumentClick_);
+ },
+
+ onClose_(e) {
+ this.visible = false;
+ if ((e.type !== 'keydown') ||
+ (e.type === 'keydown' && e.keyCode === 27)) {
+ e.stopPropagation();
+ }
+ e.preventDefault();
+ tr.b.dispatchSimpleEvent(this, 'closeclick');
+ },
+
+ onFocusIn_(e) {
+ // Prevent focus from leaving the overlay.
+
+ let node = e.target;
+ while (node) {
+ if (node === this) {
+ // |this| contains |e.target|, so nothing needs to be done. Allow
+ // focus to move from |this| to |e.target|.
+ return;
+ }
+ node = node.parentNode;
+ }
+
+ // |e.target| is outside of |this|, so focus |this|.
+ tr.b.timeout(0).then(() => this.focus());
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ didButtonBarMutate_(e) {
+ const hasButtons = this.buttons.children.length > 0;
+ if (hasButtons) {
+ Polymer.dom(this.shadow_).querySelector('button-bar').style.display =
+ undefined;
+ } else {
+ Polymer.dom(this.shadow_).querySelector('button-bar').style.display =
+ 'none';
+ }
+ },
+
+ onKeyDown_(e) {
+ // Disallow shift-tab back to another element.
+ if (e.keyCode === 9 && // tab
+ e.shiftKey &&
+ e.target === this) {
+ e.preventDefault();
+ return;
+ }
+
+ if (e.keyCode !== 27) return; // escape
+
+ this.onClose_(e);
+ },
+
+ onClick_(e) {
+ e.stopPropagation();
+ },
+
+ onDocumentClick_(e) {
+ if (!this.userCanClose_) return;
+
+ this.onClose_(e);
+ }
+ };
+
+ Overlay.showError = function(msg, opt_err) {
+ const o = new Overlay();
+ o.title = 'Error';
+ Polymer.dom(o).textContent = msg;
+ if (opt_err) {
+ const e = tr.b.normalizeException(opt_err);
+
+ const stackDiv = document.createElement('pre');
+ Polymer.dom(stackDiv).textContent = e.stack;
+ stackDiv.style.paddingLeft = '8px';
+ stackDiv.style.margin = 0;
+ Polymer.dom(o).appendChild(stackDiv);
+ }
+ const b = document.createElement('button');
+ Polymer.dom(b).textContent = 'OK';
+ b.addEventListener('click', function() {
+ o.visible = false;
+ });
+ Polymer.dom(o.buttons).appendChild(b);
+ o.visible = true;
+ return o;
+ };
+
+ return {
+ Overlay,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html
new file mode 100644
index 00000000000..ac22b08aa6b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function addShowButtonForDialog(dlg) {
+ const btn = document.createElement('button');
+ Polymer.dom(btn).textContent = 'Launch Overlay';
+ btn.addEventListener('click', function(e) {
+ dlg.visible = true;
+ e.stopPropagation();
+ });
+
+ this.addHTMLOutput(btn);
+ }
+
+ function makeButton(title) {
+ const btn = document.createElement('button');
+ Polymer.dom(btn).textContent = title;
+ return btn;
+ }
+
+ function makeCloseButton(dlg) {
+ const btn = makeButton('close');
+ btn.addEventListener('click', function(e) {
+ dlg.onClose_(e);
+ });
+ return btn;
+ }
+
+ test('instantiate', function() {
+ const dlg = new tr.ui.b.Overlay();
+ Polymer.dom(dlg).classList.add('example-overlay');
+ dlg.title = 'ExampleOverlay';
+ Polymer.dom(dlg).innerHTML = 'hello';
+ Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button'));
+ Polymer.dom(dlg.buttons).appendChild(makeCloseButton(dlg));
+ Polymer.dom(dlg.buttons).appendChild(tr.ui.b.createSpan(
+ {textContent: 'i am a span'}));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiate_noButtons', function() {
+ const dlg = new tr.ui.b.Overlay();
+ Polymer.dom(dlg).classList.add('example-overlay');
+ dlg.title = 'ExampleOverlay';
+ Polymer.dom(dlg).innerHTML = 'hello';
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiate_disableUserClose', function() {
+ const dlg = new tr.ui.b.Overlay();
+ Polymer.dom(dlg).classList.add('example-overlay');
+ dlg.userCanClose = false;
+ dlg.title = 'Unclosable';
+ Polymer.dom(dlg).innerHTML = 'This has no close X button.';
+ Polymer.dom(dlg.buttons).appendChild(makeCloseButton(dlg));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiateTall', function() {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'TallContent';
+ const contentEl = document.createElement('div');
+ contentEl.style.overflowY = 'auto';
+ Polymer.dom(dlg).appendChild(contentEl);
+
+ for (let i = 0; i < 1000; i++) {
+ const el = document.createElement('div');
+ Polymer.dom(el).textContent = 'line ' + i;
+ Polymer.dom(contentEl).appendChild(el);
+ }
+
+
+ Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button'));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiateTallWithManyDirectChildren', function() {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'TallContent';
+ for (let i = 0; i < 100; i++) {
+ const el = document.createElement('div');
+ el.style.webkitFlex = '1 0 auto';
+ Polymer.dom(el).textContent = 'line ' + i;
+ Polymer.dom(dlg).appendChild(el);
+ }
+
+ Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button'));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('closeclickEvent', function() {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'Test closeclick event';
+ const closeBtn = makeCloseButton(dlg);
+ Polymer.dom(dlg.buttons).appendChild(closeBtn);
+
+ let closeClicked = false;
+ dlg.addEventListener('closeclick', function() {
+ closeClicked = true;
+ });
+
+ dlg.visible = true;
+ return tr.b.timeout(60).then(() => {
+ closeBtn.click();
+ assert.isTrue(closeClicked);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html
new file mode 100644
index 00000000000..8a8016142ab
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<script>
+'use strict';
+
+if (!Polymer.Settings.useNativeShadow) {
+ tr.showPanic('Polymer error', 'base only works in shadow mode');
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html
new file mode 100644
index 00000000000..4c21ef991ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<script>
+'use strict';
+
+// Force Polymer into native shadowDom mode
+if (window.Polymer) {
+ throw new Error('Cannot proceed. Polymer already present.');
+}
+window.Polymer = {};
+window.Polymer.dom = 'shadow';
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html
new file mode 100644
index 00000000000..d3d91fa6c00
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html
@@ -0,0 +1,688 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/math/bbox2.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/camera.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id="quad-stack-view-template">
+ <style>
+ #chrome-left {
+ background-image: url('../images/chrome-left.png');
+ display: none;
+ }
+ #chrome-mid {
+ background-image: url('../images/chrome-mid.png');
+ display: none;
+ }
+ #chrome-right {
+ background-image: url('../images/chrome-right.png');
+ display: none;
+ }
+ </style>
+
+ <div id="header"></div>
+ <input id="stacking-distance-slider" type="range" min=1 max=400 step=1>
+ </input>
+ <div id="canvas-scroller">
+ <canvas id="canvas"></canvas>
+ </div>
+ <img id="chrome-left"/>
+ <img id="chrome-mid"/>
+ <img id="chrome-right"/>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview QuadStackView controls the content and viewing angle a
+ * QuadStack.
+ */
+tr.exportTo('tr.ui.b', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const constants = {};
+ constants.IMAGE_LOAD_RETRY_TIME_MS = 500;
+ constants.SUBDIVISION_MINIMUM = 1;
+ constants.SUBDIVISION_RECURSION_DEPTH = 3;
+ constants.SUBDIVISION_DEPTH_THRESHOLD = 100;
+ constants.FAR_PLANE_DISTANCE = 10000;
+
+ // Care of bckenney@ via
+ // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120
+ function drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2) {
+ const tmpP0 = [p0[0], p0[1]];
+ const tmpP1 = [p1[0], p1[1]];
+ const tmpP2 = [p2[0], p2[1]];
+ const tmpT0 = [t0[0], t0[1]];
+ const tmpT1 = [t1[0], t1[1]];
+ const tmpT2 = [t2[0], t2[1]];
+
+ ctx.beginPath();
+ ctx.moveTo(tmpP0[0], tmpP0[1]);
+ ctx.lineTo(tmpP1[0], tmpP1[1]);
+ ctx.lineTo(tmpP2[0], tmpP2[1]);
+ ctx.closePath();
+
+ tmpP1[0] -= tmpP0[0];
+ tmpP1[1] -= tmpP0[1];
+ tmpP2[0] -= tmpP0[0];
+ tmpP2[1] -= tmpP0[1];
+
+ tmpT1[0] -= tmpT0[0];
+ tmpT1[1] -= tmpT0[1];
+ tmpT2[0] -= tmpT0[0];
+ tmpT2[1] -= tmpT0[1];
+
+ const det = 1 / (tmpT1[0] * tmpT2[1] - tmpT2[0] * tmpT1[1]);
+
+ // linear transformation
+ const a = (tmpT2[1] * tmpP1[0] - tmpT1[1] * tmpP2[0]) * det;
+ const b = (tmpT2[1] * tmpP1[1] - tmpT1[1] * tmpP2[1]) * det;
+ const c = (tmpT1[0] * tmpP2[0] - tmpT2[0] * tmpP1[0]) * det;
+ const d = (tmpT1[0] * tmpP2[1] - tmpT2[0] * tmpP1[1]) * det;
+
+ // translation
+ const e = tmpP0[0] - a * tmpT0[0] - c * tmpT0[1];
+ const f = tmpP0[1] - b * tmpT0[0] - d * tmpT0[1];
+
+ ctx.save();
+ ctx.transform(a, b, c, d, e, f);
+ ctx.clip();
+ ctx.drawImage(img, 0, 0);
+ ctx.restore();
+ }
+
+ function drawTriangleSub(
+ ctx, img, p0, p1, p2, t0, t1, t2, opt_recursionDepth) {
+ const depth = opt_recursionDepth || 0;
+
+ // We may subdivide if we are not at the limit of recursion.
+ let subdivisionIndex = 0;
+ if (depth < constants.SUBDIVISION_MINIMUM) {
+ subdivisionIndex = 7;
+ } else if (depth < constants.SUBDIVISION_RECURSION_DEPTH) {
+ if (Math.abs(p0[2] - p1[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) {
+ subdivisionIndex += 1;
+ }
+ if (Math.abs(p0[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) {
+ subdivisionIndex += 2;
+ }
+ if (Math.abs(p1[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) {
+ subdivisionIndex += 4;
+ }
+ }
+
+ // These need to be created every time, since temporaries
+ // outside of the scope will be rewritten in recursion.
+ const p01 = vec4.create();
+ const p02 = vec4.create();
+ const p12 = vec4.create();
+ const t01 = vec2.create();
+ const t02 = vec2.create();
+ const t12 = vec2.create();
+
+ // Calculate the position before w-divide.
+ for (let i = 0; i < 2; ++i) {
+ p0[i] *= p0[2];
+ p1[i] *= p1[2];
+ p2[i] *= p2[2];
+ }
+
+ // Interpolate the 3d position.
+ for (let i = 0; i < 4; ++i) {
+ p01[i] = (p0[i] + p1[i]) / 2;
+ p02[i] = (p0[i] + p2[i]) / 2;
+ p12[i] = (p1[i] + p2[i]) / 2;
+ }
+
+ // Re-apply w-divide to the original points and the interpolated ones.
+ for (let i = 0; i < 2; ++i) {
+ p0[i] /= p0[2];
+ p1[i] /= p1[2];
+ p2[i] /= p2[2];
+
+ p01[i] /= p01[2];
+ p02[i] /= p02[2];
+ p12[i] /= p12[2];
+ }
+
+ // Interpolate the texture coordinates.
+ for (let i = 0; i < 2; ++i) {
+ t01[i] = (t0[i] + t1[i]) / 2;
+ t02[i] = (t0[i] + t2[i]) / 2;
+ t12[i] = (t1[i] + t2[i]) / 2;
+ }
+
+ // Based on the index, we subdivide the triangle differently.
+ // Assuming the triangle is p0, p1, p2 and points between i j
+ // are represented as pij (that is, a point between p2 and p0
+ // is p02, etc), then the new triangles are defined by
+ // the 3rd 4th and 5th arguments into the function.
+ switch (subdivisionIndex) {
+ case 1:
+ drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
+ break;
+ case 2:
+ drawTriangleSub(ctx, img, p0, p1, p02, t0, t1, t02, depth + 1);
+ drawTriangleSub(ctx, img, p1, p02, p2, t1, t02, t2, depth + 1);
+ break;
+ case 3:
+ drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
+ drawTriangleSub(ctx, img, p02, p01, p2, t02, t01, t2, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
+ break;
+ case 4:
+ drawTriangleSub(ctx, img, p0, p12, p2, t0, t12, t2, depth + 1);
+ drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
+ break;
+ case 5:
+ drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
+ drawTriangleSub(ctx, img, p2, p01, p12, t2, t01, t12, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
+ break;
+ case 6:
+ drawTriangleSub(ctx, img, p0, p12, p02, t0, t12, t02, depth + 1);
+ drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
+ drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
+ break;
+ case 7:
+ drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
+ drawTriangleSub(ctx, img, p01, p12, p02, t01, t12, t02, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
+ drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
+ break;
+ default:
+ // In the 0 case and all other cases, we simply draw the triangle.
+ drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2);
+ break;
+ }
+ }
+
+ // Created to avoid creating garbage when doing bulk transforms.
+ const tmpVec4 = vec4.create();
+ function transform(transformed, point, matrix, viewport) {
+ vec4.set(tmpVec4, point[0], point[1], 0, 1);
+ vec4.transformMat4(tmpVec4, tmpVec4, matrix);
+
+ let w = tmpVec4[3];
+ if (w < 1e-6) w = 1e-6;
+
+ transformed[0] = ((tmpVec4[0] / w) + 1) * viewport.width / 2;
+ transformed[1] = ((tmpVec4[1] / w) + 1) * viewport.height / 2;
+ transformed[2] = w;
+ }
+
+ function drawProjectedQuadBackgroundToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ if (quad.imageData) {
+ quadCanvas.width = quad.imageData.width;
+ quadCanvas.height = quad.imageData.height;
+ quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0);
+ const quadBBox = new tr.b.math.BBox2();
+ quadBBox.addQuad(quad);
+ const iw = quadCanvas.width;
+ const ih = quadCanvas.height;
+ drawTriangleSub(
+ ctx, quadCanvas,
+ p1, p2, p4,
+ [0, 0], [iw, 0], [0, ih]);
+ drawTriangleSub(
+ ctx, quadCanvas,
+ p2, p3, p4,
+ [iw, 0], [iw, ih], [0, ih]);
+ }
+
+ if (quad.backgroundColor) {
+ ctx.fillStyle = quad.backgroundColor;
+ ctx.beginPath();
+ ctx.moveTo(p1[0], p1[1]);
+ ctx.lineTo(p2[0], p2[1]);
+ ctx.lineTo(p3[0], p3[1]);
+ ctx.lineTo(p4[0], p4[1]);
+ ctx.closePath();
+ ctx.fill();
+ }
+ }
+
+ function drawProjectedQuadOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ ctx.beginPath();
+ ctx.moveTo(p1[0], p1[1]);
+ ctx.lineTo(p2[0], p2[1]);
+ ctx.lineTo(p3[0], p3[1]);
+ ctx.lineTo(p4[0], p4[1]);
+ ctx.closePath();
+ ctx.save();
+ if (quad.borderColor) {
+ ctx.strokeStyle = quad.borderColor;
+ } else {
+ ctx.strokeStyle = 'rgb(128,128,128)';
+ }
+
+ if (quad.shadowOffset) {
+ ctx.shadowColor = 'rgb(0, 0, 0)';
+ ctx.shadowOffsetX = quad.shadowOffset[0];
+ ctx.shadowOffsetY = quad.shadowOffset[1];
+ if (quad.shadowBlur) {
+ ctx.shadowBlur = quad.shadowBlur;
+ }
+ }
+
+ if (quad.borderWidth) {
+ ctx.lineWidth = quad.borderWidth;
+ } else {
+ ctx.lineWidth = 1;
+ }
+
+ ctx.stroke();
+ ctx.restore();
+ }
+
+ function drawProjectedQuadSelectionOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ if (!quad.upperBorderColor) return;
+
+ ctx.lineWidth = 8;
+ ctx.strokeStyle = quad.upperBorderColor;
+
+ ctx.beginPath();
+ ctx.moveTo(p1[0], p1[1]);
+ ctx.lineTo(p2[0], p2[1]);
+ ctx.lineTo(p3[0], p3[1]);
+ ctx.lineTo(p4[0], p4[1]);
+ ctx.closePath();
+ ctx.stroke();
+ }
+
+ function drawProjectedQuadToContext(
+ passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ if (passNumber === 0) {
+ drawProjectedQuadBackgroundToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas);
+ } else if (passNumber === 1) {
+ drawProjectedQuadOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas);
+ } else if (passNumber === 2) {
+ drawProjectedQuadSelectionOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas);
+ } else {
+ throw new Error('Invalid pass number');
+ }
+ }
+
+ const tmpP1 = vec3.create();
+ const tmpP2 = vec3.create();
+ const tmpP3 = vec3.create();
+ const tmpP4 = vec3.create();
+ function transformAndProcessQuads(
+ matrix, viewport, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) {
+ for (let passNumber = 0; passNumber < numPasses; passNumber++) {
+ for (let i = 0; i < quads.length; i++) {
+ const quad = quads[i];
+ transform(tmpP1, quad.p1, matrix, viewport);
+ transform(tmpP2, quad.p2, matrix, viewport);
+ transform(tmpP3, quad.p3, matrix, viewport);
+ transform(tmpP4, quad.p4, matrix, viewport);
+ handleQuadFunc(passNumber, quad,
+ tmpP1, tmpP2, tmpP3, tmpP4,
+ opt_arg1, opt_arg2);
+ }
+ }
+ }
+
+ /**
+ * @constructor
+ */
+ const QuadStackView = tr.ui.b.define('quad-stack-view');
+
+ QuadStackView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.className = 'quad-stack-view';
+ this.style.display = 'flex';
+ this.style.position = 'relative';
+
+ const node = tr.ui.b.instantiateTemplate('#quad-stack-view-template',
+ THIS_DOC);
+ Polymer.dom(this).appendChild(node);
+ this.updateHeaderVisibility_();
+ const header = Polymer.dom(this).querySelector('#header');
+ header.style.position = 'absolute';
+ header.style.fontSize = '70%';
+ header.style.top = '10px';
+ header.style.left = '10px';
+ header.style.right = '150px';
+
+ const scroller = Polymer.dom(this).querySelector('#canvas-scroller');
+ scroller.style.flexGrow = 1;
+ scroller.style.flexShrink = 1;
+ scroller.style.flexBasis = 'auto';
+ scroller.style.minWidth = 0;
+ scroller.style.minHeight = 0;
+ scroller.style.overflow = 'auto';
+
+ this.canvas_ = Polymer.dom(this).querySelector('#canvas');
+ this.chromeImages_ = {
+ left: Polymer.dom(this).querySelector('#chrome-left'),
+ mid: Polymer.dom(this).querySelector('#chrome-mid'),
+ right: Polymer.dom(this).querySelector('#chrome-right')
+ };
+
+ const stackingDistanceSlider = Polymer.dom(this).querySelector(
+ '#stacking-distance-slider');
+ stackingDistanceSlider.style.position = 'absolute';
+ stackingDistanceSlider.style.fontSize = '70%';
+ stackingDistanceSlider.style.top = '10px';
+ stackingDistanceSlider.style.right = '10px';
+ stackingDistanceSlider.value = tr.b.Settings.get(
+ 'quadStackView.stackingDistance', 45);
+ stackingDistanceSlider.addEventListener(
+ 'change', this.onStackingDistanceChange_.bind(this));
+ stackingDistanceSlider.addEventListener(
+ 'input', this.onStackingDistanceChange_.bind(this));
+
+ this.trackMouse_();
+
+ this.camera_ = new tr.ui.b.Camera(this.mouseModeSelector_);
+ this.camera_.addEventListener('renderrequired',
+ this.onRenderRequired_.bind(this));
+ this.cameraWasReset_ = false;
+ this.camera_.canvas = this.canvas_;
+
+ this.viewportRect_ = tr.b.math.Rect.fromXYWH(0, 0, 0, 0);
+
+ this.pixelRatio_ = window.devicePixelRatio || 1;
+ },
+
+ updateHeaderVisibility_() {
+ if (this.headerText) {
+ Polymer.dom(this).querySelector('#header').style.display = '';
+ } else {
+ Polymer.dom(this).querySelector('#header').style.display = 'none';
+ }
+ },
+
+ get headerText() {
+ return Polymer.dom(this).querySelector('#header').textContent;
+ },
+
+ set headerText(headerText) {
+ Polymer.dom(this).querySelector('#header').textContent = headerText;
+ this.updateHeaderVisibility_();
+ },
+
+ onStackingDistanceChange_(e) {
+ tr.b.Settings.set('quadStackView.stackingDistance',
+ this.stackingDistance);
+ this.scheduleRender();
+ e.stopPropagation();
+ },
+
+ get stackingDistance() {
+ return Polymer.dom(this).querySelector('#stacking-distance-slider').value;
+ },
+
+ get mouseModeSelector() {
+ return this.mouseModeSelector_;
+ },
+
+ get camera() {
+ return this.camera_;
+ },
+
+ set quads(q) {
+ this.quads_ = q;
+ this.scheduleRender();
+ },
+
+ set deviceRect(rect) {
+ if (!rect || rect.equalTo(this.deviceRect_)) return;
+
+ this.deviceRect_ = rect;
+ this.camera_.deviceRect = rect;
+ this.chromeQuad_ = undefined;
+ },
+
+ resize() {
+ if (!this.offsetParent) return true;
+
+ const width = parseInt(window.getComputedStyle(this.offsetParent).width);
+ const height = parseInt(window.getComputedStyle(
+ this.offsetParent).height);
+ const rect = tr.b.math.Rect.fromXYWH(0, 0, width, height);
+
+ if (rect.equalTo(this.viewportRect_)) return false;
+
+ this.viewportRect_ = rect;
+ this.canvas_.style.width = width + 'px';
+ this.canvas_.style.height = height + 'px';
+ this.canvas_.width = this.pixelRatio_ * width;
+ this.canvas_.height = this.pixelRatio_ * height;
+ if (!this.cameraWasReset_) {
+ this.camera_.resetCamera();
+ this.cameraWasReset_ = true;
+ }
+ return true;
+ },
+
+ readyToDraw() {
+ // If src isn't set yet, set it to ensure we can use
+ // the image to draw onto a canvas.
+ if (!this.chromeImages_.left.src) {
+ let leftContent =
+ window.getComputedStyle(this.chromeImages_.left).backgroundImage;
+ leftContent = tr.ui.b.extractUrlString(leftContent);
+
+ let midContent =
+ window.getComputedStyle(this.chromeImages_.mid).backgroundImage;
+ midContent = tr.ui.b.extractUrlString(midContent);
+
+ let rightContent =
+ window.getComputedStyle(this.chromeImages_.right).backgroundImage;
+ rightContent = tr.ui.b.extractUrlString(rightContent);
+
+ this.chromeImages_.left.src = leftContent;
+ this.chromeImages_.mid.src = midContent;
+ this.chromeImages_.right.src = rightContent;
+ }
+
+ // If all of the images are loaded (height > 0), then
+ // we are ready to draw.
+ return (this.chromeImages_.left.height > 0) &&
+ (this.chromeImages_.mid.height > 0) &&
+ (this.chromeImages_.right.height > 0);
+ },
+
+ get chromeQuad() {
+ if (this.chromeQuad_) return this.chromeQuad_;
+
+ // Draw the chrome border into a separate canvas.
+ const chromeCanvas = document.createElement('canvas');
+ const offsetY = this.chromeImages_.left.height;
+
+ chromeCanvas.width = this.deviceRect_.width;
+ chromeCanvas.height = this.deviceRect_.height + offsetY;
+
+ const leftWidth = this.chromeImages_.left.width;
+ const midWidth = this.chromeImages_.mid.width;
+ const rightWidth = this.chromeImages_.right.width;
+
+ const chromeCtx = chromeCanvas.getContext('2d');
+ chromeCtx.drawImage(this.chromeImages_.left, 0, 0);
+
+ chromeCtx.save();
+ chromeCtx.translate(leftWidth, 0);
+
+ // Calculate the scale of the mid image.
+ const s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth;
+ chromeCtx.scale(s, 1);
+
+ chromeCtx.drawImage(this.chromeImages_.mid, 0, 0);
+ chromeCtx.restore();
+
+ chromeCtx.drawImage(
+ this.chromeImages_.right, leftWidth + s * midWidth, 0);
+
+ // Construct the quad.
+ const chromeRect = tr.b.math.Rect.fromXYWH(
+ this.deviceRect_.x,
+ this.deviceRect_.y - offsetY,
+ this.deviceRect_.width,
+ this.deviceRect_.height + offsetY);
+ const chromeQuad = tr.b.math.Quad.fromRect(chromeRect);
+ chromeQuad.stackingGroupId = this.maxStackingGroupId_ + 1;
+ chromeQuad.imageData = chromeCtx.getImageData(
+ 0, 0, chromeCanvas.width, chromeCanvas.height);
+ chromeQuad.shadowOffset = [0, 0];
+ chromeQuad.shadowBlur = 5;
+ chromeQuad.borderWidth = 3;
+ this.chromeQuad_ = chromeQuad;
+ return this.chromeQuad_;
+ },
+
+ scheduleRender() {
+ if (this.redrawScheduled_) return false;
+ this.redrawScheduled_ = true;
+ tr.b.requestAnimationFrame(this.render, this);
+ },
+
+ onRenderRequired_(e) {
+ this.scheduleRender();
+ },
+
+ stackTransformAndProcessQuads_(
+ numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) {
+ const mv = this.camera_.modelViewMatrix;
+ const p = this.camera_.projectionMatrix;
+
+ const viewport = tr.b.math.Rect.fromXYWH(
+ 0, 0, this.canvas_.width, this.canvas_.height);
+
+ // Calculate the quad stacks.
+ const quadStacks = [];
+ for (let i = 0; i < this.quads_.length; ++i) {
+ const quad = this.quads_[i];
+ const stackingId = quad.stackingGroupId || 0;
+ while (stackingId >= quadStacks.length) {
+ quadStacks.push([]);
+ }
+
+ quadStacks[stackingId].push(quad);
+ }
+
+ const mvp = mat4.create();
+ this.maxStackingGroupId_ = quadStacks.length;
+ const effectiveStackingDistance =
+ this.stackingDistance * this.camera_.stackingDistanceDampening;
+
+ // Draw the quad stacks, raising each subsequent level.
+ mat4.multiply(mvp, p, mv);
+ for (let i = 0; i < quadStacks.length; ++i) {
+ transformAndProcessQuads(mvp, viewport, quadStacks[i],
+ numPasses, handleQuadFunc,
+ opt_arg1, opt_arg2);
+
+ mat4.translate(mv, mv, [0, 0, effectiveStackingDistance]);
+ mat4.multiply(mvp, p, mv);
+ }
+
+ if (includeChromeQuad && this.deviceRect_) {
+ transformAndProcessQuads(mvp, viewport, [this.chromeQuad],
+ numPasses, drawProjectedQuadToContext,
+ opt_arg1, opt_arg2);
+ }
+ },
+
+ render() {
+ this.redrawScheduled_ = false;
+
+ if (!this.readyToDraw()) {
+ setTimeout(this.scheduleRender.bind(this),
+ constants.IMAGE_LOAD_RETRY_TIME_MS);
+ return;
+ }
+
+ if (!this.quads_) return;
+
+ const canvasCtx = this.canvas_.getContext('2d');
+ if (!this.resize()) {
+ canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+ }
+
+ const quadCanvas = document.createElement('canvas');
+ this.stackTransformAndProcessQuads_(
+ 3, drawProjectedQuadToContext, true,
+ canvasCtx, quadCanvas);
+ quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources.
+ },
+
+ trackMouse_() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this.canvas_;
+ this.mouseModeSelector_.supportedModeMask =
+ tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION |
+ tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN |
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM |
+ tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE;
+ this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN;
+ this.mouseModeSelector_.pos = {x: 0, y: 100};
+ Polymer.dom(this).appendChild(this.mouseModeSelector_);
+ this.mouseModeSelector_.settingsKey =
+ 'quadStackView.mouseModeSelector';
+
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE, tr.ui.b.MODIFIER.SHIFT);
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN, tr.ui.b.MODIFIER.SPACE);
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM, tr.ui.b.MODIFIER.CMD_OR_CTRL);
+
+ this.mouseModeSelector_.addEventListener('updateselection',
+ this.onSelectionUpdate_.bind(this));
+ this.mouseModeSelector_.addEventListener('endselection',
+ this.onSelectionUpdate_.bind(this));
+ },
+
+ extractRelativeMousePosition_(e) {
+ const br = this.canvas_.getBoundingClientRect();
+ return [
+ this.pixelRatio_ * (e.clientX - this.canvas_.offsetLeft - br.left),
+ this.pixelRatio_ * (e.clientY - this.canvas_.offsetTop - br.top)
+ ];
+ },
+
+ onSelectionUpdate_(e) {
+ const mousePos = this.extractRelativeMousePosition_(e);
+ const res = [];
+ function handleQuad(passNumber, quad, p1, p2, p3, p4) {
+ if (tr.b.math.pointInImplicitQuad(mousePos, p1, p2, p3, p4)) {
+ res.push(quad);
+ }
+ }
+ this.stackTransformAndProcessQuads_(1, handleQuad, false);
+ e = new tr.b.Event('selectionchange');
+ e.quads = res;
+ this.dispatchEvent(e);
+ }
+ };
+
+ return {
+ QuadStackView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html
new file mode 100644
index 00000000000..27edfb59152
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-radio-picker'>
+ <template>
+ <style>
+ :host([vertical]) #container {
+ flex-direction: column;
+ }
+ :host(:not[vertical]) #container {
+ flex-direction: row;
+ }
+ #container {
+ display: flex;
+ }
+ #container > div {
+ padding-left: 1em;
+ padding-bottom: 0.5em;
+ }
+ </style>
+ <div id="container"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-radio-picker',
+
+ created() {
+ this.needsInit_ = true;
+ this.settingsKey_ = undefined;
+ this.isReady_ = false;
+ this.radioButtons_ = undefined;
+ // Keeping track of which key is selected. This member should only be set
+ // set inside select() method to make sure that logical state & the UI
+ // state is consistent.
+ this.selectedKey_ = undefined;
+ },
+
+ ready() {
+ this.isReady_ = true;
+ this.maybeInit_();
+ this.maybeRenderRadioButtons_();
+ },
+
+ get vertical() {
+ return this.getAttribute('vertical');
+ },
+
+ set vertical(vertical) {
+ if (vertical) {
+ this.setAttribute('vertical', true);
+ } else {
+ this.removeAttribute('vertical');
+ }
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.settingsKey_ = settingsKey;
+ this.maybeInit_();
+ },
+
+ maybeInit_() {
+ if (!this.needsInit_) return;
+ if (this.settingsKey_ === undefined) return;
+ this.needsInit_ = false;
+ this.select(tr.b.Settings.get(this.settingsKey_));
+ },
+
+ set items(items) {
+ this.radioButtons_ = {};
+ items.forEach(function(e) {
+ if (e.key in this.radioButtons_) {
+ throw new Error(e.key + ' already exists');
+ }
+ const radioButton = document.createElement('div');
+ const input = document.createElement('input');
+ const label = document.createElement('label');
+ input.type = 'radio';
+ input.id = e.label;
+ input.addEventListener('click', function() {
+ this.select(e.key);
+ }.bind(this));
+ Polymer.dom(label).innerHTML = e.label;
+ label.htmlFor = e.label;
+ label.style.display = 'inline';
+ Polymer.dom(radioButton).appendChild(input);
+ Polymer.dom(radioButton).appendChild(label);
+ this.radioButtons_[e.key] = input;
+ }.bind(this));
+
+ this.maybeInit_();
+ this.maybeRenderRadioButtons_();
+ },
+
+ maybeRenderRadioButtons_() {
+ if (!this.isReady_) return;
+ if (this.radioButtons_ === undefined) return;
+ for (const key in this.radioButtons_) {
+ Polymer.dom(this.$.container).appendChild(
+ this.radioButtons_[key].parentElement);
+ }
+ if (this.selectedKey_ !== undefined) {
+ this.select(this.selectedKey_);
+ }
+ },
+
+ select(key) {
+ if (key === undefined || key === this.selectedKey_) {
+ return;
+ }
+ if (this.radioButtons_ === undefined) {
+ this.selectedKey_ = key;
+ return;
+ }
+ if (!(key in this.radioButtons_)) {
+ throw new Error(key + ' does not exists');
+ }
+ // Unselect the previous radio, update the key & select the new one.
+ if (this.selectedKey_ !== undefined) {
+ this.radioButtons_[this.selectedKey_].checked = false;
+ }
+ this.selectedKey_ = key;
+ tr.b.Settings.set(this.settingsKey_, this.selectedKey_);
+ if (this.selectedKey_ !== undefined) {
+ this.radioButtons_[this.selectedKey_].checked = true;
+ }
+
+ this.dispatchEvent(new tr.b.Event('change', false));
+ },
+
+ get selectedKey() {
+ return this.selectedKey_;
+ },
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html
new file mode 100644
index 00000000000..292cfaa4c9b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/radio_picker.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ this.addHTMLOutput(rp);
+ assert.strictEqual(rp.selectedKey, undefined);
+ rp.select('Toyota');
+ assert.strictEqual(rp.selectedKey, 'Toyota');
+ });
+
+ test('persistentState_setSelectedKeyAfterSettingItems', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const container1 = tr.ui.b.createDiv({textContent: 'Radio Picker One'});
+ container1.style.border = 'solid';
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = items;
+ rp.settingsKey = 'radio-picker-test-one';
+ Polymer.dom(container1).appendChild(rp);
+ this.addHTMLOutput(container1);
+ assert.strictEqual(rp.selectedKey, undefined);
+ rp.select('Toyota');
+ assert.strictEqual(rp.selectedKey, 'Toyota');
+
+ const container2 = tr.ui.b.createDiv({
+ textContent: 'Radio Picker Two (same settingKey as Radio Picker One)'});
+ container2.style.border = 'solid';
+ const rp2 = document.createElement('tr-ui-b-radio-picker');
+ rp2.items = items;
+ rp2.settingsKey = 'radio-picker-test-one';
+ Polymer.dom(container2).appendChild(rp2);
+ this.addHTMLOutput(container2);
+
+ assert.strictEqual(rp2.selectedKey, 'Toyota');
+ });
+
+ test('persistentState_setSelectedKeyBeforeSettingItems', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const container1 = tr.ui.b.createDiv({textContent: 'Radio Picker One'});
+ container1.style.border = 'solid';
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.settingsKey = 'radio-picker-test-two';
+ rp.items = items;
+ Polymer.dom(container1).appendChild(rp);
+ this.addHTMLOutput(container1);
+ assert.strictEqual(rp.selectedKey, undefined);
+ rp.select('Boeing');
+ assert.strictEqual(rp.selectedKey, 'Boeing');
+
+ const container2 = tr.ui.b.createDiv({
+ textContent: 'Radio Picker Two (same settingKey as Radio Picker One)'});
+ container2.style.border = 'solid';
+ const rp2 = document.createElement('tr-ui-b-radio-picker');
+ rp2.settingsKey = 'radio-picker-test-two';
+ Polymer.dom(container2).appendChild(rp2);
+ this.addHTMLOutput(container2);
+ rp2.items = items;
+
+ assert.strictEqual(rp2.selectedKey, 'Boeing');
+ });
+
+ test('changeEventFired', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = items;
+ this.addHTMLOutput(rp);
+ rp.select('Boeing');
+ assert.strictEqual(rp.selectedKey, 'Boeing');
+ let fired = false;
+ rp.addEventListener('change', function(e) {
+ fired = true;
+ assert.strictEqual('Toyota', e.target.selectedKey);
+ });
+ rp.select('Toyota');
+ assert.isTrue(fired);
+ });
+
+ test('verticalAttribute', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = items;
+ this.addHTMLOutput(rp);
+ assert.isNull(rp.getAttribute('vertical'));
+ rp.vertical = true;
+ assert.strictEqual(rp.getAttribute('vertical'), 'true');
+ rp.vertical = false;
+ assert.isNull(rp.getAttribute('vertical'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html
new file mode 100644
index 00000000000..38b292e2360
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/ui/base/chart_base_2d.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const ScatterChart = tr.ui.b.define('scatter-chart', tr.ui.b.ChartBase2D);
+
+ // @constructor
+ ScatterChart.Dot = function(x, y, radius, color, breadcrumb) {
+ this.x = x;
+ this.y = y;
+ this.radius = radius;
+ this.color = color;
+ this.breadcrumb = breadcrumb;
+ };
+
+ ScatterChart.prototype = {
+ __proto__: tr.ui.b.ChartBase2D.prototype,
+
+ decorate() {
+ super.decorate();
+ this.brushedXRange_ = new tr.b.math.Range();
+ this.brushedYRange_ = new tr.b.math.Range();
+ },
+
+ get hideLegend() {
+ return true;
+ },
+
+ get defaultGraphHeight() {
+ return 100;
+ },
+
+ get defaultGraphWidth() {
+ return 100;
+ },
+
+ updateMargins_() {
+ super.updateMargins_();
+ if (this.data.length === 0) return;
+
+ const rightOverhangPx = tr.b.math.Statistics.max(
+ this.data, d => this.xScale_(d.x) + d.radius - this.graphWidth);
+ this.margin.right = Math.max(this.margin.right, rightOverhangPx);
+
+ const topOverhangPx = tr.b.math.Statistics.max(
+ this.data, d => (this.graphHeight - this.yScale_(d.y)) + d.radius) -
+ this.graphHeight;
+ this.margin.top = Math.max(this.margin.top, topOverhangPx);
+ },
+
+ setBrushedRanges(xRange, yRange) {
+ this.brushedXRange_.reset();
+ this.brushedYRange_.reset();
+ this.brushedXRange_.addRange(xRange);
+ this.brushedYRange_.addRange(yRange);
+ this.updateContents_();
+ },
+
+ updateBrushContents_(brushSel) {
+ brushSel.selectAll('*').remove();
+ if (this.brushedXRange_.isEmpty || this.brushedYRange_.isEmpty) return;
+
+ const brushRectsSel = brushSel.selectAll('rect').data([undefined]);
+ brushRectsSel.enter().append('rect')
+ .attr('x', () => this.xScale_(this.brushedXRange_.min))
+ .attr('y', () => this.yScale_(this.brushedYRange_.max))
+ .attr('width', () => this.xScale_(this.brushedXRange_.max) -
+ this.xScale_(this.brushedXRange_.min))
+ .attr('height', () => this.yScale_(this.brushedYRange_.min) -
+ this.yScale_(this.brushedYRange_.max));
+ brushRectsSel.exit().remove();
+ },
+
+ setDataFromCallbacks(data, getX, getY, getRadius, getColor) {
+ this.data = data.map(d => new ScatterChart.Dot(
+ getX(d), getY(d), getRadius(d), getColor(d), d));
+ },
+
+ isDatumFieldSeries_(fieldName) {
+ return fieldName === 'y';
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ dataSel.selectAll('circle')
+ .data(this.data_)
+ .enter()
+ .append('circle')
+ .attr('cx', d => this.xScale_(d.x))
+ .attr('cy', d => this.yScale_(d.y))
+ .attr('r', d => d.radius)
+ .attr('fill', d => d.color);
+ }
+ };
+
+ return {
+ ScatterChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html
new file mode 100644
index 00000000000..a223589a2ac
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/scatter_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.ScatterChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, y: 100, radius: 2, color: 'red'},
+ {x: 20, y: 110, radius: 20, color: 'blue'},
+ {x: 30, y: 100, radius: 10, color: 'red'},
+ {x: 40, y: 50, radius: 10, color: 'red'}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.ScatterChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, y: 50, radius: 2, color: 'blue'},
+ {x: 20, y: 60, radius: 3, color: 'red'},
+ {x: 30, y: 80, radius: 4, color: 'orange'},
+ {x: 40, y: 20, radius: 5, color: 'purple'},
+ {x: 50, y: 30, radius: 6, color: 'yellow'},
+ {x: 60, y: 20, radius: 7, color: 'green'},
+ {x: 70, y: 15, radius: 8, color: 'blue'},
+ {x: 80, y: 20, radius: 9, color: 'red'}
+ ];
+
+ let mouseDown = undefined;
+
+ function updateBrushedRange(e) {
+ const xRange = new tr.b.math.Range();
+ if (e.x !== mouseDown.x) {
+ xRange.addValue(mouseDown.x);
+ xRange.addValue(e.x);
+ }
+ const yRange = new tr.b.math.Range();
+ if (e.y !== mouseDown.y) {
+ yRange.addValue(mouseDown.y);
+ yRange.addValue(e.y);
+ }
+ chart.setBrushedRanges(xRange, yRange);
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDown = e;
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ updateBrushedRange(e);
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ updateBrushedRange(e);
+ mouseDown = undefined;
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html
new file mode 100644
index 00000000000..a651fc5cc3d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html
@@ -0,0 +1,273 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<!--
+@fileoverview A view that allows the user to control which single tab is
+displayed.
+
+We follow a fairly standard web convention of backing our tabs with hidden radio
+buttons but visible radio button labels (the tabs themselves) which toggle the
+input element when clicked. Using hidden radio buttons makes sense, as both tabs
+and radio buttons are input elements that allow user selection through clicking
+and limit users to having one option selected at a time.
+-->
+<dom-module id='tr-ui-b-tab-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #selection_description, #tabs {
+ font-size: 12px;
+ }
+
+ #selection_description {
+ display: inline-block;
+ font-weight: bold;
+ margin: 9px 0px 4px 20px;
+ }
+
+ #tabs {
+ flex: 0 0 auto;
+ border-top: 1px solid #8e8e8e;
+ border-bottom: 1px solid #8e8e8e;
+ background-color: #ececec;
+ overflow: hidden;
+ margin: 0;
+ }
+
+ #tabs input[type=radio] {
+ display: none;
+ }
+
+ #tabs tab label {
+ cursor: pointer;
+ display: inline-block;
+ border: 1px solid #ececec;
+ margin: 5px 0px 0px 15px;
+ padding: 3px 10px 3px 10px;
+ }
+
+ #tabs tab label span {
+ font-weight: bold;
+ }
+
+ #tabs:focus input[type=radio]:checked ~ label {
+ outline: dotted 1px #8e8e8e;
+ outline-offset: -2px;
+ }
+
+ #tabs input[type=radio]:checked ~ label {
+ background-color: white;
+ border: 1px solid #8e8e8e;
+ border-bottom: 1px solid white;
+ }
+
+ #subView {
+ flex: 1 1 auto;
+ min-width: 0;
+ display: flex;
+ }
+
+ #subView > * {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ </style>
+ <div id='tabs' hidden="[[tabsHidden]]">
+ <label id=selection_description>[[label_]]</label>
+ <template is=dom-repeat items=[[subViews_]]>
+ <tab>
+ <input type=radio name=tabs id$=[[computeRadioId_(item)]]
+ on-change='onTabChanged_'
+ checked='[[isChecked_(item)]]'/>
+ <label for$=[[computeRadioId_(item)]]>
+ <template is=dom-if if=[[item.tabIcon]]>
+ <span style$='[[item.tabIcon.style]]'>[[item.tabIcon.text]]</span>
+ </template>
+ [[item.tabLabel]]
+ </label>
+ </tab>
+ </template>
+ </div>
+ <div id='subView'></div>
+ <slot>
+ </slot>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-tab-view',
+
+ properties: {
+ label_: {
+ type: String,
+ value: () => ''
+ },
+ selectedSubView_: Object,
+ subViews_: {
+ type: Array,
+ value: () => []
+ },
+ tabsHidden: {
+ type: Boolean,
+ value: false,
+ observer: 'tabsHiddenChanged_'
+ }
+ },
+
+ ready() {
+ this.$.tabs.addEventListener('keydown', this.onKeyDown_.bind(this), true);
+ this.updateFocusability_();
+ },
+
+ set label(newLabel) {
+ this.set('label_', newLabel);
+ },
+
+ get tabs() {
+ return this.get('subViews_');
+ },
+
+ get selectedSubView() {
+ return this.selectedSubView_;
+ },
+
+ set selectedSubView(subView) {
+ if (subView === this.selectedSubView_) return;
+
+ if (this.selectedSubView_) {
+ Polymer.dom(this.$.subView).removeChild(this.selectedSubView_);
+ const oldInput = this.root.getElementById(this.computeRadioId_(
+ this.selectedSubView_));
+ if (oldInput) {
+ oldInput.checked = false;
+ }
+ }
+
+ this.set('selectedSubView_', subView);
+
+ if (subView) {
+ Polymer.dom(this.$.subView).appendChild(subView);
+ const newInput = this.root.getElementById(this.computeRadioId_(subView));
+ if (newInput) {
+ newInput.checked = true;
+ }
+ }
+
+ this.fire('selected-tab-change');
+ },
+
+ clearSubViews() {
+ this.splice('subViews_', 0, this.subViews_.length);
+ this.selectedSubView = undefined;
+ this.updateFocusability_();
+ },
+
+ addSubView(subView) {
+ this.push('subViews_', subView);
+ if (!this.selectedSubView_) this.selectedSubView = subView;
+
+ this.updateFocusability_();
+ },
+
+ get subViews() {
+ return this.subViews_;
+ },
+
+ resetSubViews(subViews) {
+ this.splice('subViews_', 0, this.subViews_.length);
+ if (subViews.length) {
+ for (const subView of subViews) {
+ this.push('subViews_', subView);
+ }
+ this.selectedSubView = subViews[0];
+ } else {
+ this.selectedSubView = undefined;
+ }
+ this.updateFocusability_();
+ },
+
+ onTabChanged_(event) {
+ this.selectedSubView = event.model.item;
+ },
+
+ isChecked_(subView) {
+ return this.selectedSubView_ === subView;
+ },
+
+ tabsHiddenChanged_() {
+ this.updateFocusability_();
+ },
+
+ onKeyDown_(e) {
+ if (this.tabsHidden) return;
+
+ let keyHandled = false;
+ switch (e.keyCode) {
+ // Arrow left.
+ case 37:
+ keyHandled = this.selectPreviousTabIfPossible();
+ break;
+
+ // Arrow right.
+ case 39:
+ keyHandled = this.selectNextTabIfPossible();
+ break;
+ }
+
+ if (!keyHandled) return;
+ e.stopPropagation();
+ e.preventDefault();
+ },
+
+ selectNextTabIfPossible() {
+ return this.selectTabByOffsetIfPossible_(1);
+ },
+
+ selectPreviousTabIfPossible() {
+ return this.selectTabByOffsetIfPossible_(-1);
+ },
+
+ selectTabByOffsetIfPossible_(offset) {
+ if (!this.selectedSubView_) return false;
+ const currentIndex = this.subViews_.indexOf(this.selectedSubView_);
+ const newSubView = this.tabs[currentIndex + offset];
+ if (!newSubView) return false;
+ this.selectedSubView = newSubView;
+ return true;
+ },
+
+ shouldBeFocusable_() {
+ return !this.tabsHidden && this.subViews_.length > 0;
+ },
+
+ updateFocusability_() {
+ if (this.shouldBeFocusable_()) {
+ Polymer.dom(this.$.tabs).setAttribute('tabindex', 0);
+ } else {
+ Polymer.dom(this.$.tabs).removeAttribute('tabindex');
+ }
+ },
+
+ computeRadioId_(subView) {
+ // We can't just use the tagName as the radio's ID because there are
+ // instances where a single subview type can handle multiple event types,
+ // and thus might be present multiple times in a single tab view. In order
+ // to avoid the case where we might have two tabs with the same ID, we
+ // uniquify this ID by appending the tab's label with all spaces replaced
+ // by dashes (because spaces aren't allowed in HTML IDs).
+ return subView.tagName + '-' + subView.tabLabel.replace(/ /g, '-');
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html
new file mode 100644
index 00000000000..d5e9c19e672
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+
+<dom-module id='tr-ui-b-tab-view-test-non-sub-view'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+const nonSubViewBehavior = {};
+
+Polymer({
+ is: 'tr-ui-b-tab-view-test-non-sub-view',
+ behaviors: [nonSubViewBehavior]
+});
+
+tr.b.unittest.testSuite(function() {
+ function createPowerSampleSubView() {
+ const model = tr.c.TestUtils.newModel(function(m) {
+ m.device.powerSeries = new tr.model.PowerSeries(m.device);
+
+ m.device.vSyncTimestamps = [0];
+ m.device.powerSeries.addPowerSample(1, 1);
+ m.device.powerSeries.addPowerSample(2, 2);
+ m.device.powerSeries.addPowerSample(3, 3);
+ m.device.powerSeries.addPowerSample(4, 2);
+ });
+
+ const subView = document.createElement(
+ 'tr-ui-a-multi-power-sample-sub-view');
+ subView.selection = new tr.model.EventSet(model.device.powerSeries.samples);
+ subView.tabLabel = 'Power samples';
+ return subView;
+ }
+
+ function createAlertSubView() {
+ const slice = tr.c.TestUtils.newSliceEx(
+ {title: 'b', start: 0, duration: 0.002});
+ const alertInfo = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert',
+ [{
+ label: 'Example',
+ textContent: 'Example page',
+ href: 'http://www.example.com'
+ }]);
+
+ const alert = new tr.model.Alert(alertInfo, 5, [slice]);
+ const subView = document.createElement('tr-ui-a-alert-sub-view');
+ subView.selection = new tr.model.EventSet(alert);
+ subView.tabLabel = 'Alerts';
+ subView.tabIcon = { text: '\u26A0', style: 'color: red;' };
+
+ return subView;
+ }
+
+ test('instantiate_noTabs', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = 'No items selected.';
+ this.addHTMLOutput(tabView);
+ });
+
+ test('instantiate_oneTab', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '1 item selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ this.addHTMLOutput(tabView);
+ });
+
+ test('instantiate_twoTabs', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '3 items selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+ this.addHTMLOutput(tabView);
+ });
+
+ test('clearSubViews_selectedSubViewNullAfter', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '3 items selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+
+ tabView.clearSubViews();
+
+ assert.isUndefined(tabView.selectedSubView);
+ });
+
+ test('changeSelectedSubView', function() {
+ let selectedTabChangeEventCount = 0;
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.addEventListener('selected-tab-change', function() {
+ selectedTabChangeEventCount++;
+ });
+
+ assert.isUndefined(tabView.selectedSubView);
+ assert.strictEqual(selectedTabChangeEventCount, 0);
+
+ const view1 = createPowerSampleSubView();
+ tabView.addSubView(view1);
+ assert.strictEqual(tabView.selectedSubView, view1);
+ assert.strictEqual(selectedTabChangeEventCount, 1);
+
+ const view2 = createAlertSubView();
+ tabView.addSubView(view2);
+ assert.strictEqual(tabView.selectedSubView, view1);
+ assert.strictEqual(selectedTabChangeEventCount, 1);
+
+ tabView.selectedSubView = view2;
+ assert.strictEqual(tabView.selectedSubView, view2);
+ assert.strictEqual(selectedTabChangeEventCount, 2);
+ });
+
+ // Regression test: https://github.com/catapult-project/catapult/issues/2754
+ test('instantiate_twoTabsSwitch', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '3 items selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+ this.addHTMLOutput(tabView);
+ Polymer.dom.flush();
+
+ tabView.selectedSubView = tabView.tabs[1];
+ Polymer.dom.flush();
+
+ const selectedLabel = tabView.$.tabs.querySelector(':checked ~ label');
+ assert.isTrue(selectedLabel && selectedLabel.innerText.includes('Alerts'));
+ });
+
+ // Regression test: https://github.com/catapult-project/catapult/issues/2755
+ test('instantiate_twoTabsSwitchAndChange', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ this.addHTMLOutput(tabView);
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+ Polymer.dom.flush();
+
+ tabView.$.tabs.querySelectorAll('label')[2].click();
+ tabView.$.tabs.querySelectorAll('label')[1].click();
+ tabView.clearSubViews();
+ tabView.addSubView(createPowerSampleSubView());
+ Polymer.dom.flush();
+
+ assert.isTrue(!!tabView.$.tabs.querySelector(':checked'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table.html
new file mode 100644
index 00000000000..3d707fb4b87
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table.html
@@ -0,0 +1,1808 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<!--
+@fileoverview A container that constructs a table-like container.
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const TableFormat = {};
+
+ TableFormat.SelectionMode = {
+ // Selection disabled.
+ // Default highlight: none.
+ NONE: 0,
+
+ // Row selection mode.
+ // Default highlight: dark row.
+ ROW: 1,
+
+ // Cell selection mode.
+ // Default highlight: dark cell and light row.
+ CELL: 2
+ };
+
+ TableFormat.HighlightStyle = {
+ // Highlight depends on the current selection mode.
+ DEFAULT: 0,
+
+ // No highlight.
+ NONE: 1,
+
+ // Light highlight.
+ LIGHT: 2,
+
+ // Dark highlight.
+ DARK: 3
+ };
+
+ TableFormat.ColumnAlignment = {
+ LEFT: 0 /* default */,
+ RIGHT: 1
+ };
+
+ return {
+ TableFormat,
+ };
+});
+</script>
+
+<dom-module id="tr-ui-b-table">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ border-collapse: separate;
+ border-spacing: 0;
+ border-width: 0;
+ -webkit-user-select: initial;
+ }
+
+ tr > td {
+ padding: 2px 4px 2px 4px;
+ vertical-align: top;
+ }
+
+ table > tbody:focus {
+ outline: none;
+ }
+ table > tbody:focus[selection-mode="row"] > tr[selected],
+ table > tbody:focus[selection-mode="cell"] > tr > td[selected],
+ table > tbody:focus > tr.empty-row > td {
+ outline: 1px dotted #666666;
+ outline-offset: -1px;
+ }
+
+ button.toggle-button {
+ height: 15px;
+ line-height: 60%;
+ vertical-align: middle;
+ width: 100%;
+ }
+
+ button > * {
+ height: 15px;
+ vertical-align: middle;
+ }
+
+ td.button-column {
+ width: 30px;
+ }
+
+ table > thead > tr > td.sensitive:hover {
+ background-color: #fcfcfc;
+ }
+
+ table > thead > tr > td {
+ font-weight: bold;
+ text-align: left;
+
+ background-color: #eee;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ border-top: 1px solid #ffffff;
+ border-bottom: 1px solid #aaa;
+ }
+
+ table > tfoot {
+ background-color: #eee;
+ font-weight: bold;
+ }
+
+ /* Light row and cell highlight. */
+ table > tbody[row-highlight-style="light"] > tr[selected],
+ table > tbody[cell-highlight-style="light"] > tr > td[selected] {
+ background-color: rgb(213, 236, 229); /* light turquoise */
+ }
+ table > tbody[row-highlight-style="light"] >
+ tr:not(.empty-row):not([selected]):hover,
+ table > tbody[cell-highlight-style="light"] >
+ tr:not(.empty-row):not([selected]) > td:hover {
+ background-color: #f6f6f6; /* light grey */
+ }
+
+ /* Dark row and cell highlight. */
+ table > tbody[row-highlight-style="dark"] > tr[selected],
+ table > tbody[cell-highlight-style="dark"] > tr > td[selected] {
+ background-color: rgb(103, 199, 165); /* turquoise */
+ }
+ table > tbody[row-highlight-style="dark"] >
+ tr:not(.empty-row):not([selected]):hover,
+ table > tbody[cell-highlight-style="dark"] >
+ tr:not(.empty-row):not([selected]) > td:hover {
+ background-color: #e6e6e6; /* grey */
+ }
+ table > tbody[row-highlight-style="dark"] > tr:hover[selected],
+ table > tbody[cell-highlight-style="dark"] > tr[selected] > td:hover {
+ background-color: rgb(171, 217, 202); /* semi-light turquoise */
+ }
+
+ table > colgroup > col[selected] {
+ background-color: #e6e6e6; /* grey */
+ }
+
+ table > tbody > tr.empty-row > td {
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ table > tbody.has-footer > tr:last-child > td {
+ border-bottom: 1px solid #aaa;
+ }
+
+ table > tfoot > tr:first-child > td {
+ border-top: 1px solid #ffffff;
+ }
+
+ :host([zebra]) table tbody tr:nth-child(even) {
+ background-color: #f4f4f4;
+ }
+
+ expand-button {
+ -webkit-user-select: none;
+ cursor: pointer;
+ margin-right: 3px;
+ font-size: smaller;
+ height: 1rem;
+ }
+
+ expand-button.button-expanded {
+ transform: rotate(90deg);
+ }
+ </style>
+ <table>
+ <colgroup id="cols">
+ </colgroup>
+ <thead id="head">
+ </thead>
+ <tbody id="body">
+ </tbody>
+ <tfoot id="foot">
+ </tfoot>
+ </table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+(function() {
+ const RIGHT_ARROW = String.fromCharCode(0x25b6);
+ const UNSORTED_ARROW = String.fromCharCode(0x25BF);
+ const ASCENDING_ARROW = String.fromCharCode(0x25B4);
+ const DESCENDING_ARROW = String.fromCharCode(0x25BE);
+
+ const SelectionMode = tr.ui.b.TableFormat.SelectionMode;
+ const SelectionModeValues = new Set(Object.values(SelectionMode));
+ const HighlightStyle = tr.ui.b.TableFormat.HighlightStyle;
+ const HighlightStyleValues = new Set(Object.values(HighlightStyle));
+ const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
+ const ColumnAlignmentValues = new Set(Object.values(ColumnAlignment));
+
+ Polymer({
+ is: 'tr-ui-b-table',
+
+ created() {
+ this.selectionMode_ = SelectionMode.NONE;
+ this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+
+ this.tableColumns_ = [];
+ this.tableRows_ = [];
+ this.tableRowsInfo_ = new WeakMap();
+ this.tableFooterRows_ = [];
+ this.tableFooterRowsInfo_ = new WeakMap();
+ this.sortColumnIndex_ = undefined;
+ this.sortDescending_ = false;
+ this.columnsWithExpandButtons_ = [];
+ this.headerCells_ = [];
+ this.showHeader_ = true;
+ this.emptyValue_ = undefined;
+ this.subRowsPropertyName_ = 'subRows';
+ this.customizeTableRowCallback_ = undefined;
+ this.defaultExpansionStateCallback_ = undefined;
+ this.userCanModifySortOrder_ = true;
+ this.computedFontSizePx_ = undefined;
+ },
+
+ ready() {
+ this.$.body.addEventListener(
+ 'keydown', this.onKeyDown_.bind(this), true);
+ this.$.body.addEventListener(
+ 'focus', this.onFocus_.bind(this), true);
+ },
+
+ clear() {
+ this.selectionMode_ = SelectionMode.NONE;
+ this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+
+ Polymer.dom(this).textContent = '';
+ this.tableColumns_ = [];
+ this.tableRows_ = [];
+ this.tableRowsInfo_ = new WeakMap();
+ this.tableFooterRows_ = [];
+ this.tableFooterRowsInfo_ = new WeakMap();
+ this.sortColumnIndex_ = undefined;
+ this.sortDescending_ = false;
+ this.columnsWithExpandButtons_ = [];
+ this.headerCells_ = [];
+ this.showHeader_ = true;
+ this.emptyValue_ = undefined;
+ this.subRowsPropertyName_ = 'subRows';
+ this.defaultExpansionStateCallback_ = undefined;
+ this.userCanModifySortOrder_ = true;
+ },
+
+ set zebra(zebra) {
+ if (zebra) {
+ this.setAttribute('zebra', true);
+ } else {
+ this.removeAttribute('zebra');
+ }
+ },
+
+ get zebra() {
+ return this.getAttribute('zebra');
+ },
+
+ get showHeader() {
+ return this.showHeader_;
+ },
+
+ set showHeader(showHeader) {
+ this.showHeader_ = showHeader;
+ this.scheduleRebuildHeaders_();
+ },
+
+ set subRowsPropertyName(name) {
+ this.subRowsPropertyName_ = name;
+ },
+
+ /**
+ * This callback will be called whenever a body row is built
+ * for a userRow that has subRows and does not have an explicit
+ * isExpanded field.
+ * The callback should return true if the row should be expanded,
+ * or false if the row should be collapsed.
+ * @param {function(userRow, parentUserRow): boolean} cb The callback.
+ */
+ set defaultExpansionStateCallback(cb) {
+ this.defaultExpansionStateCallback_ = cb;
+ this.scheduleRebuildBody_();
+ },
+
+ /**
+ * This callback will be called whenever a body row is built.
+ * The callback's return value is ignored.
+ * @param {function(userRow, trElement)} cb The callback.
+ */
+ set customizeTableRowCallback(cb) {
+ this.customizeTableRowCallback_ = cb;
+ this.scheduleRebuildBody_();
+ },
+
+ get emptyValue() {
+ return this.emptyValue_;
+ },
+
+ set emptyValue(emptyValue) {
+ const previousEmptyValue = this.emptyValue_;
+ this.emptyValue_ = emptyValue;
+ if (this.tableRows_.length === 0 && emptyValue !== previousEmptyValue) {
+ this.scheduleRebuildBody_();
+ }
+ },
+
+ /**
+ * Data objects should have the following fields:
+ * mandatory: title, value
+ * optional: width {string}, cmp {function}, colSpan {number},
+ * showExpandButtons {boolean},
+ * align {tr.ui.b.TableFormat.ColumnAlignment}
+ *
+ * @param {Array} columns An array of data objects.
+ */
+ set tableColumns(columns) {
+ // Figure out the columns with expand buttons...
+ let columnsWithExpandButtons = [];
+ for (let i = 0; i < columns.length; i++) {
+ if (columns[i].showExpandButtons) {
+ columnsWithExpandButtons.push(i);
+ }
+ }
+ if (columnsWithExpandButtons.length === 0) {
+ // First column if none have specified.
+ columnsWithExpandButtons = [0];
+ }
+
+ // Sanity check columns.
+ for (let i = 0; i < columns.length; i++) {
+ const colInfo = columns[i];
+ if (colInfo.width === undefined) continue;
+
+ const hasExpandButton = columnsWithExpandButtons.includes(i);
+
+ const w = colInfo.width;
+ if (w) {
+ if (/\d+px/.test(w)) {
+ continue;
+ } else if (/\d+%/.test(w)) {
+ if (hasExpandButton) {
+ throw new Error('Columns cannot be %-sized and host ' +
+ ' an expand button');
+ }
+ } else {
+ throw new Error('Unrecognized width string');
+ }
+ }
+ }
+
+ // Try to preserve the user's sort choice.
+ // This is a 'best-effort' attempt, for example we compare columns by
+ // thier titles which can be HTML nodes in which case we might consider
+ // them different even if they look the same to the user.
+ let sortIndex = undefined;
+ const currentSortColumn = this.tableColumns[this.sortColumnIndex_];
+ if (currentSortColumn) {
+ for (const [i, column] of columns.entries()) {
+ if (currentSortColumn.title === column.title) {
+ sortIndex = i;
+ break;
+ }
+ }
+ }
+
+ // Commit the change.
+ this.tableColumns_ = columns;
+ this.headerCells_ = [];
+ this.columnsWithExpandButtons_ = columnsWithExpandButtons;
+ this.scheduleRebuildHeaders_();
+ this.sortColumnIndex = sortIndex;
+
+ // Blow away the table rows, too.
+ this.tableRows = this.tableRows_;
+ },
+
+ get tableColumns() {
+ return this.tableColumns_;
+ },
+
+ /**
+ * @param {Array} rows An array of 'row' objects with the following
+ * fields:
+ * optional: subRows An array of objects that have the same 'row'
+ * structure. Set subRowsPropertyName to use an
+ * alternative field name.
+ */
+ set tableRows(rows) {
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+ this.tableRows_ = rows;
+ this.tableRowsInfo_ = new WeakMap();
+ this.scheduleRebuildBody_();
+ },
+
+ get tableRows() {
+ return this.tableRows_;
+ },
+
+ set footerRows(rows) {
+ this.tableFooterRows_ = rows;
+ this.tableFooterRowsInfo_ = new WeakMap();
+ this.scheduleRebuildFooter_();
+ },
+
+ get footerRows() {
+ return this.tableFooterRows_;
+ },
+
+ get userCanModifySortOrder() {
+ return this.userCanModifySortOrder_;
+ },
+
+ set userCanModifySortOrder(userCanModifySortOrder) {
+ const newUserCanModifySortOrder = !!userCanModifySortOrder;
+ if (newUserCanModifySortOrder === this.userCanModifySortOrder_) {
+ return;
+ }
+
+ this.userCanModifySortOrder_ = newUserCanModifySortOrder;
+ this.scheduleRebuildHeaders_();
+ },
+
+ set sortColumnIndex(number) {
+ if (number === this.sortColumnIndex_) return;
+
+ if (number !== undefined) {
+ if (this.tableColumns_.length <= number) {
+ throw new Error('Column number ' + number + ' is out of bounds.');
+ }
+ if (!this.tableColumns_[number].cmp) {
+ throw new Error('Column ' + number + ' does not have a comparator.');
+ }
+ }
+
+ this.sortColumnIndex_ = number;
+ this.updateHeaderArrows_();
+ this.scheduleRebuildBody_();
+ this.dispatchSortingChangedEvent_();
+ },
+
+ get sortColumnIndex() {
+ return this.sortColumnIndex_;
+ },
+
+ set sortDescending(value) {
+ const newValue = !!value;
+
+ if (newValue !== this.sortDescending_) {
+ this.sortDescending_ = newValue;
+ this.updateHeaderArrows_();
+ this.scheduleRebuildBody_();
+ this.dispatchSortingChangedEvent_();
+ }
+ },
+
+ get sortDescending() {
+ return this.sortDescending_;
+ },
+
+ updateHeaderArrows_() {
+ for (let i = 0; i < this.headerCells_.length; i++) {
+ const headerCell = this.headerCells_[i];
+ const isColumnCurrentlySorted = i === this.sortColumnIndex_;
+ if (!this.tableColumns_[i].cmp ||
+ (!this.userCanModifySortOrder_ && !isColumnCurrentlySorted)) {
+ headerCell.sideContent = '';
+ continue;
+ }
+ if (!isColumnCurrentlySorted) {
+ headerCell.sideContent = UNSORTED_ARROW;
+ headerCell.sideContentDisabled = false;
+ continue;
+ }
+ headerCell.sideContent = this.sortDescending_ ?
+ DESCENDING_ARROW : ASCENDING_ARROW;
+ headerCell.sideContentDisabled = !this.userCanModifySortOrder_;
+ }
+ },
+
+ generateHeaderColumns_() {
+ const selectedTableColumnIndex = this.selectedTableColumnIndex;
+ Polymer.dom(this.$.cols).textContent = '';
+ for (let i = 0; i < this.tableColumns_.length; ++i) {
+ const colElement = document.createElement('col');
+ if (i === selectedTableColumnIndex) {
+ colElement.setAttribute('selected', true);
+ }
+ Polymer.dom(this.$.cols).appendChild(colElement);
+ }
+
+ this.headerCells_ = [];
+ Polymer.dom(this.$.head).textContent = '';
+ if (!this.showHeader_) return;
+
+ const tr = this.appendNewElement_(this.$.head, 'tr');
+ for (let i = 0; i < this.tableColumns_.length; i++) {
+ const td = this.appendNewElement_(tr, 'td');
+
+ const headerCell = document.createElement('tr-ui-b-table-header-cell');
+ headerCell.column = this.tableColumns_[i];
+
+ // If the table can be sorted by this column and the user can modify
+ // the sort order, attach a tap callback to the column.
+ if (this.tableColumns_[i].cmp) {
+ const isColumnCurrentlySorted = i === this.sortColumnIndex_;
+ if (isColumnCurrentlySorted) {
+ headerCell.sideContent = this.sortDescending_ ?
+ DESCENDING_ARROW : ASCENDING_ARROW;
+ if (!this.userCanModifySortOrder_) {
+ headerCell.sideContentDisabled = true;
+ }
+ }
+ if (this.userCanModifySortOrder_) {
+ Polymer.dom(td).classList.add('sensitive');
+ if (!isColumnCurrentlySorted) {
+ headerCell.sideContent = UNSORTED_ARROW;
+ }
+ headerCell.tapCallback = this.createSortCallback_(i);
+ }
+ }
+
+ Polymer.dom(td).appendChild(headerCell);
+ this.headerCells_.push(headerCell);
+ }
+ },
+
+ applySizes_() {
+ if (this.tableRows_.length === 0 && !this.showHeader) return;
+
+ let rowToRemoveSizing;
+ let rowToSize;
+ if (this.showHeader) {
+ rowToSize = Polymer.dom(this.$.head).children[0];
+ rowToRemoveSizing = Polymer.dom(this.$.body).children[0];
+ } else {
+ rowToSize = Polymer.dom(this.$.body).children[0];
+ rowToRemoveSizing = Polymer.dom(this.$.head).children[0];
+ }
+ for (let i = 0; i < this.tableColumns_.length; i++) {
+ if (rowToRemoveSizing && Polymer.dom(rowToRemoveSizing).children[i]) {
+ const tdToRemoveSizing = Polymer.dom(rowToRemoveSizing).children[i];
+ tdToRemoveSizing.style.minWidth = '';
+ tdToRemoveSizing.style.width = '';
+ }
+
+ // Apply sizing.
+ const td = Polymer.dom(rowToSize).children[i];
+
+ let delta;
+ if (this.columnsWithExpandButtons_.includes(i)) {
+ td.style.paddingLeft = this.basicIndentation_ + 'px';
+ delta = this.basicIndentation_ + 'px';
+ } else {
+ delta = undefined;
+ }
+
+ function calc(base, delta) {
+ if (delta) {
+ return 'calc(' + base + ' - ' + delta + ')';
+ }
+ return base;
+ }
+
+ const w = this.tableColumns_[i].width;
+ if (w) {
+ if (/\d+px/.test(w)) {
+ td.style.minWidth = calc(w, delta);
+ } else if (/\d+%/.test(w)) {
+ td.style.width = w;
+ } else {
+ throw new Error('Unrecognized width string: ' + w);
+ }
+ }
+ }
+ },
+
+ createSortCallback_(columnNumber) {
+ return function() {
+ if (!this.userCanModifySortOrder_) return;
+
+ const previousIndex = this.sortColumnIndex;
+ this.sortColumnIndex = columnNumber;
+ if (previousIndex !== columnNumber) {
+ this.sortDescending = false;
+ } else {
+ this.sortDescending = !this.sortDescending;
+ }
+ }.bind(this);
+ },
+
+ generateTableRowNodes_(tableSection, userRows, rowInfoMap,
+ indentation, lastAddedRow,
+ parentRowInfo) {
+ if (this.sortColumnIndex_ !== undefined &&
+ tableSection === this.$.body) {
+ userRows = userRows.slice(); // Don't mess with the input data.
+ userRows.sort(function(rowA, rowB) {
+ let c = this.tableColumns_[this.sortColumnIndex_].cmp(
+ rowA, rowB);
+ if (this.sortDescending_) {
+ c = -c;
+ }
+ return c;
+ }.bind(this));
+ }
+
+ for (let i = 0; i < userRows.length; i++) {
+ const userRow = userRows[i];
+ const rowInfo = this.getOrCreateRowInfoFor_(rowInfoMap, userRow,
+ parentRowInfo);
+ const htmlNode = this.getHTMLNodeForRowInfo_(
+ tableSection, rowInfo, rowInfoMap, indentation);
+
+ if (lastAddedRow === undefined) {
+ // Put first into the table.
+ Polymer.dom(tableSection).insertBefore(
+ htmlNode, Polymer.dom(tableSection).firstChild);
+ } else {
+ // This is shorthand for insertAfter(htmlNode, lastAdded).
+ const nextSiblingOfLastAdded = Polymer.dom(lastAddedRow).nextSibling;
+ Polymer.dom(tableSection).insertBefore(
+ htmlNode, nextSiblingOfLastAdded);
+ }
+
+ lastAddedRow = htmlNode;
+ if (!rowInfo.isExpanded) continue;
+
+ // Append subrows now.
+ lastAddedRow = this.generateTableRowNodes_(
+ tableSection, userRow[this.subRowsPropertyName_], rowInfoMap,
+ indentation + 1, lastAddedRow, rowInfo);
+ }
+ return lastAddedRow;
+ },
+
+ getOrCreateRowInfoFor_(rowInfoMap, userRow, parentRowInfo) {
+ let rowInfo = undefined;
+
+ if (rowInfoMap.has(userRow)) {
+ rowInfo = rowInfoMap.get(userRow);
+ } else {
+ rowInfo = {
+ userRow,
+ htmlNode: undefined,
+ parentRowInfo
+ };
+ rowInfoMap.set(userRow, rowInfo);
+ }
+
+ // Recompute isExpanded in case defaultExpansionStateCallback_ has
+ // changed.
+ rowInfo.isExpanded = this.getExpandedForUserRow_(userRow);
+
+ return rowInfo;
+ },
+
+ customizeTableRow_(userRow, trElement) {
+ if (!this.customizeTableRowCallback_) return;
+ this.customizeTableRowCallback_(userRow, trElement);
+ },
+
+ get basicIndentation_() {
+ if (this.computedFontSizePx_ === undefined) {
+ this.computedFontSizePx_ = parseInt(
+ getComputedStyle(this).fontSize) || 16;
+ }
+ return this.computedFontSizePx_ - 2;
+ },
+
+ getHTMLNodeForRowInfo_(tableSection, rowInfo,
+ rowInfoMap, indentation) {
+ if (rowInfo.htmlNode) {
+ this.customizeTableRow_(rowInfo.userRow, rowInfo.htmlNode);
+ return rowInfo.htmlNode;
+ }
+
+ const INDENT_SPACE = indentation * 16;
+ const INDENT_SPACE_NO_BUTTON = indentation * 16 + this.basicIndentation_;
+ const trElement = this.ownerDocument.createElement('tr');
+ rowInfo.htmlNode = trElement;
+ rowInfo.indentation = indentation;
+ trElement.rowInfo = rowInfo;
+ this.customizeTableRow_(rowInfo.userRow, trElement);
+
+ const isBodyRow = tableSection === this.$.body;
+ const isExpandableRow = rowInfo.userRow[this.subRowsPropertyName_] &&
+ rowInfo.userRow[this.subRowsPropertyName_].length;
+
+ for (let i = 0; i < this.tableColumns_.length;) {
+ const td = this.appendNewElement_(trElement, 'td');
+ td.columnIndex = i;
+
+ const column = this.tableColumns_[i];
+ const value = column.value(rowInfo.userRow);
+ const colSpan = column.colSpan ? column.colSpan : 1;
+ td.style.colSpan = colSpan;
+
+ switch (column.align) {
+ case undefined:
+ case ColumnAlignment.LEFT:
+ break;
+
+ case ColumnAlignment.RIGHT:
+ td.style.textAlign = 'right';
+ break;
+
+ default:
+ throw new Error('Invalid alignment of column at index=' + i +
+ ': ' + column.align);
+ }
+
+ if (this.doesColumnIndexSupportSelection(i)) {
+ Polymer.dom(td).classList.add('supports-selection');
+ }
+
+ if (this.columnsWithExpandButtons_.includes(i)) {
+ if (rowInfo.userRow[this.subRowsPropertyName_] &&
+ rowInfo.userRow[this.subRowsPropertyName_].length > 0) {
+ td.style.paddingLeft = INDENT_SPACE + 'px';
+ td.style.display = 'flex';
+ const expandButton = this.appendNewElement_(td, 'expand-button');
+ Polymer.dom(expandButton).textContent = RIGHT_ARROW;
+ if (rowInfo.isExpanded) {
+ Polymer.dom(expandButton).classList.add('button-expanded');
+ }
+ } else {
+ td.style.paddingLeft = INDENT_SPACE_NO_BUTTON + 'px';
+ }
+ }
+
+ if (value !== undefined) {
+ Polymer.dom(td).appendChild(
+ tr.ui.b.asHTMLOrTextNode(value, this.ownerDocument));
+ }
+
+ td.addEventListener('click', function(i, clickEvent) {
+ // Prevent automatically focusing on the table upon clicking on the
+ // table. Explicitly focus on it when appropriate (upon clicking on a
+ // selectable row/cell) instead.
+ clickEvent.preventDefault();
+
+ if (!isBodyRow && !isExpandableRow) return;
+
+ clickEvent.stopPropagation();
+
+ if (clickEvent.target.tagName === 'EXPAND-BUTTON') {
+ this.setExpandedForUserRow_(
+ tableSection, rowInfoMap,
+ rowInfo.userRow, !rowInfo.isExpanded);
+ return;
+ }
+
+ // If the row/cell can be selected and it's not selected yet,
+ // select it.
+ if (isBodyRow && this.selectionMode_ !== SelectionMode.NONE) {
+ let shouldSelect = false;
+ let shouldFocus = false;
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ shouldSelect = this.selectedTableRowInfo_ !== rowInfo;
+ shouldFocus = true;
+ break;
+ case SelectionMode.CELL:
+ if (this.doesColumnIndexSupportSelection(i)) {
+ shouldSelect = this.selectedTableRowInfo_ !== rowInfo ||
+ this.selectedColumnIndex_ !== i;
+ shouldFocus = true;
+ }
+ break;
+ default:
+ throw new Error('Invalid selection mode ' +
+ this.selectionMode_);
+ }
+ if (shouldFocus) {
+ this.focus();
+ }
+ if (shouldSelect) {
+ this.didTableRowInfoGetClicked_(rowInfo, i);
+ return;
+ }
+ }
+
+ // Otherwise, if the row is expandable, expand/collapse it.
+ if (isExpandableRow) {
+ this.setExpandedForUserRow_(tableSection, rowInfoMap,
+ rowInfo.userRow, !rowInfo.isExpanded);
+ }
+ }.bind(this, i));
+
+ // Add a double-click handler for stepping into a row/cell (if
+ // applicable).
+ if (isBodyRow) {
+ td.addEventListener('dblclick', function(i, e) {
+ e.stopPropagation();
+ this.dispatchStepIntoEvent_(rowInfo, i);
+ }.bind(this, i));
+ }
+
+ i += colSpan;
+ }
+
+ return rowInfo.htmlNode;
+ },
+
+ removeSubNodes_(tableSection, rowInfo, rowInfoMap) {
+ if (rowInfo.userRow[this.subRowsPropertyName_] === undefined) return;
+
+ for (let i = 0;
+ i < rowInfo.userRow[this.subRowsPropertyName_].length; i++) {
+ const subRow = rowInfo.userRow[this.subRowsPropertyName_][i];
+ const subRowInfo = rowInfoMap.get(subRow);
+ if (!subRowInfo) continue;
+
+ const subNode = subRowInfo.htmlNode;
+ if (subNode && Polymer.dom(subNode).parentNode === tableSection) {
+ Polymer.dom(tableSection).removeChild(subNode);
+ this.removeSubNodes_(tableSection, subRowInfo, rowInfoMap);
+ }
+ }
+ },
+
+ scheduleRebuildHeaders_() {
+ this.headerDirty_ = true;
+ this.scheduleRebuild_();
+ },
+
+ scheduleRebuildBody_() {
+ this.bodyDirty_ = true;
+ this.scheduleRebuild_();
+ },
+
+ scheduleRebuildFooter_() {
+ this.footerDirty_ = true;
+ this.scheduleRebuild_();
+ },
+
+ scheduleRebuild_() {
+ if (this.rebuildPending_) return;
+
+ this.rebuildPending_ = true;
+ setTimeout(function() {
+ this.rebuildPending_ = false;
+ this.rebuild();
+ }.bind(this), 0);
+ },
+
+ rebuildIfNeeded_() {
+ this.rebuild();
+ },
+
+ rebuild() {
+ const wasBodyOrHeaderDirty = this.headerDirty_ || this.bodyDirty_;
+
+ if (this.headerDirty_) {
+ this.generateHeaderColumns_();
+ this.headerDirty_ = false;
+ }
+ if (this.bodyDirty_) {
+ Polymer.dom(this.$.body).textContent = '';
+ this.generateTableRowNodes_(
+ this.$.body,
+ this.tableRows_, this.tableRowsInfo_, 0,
+ undefined, undefined);
+ if (this.tableRows_.length === 0 && this.emptyValue_ !== undefined) {
+ const trElement = this.ownerDocument.createElement('tr');
+ Polymer.dom(this.$.body).appendChild(trElement);
+ Polymer.dom(trElement).classList.add('empty-row');
+ const td = this.ownerDocument.createElement('td');
+ Polymer.dom(trElement).appendChild(td);
+ td.colSpan = this.tableColumns_.length;
+ const emptyValue = this.emptyValue_;
+ Polymer.dom(td).appendChild(
+ tr.ui.b.asHTMLOrTextNode(emptyValue, this.ownerDocument));
+ }
+ this.bodyDirty_ = false;
+ }
+
+ if (wasBodyOrHeaderDirty) this.applySizes_();
+
+ if (this.footerDirty_) {
+ Polymer.dom(this.$.foot).textContent = '';
+ this.generateTableRowNodes_(
+ this.$.foot,
+ this.tableFooterRows_, this.tableFooterRowsInfo_, 0,
+ undefined, undefined);
+ if (this.tableFooterRowsInfo_.length) {
+ Polymer.dom(this.$.body).classList.add('has-footer');
+ } else {
+ Polymer.dom(this.$.body).classList.remove('has-footer');
+ }
+ this.footerDirty_ = false;
+ }
+ },
+
+ appendNewElement_(parent, tagName) {
+ const element = parent.ownerDocument.createElement(tagName);
+ Polymer.dom(parent).appendChild(element);
+ return element;
+ },
+
+ getExpandedForTableRow(userRow) {
+ this.rebuildIfNeeded_();
+ const rowInfo = this.tableRowsInfo_.get(userRow);
+ if (rowInfo === undefined) {
+ throw new Error('Row has not been seen, must expand its parents');
+ }
+ return rowInfo.isExpanded;
+ },
+
+ getExpandedForUserRow_(userRow) {
+ if (userRow[this.subRowsPropertyName_] === undefined) {
+ return false;
+ }
+ if (userRow[this.subRowsPropertyName_].length === 0) {
+ return false;
+ }
+ if (userRow.isExpanded) {
+ return true;
+ }
+ if ((userRow.isExpanded !== undefined) &&
+ (userRow.isExpanded === false)) {
+ return false;
+ }
+
+ const rowInfo = this.tableRowsInfo_.get(userRow);
+ if (rowInfo && rowInfo.isExpanded) {
+ return true;
+ }
+
+ if (this.defaultExpansionStateCallback_ === undefined) {
+ return false;
+ }
+
+ let parentUserRow = undefined;
+ if (rowInfo && rowInfo.parentRowInfo) {
+ parentUserRow = rowInfo.parentRowInfo.userRow;
+ }
+
+ return this.defaultExpansionStateCallback_(
+ userRow, parentUserRow);
+ },
+
+ setExpandedForTableRow(userRow, expanded) {
+ this.rebuildIfNeeded_();
+ const rowInfo = this.tableRowsInfo_.get(userRow);
+ if (rowInfo === undefined) {
+ throw new Error('Row has not been seen, must expand its parents');
+ }
+ return this.setExpandedForUserRow_(this.$.body, this.tableRowsInfo_,
+ userRow, expanded);
+ },
+
+ setExpandedForUserRow_(tableSection, rowInfoMap,
+ userRow, expanded) {
+ this.rebuildIfNeeded_();
+
+ const rowInfo = rowInfoMap.get(userRow);
+ if (rowInfo === undefined) {
+ throw new Error('Row has not been seen, must expand its parents');
+ }
+
+ const wasExpanded = rowInfo.isExpanded;
+
+ rowInfo.isExpanded = !!expanded;
+ // If no node, then nothing further needs doing.
+ if (rowInfo.htmlNode === undefined) return;
+
+ // If its detached, then nothing needs doing.
+ if (rowInfo.htmlNode.parentElement !== tableSection) {
+ return;
+ }
+
+ // Otherwise, rebuild.
+ const expandButton =
+ Polymer.dom(rowInfo.htmlNode).querySelector('expand-button');
+ if (rowInfo.isExpanded) {
+ Polymer.dom(expandButton).classList.add('button-expanded');
+ const lastAddedRow = rowInfo.htmlNode;
+ if (rowInfo.userRow[this.subRowsPropertyName_]) {
+ this.generateTableRowNodes_(
+ tableSection,
+ rowInfo.userRow[this.subRowsPropertyName_], rowInfoMap,
+ rowInfo.indentation + 1,
+ lastAddedRow, rowInfo);
+ }
+ } else {
+ Polymer.dom(expandButton).classList.remove('button-expanded');
+ this.removeSubNodes_(tableSection, rowInfo, rowInfoMap);
+ }
+
+ if (wasExpanded !== rowInfo.isExpanded) {
+ const e = new tr.b.Event('row-expanded-changed');
+ e.row = rowInfo.userRow;
+ this.dispatchEvent(e);
+ }
+
+ this.maybeUpdateSelectedRow_();
+ },
+
+ get selectionMode() {
+ return this.selectionMode_;
+ },
+
+ set selectionMode(selectionMode) {
+ if (!SelectionModeValues.has(selectionMode)) {
+ throw new Error('Invalid selection mode ' + selectionMode);
+ }
+ this.rebuildIfNeeded_();
+ this.selectionMode_ = selectionMode;
+ this.didSelectionStateChange_();
+ },
+
+ get rowHighlightStyle() {
+ return this.rowHighlightStyle_;
+ },
+
+ set rowHighlightStyle(rowHighlightStyle) {
+ if (!HighlightStyleValues.has(rowHighlightStyle)) {
+ throw new Error('Invalid row highlight style ' + rowHighlightStyle);
+ }
+ this.rebuildIfNeeded_();
+ this.rowHighlightStyle_ = rowHighlightStyle;
+ this.didSelectionStateChange_();
+ },
+
+ get resolvedRowHighlightStyle() {
+ if (this.rowHighlightStyle_ !== HighlightStyle.DEFAULT) {
+ return this.rowHighlightStyle_;
+ }
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return HighlightStyle.NONE;
+ case SelectionMode.ROW:
+ return HighlightStyle.DARK;
+ case SelectionMode.CELL:
+ return HighlightStyle.LIGHT;
+ default:
+ throw new Error('Invalid selection mode ' + selectionMode);
+ }
+ },
+
+ get cellHighlightStyle() {
+ return this.cellHighlightStyle_;
+ },
+
+ set cellHighlightStyle(cellHighlightStyle) {
+ if (!HighlightStyleValues.has(cellHighlightStyle)) {
+ throw new Error('Invalid cell highlight style ' + cellHighlightStyle);
+ }
+ this.rebuildIfNeeded_();
+ this.cellHighlightStyle_ = cellHighlightStyle;
+ this.didSelectionStateChange_();
+ },
+
+ get resolvedCellHighlightStyle() {
+ if (this.cellHighlightStyle_ !== HighlightStyle.DEFAULT) {
+ return this.cellHighlightStyle_;
+ }
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ case SelectionMode.ROW:
+ return HighlightStyle.NONE;
+ case SelectionMode.CELL:
+ return HighlightStyle.DARK;
+ default:
+ throw new Error('Invalid selection mode ' + selectionMode);
+ }
+ },
+
+ setHighlightStyle_(highlightAttribute, resolvedHighlightStyle) {
+ switch (resolvedHighlightStyle) {
+ case HighlightStyle.NONE:
+ Polymer.dom(this.$.body).removeAttribute(highlightAttribute);
+ break;
+ case HighlightStyle.LIGHT:
+ Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'light');
+ break;
+ case HighlightStyle.DARK:
+ Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'dark');
+ break;
+ default:
+ throw new Error('Invalid resolved highlight style ' +
+ resolvedHighlightStyle);
+ }
+ },
+
+ didSelectionStateChange_() {
+ this.setHighlightStyle_('row-highlight-style',
+ this.resolvedRowHighlightStyle);
+ this.setHighlightStyle_('cell-highlight-style',
+ this.resolvedCellHighlightStyle);
+
+ this.removeSelectedState_();
+
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ // TODO: Replace this.selectionMode_ with a proper Polymer attribute.
+ Polymer.dom(this.$.body).setAttribute('selection-mode', 'row');
+ Polymer.dom(this.$.body).setAttribute('tabindex', 0);
+ this.selectedColumnIndex_ = undefined;
+ break;
+ case SelectionMode.CELL:
+ Polymer.dom(this.$.body).setAttribute('selection-mode', 'cell');
+ Polymer.dom(this.$.body).setAttribute('tabindex', 0);
+ if (this.selectedTableRowInfo_ &&
+ this.selectedColumnIndex_ === undefined) {
+ const i = this.getFirstSelectableColumnIndex_();
+ if (i === -1) {
+ // No column is selectable.
+ this.selectedTableRowInfo_ = undefined;
+ } else {
+ this.selectedColumnIndex_ = i;
+ }
+ }
+ break;
+ case SelectionMode.NONE:
+ Polymer.dom(this.$.body).removeAttribute('selection-mode');
+ Polymer.dom(this.$.body).removeAttribute('tabindex');
+ this.$.body.blur(); // Remove focus (if applicable).
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+ break;
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+
+ this.maybeUpdateSelectedRow_();
+ },
+
+ maybeUpdateSelectedRow_() {
+ if (this.selectedTableRowInfo_ === undefined) return;
+
+ // selectedUserRow may not be visible
+ function isVisible(rowInfo) {
+ if (!rowInfo.htmlNode) return false;
+ return !!rowInfo.htmlNode.parentElement;
+ }
+ if (isVisible(this.selectedTableRowInfo_)) {
+ this.updateSelectedState_();
+ return;
+ }
+
+ this.removeSelectedState_();
+ let curRowInfo = this.selectedTableRowInfo_;
+ while (curRowInfo && !isVisible(curRowInfo)) {
+ curRowInfo = curRowInfo.parentRowInfo;
+ }
+
+ this.selectedTableRowInfo_ = curRowInfo;
+ if (this.selectedTableRowInfo_) {
+ this.updateSelectedState_();
+ } else {
+ this.selectedColumnIndex_ = undefined;
+ }
+ },
+
+ didTableRowInfoGetClicked_(rowInfo, columnIndex) {
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return;
+
+ case SelectionMode.CELL:
+ if (!this.doesColumnIndexSupportSelection(columnIndex)) {
+ return;
+ }
+ if (this.selectedColumnIndex !== columnIndex) {
+ this.selectedColumnIndex = columnIndex;
+ }
+ // Fall through.
+
+ case SelectionMode.ROW:
+ if (this.selectedTableRowInfo_ !== rowInfo) {
+ this.selectedTableRow = rowInfo.userRow;
+ }
+ }
+ },
+
+ dispatchStepIntoEvent_(rowInfo, columnIndex) {
+ const e = new tr.b.Event('step-into');
+ e.tableRow = rowInfo.userRow;
+ e.tableColumn = this.tableColumns_[columnIndex];
+ e.columnIndex = columnIndex;
+ this.dispatchEvent(e);
+ },
+
+ /**
+ * If the selectionMode is CELL and a cell is selected,
+ * return an object containing the row, column, and value of the selected
+ * cell.
+ *
+ * @return {undefined|!Object}
+ */
+ get selectedCell() {
+ const row = this.selectedTableRow;
+ const columnIndex = this.selectedColumnIndex;
+ if (row === undefined || columnIndex === undefined ||
+ this.tableColumns_.length <= columnIndex) {
+ return undefined;
+ }
+ const column = this.tableColumns_[columnIndex];
+ return {
+ row,
+ column,
+ value: column.value(row)
+ };
+ },
+
+ /**
+ * If a column is selected, return the object describing the selected
+ * column.
+ *
+ * Columns can be selected independently of rows and cells. So it is
+ * possible to select column 0 and cell [0,0], or column 1 and cell [0,0],
+ * for example. See |selectedCell| for how to access the selected cell when
+ * the selectionMode is CELL.
+ *
+ * |selectedTableColumn| is entirely independent of |selectedColumnIndex|.
+ * When the table selectionMode is CELL, use |selectedTableRow| and
+ * |selectedColumnIndex| to find the selected cell.
+ * When one or more columns have |selectable:true|, then use
+ * |selectedTableColumn| to find the selected column, which may be either
+ * the same as or different from |selectedColumnIndex|, if a cell is also
+ * selected.
+ *
+ * @return {number|undefined}
+ */
+ get selectedTableColumnIndex() {
+ const cols = Polymer.dom(this.$.cols).children;
+ for (let i = 0; i < cols.length; ++i) {
+ if (cols[i].getAttribute('selected')) {
+ return i;
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * @param {number|undefined} index
+ */
+ set selectedTableColumnIndex(selectedIndex) {
+ const cols = Polymer.dom(this.$.cols).children;
+ for (let i = 0; i < cols.length; ++i) {
+ if (i === selectedIndex) {
+ cols[i].setAttribute('selected', true);
+ } else {
+ cols[i].removeAttribute('selected');
+ }
+ }
+ },
+
+ get selectedTableRow() {
+ if (!this.selectedTableRowInfo_) return undefined;
+ return this.selectedTableRowInfo_.userRow;
+ },
+
+ set selectedTableRow(userRow) {
+ this.rebuildIfNeeded_();
+ if (this.selectionMode_ === SelectionMode.NONE) {
+ throw new Error('Selection is off.');
+ }
+
+ let rowInfo;
+ if (userRow === undefined) {
+ rowInfo = undefined;
+ } else {
+ rowInfo = this.tableRowsInfo_.get(userRow);
+ if (!rowInfo) {
+ throw new Error('Row has not been seen, must expand its parents.');
+ }
+ }
+
+ const e = this.prepareToChangeSelection_();
+
+ if (!rowInfo) {
+ this.selectedColumnIndex_ = undefined;
+ } else {
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ this.selectedColumnIndex_ = undefined;
+ break;
+
+ case SelectionMode.CELL:
+ if (this.selectedColumnIndex_ === undefined) {
+ const i = this.getFirstSelectableColumnIndex_();
+ if (i === -1) {
+ throw new Error('Cannot find a selectable column.');
+ }
+ this.selectedColumnIndex_ = i;
+ }
+ break;
+
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+ }
+
+ this.selectedTableRowInfo_ = rowInfo;
+ this.updateSelectedState_();
+ this.dispatchEvent(e);
+ },
+
+ prepareToChangeSelection_() {
+ const e = new tr.b.Event('selection-changed');
+ const previousSelectedRowInfo = this.selectedTableRowInfo_;
+ if (previousSelectedRowInfo) {
+ e.previousSelectedTableRow = previousSelectedRowInfo.userRow;
+ } else {
+ e.previousSelectedTableRow = undefined;
+ }
+
+ this.removeSelectedState_();
+
+ return e;
+ },
+
+ removeSelectedState_() {
+ this.setSelectedState_(false);
+ },
+
+ updateSelectedState_() {
+ this.setSelectedState_(true);
+ },
+
+ setSelectedState_(select) {
+ if (this.selectedTableRowInfo_ === undefined) return;
+
+ // Row selection.
+ const rowNode = this.selectedTableRowInfo_.htmlNode;
+ if (select) {
+ Polymer.dom(rowNode).setAttribute('selected', true);
+ } else {
+ Polymer.dom(rowNode).removeAttribute('selected');
+ }
+
+ // Cell selection (if applicable).
+ const cellNode = Polymer.dom(rowNode).children[this.selectedColumnIndex_];
+ if (!cellNode) return;
+ if (select) {
+ Polymer.dom(cellNode).setAttribute('selected', true);
+ } else {
+ Polymer.dom(cellNode).removeAttribute('selected');
+ }
+ },
+
+ doesColumnIndexSupportSelection(columnIndex) {
+ const columnInfo = this.tableColumns_[columnIndex];
+ const scs = columnInfo.supportsCellSelection;
+ if (scs === false) return false;
+ return true;
+ },
+
+ getFirstSelectableColumnIndex_() {
+ for (let i = 0; i < this.tableColumns_.length; i++) {
+ if (this.doesColumnIndexSupportSelection(i)) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ getSelectableNodeGivenTableRowNode_(htmlNode) {
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ return htmlNode;
+
+ case SelectionMode.CELL:
+ return Polymer.dom(htmlNode).children[this.selectedColumnIndex_];
+
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+ },
+
+ get selectedColumnIndex() {
+ if (this.selectionMode_ !== SelectionMode.CELL) {
+ return undefined;
+ }
+ return this.selectedColumnIndex_;
+ },
+
+ set selectedColumnIndex(selectedColumnIndex) {
+ this.rebuildIfNeeded_();
+ if (this.selectionMode_ === SelectionMode.NONE) {
+ throw new Error('Selection is off.');
+ }
+ if (selectedColumnIndex < 0 ||
+ selectedColumnIndex >= this.tableColumns_.length) {
+ throw new Error('Invalid index');
+ }
+ if (!this.doesColumnIndexSupportSelection(selectedColumnIndex)) {
+ throw new Error('Selection is not supported on this column');
+ }
+
+ const e = this.prepareToChangeSelection_();
+ if (this.selectedColumnIndex_ === undefined) {
+ this.selectedTableRowInfo_ = undefined;
+ } else if (!this.selectedTableRowInfo_) {
+ if (this.tableRows_.length === 0) {
+ throw new Error('No available row to be selected');
+ }
+ this.selectedTableRowInfo_ =
+ this.tableRowsInfo_.get(this.tableRows_[0]);
+ }
+ this.selectedColumnIndex_ = selectedColumnIndex;
+ this.updateSelectedState_();
+ this.dispatchEvent(e);
+ },
+
+ onKeyDown_(e) {
+ if (this.selectionMode_ === SelectionMode.NONE) return;
+
+ const CODE_TO_COMMAND_NAMES = {
+ 13: 'ENTER',
+ 32: 'SPACE',
+ 37: 'ARROW_LEFT',
+ 38: 'ARROW_UP',
+ 39: 'ARROW_RIGHT',
+ 40: 'ARROW_DOWN'
+ };
+ const cmdName = CODE_TO_COMMAND_NAMES[e.keyCode];
+ if (cmdName === undefined) return;
+
+ e.stopPropagation();
+ e.preventDefault();
+ this.performKeyCommand_(cmdName);
+ },
+
+ onFocus_(e) {
+ // This method should be idempotent. If it can't be, then focus() must be
+ // updated.
+ if (this.selectionMode_ === SelectionMode.NONE ||
+ this.selectedTableRow ||
+ this.tableRows_.length === 0) {
+ return;
+ }
+
+ if (this.selectionMode_ === SelectionMode.CELL &&
+ this.getFirstSelectableColumnIndex_() === -1) {
+ // If there are no selectable columns in cell selection mode, don't do
+ // anything.
+ return;
+ }
+
+ this.selectedTableRow = this.tableRows_[0];
+ },
+
+ focus() {
+ this.$.body.focus();
+
+ // Need to manually call onFocus_ here: if the table is invisible for any
+ // reason, then the focus event will not fire, but the table may become
+ // visible later, and should reflect the focus accurately.
+ // If the table is already visible, then this will cause onFocus_ to be
+ // called multiple times. That shouldn't be a problem since onFocus_ is
+ // idempotent.
+ this.onFocus_();
+ },
+
+ blur() {
+ this.$.body.blur();
+ },
+
+ get isFocused() {
+ return this.root.activeElement === this.$.body;
+ },
+
+ performKeyCommand_(cmdName) {
+ this.rebuildIfNeeded_();
+
+ switch (cmdName) {
+ case 'ARROW_UP':
+ this.selectPreviousOrFirstRowIfPossible_();
+ return;
+
+ case 'ARROW_DOWN':
+ this.selectNextOrFirstRowIfPossible_();
+ return;
+
+ case 'ARROW_RIGHT':
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return; // No action.
+ case SelectionMode.ROW:
+ this.expandRowAndSelectChildRowIfPossible_();
+ return;
+ case SelectionMode.CELL:
+ this.selectNextSelectableCellToTheRightIfPossible_();
+ return;
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+
+ case 'ARROW_LEFT':
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return; // No action.
+ case SelectionMode.ROW:
+ this.collapseRowOrSelectParentRowIfPossible_();
+ return;
+ case SelectionMode.CELL:
+ this.selectNextSelectableCellToTheLeftIfPossible_();
+ return;
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+
+ case 'SPACE':
+ this.toggleRowExpansionStateIfPossible_();
+ return;
+
+ case 'ENTER':
+ this.stepIntoSelectionIfPossible_();
+ return;
+
+ default:
+ throw new Error('Unrecognized command ' + cmdName);
+ }
+ },
+
+ selectPreviousOrFirstRowIfPossible_() {
+ const prev = this.selectedTableRowInfo_ ?
+ this.selectedTableRowInfo_.htmlNode.previousElementSibling :
+ this.$.body.firstChild;
+ if (!prev) return;
+
+ if (this.selectionMode_ === SelectionMode.CELL &&
+ this.getFirstSelectableColumnIndex_() === -1) {
+ // If there are no selectable columns in cell selection mode, don't do
+ // anything.
+ return;
+ }
+ tr.ui.b.scrollIntoViewIfNeeded(prev);
+ this.selectedTableRow = prev.rowInfo.userRow;
+ },
+
+ selectNextOrFirstRowIfPossible_() {
+ this.getFirstSelectableColumnIndex_;
+ const next = this.selectedTableRowInfo_ ?
+ this.selectedTableRowInfo_.htmlNode.nextElementSibling :
+ this.$.body.firstChild;
+ if (!next) return;
+
+ if (this.selectionMode_ === SelectionMode.CELL &&
+ this.getFirstSelectableColumnIndex_() === -1) {
+ // If there are no selectable columns in cell selection mode, don't do
+ // anything.
+ return;
+ }
+ tr.ui.b.scrollIntoViewIfNeeded(next);
+ this.selectedTableRow = next.rowInfo.userRow;
+ },
+
+ expandRowAndSelectChildRowIfPossible_() {
+ const selectedRowInfo = this.selectedTableRowInfo_;
+ if (!selectedRowInfo ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_] === undefined ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_].length === 0) {
+ return;
+ }
+ if (!selectedRowInfo.isExpanded) {
+ this.setExpandedForTableRow(selectedRowInfo.userRow, true);
+ }
+ this.selectedTableRow =
+ selectedRowInfo.htmlNode.nextElementSibling.rowInfo.userRow;
+ },
+
+ collapseRowOrSelectParentRowIfPossible_() {
+ const selectedRowInfo = this.selectedTableRowInfo_;
+ if (!selectedRowInfo) return;
+
+ if (selectedRowInfo.isExpanded) {
+ // If the node is expanded, collapse it.
+ this.setExpandedForTableRow(selectedRowInfo.userRow, false);
+ } else {
+ // If the node is not expanded, select its parent.
+ const parentRowInfo = selectedRowInfo.parentRowInfo;
+ if (parentRowInfo) {
+ this.selectedTableRow = parentRowInfo.userRow;
+ }
+ }
+ },
+
+ selectNextSelectableCellToTheRightIfPossible_() {
+ if (!this.selectedTableRowInfo_ ||
+ this.selectedColumnIndex_ === undefined) {
+ return;
+ }
+ for (let i = this.selectedColumnIndex_ + 1; i < this.tableColumns_.length;
+ i++) {
+ if (this.doesColumnIndexSupportSelection(i)) {
+ this.selectedColumnIndex = i;
+ return;
+ }
+ }
+ },
+
+ selectNextSelectableCellToTheLeftIfPossible_() {
+ if (!this.selectedTableRowInfo_ ||
+ this.selectedColumnIndex_ === undefined) {
+ return;
+ }
+ for (let i = this.selectedColumnIndex_ - 1; i >= 0; i--) {
+ if (this.doesColumnIndexSupportSelection(i)) {
+ this.selectedColumnIndex = i;
+ return;
+ }
+ }
+ },
+
+ toggleRowExpansionStateIfPossible_() {
+ const selectedRowInfo = this.selectedTableRowInfo_;
+ if (!selectedRowInfo ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_] === undefined ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_].length === 0) {
+ return;
+ }
+ this.setExpandedForTableRow(selectedRowInfo.userRow,
+ !selectedRowInfo.isExpanded);
+ },
+
+ stepIntoSelectionIfPossible_() {
+ if (!this.selectedTableRowInfo_) return;
+ this.dispatchStepIntoEvent_(this.selectedTableRowInfo_,
+ this.selectedColumnIndex_);
+ },
+
+ dispatchSortingChangedEvent_() {
+ const e = new tr.b.Event('sort-column-changed');
+ e.sortColumnIndex = this.sortColumnIndex_;
+ e.sortDescending = this.sortDescending_;
+ this.dispatchEvent(e);
+ }
+ });
+})();
+</script>
+
+<dom-module id="tr-ui-b-table-header-cell">
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ display: flex;
+ }
+
+ span {
+ flex: 0 1 auto;
+ }
+
+ #side {
+ -webkit-user-select: none;
+ flex: 0 0 auto;
+ padding-left: 2px;
+ padding-right: 2px;
+ vertical-align: top;
+ font-size: 15px;
+ font-family: sans-serif;
+ line-height: 85%;
+ margin-left: 5px;
+ }
+
+ #side.disabled {
+ color: rgb(140, 140, 140);
+ }
+
+ #title:empty, #side:empty {
+ display: none;
+ }
+ </style>
+
+ <span id="title"></span>
+ <span id="side"></span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
+
+Polymer({
+ is: 'tr-ui-b-table-header-cell',
+
+ created() {
+ this.tapCallback_ = undefined;
+ this.cellTitle_ = '';
+ this.align_ = undefined;
+ this.selectable_ = false;
+ this.column_ = undefined;
+ },
+
+ ready() {
+ this.addEventListener('click', this.onTap_.bind(this));
+ },
+
+ set column(column) {
+ this.column_ = column;
+ this.align = column.align;
+ this.cellTitle = column.title;
+ },
+
+ get column() {
+ return this.column_;
+ },
+
+ set cellTitle(value) {
+ this.cellTitle_ = value;
+
+ const titleNode = tr.ui.b.asHTMLOrTextNode(
+ this.cellTitle_, this.ownerDocument);
+
+ this.$.title.innerText = '';
+
+ Polymer.dom(this.$.title).appendChild(titleNode);
+ },
+
+ get cellTitle() {
+ return this.cellTitle_;
+ },
+
+ set align(align) {
+ switch (align) {
+ case undefined:
+ case ColumnAlignment.LEFT:
+ this.style.justifyContent = '';
+ break;
+
+ case ColumnAlignment.RIGHT:
+ this.style.justifyContent = 'flex-end';
+ break;
+
+ default:
+ throw new Error('Invalid alignment of column (title=\'' +
+ this.cellTitle_ + '\'): ' + align);
+ }
+ this.align_ = align;
+ },
+
+ get align() {
+ return this.align_;
+ },
+
+ clearSideContent() {
+ Polymer.dom(this.$.side).textContent = '';
+ },
+
+ set sideContent(content) {
+ Polymer.dom(this.$.side).textContent = content;
+ this.$.side.style.display = content ? 'inline' : 'none';
+ },
+
+ get sideContent() {
+ return Polymer.dom(this.$.side).textContent;
+ },
+
+ set sideContentDisabled(sideContentDisabled) {
+ this.$.side.classList.toggle('disabled', sideContentDisabled);
+ },
+
+ get sideContentDisabled() {
+ return this.$.side.classList.contains('disabled');
+ },
+
+ set tapCallback(callback) {
+ this.style.cursor = 'pointer';
+ this.tapCallback_ = callback;
+ },
+
+ get tapCallback() {
+ return this.tapCallback_;
+ },
+
+ onTap_() {
+ if (this.tapCallback_) {
+ this.tapCallback_();
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html
new file mode 100644
index 00000000000..d7e8d427cb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<dom-module id='tr-ui-b-table-header-cell'>
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ display: flex;
+ }
+
+ span {
+ flex: 0 1 auto;
+ }
+
+ side-element {
+ -webkit-user-select: none;
+ flex: 1 0 auto;
+ padding-left: 4px;
+ vertical-align: top;
+ font-size: 15px;
+ font-family: sans-serif;
+ display: inline;
+ line-height: 85%;
+ }
+ </style>
+
+ <span id="title"></span><side-element id="side"></side-element>
+ </template>
+</dom-module>
+ <script>
+ 'use strict';
+
+ Polymer({
+ is: 'tr-ui-b-table-header-cell',
+
+ listeners: {
+ 'tap': 'onTap_'
+ },
+
+ created() {
+ this.tapCallback_ = undefined;
+ this.cellTitle_ = '';
+ },
+
+ set cellTitle(value) {
+ this.cellTitle_ = value;
+
+ const titleNode =
+ tr.ui.b.asHTMLOrTextNode(this.cellTitle_, this.ownerDocument);
+
+ this.$.title.innerText = '';
+ Polymer.dom(this.$.title).appendChild(titleNode);
+ },
+
+ get cellTitle() {
+ return this.cellTitle_;
+ },
+
+ clearSideContent() {
+ Polymer.dom(this.$.side).textContent = '';
+ },
+
+ set sideContent(content) {
+ Polymer.dom(this.$.side).textContent = content;
+ },
+
+ get sideContent() {
+ return Polymer.dom(this.$.side).textContent;
+ },
+
+ set tapCallback(callback) {
+ this.style.cursor = 'pointer';
+ this.tapCallback_ = callback;
+ },
+
+ get tapCallback() {
+ return this.tapCallback_;
+ },
+
+ onTap_() {
+ if (this.tapCallback_) {
+ this.tapCallback_();
+ }
+ }
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html
new file mode 100644
index 00000000000..73e8aca4418
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html
@@ -0,0 +1,2115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const SelectionMode = tr.ui.b.TableFormat.SelectionMode;
+ const HighlightStyle = tr.ui.b.TableFormat.HighlightStyle;
+ const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
+
+ function isSelected(element) {
+ if (!element.hasAttribute('selected')) return false;
+ return element.getAttribute('selected') === 'true';
+ }
+
+ function simulateDoubleClick(element) {
+ // See https://developer.mozilla.org/en/docs/Web/API/MouseEvent#Example.
+ const event = new MouseEvent('dblclick', {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ });
+ return element.dispatchEvent(event);
+ }
+
+ test('rowExpandedChanged', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Name',
+ value: row => row.value,
+ }
+ ];
+ table.tableRows = [{value: 'a', subRows: [{value: 'b'}]}];
+ let count = 0;
+ table.addEventListener('row-expanded-changed', e => ++count);
+ this.addHTMLOutput(table);
+ table.rebuild();
+
+ assert.strictEqual(0, count);
+
+ table.setExpandedForTableRow(table.tableRows[0], true);
+ assert.strictEqual(1, count);
+
+ table.setExpandedForTableRow(table.tableRows[0], true);
+ assert.strictEqual(1, count);
+
+ table.setExpandedForTableRow(table.tableRows[0], false);
+ assert.strictEqual(2, count);
+ });
+
+ test('instantiateEmptyTable_withoutEmptyValue', function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '300px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = [];
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // Check that the width of the first column was set correctly (despite no
+ // body rows).
+ const firstColumnHeader = table.$.head.children[0].children[0];
+ assert.closeTo(firstColumnHeader.offsetWidth, 300, 20);
+
+ // Check that the first column has a non-empty header.
+ const firstColumnTitle = tr.ui.b.findDeepElementMatchingPredicate(
+ firstColumnHeader, function(element) {
+ return Polymer.dom(element).textContent === 'First Column';
+ });
+ assert.isDefined(firstColumnTitle);
+
+ // Check that empty value was not appended.
+ assert.lengthOf(table.$.body.children, 0);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('instantiateEmptyTable_withEmptyValue', function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '300px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = [];
+ table.emptyValue = 'This table is left intentionally empty';
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // Check that the width of the first column was set correctly (despite no
+ // body rows).
+ const firstColumnHeader = table.$.head.children[0].children[0];
+ assert.closeTo(firstColumnHeader.offsetWidth, 300, 20);
+
+ // Check that empty value was appended.
+ assert.lengthOf(table.$.body.children, 1);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('instantiateNestedTableNoNests', function() {
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '200px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.emptyValue = 'THIS SHOULD NOT BE VISIBLE!!!';
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // Check that empty value was not appended.
+ assert.lengthOf(table.$.body.children, 2);
+ });
+
+ test('sequentialRebuildsBehaveSanely', function() {
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '200px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+ const footerRows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.footerRows = footerRows;
+ table.rebuild();
+ table.rebuild();
+ assert.strictEqual(table.$.body.children.length, 2);
+ assert.strictEqual(table.$.foot.children.length, 2);
+
+ this.addHTMLOutput(table);
+ });
+
+ test('instantiateNestedTableWithNests', function() {
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '250px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; },
+ width: '50%'
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2',
+ subRows: [
+ {
+ firstData: 'Sub1 A1',
+ secondData: 'Sub1 A2'
+ },
+ {
+ firstData: 'Sub2 A1',
+ secondData: 'Sub2 A2',
+ subRows: [
+ {
+ firstData: 'SubSub1 A1',
+ secondData: 'SubSub1 A2'
+ },
+ {
+ firstData: 'SubSub2 A1',
+ secondData: 'SubSub2 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'Sub3 A1',
+ secondData: 'Sub3 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+ });
+
+ test('instantiateSortingCallbacksWithNests', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '50%'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; },
+ width: '250px',
+ cmp(rowA, rowB) {
+ return rowA.secondData.toString().localeCompare(
+ rowB.secondData.toString());
+ },
+ showExpandButtons: true
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2',
+ subRows: [
+ {
+ firstData: 'Sub1 A1',
+ secondData: 'Sub1 A2'
+ },
+ {
+ firstData: 'Sub2 A1',
+ secondData: 'Sub2 A2',
+ subRows: [
+ {
+ firstData: 'SubSub1 A1',
+ secondData: 'SubSub1 A2'
+ },
+ {
+ firstData: 'SubSub2 A1',
+ secondData: 'SubSub2 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'Sub3 A1',
+ secondData: 'Sub3 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const footerRows = [
+ {
+ firstData: 'F1',
+ secondData: 'F2',
+ subRows: [
+ {
+ firstData: 'Sub1F1',
+ secondData: 'Sub1F2'
+ },
+ {
+ firstData: 'Sub2F1',
+ secondData: 'Sub2F2',
+ subRows: [
+ {
+ firstData: 'SubSub1F1',
+ secondData: 'SubSub1F2'
+ },
+ {
+ firstData: 'SubSub2F1',
+ secondData: 'SubSub2F2'
+ }
+ ]
+ },
+ {
+ firstData: 'Sub3F1',
+ secondData: 'Sub3F2'
+ }
+ ]
+ },
+ {
+ firstData: 'F\'1',
+ secondData: 'F\'2'
+ }
+
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.footerRows = footerRows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ const button = THIS_DOC.createElement('button');
+ Polymer.dom(button).textContent = 'Sort By Col 0';
+ button.addEventListener('click', function() {
+ table.sortDescending = !table.sortDescending;
+ table.sortColumnIndex = 0;
+ });
+ table.rebuild();
+
+ this.addHTMLOutput(button);
+ });
+
+
+ test('instantiateNestedTableAlreadyExpanded', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'a',
+ value(row) { return row.b; },
+ width: '50%'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'aToplevel',
+ b: 'bToplevel',
+ isExpanded: true,
+ subRows: [
+ {
+ a: 'a1',
+ b: 'b1'
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ const a1El = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => Polymer.dom(e).textContent === 'a1');
+ assert.isDefined(a1El);
+
+ const bToplevelEl = tr.ui.b.findDeepElementMatchingPredicate(
+ table,
+ function(element) {
+ return Polymer.dom(element).textContent === 'bToplevel';
+ });
+ assert.isDefined(bToplevelEl);
+ const expandButton = Polymer.dom(bToplevelEl.parentElement)
+ .querySelector('expand-button');
+ assert.isTrue(Polymer.dom(expandButton).classList.contains(
+ 'button-expanded'));
+ });
+
+
+ test('subRowsThatAreRetrievedOnDemand', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) { return row.a; },
+ width: '150px'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'row1',
+ subRows: [
+ {
+ b: 'row1.1',
+ get subRows() {
+ throw new Error('Shold not be called');
+ }
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+ });
+
+
+ test('instantiateTableWithHiddenHeader', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'a',
+ value(row) { return row.b; },
+ width: '50%'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'aToplevel',
+ b: 'bToplevel'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = false;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ const tHead = table.$.head;
+ assert.strictEqual(table.$.head.children.length, 0);
+ assert.strictEqual(0, tHead.getBoundingClientRect().height);
+
+ table.showHeader = true;
+ table.rebuild();
+ table.showHeader = false;
+ table.rebuild();
+ assert.strictEqual(table.$.head.children.length, 0);
+ });
+
+
+ test('sortColumnsNotPossibleOnPercentSizedColumns', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ value(row) { return row.b; },
+ width: '100%',
+ showExpandButtons: true
+ }
+ ];
+
+ const table1 = document.createElement('tr-ui-b-table');
+ table1.showHeader = true;
+
+ assert.throws(function() {
+ table1.tableColumns = columns;
+ });
+ });
+
+ test('twoTablesFirstColumnMatching', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ value(row) { return row.b; },
+ width: '100%'
+ }
+ ];
+
+ const table1 = document.createElement('tr-ui-b-table');
+ table1.showHeader = true;
+ table1.tableColumns = columns;
+ table1.tableRows = [
+ {
+ a: 'first',
+ b: 'row'
+ }
+ ];
+ table1.rebuild();
+ this.addHTMLOutput(table1);
+
+ const table2 = document.createElement('tr-ui-b-table');
+ table2.showHeader = false;
+ table2.tableColumns = columns;
+ table2.tableRows = [
+ {
+ a: 'second',
+ b: 'row'
+ }
+ ];
+ table2.rebuild();
+ this.addHTMLOutput(table2);
+
+ const h1FirstCol = table1.$.head.children[0].children[0];
+ const h2FirstCol = table2.$.body.children[0].children[0];
+ assert.strictEqual(h1FirstCol.getBoundingClientRect().width,
+ h2FirstCol.getBoundingClientRect().width);
+ });
+
+ test('programmaticSorting', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ }
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'A1',
+ subRows: [
+ {
+ value: 'A1.1'
+ },
+ {
+ value: 'A1.2',
+ subRows: [
+ {
+ value: 'A1.2.1'
+ },
+ {
+ value: 'A1.2.2'
+ }
+ ]
+ },
+ {
+ value: 'A1.3'
+ }
+ ]
+ },
+ {
+ value: 'A2'
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ table.sortDescending = true;
+ table.sortColumnIndex = 0;
+ table.rebuild();
+ const r0 = table.$.body.children[0];
+ assert.strictEqual(r0.rowInfo.userRow, rows[1]);
+
+ const r1 = table.$.body.children[1];
+ assert.strictEqual(r1.rowInfo.userRow, rows[0]);
+ });
+
+ test('sortDispatchesEvent', function() {
+ const table = document.createElement('tr-ui-b-table');
+ const columns = [
+ {
+ title: 'Column 0',
+ value(row) { return row.value0; },
+ cmp(rowA, rowB) { return rowA.value0 - rowB.value0; }
+ },
+ {
+ title: 'Column 1',
+ value(row) { return row.value1; },
+ cmp(rowA, rowB) { return rowA.value1 - rowB.value1; }
+ }
+ ];
+
+ let sortColumnIndex = undefined;
+ let sortDescending = undefined;
+ let numListenerCalls = 0;
+ table.tableColumns = columns;
+ table.addEventListener('sort-column-changed', function(e) {
+ sortColumnIndex = e.sortColumnIndex;
+ sortDescending = e.sortDescending;
+ numListenerCalls++;
+ });
+ table.rebuild();
+
+ table.sortColumnIndex = 0;
+ assert.strictEqual(sortColumnIndex, 0);
+ assert.strictEqual(numListenerCalls, 1);
+
+ table.sortDescending = true;
+ assert.strictEqual(sortColumnIndex, 0);
+ assert.isTrue(sortDescending);
+ assert.strictEqual(numListenerCalls, 2);
+
+ table.sortColumnIndex = 1;
+ table.sortDescending = false;
+ assert.strictEqual(sortColumnIndex, 1);
+ assert.isFalse(sortDescending);
+ assert.strictEqual(numListenerCalls, 4);
+
+ table.sortColumnIndex = undefined;
+ assert.strictEqual(sortColumnIndex, undefined);
+ assert.strictEqual(numListenerCalls, 5);
+ });
+
+ test('sortingAfterExpand', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ }
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'A1',
+ isExpanded: true,
+ subRows: [
+ {
+ value: 'A1.1'
+ },
+ {
+ value: 'A1.2',
+ subRows: [
+ {
+ value: 'A1.2.1'
+ },
+ {
+ value: 'A1.2.2'
+ }
+ ]
+ },
+ {
+ value: 'A1.3'
+ }
+ ]
+ },
+ {
+ value: 'A2'
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ table.sortDescending = true;
+ table.sortColumnIndex = 0;
+ table.rebuild();
+ const r0 = table.$.body.children[0];
+ assert.strictEqual(r0.rowInfo.userRow, rows[1]);
+
+ const r1 = table.$.body.children[1];
+ assert.strictEqual(r1.rowInfo.userRow, rows[0]);
+
+ const r2 = table.$.body.children[2];
+ assert.strictEqual(r2.rowInfo.userRow, rows[0].subRows[2]);
+
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ });
+
+ function createSimpleOneColumnNestedTable() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ }
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'A1',
+ subRows: [
+ {
+ value: 'A1.1'
+ },
+ {
+ value: 'A1.2',
+ subRows: [
+ {
+ value: 'A1.2.1'
+ },
+ {
+ value: 'A1.2.2'
+ }
+ ]
+ },
+ {
+ value: 'A1.3'
+ }
+ ]
+ },
+ {
+ value: 'A2'
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ return table;
+ }
+
+ function createMultiColumnNestedTable() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'A',
+ value(row) { return row.a; },
+ width: '25%'
+ },
+ {
+ title: 'B',
+ value(row) { return row.b; },
+ width: '25%'
+ },
+ {
+ title: 'C',
+ value(row) { return row.c; },
+ width: '25%',
+ supportsCellSelection: false
+ },
+ {
+ title: 'D',
+ value(row) { return row.d; },
+ width: '25%'
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'R1',
+ a: 1, b: 2, c: 3, d: 4,
+ subRows: [
+ {
+ value: 'R1.1',
+ a: 2, b: 3, c: 4, d: 1,
+ },
+ {
+ value: 'R1.2',
+ a: 3, b: 4, c: 1, d: 2,
+ }
+ ]
+ },
+ {
+ value: 'R2',
+ a: 3, b: 4, c: 1, d: 2
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ return table;
+ }
+
+ test('expandAfterRebuild', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.rebuild();
+ const rows = table.tableRows;
+
+ this.addHTMLOutput(table);
+
+ table.rebuild();
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+ table.setExpandedForTableRow(rows[0], true);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ const r1 = table.$.body.children[1];
+ assert.strictEqual(r1.rowInfo.userRow, rows[0].subRows[0]);
+ });
+
+ test('tableSelection', function() {
+ const table = createMultiColumnNestedTable();
+ const rows = table.tableRows;
+
+ table.selectionMode = SelectionMode.ROW;
+ table.selectedTableRow = rows[0];
+ assert.isUndefined(table.selectedColumnIndex);
+
+ table.setExpandedForTableRow(rows[0], true);
+ table.selectedTableRow = rows[0].subRows[1];
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ table.selectionMode = SelectionMode.CELL;
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.setExpandedForTableRow(rows[0], false);
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.selectionMode = SelectionMode.NONE;
+ assert.strictEqual(table.selectedTableRow, undefined);
+
+ table.selectionMode = SelectionMode.ROW;
+ table.setExpandedForTableRow(rows[0].subRows[1], true);
+ this.addHTMLOutput(table);
+
+ assert.isTrue(table.$.body.hasAttribute('tabindex'));
+ });
+
+
+ test('keyMovement_rows', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+
+ const rows = table.tableRows;
+ table.selectedTableRow = rows[0];
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+
+ table.performKeyCommand_('ARROW_UP');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+
+ // Enter on collapsed row should expand.
+ table.selectedTableRow = rows[0];
+ table.performKeyCommand_('SPACE');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('SPACE');
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ // Arrow right on collapsed row should expand.
+ table.selectedTableRow = rows[0];
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]);
+
+ // Arrow left on collapsed item should select parent.
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+ // Arrow left on parent should collapse its children.
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ // Arrow right on expanded row should select first child.
+ table.selectedTableRow = rows[0];
+ table.setExpandedForTableRow(rows[0], true);
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+
+ // Arrow right on a non-expandable row should do nothing.
+ table.selectedTableRow = rows[1];
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ assert.isFalse(table.getExpandedForTableRow(rows[1]));
+ });
+
+ test('keyMovement_cells', function() {
+ const table = createMultiColumnNestedTable();
+ table.selectionMode = SelectionMode.CELL;
+ this.addHTMLOutput(table);
+
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ const rows = table.tableRows;
+ table.selectedTableRow = rows[1];
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ // No-op (leftmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_UP');
+ // No-op (top row already selected).
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_UP');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 2);
+ // Right arrow should NOT expand nested rows in cell selection mode.
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ // No-op (rightmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('SPACE');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ // Space on collapsed row should expand it.
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ // Left arrow should NOT move to parent row.
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 2);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ // No-op (leftmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_UP');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ // No-op (leftmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ // Left arrow should NOT collapse nested rows in cell selection mode.
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('SPACE');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ // Space on expanded row should collapse it.
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ // No-op (bottom row already selected).
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+ });
+
+ test('focus_empty', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.tableRows = [];
+ table.emptyValue = 'This table is left intentionally empty';
+ this.addHTMLOutput(table);
+
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+
+ for (const selectionMode of [SelectionMode.ROW, SelectionMode.CELL]) {
+ table.selectionMode = selectionMode;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually unfocus.
+ table.blur();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus again.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Unfocus via removing selection mode.
+ table.selectionMode = SelectionMode.NONE;
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ }
+
+ // Re-enable selection mode (for interactive testing).
+ table.selectionMode = SelectionMode.ROW;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ });
+
+ test('focus_rows', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually unfocus.
+ table.blur();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Trigger focus via clicking.
+ table.$.body.children[1].children[0].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[1]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Unfocus via removing selection mode.
+ table.selectionMode = SelectionMode.NONE;
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Re-enable selection mode.
+ table.selectionMode = SelectionMode.ROW;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Programatically select row (should NOT steal focus).
+ table.selectedTableRow = table.tableRows[0];
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Trigger focus on the already selected row by clicking.
+ table.$.body.children[0].children[0].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+ });
+
+ test('focus_cells', function() {
+ const table = createMultiColumnNestedTable();
+ table.selectionMode = SelectionMode.CELL;
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ // Manually unfocus.
+ table.blur();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ // Trigger focus via clicking.
+ table.$.body.children[1].children[4].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+
+ // Unfocus via removing selection mode.
+ table.selectionMode = SelectionMode.NONE;
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Re-enable selection mode.
+ table.selectionMode = SelectionMode.CELL;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Clicking on an unselectable cell should NOT trigger focus.
+ table.$.body.children[1].children[0].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Programatically select cell (should NOT steal focus).
+ table.selectedTableRow = table.tableRows[0];
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ // Trigger focus on the already selected cell by clicking.
+ table.$.body.children[0].children[1].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ });
+
+ test('focus_allCellsUnselectable', function() {
+ const table = createMultiColumnNestedTable();
+ table.selectionMode = SelectionMode.CELL;
+ for (const c of table.tableColumns) {
+ c.supportsCellSelection = false;
+ }
+ table.tableColumns = table.tableColumns;
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus (no automatic selection).
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Trigger focus via clicking (no selection).
+ table.$.body.children[1].children[2].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ });
+
+ test('RightArrowKeyWhenTableSorted', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+ table.sortDescending = true;
+ table.sortColumnIndex = 0;
+ table.rebuild();
+ const rows = table.tableRows;
+
+ // Arrow right should select the first child showing up on the viewer,
+ // rather than first child in sub rows since sorted.
+ table.selectedTableRow = rows[0];
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[2]);
+ });
+
+ test('reduceNumberOfColumnsAfterRebuild', function() {
+ // Create a table with two columns.
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '100px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; },
+ width: '100px'
+ }
+ ];
+
+ // Build the table.
+ table.rebuild();
+
+ // Check that reducing the number of columns doesn't throw an exception.
+ table.tableColumns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '200px'
+ }
+ ];
+ });
+
+ test('rowHighlightDark', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.rowHighlightStyle = HighlightStyle.DARK;
+ table.tableColumns = columns;
+ table.tableRows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+ table.rebuild();
+ this.addHTMLOutput(table);
+ });
+
+ test('cellHighlightLight', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.cellHighlightStyle = HighlightStyle.LIGHT;
+ table.tableColumns = columns;
+ table.tableRows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+ table.rebuild();
+ this.addHTMLOutput(table);
+ });
+
+ test('cellSelectionBasic', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.selectionMode = SelectionMode.CELL;
+ table.rowHighlightStyle = HighlightStyle.NONE;
+ table.tableColumns = columns;
+ table.tableRows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ table.selectedTableRow = table.tableRows[0];
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ let selectedCell = table.selectedCell;
+ assert.strictEqual(selectedCell.row, table.tableRows[0]);
+ assert.strictEqual(selectedCell.column, columns[1]);
+ assert.strictEqual(selectedCell.value, '1');
+
+ table.performKeyCommand_('ARROW_DOWN');
+ table.performKeyCommand_('ARROW_RIGHT');
+ table.performKeyCommand_('ARROW_RIGHT');
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, table.tableRows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 2);
+ selectedCell = table.selectedCell;
+ assert.strictEqual(selectedCell.row, table.tableRows[1]);
+ assert.strictEqual(selectedCell.column, columns[2]);
+ assert.strictEqual(selectedCell.value, 4);
+
+ table.selectedTableRow = undefined;
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ assert.isUndefined(table.selectedColumnIndex);
+ assert.isUndefined(table.selectedCell);
+ });
+
+ test('cellSelectionNested', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Value',
+ value(row) { return row.b; },
+ width: '150px'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'parent',
+ b: '1',
+ subRows: [
+ {
+ a: 'child',
+ b: '2'
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.selectionMode = SelectionMode.CELL;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ // Expand the parent row.
+ table.setExpandedForTableRow(rows[0], true);
+
+ // Select the second cell in the child row.
+ table.selectedTableRow = rows[0].subRows[0];
+ assert.isFalse(isSelected(table.$.body.children[0]));
+ assert.isFalse(isSelected(table.$.body.children[0].children[1]));
+ assert.isTrue(isSelected(table.$.body.children[1]));
+ assert.isTrue(isSelected(table.$.body.children[1].children[1]));
+
+ // Fold the parent row. The second cell in the parent row should be
+ // automatically selected.
+ table.setExpandedForTableRow(rows[0], false);
+ assert.isTrue(isSelected(table.$.body.children[0]));
+ assert.isTrue(isSelected(table.$.body.children[0].children[1]));
+
+ // Expand the parent row again. Only the second cell of the parent row
+ // should still be selected.
+ table.setExpandedForTableRow(rows[0], true);
+ assert.isTrue(isSelected(table.$.body.children[0]));
+ assert.isTrue(isSelected(table.$.body.children[0].children[1]));
+ assert.isFalse(isSelected(table.$.body.children[1]));
+ assert.isFalse(isSelected(table.$.body.children[1].children[1]));
+ });
+
+ test('resolvedHighlightStyle', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ // Undefined selection mode.
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.NONE);
+
+ // Row selection mode.
+ table.selectionMode = SelectionMode.ROW;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.DARK);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.NONE);
+
+ // Cell selection mode.
+ table.selectionMode = SelectionMode.CELL;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.LIGHT);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK);
+
+ // Explicit row highlight style.
+ table.rowHighlightStyle = HighlightStyle.NONE;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK);
+
+ // Explicit row and cell highlight styles.
+ table.cellHighlightStyle = HighlightStyle.LIGHT;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.LIGHT);
+
+ // Back to default highlight styles.
+ table.cellHighlightStyle = HighlightStyle.DEFAULT;
+ table.rowHighlightStyle = HighlightStyle.DEFAULT;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.LIGHT);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK);
+ });
+
+ test('headersWithHtmlElements', function() {
+ const firstColumnTitle = document.createTextNode('First Column');
+ const secondColumnTitle = document.createElement('span');
+ secondColumnTitle.innerText = 'Second Column';
+ secondColumnTitle.style.color = 'blue';
+
+ const columns = [
+ {
+ title: firstColumnTitle,
+ value(row) { return row.firstData; },
+ width: '200px'
+ },
+ {
+ title: secondColumnTitle,
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ const firstColumnHeader = table.$.head.children[0].children[0].children[0];
+ const secondColumnHeader = table.$.head.children[0].children[1].children[0];
+ assert.strictEqual(Polymer.dom(firstColumnHeader.cellTitle).textContent,
+ 'First Column');
+ assert.strictEqual(Polymer.dom(secondColumnHeader.cellTitle).textContent,
+ 'Second Column');
+ });
+
+ test('align', function() {
+ const columns = [
+ {
+ title: 'a',
+ align: ColumnAlignment.RIGHT,
+ value(row) {
+ return row.a;
+ }
+ }
+ ];
+ const rows = [{a: 1}, {a: 'long-row-so-that-alignment-would-be-visible'}];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(
+ table.$.body.children[0].children[0].style.textAlign, 'right');
+ });
+
+ test('subRowsPropertyName', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) {
+ return row.a;
+ }
+ }
+ ];
+ const rows = [
+ {
+ a: 1,
+ isExpanded: true,
+ children: [
+ {a: 2}
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.subRowsPropertyName = 'children';
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(
+ '2', Polymer.dom(table.$.body.children[1].children[0]).textContent);
+ });
+
+ test('shouldNotRenderUndefined', function() {
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.firstData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: undefined,
+ secondData: 'A2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // check that we don't have 'undefined' anywhere
+ assert.isTrue(Polymer.dom(table.$.body).innerHTML.indexOf('undefined') < 0);
+ });
+
+ test('customizeTableRowCallback', function() {
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.data; }
+ }
+ ];
+
+ const rows = [
+ {
+ data: 'data'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ let callbackCalled = false;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.customizeTableRowCallback = function(userRow, trElement) {
+ callbackCalled = (userRow === rows[0]);
+ };
+ table.rebuild();
+ assert.isTrue(callbackCalled);
+
+ this.addHTMLOutput(table);
+
+ // The callback can also be set after the table is first built.
+ table.customizeTableRowCallback = function(userRow, trElement) {
+ callbackCalled = (userRow === rows[0]);
+ };
+
+ // Setting the customize callback should set the body dirty.
+ assert.isTrue(table.bodyDirty_);
+
+ callbackCalled = false;
+
+ // Don't bother waiting for the timeout.
+ table.rebuild();
+
+ assert.isTrue(callbackCalled);
+ });
+
+ test('selectionEdgeCases', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Column',
+ value(row) { return row.data; },
+ supportsCellSelection: false
+ }
+ ];
+ table.tableRows = [{ data: 'body row' }];
+ table.footerRows = [{ data: 'footer row' }];
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+
+ // Clicking on the body row should *not* throw an exception (despite the
+ // column not supporting cell selection).
+ table.$.body.children[0].children[0].click();
+
+ // Clicking on the footer row should *not* throw an exception (despite
+ // footer rows not being selectable in general).
+ table.$.foot.children[0].children[0].click();
+ });
+
+ test('defaultExpansionStateCallback', function() {
+ const columns = [
+ {
+ title: 'Name',
+ value(row) { return row.name; }
+ },
+ {
+ title: 'Value',
+ value(row) { return row.value; }
+ }
+ ];
+
+ const rows = [
+ {
+ name: 'A',
+ value: 10,
+ subRows: [
+ {
+ name: 'B',
+ value: 8,
+ subRows: [
+ {
+ name: 'C',
+ value: 4
+ },
+ {
+ name: 'D',
+ value: 4
+ }
+ ]
+ },
+ {
+ name: 'E',
+ value: 2,
+ subRows: [
+ {
+ name: 'F',
+ value: 1
+ },
+ {
+ name: 'G',
+ value: 1
+ }
+ ]
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ let cRow = tr.ui.b.findDeepElementMatchingPredicate(
+ table, function(element) {
+ return Polymer.dom(element).textContent === 'C';
+ });
+ assert.strictEqual(cRow, undefined);
+
+ let callbackCalled = false;
+ table.defaultExpansionStateCallback = function(row, parentRow) {
+ callbackCalled = true;
+
+ if (parentRow === undefined) return true;
+
+ if (row.value >= (parentRow.value * 0.8)) return true;
+
+ return false;
+ };
+
+ // Setting the callback should set the body dirty.
+ assert.isTrue(table.bodyDirty_);
+ assert.isFalse(callbackCalled);
+
+ table.rebuild();
+
+ assert.isTrue(callbackCalled);
+ cRow = tr.ui.b.findDeepElementMatchingPredicate(table, function(element) {
+ return Polymer.dom(element).textContent === 'C';
+ });
+ assert.isDefined(cRow);
+ });
+
+ test('sortExpanded', function() {
+ const columns = [
+ {
+ title: 'Name',
+ value(row) { return row.name; }
+ },
+ {
+ title: 'Value',
+ value(row) { return row.value; },
+ cmp(x, y) { return x.value - y.value; }
+ }
+ ];
+
+ const rows = [
+ {
+ name: 'A',
+ value: 10,
+ subRows: [
+ {
+ name: 'B',
+ value: 8
+ },
+ {
+ name: 'C',
+ value: 4
+ },
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ function isB(row) {
+ return row.textContent === 'B';
+ }
+
+ // Check that 'A' row is not expanded.
+ assert.isUndefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB));
+
+ // Expand 'A' row.
+ table.setExpandedForTableRow(rows[0], true);
+
+ // Check that 'A' is expanded.
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB));
+
+ // Sort by value.
+ table.sortColumnIndex = 1;
+
+ // Rebuild the table synchronously instead of waiting for scheduleRebuild_'s
+ // setTimeout(0).
+ table.rebuild();
+
+ // Check that 'A' is still expanded.
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB));
+ });
+
+ test('shouldPreserveSortWhenColumnsChange', function() {
+ const name = {
+ title: 'Name',
+ value(row) { return row.name; },
+ };
+
+ const count = {
+ title: 'Count',
+ value(row) { return row.count; },
+ cmp: (rowA, rowB) => rowA.a - rowB.a,
+ };
+
+ const otherCount = {
+ title: 'Count',
+ value(row) { return row.count; },
+ cmp: (rowA, rowB) => rowA.a - rowB.a,
+ };
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [count];
+ table.sortColumnIndex = 0;
+ table.sortDescending = true;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ table.tableColumns = [name, count];
+ table.rebuild();
+ assert.strictEqual(1, table.sortColumnIndex);
+ assert.isTrue(table.sortDescending);
+
+ table.sortDescending = false;
+ table.tableColumns = [otherCount, name];
+ table.rebuild();
+ assert.strictEqual(0, table.sortColumnIndex);
+ assert.isFalse(table.sortDescending);
+
+ table.tableColumns = [name];
+ table.rebuild();
+ assert.isUndefined(table.sortColumnIndex);
+ });
+
+ test('userCanModifySortOrder', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Name',
+ value: row => row.name
+ },
+ {
+ title: 'colA',
+ value: row => row.a,
+ cmp: (rowA, rowB) => rowA.a - rowB.a
+ },
+ {
+ title: 'colB',
+ value: row => row.b,
+ cmp: (rowA, rowB) => rowA.b - rowB.b
+ }
+ ];
+ table.tableRows = [
+ {name: 'A', a: 42, b: 0},
+ {name: 'B', a: 89, b: 100},
+ {name: 'C', a: 65, b: -273.15}
+ ];
+ table.userCanModifySortOrder = false;
+ table.sortColumnIndex = 2;
+ table.sortDescending = true;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ const toggleButton = document.createElement('button');
+ Polymer.dom(toggleButton).textContent =
+ 'Toggle table.userCanModifySortOrder';
+ toggleButton.addEventListener('click', function() {
+ table.userCanModifySortOrder = !table.userCanModifySortOrder;
+ });
+ this.addHTMLOutput(toggleButton);
+
+ const unsetButton = document.createElement('button');
+ Polymer.dom(unsetButton).textContent = 'Unset sort order';
+ unsetButton.addEventListener('click', function() {
+ table.sortColumnIndex = undefined;
+ });
+ this.addHTMLOutput(unsetButton);
+ });
+
+ test('columnSelection', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Name',
+ value: (row) => row.name
+ },
+ {
+ title: 'colA',
+ selectable: true,
+ value: (row) => row.a,
+ cmp: (rowA, rowB) => rowA.a - rowB.a
+ },
+ {
+ title: 'colB',
+ selectable: true,
+ value: (row) => row.b,
+ cmp: (rowA, rowB) => rowA.b - rowB.b
+ }
+ ];
+ table.tableRows = [
+ {name: 'foo', a: 42, b: -42},
+ {name: 'bar', a: 57, b: 133}
+ ];
+ table.rebuild();
+ table.selectionMode = SelectionMode.CELL;
+ this.addHTMLOutput(table);
+
+ table.selectedTableColumnIndex = 1;
+ let cols = tr.ui.b.findDeepElementMatchingPredicate(table,
+ e => e.tagName === 'COLGROUP').children;
+ assert.isNull(cols[0].getAttribute('selected'));
+ assert.strictEqual(cols[1].getAttribute('selected'), 'true');
+ assert.isNull(cols[2].getAttribute('selected'));
+ assert.strictEqual(1, table.selectedTableColumnIndex);
+
+ table.selectedTableColumnIndex = undefined;
+ cols = tr.ui.b.findDeepElementMatchingPredicate(table,
+ e => e.tagName === 'COLGROUP').children;
+ assert.isNull(cols[0].getAttribute('selected'));
+ assert.isNull(cols[1].getAttribute('selected'));
+ assert.isNull(cols[2].getAttribute('selected'));
+ assert.isUndefined(table.selectedTableColumnIndex);
+
+ table.selectedTableColumnIndex = 2;
+ cols = tr.ui.b.findDeepElementMatchingPredicate(table,
+ e => e.tagName === 'COLGROUP').children;
+ assert.isNull(cols[0].getAttribute('selected'));
+ assert.isNull(cols[1].getAttribute('selected'));
+ assert.strictEqual(cols[2].getAttribute('selected'), 'true');
+ assert.strictEqual(2, table.selectedTableColumnIndex);
+ });
+
+ test('stepInto', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+ const rows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+
+ const firedStepIntoEvents = [];
+ table.addEventListener('step-into', e => firedStepIntoEvents.push(e));
+
+ table.cellHighlightStyle = HighlightStyle.DARK;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ assert.lengthOf(firedStepIntoEvents, 0);
+
+ // Double click.
+ simulateDoubleClick(table.$.body.children[0].children[1]);
+ assert.lengthOf(firedStepIntoEvents, 1);
+ assert.strictEqual(firedStepIntoEvents[0].tableRow, rows[0]);
+ assert.strictEqual(firedStepIntoEvents[0].tableColumn, columns[1]);
+ assert.strictEqual(firedStepIntoEvents[0].columnIndex, 1);
+
+ simulateDoubleClick(table.$.body.children[1].children[3]);
+ assert.lengthOf(firedStepIntoEvents, 2);
+ assert.strictEqual(firedStepIntoEvents[1].tableRow, rows[1]);
+ assert.strictEqual(firedStepIntoEvents[1].tableColumn, columns[3]);
+ assert.strictEqual(firedStepIntoEvents[1].columnIndex, 3);
+
+ // Shift+Enter in cell selection mode.
+ table.selectionMode = SelectionMode.CELL;
+ table.selectedTableRow = rows[0];
+ table.selectedColumnIndex = 2;
+ table.performKeyCommand_('ENTER');
+ assert.lengthOf(firedStepIntoEvents, 3);
+ assert.strictEqual(firedStepIntoEvents[2].tableRow, rows[0]);
+ assert.strictEqual(firedStepIntoEvents[2].tableColumn, columns[2]);
+ assert.strictEqual(firedStepIntoEvents[2].columnIndex, 2);
+
+ // Shift+Enter in row selection mode.
+ table.selectionMode = SelectionMode.ROW;
+ table.selectedTableRow = rows[1];
+ table.performKeyCommand_('ENTER');
+ assert.lengthOf(firedStepIntoEvents, 4);
+ assert.strictEqual(firedStepIntoEvents[3].tableRow, rows[1]);
+ assert.isUndefined(firedStepIntoEvents[3].tableColumn);
+ assert.isUndefined(firedStepIntoEvents[3].columnIndex);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html
new file mode 100644
index 00000000000..e142afd50f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html
@@ -0,0 +1,327 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the TimingTool class.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Tool for taking time measurements in the TimelineTrackView using
+ * Viewportmarkers.
+ * @constructor
+ */
+ function TimingTool(viewport, targetElement) {
+ this.viewport_ = viewport;
+
+ // Prepare the event handlers to be added and removed repeatedly.
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onDblClick_ = this.onDblClick_.bind(this);
+ this.targetElement_ = targetElement;
+
+ // Valid only during mousedown.
+ this.isMovingLeftEdge_ = false;
+ }
+
+ TimingTool.prototype = {
+
+ onEnterTiming(e) {
+ this.targetElement_.addEventListener('mousemove', this.onMouseMove_);
+ this.targetElement_.addEventListener('dblclick', this.onDblClick_);
+ },
+
+ onBeginTiming(e) {
+ if (!this.isTouchPointInsideTrackBounds_(e.clientX, e.clientY)) {
+ return;
+ }
+
+ const pt = this.getSnappedToEventPosition_(e);
+ this.mouseDownAt_(pt.x, pt.y);
+
+ this.updateSnapIndicators_(pt);
+ },
+
+ updateSnapIndicators_(pt) {
+ if (!pt.snapped) return;
+
+ const ir = this.viewport_.interestRange;
+ if (ir.min === pt.x) {
+ ir.leftSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height);
+ }
+ if (ir.max === pt.x) {
+ ir.rightSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height);
+ }
+ },
+
+ onUpdateTiming(e) {
+ const pt = this.getSnappedToEventPosition_(e);
+ this.mouseMoveAt_(pt.x, pt.y, true);
+ this.updateSnapIndicators_(pt);
+ },
+
+ onEndTiming(e) {
+ this.mouseUp_();
+ },
+
+ onExitTiming(e) {
+ this.targetElement_.removeEventListener('mousemove', this.onMouseMove_);
+ this.targetElement_.removeEventListener('dblclick', this.onDblClick_);
+ },
+
+ onMouseMove_(e) {
+ if (e.button) return;
+
+ const worldX = this.getWorldXFromEvent_(e);
+ this.mouseMoveAt_(worldX, e.clientY, false);
+ },
+
+ onDblClick_(e) {
+ // TODO(nduca): Implement dobuleclicking.
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+
+ isTouchPointInsideTrackBounds_(clientX, clientY) {
+ if (!this.viewport_ ||
+ !this.viewport_.modelTrackContainer ||
+ !this.viewport_.modelTrackContainer.canvas) {
+ return false;
+ }
+
+ const canvas = this.viewport_.modelTrackContainer.canvas;
+ const canvasRect = canvas.getBoundingClientRect();
+ if (clientX >= canvasRect.left && clientX <= canvasRect.right &&
+ clientY >= canvasRect.top && clientY <= canvasRect.bottom) {
+ return true;
+ }
+
+ return false;
+ },
+
+ mouseDownAt_(worldX, y) {
+ const ir = this.viewport_.interestRange;
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
+
+ if (ir.isEmpty) {
+ ir.setMinAndMax(worldX, worldX);
+ ir.rightSelected = true;
+ this.isMovingLeftEdge_ = false;
+ return;
+ }
+
+
+ // Left edge test.
+ if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
+ ir.leftSelected = true;
+ ir.min = worldX;
+ this.isMovingLeftEdge_ = true;
+ return;
+ }
+
+ // Right edge test.
+ if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
+ ir.rightSelected = true;
+ ir.max = worldX;
+ this.isMovingLeftEdge_ = false;
+ return;
+ }
+
+ ir.setMinAndMax(worldX, worldX);
+ ir.rightSelected = true;
+ this.isMovingLeftEdge_ = false;
+ },
+
+ mouseMoveAt_(worldX, y, mouseDown) {
+ if (mouseDown) {
+ this.updateMovingEdge_(worldX);
+ return;
+ }
+
+ const ir = this.viewport_.interestRange;
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
+
+ // Left edge test.
+ if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
+ ir.leftSelected = true;
+ ir.rightSelected = false;
+ return;
+ }
+
+ // Right edge test.
+ if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
+ ir.leftSelected = false;
+ ir.rightSelected = true;
+ return;
+ }
+
+ ir.leftSelected = false;
+ ir.rightSelected = false;
+ return;
+ },
+
+ updateMovingEdge_(newWorldX) {
+ const ir = this.viewport_.interestRange;
+ let a = ir.min;
+ let b = ir.max;
+ if (this.isMovingLeftEdge_) {
+ a = newWorldX;
+ } else {
+ b = newWorldX;
+ }
+
+ if (a <= b) {
+ ir.setMinAndMax(a, b);
+ } else {
+ ir.setMinAndMax(b, a);
+ }
+
+ if (ir.min === newWorldX) {
+ this.isMovingLeftEdge_ = true;
+ ir.leftSelected = true;
+ ir.rightSelected = false;
+ } else {
+ this.isMovingLeftEdge_ = false;
+ ir.leftSelected = false;
+ ir.rightSelected = true;
+ }
+ },
+
+ mouseUp_() {
+ const dt = this.viewport_.currentDisplayTransform;
+ const ir = this.viewport_.interestRange;
+
+ ir.leftSelected = false;
+ ir.rightSelected = false;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const minWidthValue = dt.xViewVectorToWorld(2 * pixelRatio);
+ if (ir.range < minWidthValue) {
+ ir.reset();
+ }
+ },
+
+ getWorldXFromEvent_(e) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const canvas = this.viewport_.modelTrackContainer.canvas;
+ const worldOffset = canvas.getBoundingClientRect().left;
+ const viewX = (e.clientX - worldOffset) * pixelRatio;
+ return this.viewport_.currentDisplayTransform.xViewToWorld(viewX);
+ },
+
+
+ /**
+ * Get the closest position of an event within a vertical range of the mouse
+ * position if possible, otherwise use the position of the mouse pointer.
+ * @param {MouseEvent} e Mouse event with the current mouse coordinates.
+ * @return {
+ * {Number} x, The x coordinate in world space.
+ * {Number} y, The y coordinate in world space.
+ * {Number} height, The height of the event.
+ * {boolean} snapped Whether the coordinates are from a snapped event or
+ * the mouse position.
+ * }
+ */
+ getSnappedToEventPosition_(e) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const EVENT_SNAP_RANGE = 16 * pixelRatio;
+
+ const modelTrackContainer = this.viewport_.modelTrackContainer;
+ const modelTrackContainerRect =
+ modelTrackContainer.getBoundingClientRect();
+
+ const viewport = this.viewport_;
+ const dt = viewport.currentDisplayTransform;
+ const worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE);
+
+ const worldX = this.getWorldXFromEvent_(e);
+ const mouseY = e.clientY;
+
+ const selection = new tr.model.EventSet();
+
+ // Look at the track under mouse position first for better performance.
+ modelTrackContainer.addClosestEventToSelection(
+ worldX, worldMaxDist, mouseY, mouseY, selection);
+
+ // Look at all tracks visible on screen.
+ if (!selection.length) {
+ modelTrackContainer.addClosestEventToSelection(
+ worldX, worldMaxDist,
+ modelTrackContainerRect.top, modelTrackContainerRect.bottom,
+ selection);
+ }
+
+ let minDistX = worldMaxDist;
+ let minDistY = Infinity;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ // Create result object with the mouse coordinates.
+ const result = {
+ x: worldX,
+ y: mouseY - modelTrackContainerRect.top,
+ height: 0,
+ snapped: false
+ };
+
+ const eventBounds = new tr.b.math.Range();
+ for (const event of selection) {
+ const track = viewport.trackForEvent(event);
+ const trackRect = track.getBoundingClientRect();
+
+ eventBounds.reset();
+ event.addBoundsToRange(eventBounds);
+ let eventX;
+ if (Math.abs(eventBounds.min - worldX) <
+ Math.abs(eventBounds.max - worldX)) {
+ eventX = eventBounds.min;
+ } else {
+ eventX = eventBounds.max;
+ }
+
+ const distX = eventX - worldX;
+
+ const eventY = trackRect.top;
+ const eventHeight = trackRect.height;
+ const distY = Math.abs(eventY + eventHeight / 2 - mouseY);
+
+ // Prefer events with a closer y position if their x difference is below
+ // the width of a pixel.
+ if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) &&
+ distY < minDistY) {
+ minDistX = distX;
+ minDistY = distY;
+
+ // Retrieve the event position from the hit.
+ result.x = eventX;
+ result.y = eventY +
+ modelTrackContainer.scrollTop - modelTrackContainerRect.top;
+ result.height = eventHeight;
+ result.snapped = true;
+ }
+ }
+
+ return result;
+ }
+ };
+
+ return {
+ TimingTool,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html
new file mode 100644
index 00000000000..c07319d3e97
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/timing_tool.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function create100PxWideViewportInto10WideWorld() {
+ const vp = new tr.ui.TimelineViewport(document.createElement('div'));
+ const tempDisplayTransform = new tr.ui.TimelineDisplayTransform();
+ tempDisplayTransform.xSetWorldBounds(0, 10, 100);
+ vp.setDisplayTransformImmediately(tempDisplayTransform);
+
+ assert.strictEqual(vp.currentDisplayTransform.xViewToWorld(0), 0);
+ assert.strictEqual(vp.currentDisplayTransform.xViewToWorld(100), 10);
+
+ return vp;
+ }
+
+ test('dragLeftInterestRegion', function() {
+ const vp = create100PxWideViewportInto10WideWorld();
+ vp.interestRange.min = 1;
+ vp.interestRange.max = 9;
+ const tool = new tr.ui.b.TimingTool(vp);
+
+ tool.mouseDownAt_(1.1, 0);
+ assert.isTrue(vp.interestRange.leftSelected);
+ tool.mouseMoveAt_(1.5, 0, true);
+ assert.strictEqual(vp.interestRange.min, 1.5);
+ tool.mouseUp_();
+ assert.strictEqual(vp.interestRange.min, 1.5);
+ assert.isFalse(vp.interestRange.leftSelected);
+ });
+
+ test('dragRightInterestRegion', function() {
+ const vp = create100PxWideViewportInto10WideWorld();
+ vp.interestRange.min = 1;
+ vp.interestRange.max = 9;
+ const tool = new tr.ui.b.TimingTool(vp);
+
+ tool.mouseDownAt_(9.1, 0);
+ assert.isTrue(vp.interestRange.rightSelected);
+ tool.mouseMoveAt_(8, 0, true);
+ assert.strictEqual(vp.interestRange.max, 8);
+ tool.mouseUp_();
+ assert.strictEqual(vp.interestRange.max, 8);
+ assert.isFalse(vp.interestRange.leftSelected);
+ });
+
+ test('dragInNewSpace', function() {
+ const vp = create100PxWideViewportInto10WideWorld();
+ vp.interestRange.min = 1;
+ vp.interestRange.max = 9;
+ const tool = new tr.ui.b.TimingTool(vp);
+
+ tool.mouseDownAt_(5, 0);
+ assert.isTrue(vp.interestRange.rightSelected);
+ assert.strictEqual(vp.interestRange.min, 5);
+ assert.strictEqual(vp.interestRange.max, 5);
+ tool.mouseMoveAt_(4, 0, true);
+ assert.strictEqual(vp.interestRange.min, 4);
+ assert.strictEqual(vp.interestRange.max, 5);
+ assert.isTrue(vp.interestRange.leftSelected);
+ tool.mouseUp_();
+ assert.strictEqual(vp.interestRange.min, 4);
+ assert.isFalse(vp.interestRange.leftSelected);
+ assert.isFalse(vp.interestRange.rightSelected);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html
new file mode 100644
index 00000000000..51108abd247
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+</script>
+<dom-module id='tr-ui-b-toolbar-button'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ background-color: #f8f8f8;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ color: rgba(0,0,0,0.8);
+ justify-content: center;
+ align-self: stretch;
+ min-width: 23px;
+ }
+
+ :host(:hover) {
+ background-color: rgba(255, 255, 255, 1.0);
+ border-color: rgba(0, 0, 0, 0.8);
+ box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 1);
+ }
+
+ #aligner {
+ display: flex;
+ flex: 0 0 auto;
+ align-self: center;
+ }
+ </style>
+ <div id="aligner">
+ <slot></slot>
+ </div>
+ </template>
+</dom-module>
+<script>
+ 'use strict';
+ Polymer({ is: 'tr-ui-b-toolbar-button' });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html
new file mode 100644
index 00000000000..d111fb637d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/toolbar_button.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('tallWithTextContent', function() {
+ const el = document.createElement('tr-ui-b-toolbar-button');
+ el.style.width = '100px';
+ el.style.height = '40px';
+
+ Polymer.dom(el).textContent = 'blahblah';
+
+ this.addHTMLOutput(el);
+ });
+
+ test('tallWithInnerSpan', function() {
+ const el = document.createElement('tr-ui-b-toolbar-button');
+ el.style.width = '100px';
+ el.style.height = '40px';
+
+ Polymer.dom(el).appendChild(tr.ui.b.createSpan({textContent: 'blahblah'}));
+
+ this.addHTMLOutput(el);
+ });
+
+ test('puny', function() {
+ const el = document.createElement('tr-ui-b-toolbar-button');
+ Polymer.dom(el).textContent = 'M';
+ this.addHTMLOutput(el);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html
new file mode 100644
index 00000000000..a1e647e78b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Decorates elements as an instance of a class.
+ * @param {string|!Element} source The way to find the element(s) to decorate.
+ * If this is a string then {@code querySeletorAll} is used to find the
+ * elements to decorate.
+ * @param {!Function} constr The constructor to decorate with. The constr
+ * needs to have a {@code decorate} function.
+ */
+ function decorate(source, constr) {
+ let elements;
+ if (typeof source === 'string') {
+ elements = Polymer.dom(tr.doc).querySelectorAll(source);
+ } else {
+ elements = [source];
+ }
+
+ for (let i = 0, el; el = elements[i]; i++) {
+ if (!(el instanceof constr)) {
+ constr.decorate(el);
+ }
+ }
+ }
+
+ /**
+ * Defines a tracing UI component, a function that can be called to construct
+ * the component.
+ *
+ * tr class:
+ * const List = tr.ui.b.define('list');
+ * List.prototype = {
+ * __proto__: HTMLUListElement.prototype,
+ * decorate: function() {
+ * ...
+ * },
+ * ...
+ * };
+ *
+ * Derived class:
+ * const CustomList = tr.ui.b.define('custom-list', List);
+ * CustomList.prototype = {
+ * __proto__: List.prototype,
+ * decorate: function() {
+ * ...
+ * },
+ * ...
+ * };
+ *
+ * @param {string} className The className of the newly created subtype. If
+ * subclassing by passing in opt_parentConstructor, this is used for
+ * debugging. If not subclassing, then it is the tag name that will be
+ * created by the component.
+
+ * @param {function=} opt_parentConstructor The parent class for this new
+ * element, if subclassing is desired. If provided, the parent class must
+ * be also a function created by tr.ui.b.define.
+ *
+ * @param {string=} opt_tagNS The namespace in which to create the base
+ * element. Has no meaning when opt_parentConstructor is passed and must
+ * either be undefined or the same namespace as the parent class.
+ *
+ * @return {function(Object=):Element} The newly created component
+ * constructor.
+ */
+ function define(className, opt_parentConstructor, opt_tagNS) {
+ if (typeof className === 'function') {
+ throw new Error('Passing functions as className is deprecated. Please ' +
+ 'use (className, opt_parentConstructor) to subclass');
+ }
+
+ className = className.toLowerCase();
+ if (opt_parentConstructor && !opt_parentConstructor.tagName) {
+ throw new Error('opt_parentConstructor was not ' +
+ 'created by tr.ui.b.define');
+ }
+
+ // Walk up the parent constructors until we can find the type of tag
+ // to create.
+ let tagName = className;
+ let tagNS = undefined;
+ if (opt_parentConstructor) {
+ if (opt_tagNS) {
+ throw new Error('Must not specify tagNS if parentConstructor is given');
+ }
+ let parent = opt_parentConstructor;
+ while (parent && parent.tagName) {
+ tagName = parent.tagName;
+ tagNS = parent.tagNS;
+ parent = parent.parentConstructor;
+ }
+ } else {
+ tagNS = opt_tagNS;
+ }
+
+ /**
+ * Creates a new UI element constructor.
+ * Arguments passed to the constuctor are provided to the decorate method.
+ * You will need to call the parent elements decorate method from within
+ * your decorate method and pass any required parameters.
+ * @constructor
+ */
+ function f() {
+ if (opt_parentConstructor &&
+ f.prototype.__proto__ !== opt_parentConstructor.prototype) {
+ throw new Error(
+ className + ' prototye\'s __proto__ field is messed up. ' +
+ 'It MUST be the prototype of ' + opt_parentConstructor.tagName);
+ }
+
+ let el;
+ if (tagNS === undefined) {
+ el = tr.doc.createElement(tagName);
+ } else {
+ el = tr.doc.createElementNS(tagNS, tagName);
+ }
+ f.decorate.call(this, el, arguments);
+ return el;
+ }
+
+ /**
+ * Decorates an element as a UI element class.
+ * @param {!Element} el The element to decorate.
+ */
+ f.decorate = function(el) {
+ el.__proto__ = f.prototype;
+ el.decorate.apply(el, arguments[1]);
+ el.constructor = f;
+ };
+
+ f.className = className;
+ f.tagName = tagName;
+ f.tagNS = tagNS;
+ f.parentConstructor = (opt_parentConstructor ? opt_parentConstructor :
+ undefined);
+ f.toString = function() {
+ if (!f.parentConstructor) {
+ return f.tagName;
+ }
+ return f.parentConstructor.toString() + '::' + f.className;
+ };
+
+ return f;
+ }
+
+ function elementIsChildOf(el, potentialParent) {
+ if (el === potentialParent) return false;
+
+ let cur = el;
+ while (Polymer.dom(cur).parentNode) {
+ if (cur === potentialParent) return true;
+ cur = Polymer.dom(cur).parentNode;
+ }
+ return false;
+ }
+
+ return {
+ decorate,
+ define,
+ elementIsChildOf,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html
new file mode 100644
index 00000000000..c87041eeb45
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/location.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const Location = tr.model.Location;
+
+ /**
+ * UIState is a class that represents the current state of the timeline by
+ * the Location of the point of interest and the current scaleX of the
+ * timeline.
+ *
+ * @constructor
+ */
+ function UIState(location, scaleX) {
+ this.location_ = location;
+ this.scaleX_ = scaleX;
+ }
+
+ /**
+ * Accepts a UIState string in the format of (timestamp)@(stableID)x(scaleX)
+ * Returns undefined if string is not in this format, or throws an Error if
+ * variables in a syntactically-correct stateString does not produce a valid
+ * UIState. Otherwise returns a constructed UIState instance.
+ */
+ UIState.fromUserFriendlyString = function(model, viewport, stateString) {
+ const navByFinderPattern = /^(-?\d+(\.\d+)?)@(.+)x(\d+(\.\d+)?)$/g;
+ const match = navByFinderPattern.exec(stateString);
+ if (!match) return;
+
+ const timestamp = parseFloat(match[1]);
+ const stableId = match[3];
+ const scaleX = parseFloat(match[4]);
+
+ if (scaleX <= 0) {
+ throw new Error('Invalid ScaleX value in UI State string.');
+ }
+
+ if (!viewport.containerToTrackMap.getTrackByStableId(stableId)) {
+ throw new Error('Invalid StableID given in UI State String.');
+ }
+
+ const loc = tr.model.Location.fromStableIdAndTimestamp(
+ viewport, stableId, timestamp);
+ return new UIState(loc, scaleX);
+ };
+
+ UIState.prototype = {
+
+ get location() {
+ return this.location_;
+ },
+
+ get scaleX() {
+ return this.scaleX_;
+ },
+
+ toUserFriendlyString(viewport) {
+ const timestamp = this.location_.xWorld;
+ const stableId =
+ this.location_.getContainingTrack(viewport).eventContainer.stableId;
+ const scaleX = this.scaleX_;
+ return timestamp.toFixed(5) + '@' + stableId + 'x' + scaleX.toFixed(5);
+ },
+
+ toDict() {
+ return {
+ location: this.location_.toDict(),
+ scaleX: this.scaleX_
+ };
+ }
+ };
+
+ return {
+ UIState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html
new file mode 100644
index 00000000000..de9c6ba6090
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/base/ui_state.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const UIState = tr.ui.b.UIState;
+
+ function FakeModel() {
+ this.processes = { 1: { threads: { 2: { stableId: '1.2' } } } };
+ }
+
+ // FakeTrack needs to be an instance of tr.ui.tracks.Track because a
+ // location is constructed in terms of Track instances.
+ function FakeTrack() { }
+ FakeTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ get eventContainer() {
+ return { stableId: '1.2' };
+ },
+
+ getBoundingClientRect() {
+ return { top: 5, height: 2 };
+ },
+
+ get parentElement() {
+ return null;
+ }
+ };
+
+ function FakeViewPort() {
+ this.containerToTrackMap = {
+ // "1.2" is the only valid stableId this test function accepts.
+ getTrackByStableId(stableId) {
+ if (stableId === '1.2') {
+ return new FakeTrack;
+ }
+ return undefined;
+ }
+ };
+ }
+
+ test('invalidStableId', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '15@1.3x6');
+ });
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '15@2.2x6');
+ });
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '505@1.x5');
+ });
+ });
+
+ test('invalidScaleX', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '1@1.2x-1'));
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '1@1.2x0');
+ });
+ });
+
+ test('invalidSyntax', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '5'));
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '5@x5'));
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, 'ab@1.2x5'));
+ });
+
+ test('validString', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ const str = '-50125.51231@1.2x1.12345';
+ const uiState = UIState.fromUserFriendlyString(model, vp, str);
+
+ assert.isDefined(uiState);
+ assert.strictEqual(uiState.location.xWorld, -50125.51231);
+ assert.strictEqual(
+ uiState.location.getContainingTrack(vp).eventContainer.stableId,
+ '1.2');
+ assert.strictEqual(uiState.scaleX, 1.12345);
+
+ assert.strictEqual(uiState.toUserFriendlyString(vp), str);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html
new file mode 100644
index 00000000000..486c1019c62
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html
@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestElement = tr.ui.b.define('div');
+ TestElement.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ if (!this.decorateCallCount) {
+ this.decorateCallCount = 0;
+ }
+ this.decorateCallCount++;
+ }
+ };
+
+ const Base = tr.ui.b.define('div');
+ Base.prototype = {
+ __proto__: HTMLDivElement.prototype,
+ decorate() {
+ this.decoratedAsBase = true;
+ },
+ set baseProperty(v) {
+ this.basePropertySet = v;
+ }
+ };
+
+ test('decorateOnceViaNew', function() {
+ const testElement = new TestElement();
+ assert.strictEqual(testElement.decorateCallCount, 1);
+ });
+
+ test('decorateOnceDirectly', function() {
+ const testElement = document.createElement('div');
+ tr.ui.b.decorate(testElement, TestElement);
+ assert.strictEqual(testElement.decorateCallCount, 1);
+ });
+
+ test('componentToString', function() {
+ assert.strictEqual(Base.toString(), 'div');
+
+ const Sub = tr.ui.b.define('Sub', Base);
+ assert.strictEqual(Sub.toString(), 'div::sub');
+
+ const SubSub = tr.ui.b.define('Marine', Sub);
+ assert.strictEqual(SubSub.toString(), 'div::sub::marine');
+ });
+
+ test('basicDefines', function() {
+ const baseInstance = new Base();
+ assert.instanceOf(baseInstance, Base);
+ assert.isTrue(baseInstance.decoratedAsBase);
+
+ assert.strictEqual(baseInstance.constructor, Base);
+ assert.strictEqual(baseInstance.constructor.toString(), 'div');
+
+ baseInstance.basePropertySet = 7;
+ assert.strictEqual(baseInstance.basePropertySet, 7);
+ });
+
+ test('subclassing', function() {
+ const Sub = tr.ui.b.define('sub', Base);
+ Sub.prototype = {
+ __proto__: Base.prototype,
+ decorate() {
+ this.decoratedAsSub = true;
+ }
+ };
+
+ const subInstance = new Sub();
+ assert.instanceOf(subInstance, Sub);
+ assert.isTrue(subInstance.decoratedAsSub);
+
+ assert.instanceOf(subInstance, Base);
+ assert.isUndefined(subInstance.decoratedAsBase);
+
+ assert.strictEqual(subInstance.constructor, Sub);
+ assert.strictEqual(subInstance.constructor.toString(), 'div::sub');
+
+ subInstance.baseProperty = true;
+ assert.isTrue(subInstance.basePropertySet);
+ });
+
+ const NoArgs = tr.ui.b.define('div');
+ NoArgs.prototype = {
+ __proto__: HTMLDivElement.prototype,
+ decorate() {
+ this.noArgsDecorated_ = true;
+ },
+ get noArgsDecorated() {
+ return this.noArgsDecorated_;
+ }
+ };
+
+ const Args = tr.ui.b.define('args', NoArgs);
+ Args.prototype = {
+ __proto__: NoArgs.prototype,
+ decorate(first) {
+ this.first_ = first;
+ this.argsDecorated_ = true;
+ },
+ get first() {
+ return this.first_;
+ },
+ get argsDecorated() {
+ return this.argsDecorated_;
+ }
+ };
+
+ const ArgsChild = tr.ui.b.define('args-child', Args);
+ ArgsChild.prototype = {
+ __proto__: Args.prototype,
+ decorate(_, second) {
+ this.second_ = second;
+ this.argsChildDecorated_ = true;
+ },
+ get second() {
+ return this.second_;
+ },
+ get decorated() {
+ return this.decorated_;
+ },
+ get argsChildDecorated() {
+ return this.argsChildDecorated_ = true;
+ }
+ };
+
+ const ArgsDecoratingChild = tr.ui.b.define('args-decorating-child', Args);
+ ArgsDecoratingChild.prototype = {
+ __proto__: Args.prototype,
+ decorate(first, second) {
+ Args.prototype.decorate.call(this, first);
+ this.second_ = second;
+ this.argsDecoratingChildDecorated_ = true;
+ },
+ get second() {
+ return this.second_;
+ },
+ get decorated() {
+ return this.decorated_;
+ },
+ get argsDecoratingChildDecorated() {
+ return this.argsChildDecorated_ = true;
+ }
+ };
+
+ test('decorate_noArguments', function() {
+ let noArgs;
+ assert.doesNotThrow(function() {
+ noArgs = new NoArgs();
+ });
+ assert.isTrue(noArgs.noArgsDecorated);
+ });
+
+ test('decorate_arguments', function() {
+ const args = new Args('this is first');
+ assert.strictEqual(args.first, 'this is first');
+ assert.isTrue(args.argsDecorated);
+ assert.isUndefined(args.noArgsDecorated);
+ });
+
+ test('decorate_subclassArguments', function() {
+ const argsChild = new ArgsChild('this is first', 'and second');
+ assert.isUndefined(argsChild.first);
+ assert.strictEqual(argsChild.second, 'and second');
+
+ assert.isTrue(argsChild.argsChildDecorated);
+ assert.isUndefined(argsChild.argsDecorated);
+ assert.isUndefined(argsChild.noArgsDecorated);
+ });
+
+ test('decorate_subClassCallsParentDecorate', function() {
+ const argsDecoratingChild = new ArgsDecoratingChild(
+ 'this is first', 'and second');
+ assert.strictEqual(argsDecoratingChild.first, 'this is first');
+ assert.strictEqual(argsDecoratingChild.second, 'and second');
+ assert.isTrue(argsDecoratingChild.argsDecoratingChildDecorated);
+ assert.isTrue(argsDecoratingChild.argsDecorated);
+ assert.isUndefined(argsDecoratingChild.noArgsDecorated);
+ });
+
+ test('defineWithNamespace', function() {
+ const svgNS = 'http://www.w3.org/2000/svg';
+ const cls = tr.ui.b.define('svg', undefined, svgNS);
+ cls.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ Polymer.dom(this).setAttribute('width', 200);
+ Polymer.dom(this).setAttribute('height', 200);
+ Polymer.dom(this).setAttribute('viewPort', '0 0 200 200');
+ const rectEl = document.createElementNS(svgNS, 'rect');
+ Polymer.dom(rectEl).setAttribute('x', 10);
+ Polymer.dom(rectEl).setAttribute('y', 10);
+ Polymer.dom(rectEl).setAttribute('width', 180);
+ Polymer.dom(rectEl).setAttribute('height', 180);
+ Polymer.dom(this).appendChild(rectEl);
+ }
+ };
+ const el = new cls();
+ assert.strictEqual(el.tagName, 'svg');
+ assert.strictEqual(el.namespaceURI, svgNS);
+ this.addHTMLOutput(el);
+ });
+
+ test('defineSubclassWithNamespace', function() {
+ const svgNS = 'http://www.w3.org/2000/svg';
+ const cls = tr.ui.b.define('svg', undefined, svgNS);
+ cls.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ Polymer.dom(this).setAttribute('width', 200);
+ Polymer.dom(this).setAttribute('height', 200);
+ Polymer.dom(this).setAttribute('viewPort', '0 0 200 200');
+ const rectEl = document.createElementNS(svgNS, 'rect');
+ Polymer.dom(rectEl).setAttribute('x', 10);
+ Polymer.dom(rectEl).setAttribute('y', 10);
+ Polymer.dom(rectEl).setAttribute('width', 180);
+ Polymer.dom(rectEl).setAttribute('height', 180);
+ Polymer.dom(this).appendChild(rectEl);
+ }
+ };
+
+ const subCls = tr.ui.b.define('sub', cls);
+ subCls.prototype = {
+ __proto__: cls.prototype
+ };
+ assert.strictEqual(subCls.toString(), 'svg::sub');
+
+ const el = new subCls();
+ this.addHTMLOutput(el);
+ assert.strictEqual(el.tagName, 'svg');
+ assert.strictEqual(el.namespaceURI, svgNS);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html b/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html
new file mode 100644
index 00000000000..cca1003cc06
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+
+<script>
+'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);
+ // return el.createInstance();
+ }
+
+ 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]);
+ }
+
+ function scrollIntoViewIfNeeded(el) {
+ const pr = el.parentElement.getBoundingClientRect();
+ const cr = el.getBoundingClientRect();
+ if (cr.top < pr.top) {
+ el.scrollIntoView(true);
+ } else if (cr.bottom > pr.bottom) {
+ el.scrollIntoView(false);
+ }
+ }
+
+ function extractUrlString(url) {
+ let extracted = url.replace(/url\((.*)\)/, '$1');
+
+ // In newer versions of chrome, the contents of url() will be quoted. Remove
+ // these quotes as well. If quotes are not present, match will fail and this
+ // becomes a no-op.
+ extracted = extracted.replace(/\"(.*)\"/, '$1');
+
+ return extracted;
+ }
+
+ function toThreeDigitLocaleString(value) {
+ return value.toLocaleString(
+ undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3});
+ }
+
+ /**
+ * Returns true if |name| is the name of an unknown HTML element. Registered
+ * polymer elements are known, so this returns false. Typos of registered
+ * polymer element names are unknown, so this returns true for typos.
+ *
+ * @return {boolean}
+ */
+ function isUnknownElementName(name) {
+ return document.createElement(name) instanceof HTMLUnknownElement;
+ }
+
+ return {
+ isUnknownElementName,
+ toThreeDigitLocaleString,
+ instantiateTemplate,
+ windowRectForElement,
+ scrollIntoViewIfNeeded,
+ extractUrlString,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html
new file mode 100644
index 00000000000..4ab76e1d221
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<dom-module id='instantiate-template-polymer-element-test'>
+ <template></template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'instantiate-template-polymer-element-test',
+ testProperty: 'Test'
+});
+</script>
+<template id="instantiate-template-polymer-test">
+ <instantiate-template-polymer-element-test>
+ </instantiate-template-polymer-element-test>
+</template>
+
+<template id="multiple-template-test">
+ <template>
+ <instantiate-template-polymer-element-test>
+ </instantiate-template-polymer-element-test>
+ <span test-attribute='TestAttribute'>Foo</span>
+ </template>
+ <instantiate-template-polymer-element-test>
+ </instantiate-template-polymer-element-test>
+</template>
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ test('instantiateTemplatePolymer', function() {
+ const e = tr.ui.b.instantiateTemplate(
+ '#instantiate-template-polymer-test',
+ THIS_DOC);
+ assert.strictEqual(e.children.length, 1);
+ assert.strictEqual(e.children[0].testProperty, 'Test');
+ });
+
+ test('instantiateTemplateMultipleTemplates', function() {
+ const outerElement = tr.ui.b.instantiateTemplate(
+ '#multiple-template-test',
+ THIS_DOC);
+ assert.strictEqual(outerElement.children.length, 2);
+ assert.strictEqual(outerElement.children[1].testProperty, 'Test');
+
+ // Make sure we can still instantiate inner templates, if we need them.
+ const innerElement = THIS_DOC.importNode(
+ outerElement.children[0].content, true);
+ assert.strictEqual(innerElement.children.length, 2);
+ assert.strictEqual(innerElement.children[0].testProperty, 'Test');
+ assert.strictEqual(
+ innerElement.children[1].getAttribute('test-attribute'),
+ 'TestAttribute');
+ assert.strictEqual(
+ Polymer.dom(innerElement.children[1]).textContent, 'Foo');
+ });
+
+ test('extractUrlStringAcceptsBothVersions', function() {
+ const oldStyleUrl = 'url(content)';
+ const newStyleUrl = 'url("content")';
+ const expectedResult = 'content';
+
+ assert.strictEqual(tr.ui.b.extractUrlString(oldStyleUrl), expectedResult);
+ assert.strictEqual(tr.ui.b.extractUrlString(newStyleUrl), expectedResult);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html
new file mode 100644
index 00000000000..f88e7a9bca3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html
@@ -0,0 +1,280 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+
+ function BrushingState() {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.selection_ = new EventSet();
+ this.findMatches_ = new EventSet();
+ this.analysisViewRelatedEvents_ = new EventSet();
+ this.analysisLinkHoveredEvents_ = new EventSet();
+ this.appliedToModel_ = undefined;
+ this.viewSpecificBrushingStates_ = {};
+ }
+ BrushingState.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ clone() {
+ const that = new BrushingState();
+ that.selection_ = this.selection_;
+ that.findMatches_ = this.findMatches_;
+ that.analysisViewRelatedEvents_ = this.analysisViewRelatedEvents_;
+ that.analysisLinkHoveredEvents_ = this.analysisLinkHoveredEvents_;
+ that.viewSpecificBrushingStates_ = this.viewSpecificBrushingStates_;
+
+ return that;
+ },
+
+ equals(that) {
+ if (!this.selection_.equals(that.selection_)) {
+ return false;
+ }
+ if (!this.findMatches_.equals(that.findMatches_)) {
+ return false;
+ }
+ if (!this.analysisViewRelatedEvents_.equals(
+ that.analysisViewRelatedEvents_)) {
+ return false;
+ }
+ if (!this.analysisLinkHoveredEvents_.equals(
+ that.analysisLinkHoveredEvents_)) {
+ return false;
+ }
+ // We currently do not take the view-specific brushing states into
+ // account. If we did, every change of the view-specific brushing state
+ // of any view would cause a redraw of the whole UI (see the
+ // BrushingStateController.currentBrushingState setter).
+ return true;
+ },
+
+ get selectionOfInterest() {
+ if (this.selection_.length) {
+ return this.selection_;
+ }
+
+ if (this.highlight_.length) {
+ return this.highlight_;
+ }
+
+ if (this.analysisViewRelatedEvents_.length) {
+ return this.analysisViewRelatedEvents_;
+ }
+
+ if (this.analysisLinkHoveredEvents_.length) {
+ return this.analysisLinkHoveredEvents_;
+ }
+
+ return this.selection_;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ if (selection === undefined) {
+ selection = new EventSet();
+ }
+ this.selection_ = selection;
+ },
+
+ get findMatches() {
+ return this.findMatches_;
+ },
+
+ set findMatches(findMatches) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ if (findMatches === undefined) {
+ findMatches = new EventSet();
+ }
+ this.findMatches_ = findMatches;
+ },
+
+ get analysisViewRelatedEvents() {
+ return this.analysisViewRelatedEvents_;
+ },
+
+ set analysisViewRelatedEvents(analysisViewRelatedEvents) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ 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;
+ },
+
+ /**
+ * This function sets the SelectionStates according to these rules:
+ *
+ * - Events in ONE of findMatches or analysisViewRelatedEvents
+ * are set to SelectionState.BRIGHTENED0.
+ * - Events in BOTH of findMatches and analysisViewRelatedEvents
+ * are set to SelectionState.BRIGHTENED1.
+ * - Events in ONE of selection or analysisLinkHoveredEvents
+ * are set to SelectionState.DIMMED1.
+ * - Events in BOTH selection and analysisLinkHoveredEvents
+ * are set to SelectionState.DIMMED2.
+ * - Events not in any of the above are set to SelectionState.NONE
+ * if there are no events in selection or analysisLinkHoveredEvents
+ * (i.e. model is "default bright") or SelectionState.DIMMED0 (i.e.
+ * model is "default dimmed").
+ *
+ * It is up to the caller to assure that all of the SelectionStates
+ * are the same before calling this function. Normally,
+ * this is done by calling unapplyFromModelSelectionState on the
+ * old brushing state first.
+ */
+ applyToEventSelectionStates(model) {
+ this.appliedToModel_ = model;
+
+ const dimmedEvents = this.dimmedEvents_;
+
+ // It's possible for this to get called with an undefined model pointer.
+ // If so, skip adjusting the defaults.
+ if (model) {
+ const newDefaultState = (
+ dimmedEvents.length ? SelectionState.DIMMED0 : SelectionState.NONE);
+
+ // Since all the states are the same, we can get the current default
+ // state by looking at the first element.
+ const currentDefaultState = tr.b.getFirstElement(
+ model.getDescendantEvents()).selectionState;
+
+ // If the default state was changed, then we have to iterate through
+ // and reset all the events to the new default state.
+ if (currentDefaultState !== newDefaultState) {
+ for (const e of model.getDescendantEvents()) {
+ e.selectionState = newDefaultState;
+ }
+ }
+ }
+
+ // Now we apply the other rules above.
+ 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');
+ }
+ // Assumes this.equals(that).
+ that.appliedToModel_ = this.appliedToModel_;
+ this.appliedToModel_ = undefined;
+ },
+
+ /**
+ * Unapplies this brushing state from the model selection state.
+ * Resets all the SelectionStates to their default value (DIMMED0 or NONE)
+ * and returns the default selection states. The caller should store this
+ * value and pass it into applyFromModelSelectionStat when that is called.
+ */
+ 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;
+ }
+ return defaultState;
+ }
+ };
+
+ return {
+ BrushingState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html
new file mode 100644
index 00000000000..6a547c339c3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html
@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/ui_state.html">
+<link rel="import" href="/tracing/ui/brushing_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.c', function() {
+ const BrushingState = tr.ui.b.BrushingState;
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ function BrushingStateController(timelineView) {
+ tr.b.EventTarget.call(this);
+
+ this.timelineView_ = timelineView;
+ this.currentBrushingState_ = new BrushingState();
+
+ this.onPopState_ = this.onPopState_.bind(this);
+ this.historyEnabled_ = false;
+ this.selections_ = {};
+ }
+
+ BrushingStateController.prototype = {
+ __proto__: tr.b.EventTarget.prototype,
+
+ dispatchChangeEvent_() {
+ const e = new tr.b.Event('change', false, false);
+ this.dispatchEvent(e);
+ },
+
+ get model() {
+ if (!this.timelineView_) {
+ return undefined;
+ }
+ return this.timelineView_.model;
+ },
+
+ get trackView() {
+ if (!this.timelineView_) {
+ return undefined;
+ }
+ return this.timelineView_.trackView;
+ },
+
+ get viewport() {
+ if (!this.timelineView_) {
+ return undefined;
+ }
+ if (!this.timelineView_.trackView) {
+ return undefined;
+ }
+ return this.timelineView_.trackView.viewport;
+ },
+
+ /* History system */
+ get historyEnabled() {
+ return this.historyEnabled_;
+ },
+
+ set historyEnabled(historyEnabled) {
+ this.historyEnabled_ = !!historyEnabled;
+ if (historyEnabled) {
+ window.addEventListener('popstate', this.onPopState_);
+ } else {
+ window.removeEventListener('popstate', this.onPopState_);
+ }
+ },
+
+ modelWillChange() {
+ if (this.currentBrushingState_.isAppliedToModel) {
+ this.currentBrushingState_.unapplyFromEventSelectionStates();
+ }
+ },
+
+ modelDidChange() {
+ this.selections_ = {};
+
+ this.currentBrushingState_ = new BrushingState();
+ this.currentBrushingState_.applyToEventSelectionStates(this.model);
+
+ const e = new tr.b.Event('model-changed', false, false);
+ this.dispatchEvent(e);
+
+ this.dispatchChangeEvent_();
+ },
+
+ onUserInitiatedSelectionChange_() {
+ const selection = this.selection;
+ if (this.historyEnabled) {
+ // Save the selection so that when back button is pressed,
+ // it could be retrieved.
+ this.selections_[selection.guid] = selection;
+ const state = {
+ selection_guid: selection.guid
+ };
+
+ window.history.pushState(state, document.title);
+ }
+ },
+
+ onPopState_(e) {
+ if (e.state === null) return;
+
+ const selection = this.selections_[e.state.selection_guid];
+ if (selection) {
+ const newState = this.currentBrushingState_.clone();
+ newState.selection = selection;
+ this.currentBrushingState = newState;
+ }
+ e.stopPropagation();
+ },
+
+ get selection() {
+ return this.currentBrushingState_.selection;
+ },
+ get findMatches() {
+ return this.currentBrushingState_.findMatches;
+ },
+
+ get selectionOfInterest() {
+ return this.currentBrushingState_.selectionOfInterest;
+ },
+
+ get currentBrushingState() {
+ return this.currentBrushingState_;
+ },
+
+ set currentBrushingState(newBrushingState) {
+ if (newBrushingState.isAppliedToModel) {
+ throw new Error('Cannot apply this state, it is applied');
+ }
+
+ // This function uses value-equality on the states so that state can
+ // changed to a clone of itself without causing a change event, while
+ // still having the actual state object change to the new clone.
+ const hasValueChanged = !this.currentBrushingState_.equals(
+ newBrushingState);
+
+ if (newBrushingState !== this.currentBrushingState_ && !hasValueChanged) {
+ if (this.currentBrushingState_.isAppliedToModel) {
+ this.currentBrushingState_.transferModelOwnershipToClone(
+ newBrushingState);
+ }
+ this.currentBrushingState_ = newBrushingState;
+ return;
+ }
+
+ if (this.currentBrushingState_.isAppliedToModel) {
+ this.currentBrushingState_.unapplyFromEventSelectionStates();
+ }
+
+ this.currentBrushingState_ = newBrushingState;
+
+ this.currentBrushingState_.applyToEventSelectionStates(this.model);
+
+ this.dispatchChangeEvent_();
+ },
+
+ /**
+ * @param {Filter} filter The filter to use for finding matches.
+ * @param {Selection} selection The selection to add matches to.
+ * @return {Task} which performs the filtering.
+ */
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const timelineView = this.timelineView_.trackView;
+ if (!timelineView) {
+ return new tr.b.Task();
+ }
+ return timelineView.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ },
+
+ findTextChangedTo(allPossibleMatches) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.findMatches = allPossibleMatches;
+ this.currentBrushingState = newBrushingState;
+ },
+
+ findFocusChangedTo(currentFocus) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = currentFocus;
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ findTextCleared() {
+ if (this.xNavStringMarker_ !== undefined) {
+ this.model.removeAnnotation(this.xNavStringMarker_);
+ this.xNavStringMarker_ = undefined;
+ }
+
+ if (this.guideLineAnnotation_ !== undefined) {
+ this.model.removeAnnotation(this.guideLineAnnotation_);
+ this.guideLineAnnotation_ = undefined;
+ }
+
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = new EventSet();
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ uiStateFromString(string) {
+ return tr.ui.b.UIState.fromUserFriendlyString(
+ this.model, this.viewport, string);
+ },
+
+ navToPosition(uiState, showNavLine) {
+ this.trackView.navToPosition(uiState, showNavLine);
+ },
+
+ changeSelectionFromTimeline(selection) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = selection;
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ showScriptControlSelection(selection) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = selection;
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+ },
+
+ changeSelectionFromRequestSelectionChangeEvent(selection) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = selection;
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ changeAnalysisViewRelatedEvents(eventSet) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.analysisViewRelatedEvents = eventSet;
+ this.currentBrushingState = newBrushingState;
+ },
+
+ changeAnalysisLinkHoveredEvents(eventSet) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.analysisLinkHoveredEvents = eventSet;
+ this.currentBrushingState = newBrushingState;
+ },
+
+ getViewSpecificBrushingState(viewId) {
+ return this.currentBrushingState.viewSpecificBrushingStates[viewId];
+ },
+
+ changeViewSpecificBrushingState(viewId, newState) {
+ const oldStates = this.currentBrushingState_.viewSpecificBrushingStates;
+ const newStates = {};
+ for (const id in oldStates) {
+ newStates[id] = oldStates[id];
+ }
+ if (newState === undefined) {
+ delete newStates[viewId];
+ } else {
+ newStates[viewId] = newState;
+ }
+
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.viewSpecificBrushingStates = newStates;
+ this.currentBrushingState = newBrushingState;
+ }
+ };
+
+ BrushingStateController.getControllerForElement = function(element) {
+ if (tr.isHeadless) {
+ throw new Error('Unsupported');
+ }
+ let currentElement = element;
+ while (currentElement) {
+ if (currentElement.brushingStateController) {
+ return currentElement.brushingStateController;
+ }
+
+ // Walk up the DOM.
+ if (currentElement.parentElement) {
+ currentElement = currentElement.parentElement;
+ continue;
+ }
+
+ // Possibly inside a shadow DOM.
+ let currentNode = currentElement;
+ while (Polymer.dom(currentNode).parentNode) {
+ currentNode = Polymer.dom(currentNode).parentNode;
+ }
+ currentElement = currentNode.host;
+ }
+ return undefined;
+ };
+
+ return {
+ BrushingStateController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html
new file mode 100644
index 00000000000..b72aeccf16a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Task = tr.b.Task;
+
+ function newSimpleFakeTimelineView() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5}));
+ m.sB = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15}));
+ m.sC = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20}));
+ });
+
+ // Fake timeline view. So fake its ... just wow.
+ const timelineView = {
+ model: m
+ };
+ return timelineView;
+ }
+
+ function doesCauseChangeToFire(brushingStateController, cb, opt_this) {
+ let didFire = false;
+ function didFireCb() {
+ didFire = true;
+ }
+ brushingStateController.addEventListener('change', didFireCb);
+ cb.call(opt_this);
+ brushingStateController.removeEventListener('change', didFireCb);
+ return didFire;
+ }
+
+ test('simpleStateChanges', function() {
+ const timelineView = newSimpleFakeTimelineView();
+ const brushingStateController =
+ new tr.c.BrushingStateController(timelineView);
+ const m = timelineView.model;
+
+ // Setting empty brushing state doesn't cause change event. This behavior
+ // is triggered when the user tries to search for something when no trace
+ // has been loaded yet in chrome://tracing.
+ const bs0 = new tr.ui.b.BrushingState();
+ assert.isFalse(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.currentBrushingState = bs0;
+ }));
+ assert.isFalse(bs0.isAppliedToModel);
+ assert.strictEqual(brushingStateController.currentBrushingState, bs0);
+
+ // Setting causes change.
+ const bs1 = new tr.ui.b.BrushingState();
+ bs1.selection = new EventSet([m.sA]);
+ assert.isTrue(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.currentBrushingState = bs1;
+ }));
+ assert.isTrue(bs1.isAppliedToModel);
+
+ // Setting value equivalent doesn't cause change event.
+ const bs2 = bs1.clone();
+ assert.isFalse(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.currentBrushingState = bs2;
+ }));
+ assert.strictEqual(brushingStateController.currentBrushingState, bs2);
+ assert.isTrue(
+ brushingStateController.currentBrushingState.isAppliedToModel);
+
+ // Setting to something different unapplies the old bs.
+ const bs3 = new tr.ui.b.BrushingState();
+ bs3.findMatches = new EventSet([m.sA, m.sB]);
+ brushingStateController.currentBrushingState = bs3;
+ assert.isTrue(bs3.isAppliedToModel);
+ assert.isFalse(bs2.isAppliedToModel);
+ });
+
+ test('modelCausesStateChange', function() {
+ const timelineView = newSimpleFakeTimelineView();
+ const brushingStateController =
+ new tr.c.BrushingStateController(timelineView);
+
+ const m1 = timelineView.model;
+
+ const bs1 = new tr.ui.b.BrushingState();
+ bs1.selection = new EventSet([m1.sA]);
+
+ // Change the model.
+ const m2 = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5}));
+ });
+ assert.isTrue(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.modelWillChange();
+ timelineView.model = m2;
+ brushingStateController.modelDidChange();
+ }));
+ assert.strictEqual(
+ brushingStateController.currentBrushingState.selection.length, 0);
+ });
+
+ function addChildDiv(element) {
+ const child = element.ownerDocument.createElement('div');
+ Polymer.dom(element).appendChild(child);
+ return child;
+ }
+
+ function addShadowChildDiv(element) {
+ const shadowRoot = element.createShadowRoot();
+ return addChildDiv(shadowRoot);
+ }
+
+ if (!tr.isHeadless) {
+ test('getControllerForElement_none', function() {
+ const element = document.createElement('div');
+
+ assert.isUndefined(
+ tr.c.BrushingStateController.getControllerForElement(element));
+ });
+
+ test('getControllerForElement_self', function() {
+ const controller = new tr.c.BrushingStateController(undefined);
+ const element = document.createElement('div');
+ element.brushingStateController = controller;
+
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller);
+ });
+
+ test('getControllerForElement_ancestor', function() {
+ const controller = new tr.c.BrushingStateController(undefined);
+ const ancestor = document.createElement('div');
+ ancestor.brushingStateController = controller;
+
+ const element = addChildDiv(addChildDiv(addChildDiv(ancestor)));
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller);
+ });
+
+ test('getControllerForElement_host', function() {
+ const controller = new tr.c.BrushingStateController(undefined);
+ const host = document.createElement('div');
+ host.brushingStateController = controller;
+
+ const element = addShadowChildDiv(host);
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller);
+ });
+
+ test('getControllerForElement_hierarchy', function() {
+ const controller1 = new tr.c.BrushingStateController(undefined);
+ const root = document.createElement('div');
+ root.brushingStateController = controller1;
+
+ const controller2 = new tr.c.BrushingStateController(undefined);
+ const child = addChildDiv(root);
+ child.brushingStateController = controller2;
+
+ const controller3 = new tr.c.BrushingStateController(undefined);
+ const shadowChild = addShadowChildDiv(child);
+ shadowChild.brushingStateController = controller3;
+
+ const element = addChildDiv(addChildDiv(addShadowChildDiv(
+ addChildDiv(addChildDiv(addShadowChildDiv(
+ addChildDiv(shadowChild)))))));
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller3);
+ });
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html
new file mode 100644
index 00000000000..aa33b12d116
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+
+ function newSimpleModel() {
+ return tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5}));
+ m.sB = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15}));
+ m.sC = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20}));
+ });
+ }
+
+ test('brushingStateSimple', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.SELECTED);
+ bs.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.NONE);
+ });
+
+
+ test('selectionAndAnalysisHover', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+ bs.analysisLinkHoveredEvents = new EventSet([m.sA, m.sB]);
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.BRIGHTENED1);
+ assert.strictEqual(m.sB.selectionState, SelectionState.BRIGHTENED0);
+ bs.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.NONE);
+ });
+
+ test('brushingStateWithFindMatches', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+ bs.findMatches = new EventSet([m.sA, m.sB]);
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.BRIGHTENED0);
+ assert.strictEqual(m.sB.selectionState, SelectionState.DIMMED1);
+ assert.strictEqual(m.sC.selectionState, SelectionState.DIMMED0);
+ bs.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.DIMMED0);
+ assert.strictEqual(m.sB.selectionState, SelectionState.DIMMED0);
+ assert.strictEqual(m.sC.selectionState, SelectionState.DIMMED0);
+ });
+
+ test('brushingTransfer', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+
+ const bs2 = bs.clone();
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.SELECTED);
+ bs.transferModelOwnershipToClone(bs2);
+ assert.isFalse(bs.isAppliedToModel);
+ assert.isTrue(bs2.isAppliedToModel);
+
+ bs2.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.NONE);
+ assert.strictEqual(m.sB.selectionState, SelectionState.NONE);
+ assert.strictEqual(m.sC.selectionState, SelectionState.NONE);
+ });
+
+ test('equality', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+ bs.findMatches = new EventSet([m.sB]);
+ bs.applyToEventSelectionStates = new EventSet([m.sC]);
+
+ // Clone equality, but with shared refs.
+ const bs2 = bs.clone();
+ assert.isTrue(bs.equals(bs2));
+
+ // Same value, different refs.
+ bs2.selection = new EventSet([m.sA]);
+ assert.isTrue(bs.equals(bs2));
+
+ // Different actual values.
+ bs2.selection = new EventSet([m.sB]);
+ assert.isFalse(bs.equals(bs2));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html
new file mode 100644
index 00000000000..8e579d90921
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="stylesheet" href="/tracing/ui/extras/about_tracing/common.css">
+<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ window.profilingView = undefined; // Made global for debugging purposes only.
+
+ document.addEventListener('DOMContentLoaded', function() {
+ window.profilingView = new tr.ui.e.about_tracing.ProfilingView();
+ profilingView.timelineView.globalMode = true;
+ Polymer.dom(document.body).appendChild(profilingView);
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css
new file mode 100644
index 00000000000..3db21b67ffa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css
@@ -0,0 +1,25 @@
+/* Copyright (c) 2012 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.
+ */
+html,
+body {
+ height: 100%;
+}
+
+body {
+ flex-direction: column;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+body > x-profiling-view {
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+body > x-profiling-view > x-timeline-view:focus {
+ outline: 0
+}
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html
new file mode 100644
index 00000000000..fa348a7b661
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/base64.html">
+<script>
+
+'use strict';
+
+/**
+ * A devtools protocol stream object.
+ *
+ * This reads a stream of data over the remote debugging connection.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ class DevtoolsStream {
+ constructor(connection, streamHandle) {
+ this.connection_ = connection;
+ this.streamHandle_ = streamHandle;
+ this.closed_ = false;
+ }
+
+ async read() {
+ if (this.closed_) {
+ throw new Error('stream is closed');
+ }
+
+ const pendingRequests = [];
+
+ const READ_REQUEST_BYTES = 32768;
+ const makeRequest = () => {
+ pendingRequests.push(this.connection_.req(
+ 'IO.read',
+ {
+ handle: this.streamHandle_,
+ size: READ_REQUEST_BYTES,
+ }));
+ };
+
+ const MAX_CONCURRENT_REQUESTS = 2;
+ for (let i = 0; i < MAX_CONCURRENT_REQUESTS; ++i) {
+ makeRequest();
+ }
+
+ const chunks = [];
+ let base64 = false;
+ while (true) {
+ const request = pendingRequests.shift();
+ const response = await request;
+
+ chunks.push(response.data);
+ if (response.base64Encoded) {
+ base64 = true;
+ }
+ if (response.eof) {
+ break;
+ }
+
+ makeRequest();
+ }
+
+ if (base64) {
+ let totalSize = 0;
+ for (const chunk of chunks) {
+ totalSize += tr.b.Base64.getDecodedBufferLength(chunk);
+ }
+ const buffer = new ArrayBuffer(totalSize);
+ let offset = 0;
+ for (const chunk of chunks) {
+ offset += tr.b.Base64.DecodeToTypedArray(
+ chunk,
+ new DataView(buffer, offset));
+ }
+ return buffer;
+ }
+
+ return chunks.join('');
+ }
+
+ close() {
+ this.closed_ = true;
+ return this.connection_.req('IO.close', { handle: this.streamHandle_ });
+ }
+
+ async readAndClose() {
+ const data = await this.read();
+ this.close();
+ return data;
+ }
+ }
+
+ return {
+ DevtoolsStream,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html
new file mode 100644
index 00000000000..791f5e77705
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+
+'use strict';
+
+/**
+ * Contains connection code that inspector's embedding framework calls on
+ * tracing, and that tracing can use to talk to inspector.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ class InspectorConnection {
+ constructor(windowGlobal) {
+ if (!windowGlobal.DevToolsHost) {
+ throw new Error('Requires window.DevToolsHost');
+ }
+ this.devToolsHost_ = windowGlobal.DevToolsHost;
+ this.installDevToolsAPI_(windowGlobal);
+
+ this.nextRequestId_ = 1;
+ this.pendingRequestResolversId_ = {};
+
+ this.notificationListenersByMethodName_ = {};
+ }
+
+ req(method, params) {
+ const id = this.nextRequestId_++;
+ const msg = JSON.stringify({
+ id,
+ method,
+ params
+ });
+ const devtoolsMessageStr = JSON.stringify(
+ {id, 'method': 'dispatchProtocolMessage', 'params': [msg]});
+ this.devToolsHost_.sendMessageToEmbedder(devtoolsMessageStr);
+
+ return new Promise(function(resolve, reject) {
+ this.pendingRequestResolversId_[id] = {
+ resolve,
+ reject
+ };
+ }.bind(this));
+ }
+
+ setNotificationListener(method, listener) {
+ this.notificationListenersByMethodName_[method] = listener;
+ }
+
+ dispatchMessage_(payload) {
+ const isStringPayload = typeof payload === 'string';
+ // Special handling for Tracing.dataCollected because it is high
+ // bandwidth.
+ const isDataCollectedMessage = isStringPayload ?
+ payload.includes('"method": "Tracing.dataCollected"') :
+ payload.method === 'Tracing.dataCollected';
+ if (isDataCollectedMessage) {
+ const listener = this.notificationListenersByMethodName_[
+ 'Tracing.dataCollected'];
+ if (listener) {
+ // FIXME(loislo): trace viewer should be able to process
+ // raw message object because string based version a few times
+ // slower on the browser side.
+ // see https://codereview.chromium.org/784513002.
+ listener(isStringPayload ? payload : JSON.stringify(payload));
+ return;
+ }
+ }
+
+ const message = isStringPayload ? JSON.parse(payload) : payload;
+ if (message.id) {
+ const resolver = this.pendingRequestResolversId_[message.id];
+ if (resolver === undefined) {
+ return;
+ }
+ if (message.error) {
+ resolver.reject(message.error);
+ return;
+ }
+ resolver.resolve(message.result);
+ return;
+ }
+
+ if (message.method) {
+ const listener = this.notificationListenersByMethodName_[
+ message.method];
+ if (listener === undefined) return;
+ listener(message.params);
+ return;
+ }
+ }
+
+ installDevToolsAPI_(windowGlobal) {
+ // Interface used by inspector when it hands data to us from the backend.
+ windowGlobal.DevToolsAPI = {
+ setToolbarColors() { },
+ addExtensions() { },
+ setInspectedPageId() { },
+ dispatchMessage: this.dispatchMessage_.bind(this),
+ };
+
+ // Temporary until inspector backend switches to DevToolsAPI.
+ windowGlobal.InspectorFrontendAPI = windowGlobal.DevToolsAPI;
+ }
+ }
+
+ return {
+ InspectorConnection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html
new file mode 100644
index 00000000000..ac5afabae12
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html
@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/extras/about_tracing/devtools_stream.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/inspector_connection.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ function createResolvedPromise(data) {
+ const promise = new Promise(function(resolve, reject) {
+ if (data) {
+ resolve(data);
+ } else {
+ resolve();
+ }
+ });
+ return promise;
+ }
+
+ function appendTraceChunksTo(chunks, messageString) {
+ if (typeof messageString !== 'string') {
+ throw new Error('Invalid data');
+ }
+ const re = /"params":\s*\{\s*"value":\s*\[([^]+)\]\s*\}\s*\}/;
+ const m = re.exec(messageString);
+ if (!m) {
+ throw new Error('Malformed response');
+ }
+
+ if (chunks.length > 1) {
+ chunks.push(',');
+ }
+ chunks.push(m[1]);
+ }
+
+ /**
+ * Controls tracing using the inspector's FrontendAgentHost APIs.
+ */
+ class InspectorTracingControllerClient extends
+ tr.ui.e.about_tracing.TracingControllerClient {
+ constructor(connection) {
+ super();
+ this.recording_ = false;
+ this.bufferUsage_ = 0;
+ this.conn_ = connection;
+ this.currentTraceTextChunks_ = undefined;
+ }
+
+ beginMonitoring(monitoringOptions) {
+ throw new Error('Not implemented');
+ }
+
+ endMonitoring() {
+ throw new Error('Not implemented');
+ }
+
+ captureMonitoring() {
+ throw new Error('Not implemented');
+ }
+
+ getMonitoringStatus() {
+ return createResolvedPromise({
+ isMonitoring: false,
+ categoryFilter: '',
+ useSystemTracing: false,
+ useContinuousTracing: false,
+ useSampling: false
+ });
+ }
+
+ getCategories() {
+ const res = this.conn_.req('Tracing.getCategories', {});
+ return res.then(function(result) {
+ return result.categories;
+ }, function(err) {
+ return [];
+ });
+ }
+
+ beginRecording(recordingOptions) {
+ if (this.recording_) {
+ throw new Error('Already recording');
+ }
+ this.recording_ = 'starting';
+
+ // The devtools and tracing endpoints have slightly different parameter
+ // configurations. Noteably, recordMode has different spelling
+ // requirements.
+ function RewriteRecordMode(recordMode) {
+ if (recordMode === 'record-until-full') {
+ return 'recordUntilFull';
+ }
+ if (recordMode === 'record-continuously') {
+ return 'recordContinuously';
+ }
+ if (recordMode === 'record-as-much-as-possible') {
+ return 'recordAsMuchAsPossible';
+ }
+ return 'unsupported record mode';
+ }
+
+ const traceConfigStr = {
+ includedCategories: recordingOptions.included_categories,
+ excludedCategories: recordingOptions.excluded_categories,
+ recordMode: RewriteRecordMode(recordingOptions.record_mode),
+ enableSystrace: recordingOptions.enable_systrace
+ };
+ if ('memory_dump_config' in recordingOptions) {
+ traceConfigStr.memoryDumpConfig = recordingOptions.memory_dump_config;
+ }
+ let res = this.conn_.req(
+ 'Tracing.start',
+ {
+ traceConfig: traceConfigStr,
+ transferMode: 'ReturnAsStream',
+ streamCompression: 'gzip',
+ bufferUsageReportingInterval: 1000
+ });
+ res = res.then(
+ function ok() {
+ this.conn_.setNotificationListener(
+ 'Tracing.bufferUsage',
+ this.onBufferUsageUpdateFromInspector_.bind(this));
+ this.recording_ = true;
+ }.bind(this),
+ function error() {
+ this.recording_ = false;
+ }.bind(this));
+ return res;
+ }
+
+ onBufferUsageUpdateFromInspector_(params) {
+ this.bufferUsage_ = params.value || params.percentFull;
+ }
+
+ beginGetBufferPercentFull() {
+ return tr.b.timeout(100).then(() => this.bufferUsage_);
+ }
+
+ onDataCollected_(messageString) {
+ appendTraceChunksTo(this.currentTraceTextChunks_, messageString);
+ }
+
+ async endRecording() {
+ if (this.recording_ === false) {
+ return createResolvedPromise();
+ }
+
+ if (this.recording_ !== true) {
+ throw new Error('Cannot end');
+ }
+
+ this.currentTraceTextChunks_ = ['['];
+ const clearListeners = () => {
+ this.conn_.setNotificationListener(
+ 'Tracing.bufferUsage', undefined);
+ this.conn_.setNotificationListener(
+ 'Tracing.tracingComplete', undefined);
+ this.conn_.setNotificationListener(
+ 'Tracing.dataCollected', undefined);
+ };
+
+ try {
+ this.conn_.setNotificationListener(
+ 'Tracing.dataCollected', this.onDataCollected_.bind(this));
+
+ const tracingComplete = new Promise((resolve, reject) => {
+ this.conn_.setNotificationListener(
+ 'Tracing.tracingComplete', resolve);
+ });
+
+ this.recording_ = 'stopping';
+ await this.conn_.req('Tracing.end', {});
+ const params = await tracingComplete;
+
+ this.traceName_ = 'trace.json';
+ if ('stream' in params) {
+ const stream = new tr.ui.e.about_tracing.DevtoolsStream(
+ this.conn_, params.stream);
+ const streamCompression = params.streamCompression || 'none';
+ if (streamCompression === 'gzip') {
+ this.traceName_ = 'trace.json.gz';
+ }
+
+ return await stream.readAndClose();
+ }
+
+ this.currentTraceTextChunks_.push(']');
+ const traceText = this.currentTraceTextChunks_.join('');
+ this.currentTraceTextChunks_ = undefined;
+ return traceText;
+ } finally {
+ clearListeners();
+ this.recording_ = false;
+ }
+ }
+
+ defaultTraceName() {
+ return this.traceName_;
+ }
+ }
+
+ return {
+ InspectorTracingControllerClient,
+ appendTraceChunksTo,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html
new file mode 100644
index 00000000000..4a6585ac9e8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html
@@ -0,0 +1,396 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/inspector_connection.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html">
+
+<script>
+'use strict';
+
+function makeController() {
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient();
+ controller.conn_ = new (function() {
+ this.req = function(method, params) {
+ const msg = JSON.stringify({
+ id: 1,
+ method,
+ params
+ });
+ return new (function() {
+ this.msg = msg;
+ this.then = function(m1, m2) {
+ return this;
+ };
+ })();
+ };
+ this.setNotificationListener = function(method, listener) {
+ };
+ })();
+ return controller;
+}
+
+tr.b.unittest.testSuite(function() {
+ test('beginRecording_sendCategoriesAndOptions', function() {
+ const controller = makeController();
+
+ const recordingOptions = {
+ included_categories: ['a', 'b', 'c'],
+ excluded_categories: ['e'],
+ enable_systrace: false,
+ record_mode: 'record-until-full',
+ };
+
+ const result = JSON.parse(controller.beginRecording(recordingOptions).msg);
+ assert.deepEqual(
+ result.params.traceConfig.includedCategories, ['a', 'b', 'c']);
+ assert.deepEqual(
+ result.params.traceConfig.excludedCategories, ['e']);
+ assert.strictEqual(
+ result.params.traceConfig.recordMode, 'recordUntilFull');
+ assert.isFalse(
+ result.params.traceConfig.enableSystrace);
+ assert.isFalse('memoryDumpConfig' in result.params.traceConfig);
+ });
+
+ test('beginRecording_sendCategoriesAndOptionsWithMemoryInfra', function() {
+ const controller = makeController();
+
+ const memoryConfig = { triggers: [] };
+ memoryConfig.triggers.push(
+ {'mode': 'detailed', 'periodic_interval_ms': 10000});
+ const recordingOptions = {
+ included_categories: ['c', 'disabled-by-default-memory-infra', 'a'],
+ excluded_categories: ['e'],
+ enable_systrace: false,
+ record_mode: 'test-mode',
+ memory_dump_config: memoryConfig,
+ };
+
+ const result = JSON.parse(controller.beginRecording(recordingOptions).msg);
+ assert.isTrue(
+ result.params.traceConfig.memoryDumpConfig.triggers.length === 1);
+ assert.strictEqual(result.params.traceConfig.memoryDumpConfig.
+ triggers[0].mode, 'detailed');
+ assert.strictEqual(result.params.traceConfig.memoryDumpConfig.
+ triggers[0].periodic_interval_ms, 10000);
+ });
+
+ test('oldFormat', function() {
+ const chunks = [];
+ tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [ {"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}} ] } }"'); // @suppress longLineCheck
+ assert.strictEqual(chunks.length, 1);
+ JSON.parse('[' + chunks.join('') + ']');
+ });
+
+ test('newFormat', function() {
+ const chunks = [];
+ tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [{"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}}] } }"'); // @suppress longLineCheck
+ assert.strictEqual(chunks.length, 1);
+ JSON.parse('[' + chunks.join('') + ']');
+ });
+
+ test('stringAndObjectPayload', function() {
+ const connection =
+ new tr.ui.e.about_tracing.InspectorConnection({DevToolsHost: {}});
+ connection.setNotificationListener('Tracing.dataCollected',
+ function(message) {
+ assert.typeOf(message, 'string');
+ JSON.parse(message);
+ }
+ );
+ connection.dispatchMessage_('{ "method": "Tracing.dataCollected", "params": { "value": [] } }'); // @suppress longLineCheck
+ connection.dispatchMessage_({'method': 'Tracing.dataCollected', 'params': {'value': [] } }); // @suppress longLineCheck
+ });
+
+ // Makes a fake version of DevToolsHost, which is the object injected
+ // by the chrome inspector to allow tracing a remote instance of chrome.
+ //
+ // The fake host doesn't do much by itself - you have to install
+ // callbacks for incoming messages via handleMessage().
+ function makeFakeDevToolsHost() {
+ return new (function() {
+ this.pendingMethods_ = [];
+ this.messageHandlers_ = [];
+
+ // Sends a message to DevTools host. This is used by
+ // InspectorTracingControllerClient to communicate with the remote
+ // debugging tracing backend.
+ this.sendMessageToEmbedder = function(devtoolsMessageStr) {
+ this.pendingMethods_.push(JSON.parse(devtoolsMessageStr));
+ this.tryMessageHandlers_();
+ };
+
+ // Runs remote debugging message handlers. Handlers are installed
+ // by test code via handleMessage().
+ this.tryMessageHandlers_ = function() {
+ while (this.pendingMethods_.length !== 0) {
+ const message = this.pendingMethods_[0];
+ const params = JSON.parse(message.params);
+ let handled = false;
+ const handlersToRemove = [];
+
+ // Try to find a handler for this method.
+ for (const handler of this.messageHandlers_) {
+ if (handler(params, () => handlersToRemove.push(handler))) {
+ handled = true;
+ break;
+ }
+ }
+
+ // Remove any handlers that requested removal.
+ this.messageHandlers_ = this.messageHandlers_.filter(
+ (handler) => !handlersToRemove.includes(handler));
+
+ // Remove any handled messages.
+ if (handled) {
+ this.pendingMethods_.shift();
+ } else {
+ return; // Methods must be handled in order.
+ }
+ }
+ };
+
+ // Installs a message handler that will be invoked for each
+ // incoming message from InspectorTracingControllerClient.
+ //
+ // handleMessage((message, removeSelf) => {
+ // // Try to handle |message|.
+ // // Call |removeSelf| to remove this handler for future messages.
+ // // Return whether |message| was handled. Otherwise other handlers
+ // // will be run until one of them succeeds.
+ // }
+ this.handleMessage = function(handler) {
+ this.messageHandlers_.push(handler);
+ this.tryMessageHandlers_();
+ };
+
+ // Installs a message handler that will handle the first call to the named
+ // method. Returns a promise for the parameters passed to the method.
+ this.handleMethod = function(method) {
+ const result = new Promise((resolve, reject) => {
+ this.handleMessage(
+ (requestParams, removeHandler) => {
+ if (requestParams.method === method) {
+ removeHandler();
+ resolve(requestParams);
+ return true;
+ }
+ return false;
+ });
+ });
+ return result;
+ };
+
+ // Sends a response to a remote debugging method call (i.e.,
+ // "return") to InspectorTracingControllerClient.
+ this.respondToMethod = function(id, params) {
+ this.devToolsAPI_.dispatchMessage(JSON.stringify({
+ id,
+ result: params,
+ }));
+ };
+
+ // Sets the object used to send messages back to
+ // InspectorTracingControllerClient.
+ this.setDevToolsAPI = function(api) {
+ this.devToolsAPI_ = api;
+ };
+
+ // Sends a notification to InspectorTracingControllerClient.
+ this.sendNotification = function(method, params) {
+ this.devToolsAPI_.dispatchMessage(JSON.stringify({ method, params }));
+ };
+ })();
+ }
+
+ test('shouldUseLegacyTraceFormatIfNoStreamId', async function() {
+ const fakeDevToolsHost = makeFakeDevToolsHost();
+ const fakeWindow = {
+ DevToolsHost: fakeDevToolsHost,
+ };
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(fakeWindow));
+ fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI);
+
+ const runHost = (async() => {
+ const startParams = await fakeDevToolsHost.handleMethod('Tracing.start');
+ fakeDevToolsHost.respondToMethod(startParams.id, {});
+ const endParams = await fakeDevToolsHost.handleMethod('Tracing.end');
+ fakeDevToolsHost.respondToMethod(endParams.id, {});
+ fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {});
+ })();
+
+ await controller.beginRecording({});
+ const traceData = await controller.endRecording();
+ await runHost;
+
+ assert.strictEqual('[]', traceData);
+ });
+
+ test('shouldReassembleTextDataChunks', async function() {
+ const fakeDevToolsHost = makeFakeDevToolsHost();
+ const fakeWindow = {
+ DevToolsHost: fakeDevToolsHost,
+ };
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(fakeWindow));
+ fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI);
+
+ const STREAM_HANDLE = 7;
+
+ const streamChunks = [
+ '[',
+ ']',
+ '\n',
+ ];
+
+ let streamClosed = false;
+
+ const handleIoRead = (index, params) => {
+ if (params.params.handle !== STREAM_HANDLE) {
+ throw new Error('Invalid stream handle');
+ }
+ if (streamClosed) {
+ throw new Error('stream is closed');
+ }
+ let data = '';
+ if (index < streamChunks.length) {
+ data = streamChunks[index];
+ }
+ const eof = (index >= streamChunks.length - 1);
+ fakeDevToolsHost.respondToMethod(params.id, {
+ eof,
+ base64Encoded: false,
+ data,
+ });
+ const nextIndex = eof ? streamChunks.length : index + 1;
+ return (async() =>
+ handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read'))
+ )();
+ };
+
+ const runHost = (async() => {
+ const startParams = await fakeDevToolsHost.handleMethod('Tracing.start');
+ fakeDevToolsHost.respondToMethod(startParams.id, {});
+ const endParams = await fakeDevToolsHost.handleMethod('Tracing.end');
+ fakeDevToolsHost.respondToMethod(endParams.id, {});
+ fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {
+ 'stream': STREAM_HANDLE,
+ });
+
+ const closePromise = (async() => {
+ const closeParams = await fakeDevToolsHost.handleMethod('IO.close');
+ assert.strictEqual(closeParams.params.handle, STREAM_HANDLE);
+ streamClosed = true;
+ })();
+
+ const readPromise = (async() =>
+ handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read'))
+ )();
+
+ await Promise.race([closePromise, readPromise]);
+ await closePromise;
+ })();
+
+ await controller.beginRecording({});
+ const traceData = await controller.endRecording();
+ await runHost;
+
+ assert.strictEqual(traceData, '[]\n');
+ });
+
+ test('shouldReassembleBase64TraceDataChunks', async function() {
+ const fakeDevToolsHost = makeFakeDevToolsHost();
+ const fakeWindow = {
+ DevToolsHost: fakeDevToolsHost,
+ };
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(fakeWindow));
+ fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI);
+
+ const STREAM_HANDLE = 7;
+
+ // This is the empty trace ('[]') gzip compressed and chunked to make
+ // sure reassembling base64 strings works properly.
+ const streamChunks = [
+ 'Hw==',
+ 'iwg=',
+ 'ALg4',
+ 'L1oAA4uOBQApu0wNAgAAAA==',
+ ];
+
+ let streamClosed = false;
+
+ const handleIoRead = (index, params) => {
+ if (params.params.handle !== STREAM_HANDLE) {
+ throw new Error('Invalid stream handle');
+ }
+ if (streamClosed) {
+ throw new Error('stream is closed');
+ }
+ let data = '';
+ if (index < streamChunks.length) {
+ data = streamChunks[index];
+ }
+ const eof = (index >= streamChunks.length - 1);
+ fakeDevToolsHost.respondToMethod(params.id, {
+ eof,
+ base64Encoded: true,
+ data,
+ });
+ const nextIndex = eof ? streamChunks.length : index + 1;
+ return (async() => {
+ handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read'));
+ })();
+ };
+
+ const runHost = (async() => {
+ const startParams = await fakeDevToolsHost.handleMethod('Tracing.start');
+ fakeDevToolsHost.respondToMethod(startParams.id, {});
+ const endParams = await fakeDevToolsHost.handleMethod('Tracing.end');
+ fakeDevToolsHost.respondToMethod(endParams.id, {});
+ fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {
+ 'stream': STREAM_HANDLE,
+ 'streamCompression': 'gzip'
+ });
+ const closePromise = (async() => {
+ const closeParams = await fakeDevToolsHost.handleMethod('IO.close');
+ assert.strictEqual(closeParams.params.handle, STREAM_HANDLE);
+ streamClosed = true;
+ })();
+
+ const readPromise = (async() => {
+ handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read'));
+ })();
+
+ await Promise.race([closePromise, readPromise]);
+ await closePromise;
+ })();
+
+ await controller.beginRecording({});
+ const traceData = await controller.endRecording();
+ await runHost;
+
+ const dataArray = new Uint8Array(traceData);
+ const expectedArray = new Uint8Array([
+ 0x1f, 0x8b, 0x8, 0x0, 0xb8, 0x38, 0x2f, 0x5a, 0x0, 0x3, 0x8b, 0x8e,
+ 0x5, 0x0, 0x29, 0xbb, 0x4c, 0xd, 0x2, 0x0, 0x0, 0x0]);
+
+ assert.strictEqual(dataArray.length, expectedArray.length);
+
+ for (let i = 0; i < dataArray.length; ++i) {
+ assert.strictEqual(dataArray[i], expectedArray[i]);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html
new file mode 100644
index 00000000000..cfefdc05cc7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ function MockTracingControllerClient() {
+ this.requests = [];
+ this.nextRequestIndex = 0;
+ this.allowLooping = false;
+ }
+
+ MockTracingControllerClient.prototype = {
+ __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype,
+
+ expectRequest(method, generateResponse) {
+ let generateResponseCb;
+ if (typeof generateResponse === 'function') {
+ generateResponseCb = generateResponse;
+ } else {
+ generateResponseCb = function() {
+ return generateResponse;
+ };
+ }
+
+ this.requests.push({
+ method,
+ generateResponseCb});
+ },
+
+ _request(method, args) {
+ return new Promise(function(resolve) {
+ const requestIndex = this.nextRequestIndex;
+ if (requestIndex >= this.requests.length) {
+ throw new Error('Unhandled request');
+ }
+ if (!this.allowLooping) {
+ this.nextRequestIndex++;
+ } else {
+ this.nextRequestIndex = (this.nextRequestIndex + 1) %
+ this.requests.length;
+ }
+
+ const req = this.requests[requestIndex];
+ assert.strictEqual(method, req.method);
+ const resp = req.generateResponseCb(args);
+ resolve(resp);
+ }.bind(this));
+ },
+
+ assertAllRequestsHandled() {
+ if (this.allowLooping) {
+ throw new Error('Incompatible with allowLooping');
+ }
+ assert.strictEqual(this.requests.length, this.nextRequestIndex);
+ },
+
+ getCategories() {
+ return this._request('getCategories');
+ },
+
+ beginRecording(recordingOptions) {
+ return this._request('beginRecording', recordingOptions);
+ },
+
+ beginGetBufferPercentFull() {
+ return this._request('beginGetBufferPercentFull');
+ },
+
+ endRecording() {
+ return this._request('endRecording');
+ }
+ };
+
+ return {
+ MockTracingControllerClient,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html
new file mode 100644
index 00000000000..77c0e80af12
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html
@@ -0,0 +1,372 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/ui/base/file.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/record_controller.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<style>
+x-profiling-view {
+ flex-direction: column;
+ display: flex;
+ padding: 0;
+}
+
+x-profiling-view .controls #save-button {
+ margin-left: 64px !important;
+}
+
+x-profiling-view > tr-ui-timeline-view {
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+.report-id-message {
+ -webkit-user-select: text;
+}
+
+x-timeline-view-buttons {
+ display: flex;
+ align-items: center;
+}
+</style>
+
+<template id="profiling-view-template">
+ <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
+ <x-timeline-view-buttons>
+ <button id="record-button">Record</button>
+ <button id="save-button">Save</button>
+ <button id="load-button">Load</button>
+ </x-timeline-view-buttons>
+ <tr-ui-timeline-view>
+ <track-view-container id='track_view_container'></track-view-container>
+ </tr-ui-timeline-view>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview ProfilingView glues the View control to
+ * TracingController.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ /**
+ * ProfilingView
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ const ProfilingView = tr.ui.b.define('x-profiling-view');
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ ProfilingView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate(tracingControllerClient) {
+ Polymer.dom(this).appendChild(
+ tr.ui.b.instantiateTemplate('#profiling-view-template', THIS_DOC));
+
+ this.timelineView_ =
+ Polymer.dom(this).querySelector('tr-ui-timeline-view');
+ this.infoBarGroup_ =
+ Polymer.dom(this).querySelector('tr-ui-b-info-bar-group');
+
+ // Detach the buttons. We will reattach them to the timeline view.
+ // TODO(nduca): Make timeline-view have a content select="x-buttons"
+ // that pulls in any buttons.
+ this.recordButton_ = Polymer.dom(this).querySelector('#record-button');
+ this.loadButton_ = Polymer.dom(this).querySelector('#load-button');
+ this.saveButton_ = Polymer.dom(this).querySelector('#save-button');
+
+ const buttons = Polymer.dom(this).querySelector(
+ 'x-timeline-view-buttons');
+ Polymer.dom(buttons.parentElement).removeChild(buttons);
+ Polymer.dom(this.timelineView_.leftControls).appendChild(buttons);
+ this.initButtons_();
+
+ this.timelineView_.hotkeyController.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: 'r'.charCodeAt(0),
+ callback(e) {
+ this.beginRecording();
+ event.stopPropagation();
+ },
+ thisArg: this
+ }));
+
+ this.initDragAndDrop_();
+
+ if (tracingControllerClient) {
+ this.tracingControllerClient_ = tracingControllerClient;
+ } else if (window.DevToolsHost !== undefined) {
+ this.tracingControllerClient_ =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(window));
+ } else {
+ this.tracingControllerClient_ =
+ new tr.ui.e.about_tracing.XhrBasedTracingControllerClient();
+ }
+
+ this.isRecording_ = false;
+ this.activeTrace_ = undefined;
+
+ this.updateTracingControllerSpecificState_();
+ },
+
+ // Detach all document event listeners. Without this the tests can get
+ // confused as the element may still be listening when the next test runs.
+ detach_() {
+ this.detachDragAndDrop_();
+ },
+
+ get isRecording() {
+ return this.isRecording_;
+ },
+
+ set tracingControllerClient(tracingControllerClient) {
+ this.tracingControllerClient_ = tracingControllerClient;
+ this.updateTracingControllerSpecificState_();
+ },
+
+ updateTracingControllerSpecificState_() {
+ const isInspector = this.tracingControllerClient_ instanceof
+ tr.ui.e.about_tracing.InspectorTracingControllerClient;
+
+ if (isInspector) {
+ this.infoBarGroup_.addMessage(
+ 'This about:tracing is connected to a remote device...',
+ [{buttonText: 'Wow!', onClick() {}}]);
+ }
+ },
+
+ beginRecording() {
+ if (this.isRecording_) {
+ throw new Error('Already recording');
+ }
+ this.isRecording_ = true;
+ const resultPromise = tr.ui.e.about_tracing.beginRecording(
+ this.tracingControllerClient_);
+ resultPromise.then(
+ function(data) {
+ this.isRecording_ = false;
+ const traceName = tr.ui.e.about_tracing.defaultTraceName(
+ this.tracingControllerClient_);
+ this.setActiveTrace(traceName, data, false);
+ }.bind(this),
+ function(err) {
+ this.isRecording_ = false;
+ if (err instanceof tr.ui.e.about_tracing.UserCancelledError) {
+ return;
+ }
+ tr.ui.b.Overlay.showError('Error while recording', err);
+ }.bind(this));
+ return resultPromise;
+ },
+
+ get timelineView() {
+ return this.timelineView_;
+ },
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ clearActiveTrace() {
+ this.saveButton_.disabled = true;
+ this.activeTrace_ = undefined;
+ },
+
+ setActiveTrace(filename, data) {
+ this.activeTrace_ = {
+ filename,
+ data
+ };
+
+ this.infoBarGroup_.clearMessages();
+ this.updateTracingControllerSpecificState_();
+ this.saveButton_.disabled = false;
+ this.timelineView_.viewTitle = filename;
+
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m);
+ const p = i.importTracesWithProgressDialog([data]);
+ p.then(
+ function() {
+ this.timelineView_.model = m;
+ this.timelineView_.updateDocumentFavicon();
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('While importing: ', err);
+ }.bind(this));
+ },
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ initButtons_() {
+ this.recordButton_.addEventListener(
+ 'click', function(event) {
+ event.stopPropagation();
+ this.beginRecording();
+ }.bind(this));
+
+ this.loadButton_.addEventListener(
+ 'click', function(event) {
+ event.stopPropagation();
+ this.onLoadClicked_();
+ }.bind(this));
+
+ this.saveButton_.addEventListener('click',
+ this.onSaveClicked_.bind(this));
+ this.saveButton_.disabled = true;
+ },
+
+ requestFilename_() {
+ // unsafe filename patterns:
+ const illegalRe = /[\/\?<>\\:\*\|":]/g;
+ const controlRe = /[\x00-\x1f\x80-\x9f]/g;
+ const reservedRe = /^\.+$/;
+
+ const defaultName = this.activeTrace_.filename;
+ let fileExtension = '.json';
+ let fileRegex = /\.json$/;
+ if (/[.]gz$/.test(defaultName)) {
+ fileExtension += '.gz';
+ fileRegex = /\.json\.gz$/;
+ } else if (/[.]zip$/.test(defaultName)) {
+ fileExtension = '.zip';
+ fileRegex = /\.zip$/;
+ }
+
+ const custom = prompt('Filename? (' + fileExtension +
+ ' appended) Or leave blank:');
+ if (custom === null) {
+ return undefined;
+ }
+
+ let name;
+ if (custom) {
+ name = ' ' + custom;
+ } else {
+ const date = new Date();
+ const dateText = ' ' + date.toDateString() +
+ ' ' + date.toLocaleTimeString();
+ name = dateText;
+ }
+
+ const filename = defaultName.replace(fileRegex, name) + fileExtension;
+
+ return filename
+ .replace(illegalRe, '.')
+ .replace(controlRe, '\u2022')
+ .replace(reservedRe, '')
+ .replace(/\s+/g, '_');
+ },
+
+ onSaveClicked_() {
+ // Create a blob URL from the binary array.
+ const blob = new Blob([this.activeTrace_.data],
+ {type: 'application/octet-binary'});
+ const blobUrl = window.webkitURL.createObjectURL(blob);
+
+ // Create a link and click on it. BEST API EVAR!
+ const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+ link.href = blobUrl;
+ const filename = this.requestFilename_();
+ if (filename) {
+ link.download = filename;
+ link.click();
+ }
+ },
+
+ onLoadClicked_() {
+ const inputElement = document.createElement('input');
+ inputElement.type = 'file';
+ inputElement.multiple = false;
+
+ let changeFired = false;
+ inputElement.addEventListener(
+ 'change',
+ function(e) {
+ if (changeFired) return;
+ changeFired = true;
+
+ const file = inputElement.files[0];
+ tr.ui.b.readFile(file).then(
+ function(data) {
+ this.setActiveTrace(file.name, data);
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Error while loading file: ' + err);
+ });
+ }.bind(this), false);
+ inputElement.click();
+ },
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ initDragAndDrop_() {
+ this.dropHandler_ = this.dropHandler_.bind(this);
+ this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this);
+ document.addEventListener('dragstart', this.ignoreDragEvent_, false);
+ document.addEventListener('dragend', this.ignoreDragEvent_, false);
+ document.addEventListener('dragenter', this.ignoreDragEvent_, false);
+ document.addEventListener('dragleave', this.ignoreDragEvent_, false);
+ document.addEventListener('dragover', this.ignoreDragEvent_, false);
+ document.addEventListener('drop', this.dropHandler_, false);
+ },
+
+ detachDragAndDrop_() {
+ document.removeEventListener('dragstart', this.ignoreDragEvent_);
+ document.removeEventListener('dragend', this.ignoreDragEvent_);
+ document.removeEventListener('dragenter', this.ignoreDragEvent_);
+ document.removeEventListener('dragleave', this.ignoreDragEvent_);
+ document.removeEventListener('dragover', this.ignoreDragEvent_);
+ document.removeEventListener('drop', this.dropHandler_);
+ },
+
+ ignoreDragEvent_(e) {
+ e.preventDefault();
+ return false;
+ },
+
+ dropHandler_(e) {
+ if (this.isAnyDialogUp_) return;
+
+ e.stopPropagation();
+ e.preventDefault();
+
+ const files = e.dataTransfer.files;
+ if (files.length !== 1) {
+ tr.ui.b.Overlay.showError('1 file supported at a time.');
+ return;
+ }
+
+ tr.ui.b.readFile(files[0]).then(
+ function(data) {
+ this.setActiveTrace(files[0].name, data);
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Error while loading file: ' + err);
+ });
+ return false;
+ }
+ };
+
+ return {
+ ProfilingView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html
new file mode 100644
index 00000000000..f52c491207f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+ const testData = [
+ {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const monitoringOptions = {
+ isMonitoring: false,
+ categoryFilter: '*',
+ useSystemTracing: false,
+ useContinuousTracing: false,
+ useSampling: false
+ };
+
+ const ProfilingView = tr.ui.e.about_tracing.ProfilingView;
+
+ test('recording', function() {
+ const mock = new tr.ui.e.about_tracing.MockTracingControllerClient();
+ mock.allowLooping = true;
+ mock.expectRequest('endRecording', function() {
+ return '';
+ });
+ mock.expectRequest('getCategories', function() {
+ return ['a', 'b', 'c'];
+ });
+ mock.expectRequest('beginRecording', function(data) {
+ return '';
+ });
+ mock.expectRequest('endRecording', function(data) {
+ return JSON.stringify(testData);
+ });
+
+ const view = new ProfilingView(mock);
+ view.style.height = '400px';
+ view.style.border = '1px solid black';
+ this.addHTMLOutput(view);
+
+ const recordingPromise = view.beginRecording();
+
+ let didAbort = false;
+
+ tr.b.timeout(60).then(() => {
+ if (didAbort) return;
+ recordingPromise.selectionDlg.clickRecordButton();
+ }).then(() => tr.b.timeout(60)).then(() => {
+ recordingPromise.progressDlg.clickStopButton();
+ });
+
+ return recordingPromise.then(null, err => {
+ didAbort = true;
+ assert.fail(err);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html
new file mode 100644
index 00000000000..a9b42b589d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html
@@ -0,0 +1,187 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ function beginRecording(tracingControllerClient) {
+ let finalPromiseResolver;
+ const finalPromise = new Promise(function(resolve, reject) {
+ finalPromiseResolver = {
+ resolve,
+ reject
+ };
+ });
+ finalPromise.selectionDlg = undefined;
+ finalPromise.progressDlg = undefined;
+
+ function beginRecordingError(err) {
+ finalPromiseResolver.reject(err);
+ }
+
+ // Step 0: End recording. This is necessary when the user reloads the
+ // about:tracing page when we are recording. Window.onbeforeunload is not
+ // reliable to end recording on reload.
+ endRecording(tracingControllerClient).then(
+ getCategories,
+ getCategories); // Ignore error.
+
+ // But just in case, bind onbeforeunload anyway.
+ window.onbeforeunload = function(e) {
+ endRecording(tracingControllerClient);
+ };
+
+ // Step 1: Get categories.
+ function getCategories() {
+ const p = tracingControllerClient.getCategories().then(
+ showTracingDialog,
+ beginRecordingError);
+ p.catch(function(err) {
+ beginRecordingError(err);
+ });
+ }
+
+ // Step 2: Show tracing dialog.
+ let selectionDlg;
+ function showTracingDialog(categories) {
+ selectionDlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ selectionDlg.categories = categories;
+ selectionDlg.settings_key =
+ 'tr.ui.e.about_tracing.record_selection_dialog';
+ selectionDlg.addEventListener('recordclick', startTracing);
+ selectionDlg.addEventListener('closeclick', cancelRecording);
+ selectionDlg.visible = true;
+
+ finalPromise.selectionDlg = selectionDlg;
+ }
+
+ function cancelRecording() {
+ finalPromise.selectionDlg = undefined;
+ finalPromiseResolver.reject(new UserCancelledError());
+ }
+
+ // Step 2: Do the actual tracing dialog.
+ let progressDlg;
+ let bufferPercentFullDiv;
+ function startTracing() {
+ progressDlg = new tr.ui.b.Overlay();
+ Polymer.dom(progressDlg).textContent = 'Recording...';
+ progressDlg.userCanClose = false;
+
+ bufferPercentFullDiv = document.createElement('div');
+ Polymer.dom(progressDlg).appendChild(bufferPercentFullDiv);
+
+ const stopButton = document.createElement('button');
+ Polymer.dom(stopButton).textContent = 'Stop';
+ progressDlg.clickStopButton = function() {
+ stopButton.click();
+ };
+ Polymer.dom(progressDlg).appendChild(stopButton);
+
+ const categories = selectionDlg.includedAndExcludedCategories();
+ const recordingOptions = {
+ included_categories: categories.included,
+ excluded_categories: categories.excluded,
+ enable_systrace: selectionDlg.useSystemTracing,
+ record_mode: selectionDlg.tracingRecordMode,
+ };
+ if (categories.included.indexOf(
+ 'disabled-by-default-memory-infra') !== -1) {
+ const memoryConfig = { triggers: [] };
+ memoryConfig.triggers.push(
+ {'mode': 'detailed', 'periodic_interval_ms': 10000});
+ recordingOptions.memory_dump_config = memoryConfig;
+ }
+
+ const requestPromise = tracingControllerClient.beginRecording(
+ recordingOptions);
+ requestPromise.then(
+ function() {
+ progressDlg.visible = true;
+ stopButton.focus();
+ updateBufferPercentFull('0');
+ },
+ recordFailed);
+
+ stopButton.addEventListener('click', function() {
+ // TODO(chrishenry): Currently, this only dismiss the progress
+ // dialog when tracingComplete event is received. When performing
+ // remote debugging, the tracingComplete event may be delayed
+ // considerable. We should indicate to user that we are waiting
+ // for tracingComplete event instead of being unresponsive. (For
+ // now, I disable the "stop" button, since clicking on the button
+ // again now cause exception.)
+ const recordingPromise = endRecording(tracingControllerClient);
+ recordingPromise.then(
+ recordFinished,
+ recordFailed);
+ stopButton.disabled = true;
+ bufferPercentFullDiv = undefined;
+ });
+ finalPromise.progressDlg = progressDlg;
+ }
+
+ function recordFinished(tracedData) {
+ progressDlg.visible = false;
+ finalPromise.progressDlg = undefined;
+ finalPromiseResolver.resolve(tracedData);
+ }
+
+ function recordFailed(err) {
+ progressDlg.visible = false;
+ finalPromise.progressDlg = undefined;
+ finalPromiseResolver.reject(err);
+ }
+
+ function getBufferPercentFull() {
+ if (!bufferPercentFullDiv) return;
+
+ tracingControllerClient.beginGetBufferPercentFull().then(
+ updateBufferPercentFull);
+ }
+
+ function updateBufferPercentFull(percentFull) {
+ if (!bufferPercentFullDiv) return;
+
+ percentFull = Math.round(100 * parseFloat(percentFull));
+ const newText = 'Buffer usage: ' + percentFull + '%';
+ if (Polymer.dom(bufferPercentFullDiv).textContent !== newText) {
+ Polymer.dom(bufferPercentFullDiv).textContent = newText;
+ }
+
+ window.setTimeout(getBufferPercentFull, 500);
+ }
+
+ // Thats it! We're done.
+ return finalPromise;
+ }
+
+ function endRecording(tracingControllerClient) {
+ return tracingControllerClient.endRecording();
+ }
+
+ function defaultTraceName(tracingControllerClient) {
+ return tracingControllerClient.defaultTraceName();
+ }
+
+ function UserCancelledError() {
+ Error.apply(this, arguments);
+ }
+ UserCancelledError.prototype = {
+ __proto__: Error.prototype
+ };
+
+ return {
+ beginRecording,
+ UserCancelledError,
+ defaultTraceName,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html
new file mode 100644
index 00000000000..e3e0438f3a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/record_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const testData = [
+ {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ test('fullRecording', function() {
+ const mock = new tr.ui.e.about_tracing.MockTracingControllerClient();
+ mock.expectRequest('endRecording', function() {
+ return '';
+ });
+ mock.expectRequest('getCategories', function() {
+ tr.b.timeout(20).then(() =>
+ recordingPromise.selectionDlg.clickRecordButton());
+ return ['a', 'b', 'c'];
+ });
+ mock.expectRequest('beginRecording', function(recordingOptions) {
+ assert.typeOf(recordingOptions.included_categories, 'array');
+ assert.typeOf(recordingOptions.excluded_categories, 'array');
+ assert.typeOf(recordingOptions.enable_systrace, 'boolean');
+ assert.typeOf(recordingOptions.record_mode, 'string');
+ tr.b.timeout(10).then(() =>
+ recordingPromise.progressDlg.clickStopButton());
+ return '';
+ });
+ mock.expectRequest('endRecording', function(data) {
+ return JSON.stringify(testData);
+ });
+
+ const recordingPromise = tr.ui.e.about_tracing.beginRecording(mock);
+
+ return recordingPromise.then(function(data) {
+ mock.assertAllRequestsHandled();
+ assert.strictEqual(data, JSON.stringify(testData));
+ }, function(error) {
+ assert.fail(error);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html
new file mode 100644
index 00000000000..a5383973a80
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html
@@ -0,0 +1,689 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id="record-selection-dialog-template">
+ <style>
+ .categories-column-view {
+ display: flex;
+ flex-direction: column;
+ font-family: sans-serif;
+ max-width: 640px;
+ min-height: 0;
+ min-width: 0;
+ opacity: 1;
+ transition: max-height 1s ease, max-width 1s ease, opacity 1s ease;
+ will-change: opacity;
+ }
+
+ .categories-column-view-hidden {
+ max-height: 0;
+ max-width: 0;
+ opacity: 0;
+ overflow: hidden;
+ display: none;
+ }
+
+ .categories-selection {
+ display: flex;
+ flex-direction: row;
+ }
+
+ .category-presets {
+ padding: 4px;
+ }
+
+ .category-description {
+ color: #aaa;
+ font-size: small;
+ max-height: 1em;
+ opacity: 1;
+ padding-left: 4px;
+ padding-right: 4px;
+ text-align: right;
+ transition: max-height 1s ease, opacity 1s ease;
+ will-change: opacity;
+ }
+
+ .category-description-hidden {
+ max-height: 0;
+ opacity: 0;
+ }
+
+ .default-enabled-categories,
+ .default-disabled-categories {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ padding: 4px;
+ width: 300px;
+ }
+
+ .default-enabled-categories > div,
+ .default-disabled-categories > div {
+ padding: 4px;
+ }
+
+ .tracing-modes {
+ flex: 1 0 auto;
+ display: flex;
+ flex-direction: reverse;
+ padding: 4px;
+ border-bottom: 2px solid #ddd;
+ border-top: 2px solid #ddd;
+ }
+
+ .default-disabled-categories {
+ border-left: 2px solid #ddd;
+ }
+
+ .warning-default-disabled-categories {
+ display: inline-block;
+ font-weight: bold;
+ text-align: center;
+ color: #BD2E2E;
+ width: 2.0ex;
+ height: 2.0ex;
+ border-radius: 2.0ex;
+ border: 1px solid #BD2E2E;
+ }
+
+ .categories {
+ font-size: 80%;
+ padding: 10px;
+ flex: 1 1 auto;
+ }
+
+ .group-selectors {
+ font-size: 80%;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 6px;
+ flex: 0 0 auto;
+ }
+
+ .group-selectors button {
+ padding: 1px;
+ }
+
+ .record-selection-dialog .labeled-option-group {
+ flex: 0 0 auto;
+ flex-direction: column;
+ display: flex;
+ }
+
+ .record-selection-dialog .labeled-option {
+ border-top: 5px solid white;
+ border-bottom: 5px solid white;
+ }
+
+ .record-selection-dialog .edit-categories {
+ padding-left: 6px;
+ }
+
+ .record-selection-dialog .edit-categories:after {
+ padding-left: 15px;
+ font-size: 125%;
+ }
+
+ .record-selection-dialog .labeled-option-group:not(.categories-expanded)
+ .edit-categories:after {
+ content: '\25B8'; /* Right triangle */
+ }
+
+ .record-selection-dialog .labeled-option-group.categories-expanded
+ .edit-categories:after {
+ content: '\25BE'; /* Down triangle */
+ }
+
+ </style>
+
+ <div class="record-selection-dialog">
+ <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
+ <div class="category-presets">
+ </div>
+ <div class="category-description"></div>
+ <div class="categories-column-view">
+ <div class="tracing-modes"></div>
+ <div class="categories-selection">
+ <div class="default-enabled-categories">
+ <div>Record&nbsp;Categories</div>
+ <div class="group-selectors">
+ Select
+ <button class="all-btn">All</button>
+ <button class="none-btn">None</button>
+ </div>
+ <div class="categories"></div>
+ </div>
+ <div class="default-disabled-categories">
+ <div>Disabled&nbsp;by&nbsp;Default&nbsp;Categories
+ <a class="warning-default-disabled-categories">!</a>
+ </div>
+ <div class="group-selectors">
+ Select
+ <button class="all-btn">All</button>
+ <button class="none-btn">None</button>
+ </div>
+ <div class="categories"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview RecordSelectionDialog presents the available categories
+ * to be enabled/disabled during tr.c.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const RecordSelectionDialog = tr.ui.b.define('div');
+
+ const DEFAULT_PRESETS = [
+ {title: 'Web developer',
+ categoryFilter: ['blink', 'cc', 'netlog', 'renderer.scheduler',
+ 'sequence_manager', 'toplevel', 'v8']},
+ {title: 'Input latency',
+ categoryFilter: ['benchmark', 'input', 'evdev', 'renderer.scheduler',
+ 'sequence_manager', 'toplevel']},
+ {title: 'Rendering',
+ categoryFilter: ['blink', 'cc', 'gpu', 'toplevel', 'viz']},
+ {title: 'Javascript and rendering',
+ categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler',
+ 'sequence_manager', 'v8', 'toplevel', 'viz']},
+ {title: 'Frame Viewer',
+ categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler',
+ 'sequence_manager', 'v8', 'toplevel',
+ 'disabled-by-default-blink.invalidation',
+ 'disabled-by-default-cc.debug',
+ 'disabled-by-default-cc.debug.picture',
+ 'disabled-by-default-cc.debug.display_items']},
+ {title: 'Manually select settings',
+ categoryFilter: []}
+ ];
+ const RECORDING_MODES = [
+ {'label': 'Record until full',
+ 'value': 'record-until-full'},
+ {'label': 'Record continuously',
+ 'value': 'record-continuously'},
+ {'label': 'Record as much as possible',
+ 'value': 'record-as-much-as-possible'}];
+ const DEFAULT_RECORD_MODE = 'record-until-full';
+ const DEFAULT_CONTINUOUS_TRACING = true;
+ const DEFAULT_SYSTEM_TRACING = true;
+ const DEFAULT_SAMPLING_TRACING = false;
+
+ RecordSelectionDialog.prototype = {
+ __proto__: tr.ui.b.Overlay.prototype,
+
+ decorate() {
+ tr.ui.b.Overlay.prototype.decorate.call(this);
+ this.title = 'Record a new trace...';
+
+ Polymer.dom(this).classList.add('record-dialog-overlay');
+
+ const node =
+ tr.ui.b.instantiateTemplate('#record-selection-dialog-template',
+ THIS_DOC);
+ Polymer.dom(this).appendChild(node);
+
+ this.recordButtonEl_ = document.createElement('button');
+ Polymer.dom(this.recordButtonEl_).textContent = 'Record';
+ this.recordButtonEl_.addEventListener(
+ 'click',
+ this.onRecordButtonClicked_.bind(this));
+ this.recordButtonEl_.style.fontSize = '110%';
+ Polymer.dom(this.buttons).appendChild(this.recordButtonEl_);
+
+ this.categoriesView_ = Polymer.dom(this).querySelector(
+ '.categories-column-view');
+ this.presetsEl_ = Polymer.dom(this).querySelector('.category-presets');
+ Polymer.dom(this.presetsEl_).appendChild(tr.ui.b.createOptionGroup(
+ this, 'currentlyChosenPreset',
+ 'about_tracing.record_selection_dialog_preset',
+ DEFAULT_PRESETS[0].categoryFilter,
+ DEFAULT_PRESETS.map(function(p) {
+ return { label: p.title, value: p.categoryFilter };
+ })));
+
+ this.tracingRecordModeSltr_ = tr.ui.b.createSelector(
+ this, 'tracingRecordMode',
+ 'recordSelectionDialog.tracingRecordMode',
+ DEFAULT_RECORD_MODE, RECORDING_MODES);
+
+ this.systemTracingBn_ = tr.ui.b.createCheckBox(
+ undefined, undefined,
+ 'recordSelectionDialog.useSystemTracing', DEFAULT_SYSTEM_TRACING,
+ 'System tracing');
+ this.samplingTracingBn_ = tr.ui.b.createCheckBox(
+ undefined, undefined,
+ 'recordSelectionDialog.useSampling', DEFAULT_SAMPLING_TRACING,
+ 'State sampling');
+ this.tracingModesContainerEl_ = Polymer.dom(this).querySelector(
+ '.tracing-modes');
+ Polymer.dom(this.tracingModesContainerEl_).appendChild(
+ this.tracingRecordModeSltr_);
+ Polymer.dom(this.tracingModesContainerEl_).appendChild(
+ this.systemTracingBn_);
+ Polymer.dom(this.tracingModesContainerEl_).appendChild(
+ this.samplingTracingBn_);
+
+ this.enabledCategoriesContainerEl_ =
+ Polymer.dom(this).querySelector(
+ '.default-enabled-categories .categories');
+
+ this.disabledCategoriesContainerEl_ =
+ Polymer.dom(this).querySelector(
+ '.default-disabled-categories .categories');
+
+ this.createGroupSelectButtons_(
+ Polymer.dom(this).querySelector('.default-enabled-categories'));
+ this.createGroupSelectButtons_(
+ Polymer.dom(this).querySelector('.default-disabled-categories'));
+ this.createDefaultDisabledWarningDialog_(
+ Polymer.dom(this).querySelector(
+ '.warning-default-disabled-categories'));
+ this.editCategoriesOpened_ = false;
+
+ // TODO(chrishenry): When used with tr.ui.b.Overlay (such as in
+ // chrome://tracing, this does not yet look quite right due to
+ // the 10px overlay content padding (but it's good enough).
+ this.infoBarGroup_ = Polymer.dom(this).querySelector(
+ 'tr-ui-b-info-bar-group');
+
+ this.addEventListener('visible-change', this.onVisibleChange_.bind(this));
+ },
+
+ set supportsSystemTracing(s) {
+ if (s) {
+ this.systemTracingBn_.style.display = undefined;
+ } else {
+ this.systemTracingBn_.style.display = 'none';
+ this.useSystemTracing = false;
+ }
+ },
+
+ get tracingRecordMode() {
+ return this.tracingRecordModeSltr_.selectedValue;
+ },
+ set tracingRecordMode(value) {
+ this.tracingRecordMode_ = value;
+ },
+
+ get useSystemTracing() {
+ return this.systemTracingBn_.checked;
+ },
+ set useSystemTracing(value) {
+ this.systemTracingBn_.checked = !!value;
+ },
+
+ get useSampling() {
+ return this.samplingTracingBn_.checked;
+ },
+ set useSampling(value) {
+ this.samplingTracingBn_.checked = !!value;
+ },
+
+ set categories(c) {
+ if (!(c instanceof Array)) {
+ throw new Error('categories must be an array');
+ }
+ this.categories_ = c;
+
+ for (let i = 0; i < this.categories_.length; i++) {
+ const split = this.categories_[i].split(',');
+ this.categories_[i] = split.shift();
+ if (split.length > 0) {
+ this.categories_ = this.categories_.concat(split);
+ }
+ }
+ },
+
+ set settings_key(k) {
+ this.settings_key_ = k;
+ },
+
+ set settings(s) {
+ throw new Error('Dont use this!');
+ },
+
+ usingPreset_() {
+ return this.currentlyChosenPreset_.length > 0;
+ },
+
+ get currentlyChosenPreset() {
+ return this.currentlyChosenPreset_;
+ },
+
+ set currentlyChosenPreset(preset) {
+ if (!(preset instanceof Array)) {
+ throw new Error('RecordSelectionDialog.currentlyChosenPreset:' +
+ ' preset must be an array.');
+ }
+ this.currentlyChosenPreset_ = preset;
+
+ if (this.usingPreset_()) {
+ this.changeEditCategoriesState_(false);
+ } else {
+ this.updateCategoryColumnView_(true);
+ this.changeEditCategoriesState_(true);
+ }
+ this.updateManualSelectionView_();
+ this.updatePresetDescription_();
+ },
+
+ updateManualSelectionView_() {
+ const classList = Polymer.dom(this.categoriesView_).classList;
+ if (!this.usingPreset_()) {
+ classList.remove('categories-column-view-hidden');
+ } else {
+ if (this.editCategoriesOpened_) {
+ classList.remove('categories-column-view-hidden');
+ } else {
+ classList.add('categories-column-view-hidden');
+ }
+ }
+ },
+
+ updateCategoryColumnView_(shouldReadFromSettings) {
+ const categorySet = Polymer.dom(this).querySelectorAll('.categories');
+ for (let i = 0; i < categorySet.length; ++i) {
+ const categoryGroup = categorySet[i].children;
+ for (let j = 0; j < categoryGroup.length; ++j) {
+ const categoryEl = categoryGroup[j].children[0];
+ categoryEl.checked = shouldReadFromSettings ?
+ tr.b.Settings.get(categoryEl.value, false, this.settings_key_) :
+ false;
+ }
+ }
+ },
+
+ onClickEditCategories() {
+ if (!this.usingPreset_()) return;
+
+ if (!this.editCategoriesOpened_) {
+ this.updateCategoryColumnView_(false);
+ for (let i = 0; i < this.currentlyChosenPreset_.length; ++i) {
+ const categoryEl = this.querySelector('#' +
+ this.currentlyChosenPreset_[i]);
+ if (!categoryEl) continue;
+ categoryEl.checked = true;
+ }
+ }
+
+ this.changeEditCategoriesState_(!this.editCategoriesOpened_);
+ this.updateManualSelectionView_();
+ this.recordButtonEl_.focus();
+ },
+
+ changeEditCategoriesState_(editCategoriesState) {
+ const presetOptionsGroup = Polymer.dom(this).querySelector(
+ '.labeled-option-group');
+ if (!presetOptionsGroup) return;
+
+ this.editCategoriesOpened_ = editCategoriesState;
+ if (this.editCategoriesOpened_) {
+ Polymer.dom(presetOptionsGroup).classList.add('categories-expanded');
+ } else {
+ Polymer.dom(presetOptionsGroup).classList.remove(
+ 'categories-expanded');
+ }
+ },
+
+ updatePresetDescription_() {
+ const description = Polymer.dom(this).querySelector(
+ '.category-description');
+ if (this.usingPreset_()) {
+ description.innerText = this.currentlyChosenPreset_;
+ Polymer.dom(description).classList.remove(
+ 'category-description-hidden');
+ } else {
+ description.innerText = '';
+ if (!Polymer.dom(description).classList.contains(
+ 'category-description-hidden')) {
+ Polymer.dom(description).classList.add('category-description-hidden');
+ }
+ }
+ },
+
+ includedAndExcludedCategories() {
+ let includedCategories = [];
+ let excludedCategories = [];
+ if (this.usingPreset_()) {
+ const allCategories = this.allCategories_();
+ for (const category in allCategories) {
+ const disabledByDefault =
+ category.indexOf('disabled-by-default-') === 0;
+ if (this.currentlyChosenPreset_.indexOf(category) >= 0) {
+ if (disabledByDefault) {
+ includedCategories.push(category);
+ }
+ } else {
+ if (!disabledByDefault) {
+ excludedCategories.push(category);
+ }
+ }
+ }
+ return {
+ included: includedCategories,
+ excluded: excludedCategories
+ };
+ }
+
+ excludedCategories = this.unselectedCategories_();
+ includedCategories = this.enabledDisabledByDefaultCategories_();
+ return {
+ included: includedCategories,
+ excluded: excludedCategories
+ };
+ },
+
+ clickRecordButton() {
+ this.recordButtonEl_.click();
+ },
+
+ onRecordButtonClicked_() {
+ this.visible = false;
+ tr.b.dispatchSimpleEvent(this, 'recordclick');
+ return false;
+ },
+
+ collectInputs_(inputs, isChecked) {
+ const inputsLength = inputs.length;
+ const categories = [];
+ for (let i = 0; i < inputsLength; ++i) {
+ const input = inputs[i];
+ if (input.checked === isChecked) {
+ categories.push(input.value);
+ }
+ }
+ return categories;
+ },
+
+ unselectedCategories_() {
+ const inputs =
+ Polymer.dom(this.enabledCategoriesContainerEl_).querySelectorAll(
+ 'input');
+ return this.collectInputs_(inputs, false);
+ },
+
+ enabledDisabledByDefaultCategories_() {
+ const inputs =
+ Polymer.dom(this.disabledCategoriesContainerEl_).querySelectorAll(
+ 'input');
+ return this.collectInputs_(inputs, true);
+ },
+
+ onVisibleChange_() {
+ if (this.visible) {
+ this.updateForm_();
+ }
+ },
+
+ buildInputs_(inputs, checkedDefault, parent) {
+ const inputsLength = inputs.length;
+ for (let i = 0; i < inputsLength; i++) {
+ const category = inputs[i];
+
+ const inputEl = document.createElement('input');
+ inputEl.type = 'checkbox';
+ inputEl.id = category;
+ inputEl.value = category;
+
+ inputEl.checked = tr.b.Settings.get(
+ category, checkedDefault, this.settings_key_);
+ inputEl.onclick = this.updateSetting_.bind(this);
+
+ const labelEl = document.createElement('label');
+ Polymer.dom(labelEl).textContent =
+ category.replace('disabled-by-default-', '');
+ Polymer.dom(labelEl).setAttribute('for', category);
+
+ const divEl = document.createElement('div');
+ Polymer.dom(divEl).appendChild(inputEl);
+ Polymer.dom(divEl).appendChild(labelEl);
+
+ Polymer.dom(parent).appendChild(divEl);
+ }
+ },
+
+ allCategories_() {
+ // Dedup the categories. We may have things in settings that are also
+ // returned when we query the category list.
+ const categorySet = {};
+ const allCategories =
+ this.categories_.concat(tr.b.Settings.keys(this.settings_key_));
+ const allCategoriesLength = allCategories.length;
+ for (let i = 0; i < allCategoriesLength; ++i) {
+ categorySet[allCategories[i]] = true;
+ }
+ return categorySet;
+ },
+
+ updateForm_() {
+ function ignoreCaseCompare(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ }
+
+ // Clear old categories
+ Polymer.dom(this.enabledCategoriesContainerEl_).innerHTML = '';
+ Polymer.dom(this.disabledCategoriesContainerEl_).innerHTML = '';
+
+ this.recordButtonEl_.focus();
+
+ const allCategories = this.allCategories_();
+ let categories = [];
+ let disabledCategories = [];
+ for (const category in allCategories) {
+ if (category.indexOf('disabled-by-default-') === 0) {
+ disabledCategories.push(category);
+ } else {
+ categories.push(category);
+ }
+ }
+ disabledCategories = disabledCategories.sort(ignoreCaseCompare);
+ categories = categories.sort(ignoreCaseCompare);
+
+ if (this.categories_.length === 0) {
+ this.infoBarGroup_.addMessage(
+ 'No categories found; recording will use default categories.');
+ }
+
+ this.buildInputs_(categories, true, this.enabledCategoriesContainerEl_);
+
+ if (disabledCategories.length > 0) {
+ this.disabledCategoriesContainerEl_.hidden = false;
+ this.buildInputs_(disabledCategories, false,
+ this.disabledCategoriesContainerEl_);
+ }
+ },
+
+ updateSetting_(e) {
+ const checkbox = e.target;
+ tr.b.Settings.set(checkbox.value, checkbox.checked, this.settings_key_);
+
+ // Change the current record mode to 'Manually select settings' from
+ // preset mode if and only if currently user is in preset record mode
+ // and user selects/deselects any category in 'Edit Categories' mode.
+ if (this.usingPreset_()) {
+ this.currentlyChosenPreset_ = []; /* manually select settings */
+ const categoryEl = this.querySelector(
+ '#category-preset-Manually-select-settings');
+ categoryEl.checked = true;
+ const description = Polymer.dom(this).querySelector(
+ '.category-description');
+ description.innerText = '';
+ Polymer.dom(description).classList.add('category-description-hidden');
+ }
+ },
+
+ createGroupSelectButtons_(parent) {
+ const flipInputs = function(dir) {
+ const inputs = Polymer.dom(parent).querySelectorAll('input');
+ for (let i = 0; i < inputs.length; i++) {
+ if (inputs[i].checked === dir) continue;
+ // click() is used so the settings will be correclty stored. Setting
+ // checked does not trigger the onclick (or onchange) callback.
+ inputs[i].click();
+ }
+ };
+
+ const allBtn = Polymer.dom(parent).querySelector('.all-btn');
+ allBtn.onclick = function(evt) {
+ flipInputs(true);
+ evt.preventDefault();
+ };
+
+ const noneBtn = Polymer.dom(parent).querySelector('.none-btn');
+ noneBtn.onclick = function(evt) {
+ flipInputs(false);
+ evt.preventDefault();
+ };
+ },
+
+ setWarningDialogOverlayText_(messages) {
+ const contentDiv = document.createElement('div');
+
+ for (let i = 0; i < messages.length; ++i) {
+ const messageDiv = document.createElement('div');
+ Polymer.dom(messageDiv).textContent = messages[i];
+ Polymer.dom(contentDiv).appendChild(messageDiv);
+ }
+ Polymer.dom(this.warningOverlay_).textContent = '';
+ Polymer.dom(this.warningOverlay_).appendChild(contentDiv);
+ },
+
+ createDefaultDisabledWarningDialog_(warningLink) {
+ function onClickHandler(evt) {
+ this.warningOverlay_ = tr.ui.b.Overlay();
+ this.warningOverlay_.parentEl_ = this;
+ this.warningOverlay_.title = 'Warning...';
+ this.warningOverlay_.userCanClose = true;
+ this.warningOverlay_.visible = true;
+
+ this.setWarningDialogOverlayText_([
+ 'Enabling the default disabled categories may have',
+ 'performance and memory impact while tr.c.'
+ ]);
+
+ evt.preventDefault();
+ }
+ warningLink.onclick = onClickHandler.bind(this);
+ }
+ };
+
+ return {
+ RecordSelectionDialog,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html
new file mode 100644
index 00000000000..7c62b487305
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html
@@ -0,0 +1,426 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantitate', function() {
+ const showButton = document.createElement('button');
+ Polymer.dom(showButton).textContent = 'Show record selection dialog';
+ this.addHTMLOutput(showButton);
+
+ showButton.addEventListener('click', function(e) {
+ e.stopPropagation();
+
+ const categories = [];
+ for (let i = 0; i < 30; i++) {
+ categories.push('cat-' + i);
+ }
+ for (let i = 0; i < 20; i++) {
+ categories.push('disabled-by-default-cat-' + i);
+ }
+ categories.push(
+ 'really-really-really-really-really-really-very-loong-cat');
+ categories.push('first,second,third');
+ categories.push('cc,disabled-by-default-cc.debug');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = categories;
+ dlg.settings_key = 'key';
+ dlg.visible = true;
+ });
+ });
+
+ test('recordSelectionDialog_splitCategories', function() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories =
+ ['cc,disabled-by-default-one,cc.debug', 'two,three', 'three'];
+ dlg.settings_key = 'key';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ const expected =
+ ['"cc"', '"cc.debug"', '"disabled-by-default-one"', '"three"', '"two"'];
+
+ const labels = Polymer.dom(dlg).querySelectorAll('.categories input');
+ let results = [];
+ for (let i = 0; i < labels.length; i++) {
+ results.push('"' + labels[i].value + '"');
+ }
+ results = results.sort();
+
+ assert.deepEqual(results, expected);
+ });
+
+ test('recordSelectionDialog_UpdateForm_NoSettings', function() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one', 'two', 'three'];
+ dlg.settings_key = 'key';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input');
+ assert.strictEqual(checkboxes.length, 3);
+ assert.strictEqual(checkboxes[0].id, 'three');
+ assert.strictEqual(checkboxes[0].value, 'three');
+ assert.isTrue(checkboxes[0].checked);
+ assert.strictEqual(checkboxes[1].id, 'two');
+ assert.strictEqual(checkboxes[1].value, 'two');
+ assert.isTrue(checkboxes[1].checked);
+ assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one');
+ assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one');
+ assert.isFalse(checkboxes[2].checked);
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+
+ const labels = Polymer.dom(dlg).querySelectorAll('.categories label');
+ assert.strictEqual(labels.length, 3);
+ assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three');
+ assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two');
+ assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one');
+ });
+
+ test('recordSelectionDialog_UpdateForm_Settings', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input');
+ assert.strictEqual(checkboxes.length, 3);
+ assert.strictEqual(checkboxes[0].id, 'three');
+ assert.strictEqual(checkboxes[0].value, 'three');
+ assert.isFalse(checkboxes[0].checked);
+ assert.strictEqual(checkboxes[1].id, 'two');
+ assert.strictEqual(checkboxes[1].value, 'two');
+ assert.isTrue(checkboxes[1].checked);
+ assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one');
+ assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one');
+ assert.isFalse(checkboxes[2].checked);
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']);
+
+ const labels = Polymer.dom(dlg).querySelectorAll('.categories label');
+ assert.strictEqual(labels.length, 3);
+ assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three');
+ assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two');
+ assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one');
+ });
+
+ test('recordSelectionDialog_UpdateForm_DisabledByDefault', function() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-bar', 'baz'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+
+ const inputs =
+ Polymer.dom(dlg).querySelector('input#disabled-by-default-bar').click();
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-bar']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+
+ assert.isFalse(
+ tr.b.Settings.get('disabled-by-default-foo', false, 'categories'));
+ });
+
+ test('selectAll', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+ });
+
+ test('selectNone', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ // Enables the three option, two already enabled.
+ Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isTrue(tr.b.Settings.get('three', false, 'categories'));
+
+ // Disables three and two.
+ Polymer.dom(dlg).querySelector('.default-enabled-categories .none-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+ assert.isFalse(tr.b.Settings.get('two', false, 'categories'));
+ assert.isFalse(tr.b.Settings.get('three', false, 'categories'));
+
+ // Turn categories back on so they can be ignored.
+ Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn')
+ .click();
+
+ // Enables disabled category.
+ Polymer.dom(dlg).querySelector('.default-disabled-categories .all-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-one']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isTrue(
+ tr.b.Settings.get('disabled-by-default-one', false, 'categories'));
+
+ // Turn disabled by default back off.
+ Polymer.dom(dlg).querySelector('.default-disabled-categories .none-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isFalse(
+ tr.b.Settings.get('disabled-by-default-one', false, 'categories'));
+ });
+
+ test('recordSelectionDialog_noPreset', function() {
+ tr.b.Settings.set('about_tracing.record_selection_dialog_preset', []);
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ assert.isFalse(dlg.usingPreset_());
+ });
+
+ test('recordSelectionDialog_defaultPreset', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ // Note: currentlyChosenPreset is not set here, so the default is used.
+ dlg.updateForm_();
+
+ // Make sure the default filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+
+ // Make sure the default tracing types are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-until-full');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isFalse(dlg.useSampling);
+
+ // Make sure the manual settings are not visible.
+ const classList = Polymer.dom(dlg.categoriesView_).classList;
+ assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+ // Verify manual settings do not modify the checkboxes.
+ const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input');
+ assert.strictEqual(checkboxes.length, 3);
+ assert.strictEqual(checkboxes[0].id, 'three');
+ assert.strictEqual(checkboxes[0].value, 'three');
+ assert.isFalse(checkboxes[0].checked);
+ assert.strictEqual(checkboxes[1].id, 'two');
+ assert.strictEqual(checkboxes[1].value, 'two');
+ assert.isTrue(checkboxes[1].checked);
+ assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one');
+ assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one');
+ assert.isFalse(checkboxes[2].checked);
+ });
+
+ test('recordSelectionDialog_editPreset', function() {
+ function createDialog() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['one', 'two', 'disabled-by-default-three'];
+ dlg.settings_key = 'categories';
+ // Note: currentlyChosenPreset is not set here, so the default is used.
+ dlg.updateForm_();
+ return dlg;
+ }
+
+ // After the dialog is created, it should be using the default preset.
+ let dlg = createDialog();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['one', 'two']);
+ assert.isTrue(dlg.usingPreset_());
+ assert.isFalse(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+
+ // After clicking on "Edit Categories", the default preset should still be
+ // used.
+ dlg.onClickEditCategories();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['one', 'two']);
+ assert.isTrue(dlg.usingPreset_());
+ assert.isFalse(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+
+ // After clicking on category checkbox(es), the mode should be changed to
+ // "Manually select settings".
+ Array.prototype.forEach.call(dlg.querySelectorAll('.categories input'),
+ checkbox => checkbox.click());
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-three']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isFalse(dlg.usingPreset_());
+ assert.isTrue(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+
+ // After the dialog is opened again, it should be using the default preset.
+ // More importantly, the default preset should NOT be modified.
+ dlg = createDialog();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['one', 'two']);
+ assert.isTrue(dlg.usingPreset_());
+ assert.isFalse(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+ });
+
+ test('recordSelectionDialog_changePresets', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+ tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories');
+ tr.b.Settings.set('recordSelectionDialog.tracingRecordMode',
+ 'record-as-much-as-possible');
+ tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true);
+ tr.b.Settings.set('recordSelectionDialog.useSampling', false);
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ // Note: currentlyChosenPreset is not set here, so the default is used.
+ dlg.updateForm_();
+
+ // Preset mode is on.
+ assert.isTrue(dlg.usingPreset_());
+
+ // Make sure the default filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+
+ // Make sure the default tracing types are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isFalse(dlg.useSampling);
+
+ // Make sure the manual settings are not visible.
+ const classList = Polymer.dom(dlg.categoriesView_).classList;
+ assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+ // Switch to manual settings and verify the default values are not returned.
+ dlg.currentlyChosenPreset = [];
+
+ // Preset mode is off.
+ assert.isFalse(dlg.usingPreset_());
+
+ // Make sure the default filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']);
+
+ // Make sure the tracing types set by catalog are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isFalse(dlg.useSampling);
+ assert.isFalse(classList.contains('categories-column-view-hidden'));
+
+ // Switch to the graphics, rendering, and rasterization preset.
+ dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer',
+ 'disabled-by-default-cc.debug'];
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+ });
+
+ test('recordSelectionDialog_savedPreset', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+ tr.b.Settings.set('recordSelectionDialog.tracingRecordMode',
+ 'record-continuously');
+ tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true);
+ tr.b.Settings.set('recordSelectionDialog.useSampling', true);
+ tr.b.Settings.set('tr.ui.e.about_tracing.record_selection_dialog_preset',
+ ['blink', 'cc', 'renderer', 'cc.debug']);
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.updateForm_();
+
+ // Make sure the correct filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+
+ // Make sure the correct tracing types are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-continuously');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isTrue(dlg.useSampling);
+
+ // Make sure the manual settings are not visible.
+ const classList = Polymer.dom(dlg.categoriesView_).classList;
+ assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+ // Switch to manual settings and verify the default values are not returned.
+ dlg.currentlyChosenPreset = [];
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']);
+ assert.strictEqual(dlg.tracingRecordMode, 'record-continuously');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isTrue(dlg.useSampling);
+ assert.isFalse(classList.contains('categories-column-view-hidden'));
+ });
+
+ test('recordSelectionDialog_categoryFilters', function() {
+ tr.b.Settings.set('default1', true, 'categories');
+ tr.b.Settings.set('disabled1', false, 'categories');
+ tr.b.Settings.set('disabled-by-default-cc.disabled2', false, 'categories');
+ tr.b.Settings.set('input', true, 'categories');
+ tr.b.Settings.set('blink', true, 'categories');
+ tr.b.Settings.set('cc', false, 'categories');
+ tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.settings_key = 'categories';
+ dlg.categories = [];
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['cc', 'disabled1']);
+
+ // Switch to the graphics, rendering, and rasterization preset.
+ dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer',
+ 'disabled-by-default-cc.debug'];
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['default1', 'disabled1', 'input']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html
new file mode 100644
index 00000000000..c00bbe915e4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ /**
+ * Communicates with content/browser/tracing_controller_impl.cc
+ *
+ * @constructor
+ */
+ class TracingControllerClient {
+ beginMonitoring(monitoringOptions) { }
+ endMonitoring() { }
+ captureMonitoring() { }
+ getMonitoringStatus() { }
+ getCategories() { }
+ beginRecording(recordingOptions) { }
+ beginGetBufferPercentFull() { }
+ endRecording() { }
+ defaultTraceName() { }
+ }
+
+ return {
+ TracingControllerClient,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html
new file mode 100644
index 00000000000..d2c6adcac2a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ const Base64 = tr.b.Base64;
+
+ function beginXhr(method, path, data) {
+ if (data === undefined) data = null;
+
+ return new Promise(function(resolve, reject) {
+ const req = new XMLHttpRequest();
+ if (method !== 'POST' && data !== null) {
+ throw new Error('Non-POST should have data==null');
+ }
+ req.open(method, path, true);
+ req.onreadystatechange = function(e) {
+ if (req.readyState === 4) {
+ window.setTimeout(function() {
+ if (req.status === 200 && req.responseText !== '##ERROR##') {
+ resolve(req.responseText);
+ } else {
+ reject(new Error('Error occured at ' + path));
+ }
+ }, 0);
+ }
+ };
+ req.send(data);
+ });
+ }
+
+ /**
+ * @constructor
+ */
+ function XhrBasedTracingControllerClient() { }
+
+ XhrBasedTracingControllerClient.prototype = {
+ __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype,
+
+ beginMonitoring(monitoringOptions) {
+ const monitoringOptionsB64 = Base64.btoa(JSON.stringify(
+ monitoringOptions));
+ return beginXhr('GET', '/json/begin_monitoring?' + monitoringOptionsB64);
+ },
+
+ endMonitoring() {
+ return beginXhr('GET', '/json/end_monitoring');
+ },
+
+ captureMonitoring() {
+ return beginXhr('GET', '/json/capture_monitoring_compressed').then(
+ function(data) {
+ const decodedSize = Base64.getDecodedBufferLength(data);
+ const buffer = new ArrayBuffer(decodedSize);
+ Base64.DecodeToTypedArray(data, new DataView(buffer));
+ return buffer;
+ }
+ );
+ },
+
+ getMonitoringStatus() {
+ return beginXhr('GET', '/json/get_monitoring_status').then(
+ function(monitoringOptionsB64) {
+ return JSON.parse(Base64.atob(monitoringOptionsB64));
+ });
+ },
+
+ getCategories() {
+ return beginXhr('GET', '/json/categories').then(
+ function(json) {
+ return JSON.parse(json);
+ });
+ },
+
+ beginRecording(recordingOptions) {
+ const recordingOptionsB64 = Base64.btoa(JSON.stringify(recordingOptions));
+ return beginXhr('GET', '/json/begin_recording?' +
+ recordingOptionsB64);
+ },
+
+ beginGetBufferPercentFull() {
+ return beginXhr('GET', '/json/get_buffer_percent_full');
+ },
+
+ endRecording() {
+ return beginXhr('GET', '/json/end_recording_compressed').then(
+ function(data) {
+ const decodedSize = Base64.getDecodedBufferLength(data);
+ const buffer = new ArrayBuffer(decodedSize);
+ Base64.DecodeToTypedArray(data, new DataView(buffer));
+ return buffer;
+ }
+ );
+ },
+
+ defaultTraceName() {
+ return 'trace.json.gz';
+ }
+ };
+
+ return {
+ XhrBasedTracingControllerClient,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html
new file mode 100644
index 00000000000..79ba7e593c0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/tile_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html
new file mode 100644
index 00000000000..f8bfd671355
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_item.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html">
+
+<template id="tr-ui-e-chrome-cc-display-item-debugger-template">
+ <left-panel>
+ <display-item-info>
+ <header>
+ <span class='title'>Display Item List</span>
+ <span class='size'></span>
+ <div class='export'>
+ <input class='dlfilename' type='text' value='displayitemlist.json' />
+ <button class='dlexport'>Export display item list</button>
+ </div>
+ <div class='export'>
+ <input class='skpfilename' type='text' value='skpicture.skp' />
+ <button class='skpexport'>Export list as SkPicture</button>
+ </div>
+ </header>
+ </display-item-info>
+ </left-panel>
+ <right-panel>
+ <raster-area>
+ <canvas-scroller>
+ <canvas></canvas>
+ </canvas-scroller>
+ </raster-area>
+ </right-panel>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ /**
+ * DisplayItemDebugger is a view of a DisplayItemListSnapshot for inspecting
+ * a display item list and the pictures within it.
+ *
+ * @constructor
+ */
+ const DisplayItemDebugger = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-display-item-debugger');
+
+ DisplayItemDebugger.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ const node = tr.ui.b.instantiateTemplate(
+ '#tr-ui-e-chrome-cc-display-item-debugger-template', THIS_DOC);
+
+ Polymer.dom(this).appendChild(node);
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.display = 'flex';
+ this.style.minWidth = 0;
+
+ this.pictureAsImageData_ = undefined;
+ this.zoomScaleValue_ = 1;
+
+ this.sizeInfo_ = Polymer.dom(this).querySelector('.size');
+ this.rasterArea_ = Polymer.dom(this).querySelector('raster-area');
+ this.rasterArea_.style.flexGrow = 1;
+ this.rasterArea_.style.flexShrink = 1;
+ this.rasterArea_.style.flexBasis = 'auto';
+ this.rasterArea_.style.backgroundColor = '#ddd';
+ this.rasterArea_.style.minHeight = '200px';
+ this.rasterArea_.style.minWidth = '200px';
+ this.rasterArea_.style.paddingLeft = '5px';
+ this.rasterArea_.style.display = 'flex';
+ this.rasterArea_.style.flexDirection = 'column';
+ this.rasterCanvas_ =
+ Polymer.dom(this.rasterArea_).querySelector('canvas');
+ this.rasterCtx_ = this.rasterCanvas_.getContext('2d');
+
+ const canvasScroller = Polymer.dom(this).querySelector('canvas-scroller');
+ canvasScroller.style.flexGrow = 1;
+ canvasScroller.style.flexShrink = 1;
+ canvasScroller.style.flexBasis = 'auto';
+ canvasScroller.style.minWidth = 0;
+ canvasScroller.style.minHeight = 0;
+ canvasScroller.style.overflow = 'auto';
+
+ this.trackMouse_();
+
+ this.displayItemInfo_ =
+ Polymer.dom(this).querySelector('display-item-info');
+ this.displayItemInfo_.addEventListener(
+ 'click', this.onDisplayItemInfoClick_.bind(this), false);
+
+ this.displayItemListView_ = new tr.ui.b.ListView();
+ this.displayItemListView_.addEventListener('selection-changed',
+ this.onDisplayItemListSelection_.bind(this));
+ Polymer.dom(this.displayItemInfo_).appendChild(this.displayItemListView_);
+
+ this.displayListFilename_ =
+ Polymer.dom(this).querySelector('.dlfilename');
+ this.displayListExportButton_ =
+ Polymer.dom(this).querySelector('.dlexport');
+ this.displayListExportButton_.addEventListener(
+ 'click', this.onExportDisplayListClicked_.bind(this));
+
+ this.skpFilename_ = Polymer.dom(this).querySelector('.skpfilename');
+ this.skpExportButton_ = Polymer.dom(this).querySelector('.skpexport');
+ this.skpExportButton_.addEventListener(
+ 'click', this.onExportSkPictureClicked_.bind(this));
+
+ const leftPanel = Polymer.dom(this).querySelector('left-panel');
+ leftPanel.style.flexGrow = 0;
+ leftPanel.style.flexShrink = 0;
+ leftPanel.style.flexBasis = 'auto';
+ leftPanel.style.minWidth = '200px';
+ leftPanel.style.overflow = 'auto';
+
+ leftPanel.children[0].paddingTop = '2px';
+ leftPanel.children[0].children[0].style.borderBottom = '1px solid #555';
+
+ const leftPanelTitle = leftPanel.querySelector('.title');
+ leftPanelTitle.style.fontWeight = 'bold';
+ leftPanelTitle.style.marginLeft = '5px';
+ leftPanelTitle.style.marginright = '5px';
+
+ for (const div of leftPanel.querySelectorAll('.export')) {
+ div.style.margin = '5px';
+ }
+
+ const middleDragHandle = document.createElement('tr-ui-b-drag-handle');
+ middleDragHandle.style.flexGrow = 0;
+ middleDragHandle.style.flexShrink = 0;
+ middleDragHandle.style.flexBasis = 'auto';
+ middleDragHandle.horizontal = false;
+ middleDragHandle.target = leftPanel;
+
+ const rightPanel = Polymer.dom(this).querySelector('right-panel');
+ rightPanel.style.display = 'flex';
+ rightPanel.style.flexGrow = 1;
+ rightPanel.style.flexShrink = 1;
+ rightPanel.style.flexBasis = 'auto';
+ rightPanel.style.minWidth = 0;
+
+ this.infoBar_ = document.createElement('tr-ui-b-info-bar');
+ Polymer.dom(this.rasterArea_).insertBefore(this.infoBar_, canvasScroller);
+
+ Polymer.dom(this).insertBefore(middleDragHandle, rightPanel);
+
+ this.picture_ = undefined;
+
+ this.pictureOpsListView_ = new tr.ui.e.chrome.cc.PictureOpsListView();
+ this.pictureOpsListView_.style.flexGrow = 0;
+ this.pictureOpsListView_.style.flexShrink = 0;
+ this.pictureOpsListView_.style.flexBasis = 'auto';
+ this.pictureOpsListView_.style.overflow = 'auto';
+ this.pictureOpsListView_.style.minWidth = '100px';
+ Polymer.dom(rightPanel).insertBefore(
+ this.pictureOpsListView_, this.rasterArea_);
+
+ this.pictureOpsListDragHandle_ =
+ document.createElement('tr-ui-b-drag-handle');
+ this.pictureOpsListDragHandle_.horizontal = false;
+ this.pictureOpsListDragHandle_.target = this.pictureOpsListView_;
+ Polymer.dom(rightPanel).insertBefore(
+ this.pictureOpsListDragHandle_, this.rasterArea_);
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set displayItemList(displayItemList) {
+ this.displayItemList_ = displayItemList;
+ this.picture = this.displayItemList_;
+
+ this.displayItemListView_.clear();
+ this.displayItemList_.items.forEach(function(item) {
+ const listItem = document.createElement(
+ 'tr-ui-e-chrome-cc-display-item-list-item');
+ listItem.data = item;
+ Polymer.dom(this.displayItemListView_).appendChild(listItem);
+ }.bind(this));
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+
+ // Hide the ops list if we are showing the "main" display item list.
+ const showOpsList = picture && picture !== this.displayItemList_;
+ this.updateDrawOpsList_(showOpsList);
+
+ if (picture) {
+ const size = this.getRasterCanvasSize_();
+ this.rasterCanvas_.width = size.width;
+ this.rasterCanvas_.height = size.height;
+ }
+
+ const bounds = this.rasterArea_.getBoundingClientRect();
+ const selectorBounds = this.mouseModeSelector_.getBoundingClientRect();
+ this.mouseModeSelector_.pos = {
+ x: (bounds.right - selectorBounds.width - 10),
+ y: bounds.top
+ };
+
+ this.rasterize_();
+
+ this.scheduleUpdateContents_();
+ },
+
+ getRasterCanvasSize_() {
+ const style = window.getComputedStyle(this.rasterArea_);
+ let width = parseInt(style.width);
+ let height = parseInt(style.height);
+ if (this.picture_) {
+ width = Math.max(width, this.picture_.layerRect.width);
+ height = Math.max(height, this.picture_.layerRect.height);
+ }
+
+ return {
+ width,
+ height
+ };
+ },
+
+ scheduleUpdateContents_() {
+ if (this.updateContentsPending_) return;
+
+ this.updateContentsPending_ = true;
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContents_.bind(this)
+ );
+ },
+
+ updateContents_() {
+ this.updateContentsPending_ = false;
+
+ if (this.picture_) {
+ Polymer.dom(this.sizeInfo_).textContent = '(' +
+ this.picture_.layerRect.width + ' x ' +
+ this.picture_.layerRect.height + ')';
+ }
+
+ // Return if picture hasn't finished rasterizing.
+ if (!this.pictureAsImageData_) return;
+
+ this.infoBar_.visible = false;
+ this.infoBar_.removeAllButtons();
+ if (this.pictureAsImageData_.error) {
+ this.infoBar_.message = 'Cannot rasterize...';
+ this.infoBar_.addButton('More info...', function(e) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = this.pictureAsImageData_.error;
+ overlay.visible = true;
+ e.stopPropagation();
+ return false;
+ }.bind(this));
+ this.infoBar_.visible = true;
+ }
+
+ this.drawPicture_();
+ },
+
+ drawPicture_() {
+ const size = this.getRasterCanvasSize_();
+ if (size.width !== this.rasterCanvas_.width) {
+ this.rasterCanvas_.width = size.width;
+ }
+ if (size.height !== this.rasterCanvas_.height) {
+ this.rasterCanvas_.height = size.height;
+ }
+
+ this.rasterCtx_.clearRect(0, 0, size.width, size.height);
+
+ if (!this.picture_ || !this.pictureAsImageData_.imageData) return;
+
+ const imgCanvas = this.pictureAsImageData_.asCanvas();
+ const w = imgCanvas.width;
+ const h = imgCanvas.height;
+ this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h,
+ 0, 0, w * this.zoomScaleValue_,
+ h * this.zoomScaleValue_);
+ },
+
+ rasterize_() {
+ if (this.picture_) {
+ this.picture_.rasterize(
+ {
+ showOverdraw: false
+ },
+ this.onRasterComplete_.bind(this));
+ }
+ },
+
+ onRasterComplete_(pictureAsImageData) {
+ this.pictureAsImageData_ = pictureAsImageData;
+ this.scheduleUpdateContents_();
+ },
+
+ onDisplayItemListSelection_(e) {
+ const selected = this.displayItemListView_.selectedElement;
+
+ if (!selected) {
+ this.picture = this.displayItemList_;
+ return;
+ }
+
+ const index = Array.prototype.indexOf.call(
+ this.displayItemListView_.children, selected);
+ const displayItem = this.displayItemList_.items[index];
+ if (displayItem && displayItem.skp64) {
+ this.picture = new tr.e.cc.Picture(
+ displayItem.skp64, this.displayItemList_.layerRect);
+ } else {
+ this.picture = undefined;
+ }
+ },
+
+ onDisplayItemInfoClick_(e) {
+ if (e && e.target === this.displayItemInfo_) {
+ this.displayItemListView_.selectedElement = undefined;
+ }
+ },
+
+ updateDrawOpsList_(showOpsList) {
+ if (showOpsList) {
+ this.pictureOpsListView_.picture = this.picture_;
+ if (this.pictureOpsListView_.numOps > 0) {
+ this.pictureOpsListView_.style.display = 'block';
+ this.pictureOpsListDragHandle_.style.display = 'block';
+ }
+ } else {
+ this.pictureOpsListView_.style.display = 'none';
+ this.pictureOpsListDragHandle_.style.display = 'none';
+ }
+ },
+
+ trackMouse_() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this.rasterArea_;
+ Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_);
+
+ this.mouseModeSelector_.supportedModeMask =
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector';
+
+ this.mouseModeSelector_.addEventListener('beginzoom',
+ this.onBeginZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatezoom',
+ this.onUpdateZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('endzoom',
+ this.onEndZoom_.bind(this));
+ },
+
+ onBeginZoom_(e) {
+ this.isZooming_ = true;
+
+ this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+
+ e.preventDefault();
+ },
+
+ onUpdateZoom_(e) {
+ if (!this.isZooming_) return;
+
+ const currentMouseViewPos = this.extractRelativeMousePosition_(e);
+
+ // Take the distance the mouse has moved and we want to zoom at about
+ // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned
+ // more if people feel it's too slow.
+ this.zoomScaleValue_ +=
+ ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001);
+ this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1);
+
+ this.drawPicture_();
+
+ this.lastMouseViewPos_ = currentMouseViewPos;
+ },
+
+ onEndZoom_(e) {
+ this.lastMouseViewPos_ = undefined;
+ this.isZooming_ = false;
+ e.preventDefault();
+ },
+
+ extractRelativeMousePosition_(e) {
+ return {
+ x: e.clientX - this.rasterArea_.offsetLeft,
+ y: e.clientY - this.rasterArea_.offsetTop
+ };
+ },
+
+ saveFile_(filename, rawData) {
+ if (!rawData) return;
+
+ // Convert this String into an Uint8Array
+ const length = rawData.length;
+ const arrayBuffer = new ArrayBuffer(length);
+ const uint8Array = new Uint8Array(arrayBuffer);
+ for (let c = 0; c < length; c++) {
+ uint8Array[c] = rawData.charCodeAt(c);
+ }
+
+ // Create a blob URL from the binary array.
+ const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
+ const blobUrl = window.URL.createObjectURL(blob);
+
+ // Create a link and click on it.
+ const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+ link.href = blobUrl;
+ link.download = filename;
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(
+ 'click', true, false, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ link.dispatchEvent(event);
+ },
+
+ onExportDisplayListClicked_() {
+ const rawData = JSON.stringify(this.displayItemList_.items);
+ this.saveFile_(this.displayListFilename_.value, rawData);
+ },
+
+ onExportSkPictureClicked_() {
+ const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData());
+ this.saveFile_(this.skpFilename_.value, rawData);
+ }
+ };
+
+ return {
+ DisplayItemDebugger,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html
new file mode 100644
index 00000000000..c10d6995db3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const displayItemList = new tr.e.cc.DisplayItemListSnapshot(
+ {id: '31415'},
+ 10,
+ {
+ 'params': {
+ 'layer_rect': [-15, -15, 46, 833],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': '[another skia picture in base64]'});
+ displayItemList.preInitialize();
+ displayItemList.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.addHTMLOutput(dbg);
+ assert.isUndefined(dbg.displayItemList_);
+ assert.isUndefined(dbg.picture_);
+ dbg.displayItemList = displayItemList;
+ assert.isDefined(dbg.displayItemList_);
+ assert.isDefined(dbg.picture_);
+ assert.strictEqual(dbg.displayItemList_.items.length, 2);
+ dbg.style.border = '1px solid black';
+ });
+
+ test('selections', function() {
+ const displayItemList = new tr.e.cc.DisplayItemListSnapshot(
+ {id: '31415'},
+ 10,
+ {
+ 'params': {
+ 'layer_rect': [-15, -15, 46, 833],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'TransformDisplayItem',
+ {
+ 'name': 'DrawingDisplayItem',
+ 'skp64': '[skia picture in base64]',
+ },
+ 'EndTransformDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': '[another skia picture in base64]'});
+ displayItemList.preInitialize();
+ displayItemList.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.addHTMLOutput(dbg);
+ dbg.displayItemList = displayItemList;
+ assert.isDefined(dbg.displayItemList_);
+ assert.isDefined(dbg.picture_);
+ assert.strictEqual(dbg.displayItemList_.items.length, 5);
+
+ const initialPicture = dbg.picture_;
+ assert.isAbove(initialPicture.guid, 0);
+
+ // Select the drawing display item and make sure the picture updates.
+ const listView = dbg.displayItemListView_;
+ listView.selectedElement = listView.getElementByIndex(3);
+ let updatedPicture = dbg.picture_;
+ assert.isAbove(updatedPicture.guid, 0);
+ assert.notEqual(initialPicture.guid, updatedPicture.guid);
+
+ // Select the TransformDisplayItem and make sure the picture is blank.
+ listView.selectedElement = listView.getElementByIndex(2);
+ assert.isUndefined(dbg.picture_);
+
+ // Deselect a list item and make sure the picture is reset to the original.
+ listView.selectedElement = undefined;
+ updatedPicture = dbg.picture_;
+ assert.isAbove(updatedPicture.guid, 0);
+ assert.strictEqual(initialPicture.guid, updatedPicture.guid);
+
+ dbg.style.border = '1px solid black';
+ });
+
+ test('export', function() {
+ const displayItemList = new tr.e.cc.DisplayItemListSnapshot(
+ {id: '31415'},
+ 10,
+ {
+ 'params': {
+ 'layer_rect': [-15, -15, 46, 833],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': 'c2twaWN0dXJl'});
+ displayItemList.preInitialize();
+ displayItemList.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.addHTMLOutput(dbg);
+ dbg.displayItemList = displayItemList;
+
+ let onSaveDisplayListCalled = false;
+ dbg.saveFile_ = function(filename, rawData) {
+ onSaveDisplayListCalled = true;
+ assert.strictEqual(filename, 'displayitemlist.json');
+ assert.strictEqual(
+ rawData, '["BeginClipDisplayItem","EndClipDisplayItem"]');
+ };
+ dbg.onExportDisplayListClicked_();
+ assert(onSaveDisplayListCalled);
+
+ let onSaveSkPictureCalled = false;
+ dbg.saveFile_ = function(filename, rawData) {
+ onSaveSkPictureCalled = true;
+ assert.strictEqual(filename, 'skpicture.skp');
+ assert.strictEqual(rawData, 'skpicture');
+ };
+ dbg.onExportSkPictureClicked_();
+ assert(onSaveSkPictureCalled);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html
new file mode 100644
index 00000000000..3024e8d2d22
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<!--
+An element displaying basic information about a display item in a list view.
+-->
+<dom-module id='tr-ui-e-chrome-cc-display-item-list-item'>
+ <template>
+ <style>
+ :host {
+ border-bottom: 1px solid #555;
+ display: block;
+ font-size: 12px;
+ padding: 3px 5px;
+ }
+
+ :host(:hover) {
+ background-color: #f0f0f0;
+ cursor: pointer;
+ }
+
+ .header {
+ font-weight: bold;
+ margin: 2px 0;
+ }
+
+ .header > .extra {
+ background-color: #777;
+ border-radius: 4px;
+ color: white;
+ margin: 0 6px;
+ text-decoration: none;
+ padding: 2px 4px;
+ }
+
+ .raw-details {
+ white-space: pre-wrap;
+ }
+
+ .details > dl {
+ margin: 0;
+ }
+
+ :host(:not([selected])) .details {
+ display: none;
+ }
+ </style>
+ <div class="header">
+ {{name}}
+ <template is="dom-if" if="{{_computeIfSKP(richDetails)}}">
+ <a class="extra" href$="{{_computeHref(richDetails)}}"
+ download="drawing.skp" on-click="{{stopPropagation}}">SKP</a>
+ </template>
+ </div>
+ <div class="details">
+ <template is="dom-if" if="{{rawDetails}}">
+ <div class="raw-details">{{rawDetails}}</div>
+ </template>
+ <template is="dom-if" if="{{richDetails}}">
+ <dl>
+ <template is="dom-if" if="{{richDetails.visualRect}}">
+ <dt>Visual rect</dt>
+ <dd>{{richDetails.visualRect.x}},{{richDetails.visualRect.y}}
+ {{richDetails.visualRect.width}}&times;{{richDetails.visualRect.height}}
+ </dd>
+ </template>
+ </dl>
+ </template>
+ </div>
+ </template>
+<script>
+'use strict';
+(function() {
+ // Extracts the "type" and "details" parts of the unstructured (plaintext)
+ // display item format, even if the details span multiple lines.
+ // For example, given "FooDisplayItem type=hello\nworld", produces
+ // "FooDisplayItem" as the first capture and "type=hello\nworld" as the
+ // second. Either capture could be the empty string, but this regex will
+ // still successfully match.
+ const DETAILS_SPLIT_REGEX = /^(\S*)\s*([\S\s]*)$/;
+
+ Polymer({
+ is: 'tr-ui-e-chrome-cc-display-item-list-item',
+
+ created() {
+ // TODO(charliea): Why is setAttribute necessary here but not below? We
+ // should reach out to the Polymer team to figure out.
+ Polymer.dom(this).setAttribute('name', '');
+ Polymer.dom(this).setAttribute('rawDetails', '');
+ Polymer.dom(this).setAttribute('richDetails', undefined);
+ Polymer.dom(this).setAttribute('data_', undefined);
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ set data(data) {
+ this.data_ = data;
+
+ if (!data) {
+ this.name = 'DATA MISSING';
+ this.rawDetails = '';
+ this.richDetails = undefined;
+ } else if (typeof data === 'string') {
+ const match = data.match(DETAILS_SPLIT_REGEX);
+ this.name = match[1];
+ this.rawDetails = match[2];
+ this.richDetails = undefined;
+ } else {
+ this.name = data.name;
+ this.rawDetails = '';
+ this.richDetails = data;
+ }
+ },
+
+ stopPropagation(e) {
+ e.stopPropagation();
+ },
+
+ _computeIfSKP(richDetails) {
+ return richDetails && richDetails.skp64;
+ },
+
+ _computeHref(richDetails) {
+ return 'data:application/octet-stream;base64,' + richDetails.skp64;
+ }
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html
new file mode 100644
index 00000000000..97598aaf3a7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a display item snapshot in a human readable form.
+ * @constructor
+ */
+ const DisplayItemSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-display-item-list-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ DisplayItemSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ this.style.display = 'flex';
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.minWidth = 0;
+ this.displayItemDebugger_ = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.displayItemDebugger_.style.flexGrow = 1;
+ this.displayItemDebugger_.style.flexShrink = 1;
+ this.displayItemDebugger_.style.flexBasis = 'auto';
+ this.displayItemDebugger_.style.minWidth = 0;
+ Polymer.dom(this).appendChild(this.displayItemDebugger_);
+ },
+
+ updateContents() {
+ if (this.objectSnapshot_ && this.displayItemDebugger_) {
+ this.displayItemDebugger_.displayItemList = this.objectSnapshot_;
+ }
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ DisplayItemSnapshotView,
+ {
+ typeNames: ['cc::DisplayItemList'],
+ showInstances: false
+ });
+
+ return {
+ DisplayItemSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png
new file mode 100644
index 00000000000..a2b7710d3c4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg
new file mode 100644
index 00000000000..00531ac68d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="New document 1">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ id="filter3791">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.7246316"
+ id="feGaussianBlur3793" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="195.13782"
+ inkscape:cy="982.30556"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1215"
+ inkscape:window-height="860"
+ inkscape:window-x="2219"
+ inkscape:window-y="113"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="g3882"
+ style="opacity:0.5"
+ inkscape:export-filename="/tmp/input-event.png"
+ inkscape:export-xdpi="82.07"
+ inkscape:export-ydpi="82.07">
+ <path
+ transform="matrix(1.0152631,0,0,1.0152631,-0.71357503,0.46150497)"
+ sodipodi:type="arc"
+ style="opacity:0.50934604000000006;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter3791);enable-background:accumulate"
+ id="path3755"
+ sodipodi:cx="177.78685"
+ sodipodi:cy="100.79848"
+ sodipodi:rx="42.426407"
+ sodipodi:ry="42.426407"
+ d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" />
+ <path
+ transform="translate(-2,-2)"
+ d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z"
+ sodipodi:ry="42.426407"
+ sodipodi:rx="42.426407"
+ sodipodi:cy="100.79848"
+ sodipodi:cx="177.78685"
+ id="path2985"
+ style="color:#000000;fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3853"
+ d="m 175.28125,96.03125 0,8.46875 1,0 0,-8.46875 -1,0 z"
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3859"
+ d="m 171.53125,99.75 0,1 8.46875,0 0,-1 -8.46875,0 z"
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+ </g>
+ <path
+ transform="matrix(1.2923213,0,0,1.2923213,-53.970887,-31.465544)"
+ d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z"
+ sodipodi:ry="42.426407"
+ sodipodi:rx="42.426407"
+ sodipodi:cy="100.79848"
+ sodipodi:cx="177.78685"
+ id="path3867"
+ style="color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc"
+ inkscape:export-filename="/tmp/input-event.png"
+ inkscape:export-xdpi="82.07"
+ inkscape:export-ydpi="82.07" />
+ </g>
+</svg>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html
new file mode 100644
index 00000000000..9f81199e358
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html
@@ -0,0 +1,336 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const constants = tr.e.cc.constants;
+ const RENDER_PASS_QUADS =
+ Math.max(constants.ACTIVE_TREE, constants.PENDING_TREE) + 1;
+
+ /**
+ * @constructor
+ */
+ const LayerPicker = tr.ui.b.define('tr-ui-e-chrome-cc-layer-picker');
+
+ LayerPicker.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ decorate() {
+ this.lthi_ = undefined;
+ this.controls_ = document.createElement('top-controls');
+ this.renderPassQuads_ = false;
+
+ this.style.display = 'flex';
+ this.style.flexDirection = 'column';
+ this.controls_.style.flexGrow = 0;
+ this.controls_.style.flexShrink = 0;
+ this.controls_.style.flexBasis = 'auto';
+ this.controls_.style.backgroundImage =
+ '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))';
+ this.controls_.style.borderBottom = '1px solid #8e8e8e';
+ this.controls_.style.borderTop = '1px solid white';
+ this.controls_.style.display = 'inline';
+ this.controls_.style.fontSize = '14px';
+ this.controls_.style.paddingLeft = '2px';
+
+ this.itemList_ = new tr.ui.b.ListView();
+ this.itemList_.style.flexGrow = 1;
+ this.itemList_.style.flexShrink = 1;
+ this.itemList_.style.flexBasis = 'auto';
+ this.itemList_.style.fontFamily = 'monospace';
+ this.itemList_.style.overflow = 'auto';
+ Polymer.dom(this).appendChild(this.controls_);
+
+ Polymer.dom(this).appendChild(this.itemList_);
+
+ this.itemList_.addEventListener(
+ 'selection-changed', this.onItemSelectionChanged_.bind(this));
+
+ Polymer.dom(this.controls_).appendChild(tr.ui.b.createSelector(
+ this, 'whichTree',
+ 'layerPicker.whichTree', constants.ACTIVE_TREE,
+ [{label: 'Active tree', value: constants.ACTIVE_TREE},
+ {label: 'Pending tree', value: constants.PENDING_TREE},
+ {label: 'Render pass quads', value: RENDER_PASS_QUADS}]));
+
+ this.showPureTransformLayers_ = false;
+ const showPureTransformLayers = tr.ui.b.createCheckBox(
+ this, 'showPureTransformLayers',
+ 'layerPicker.showPureTransformLayers', false,
+ 'Transform layers');
+ Polymer.dom(showPureTransformLayers).classList.add(
+ 'show-transform-layers');
+ showPureTransformLayers.title =
+ 'When checked, pure transform layers are shown';
+ Polymer.dom(this.controls_).appendChild(showPureTransformLayers);
+ },
+
+ get lthiSnapshot() {
+ return this.lthiSnapshot_;
+ },
+
+ set lthiSnapshot(lthiSnapshot) {
+ this.lthiSnapshot_ = lthiSnapshot;
+ this.updateContents_();
+ },
+
+ get whichTree() {
+ return this.renderPassQuads_ ? constants.ACTIVE_TREE : this.whichTree_;
+ },
+
+ set whichTree(whichTree) {
+ this.whichTree_ = whichTree;
+ this.renderPassQuads_ = (whichTree === RENDER_PASS_QUADS);
+ this.updateContents_();
+ tr.b.dispatchSimpleEvent(this, 'selection-change', false);
+ },
+
+ get layerTreeImpl() {
+ if (this.lthiSnapshot === undefined) return undefined;
+
+ return this.lthiSnapshot.getTree(this.whichTree);
+ },
+
+ get isRenderPassQuads() {
+ return this.renderPassQuads_;
+ },
+
+ get showPureTransformLayers() {
+ return this.showPureTransformLayers_;
+ },
+
+ set showPureTransformLayers(show) {
+ if (this.showPureTransformLayers_ === show) return;
+
+ this.showPureTransformLayers_ = show;
+ this.updateContents_();
+ },
+
+ getRenderPassInfos_() {
+ if (!this.lthiSnapshot_) return [];
+
+ const renderPassInfo = [];
+ if (!this.lthiSnapshot_.args.frame ||
+ !this.lthiSnapshot_.args.frame.renderPasses) {
+ return renderPassInfo;
+ }
+
+ const renderPasses = this.lthiSnapshot_.args.frame.renderPasses;
+ for (let i = 0; i < renderPasses.length; ++i) {
+ const info = {renderPass: renderPasses[i],
+ depth: 0,
+ id: i,
+ name: 'cc::RenderPass'};
+ renderPassInfo.push(info);
+ }
+ return renderPassInfo;
+ },
+
+ getLayerInfos_() {
+ if (!this.lthiSnapshot_) return [];
+
+ const tree = this.lthiSnapshot_.getTree(this.whichTree_);
+ if (!tree) return [];
+
+ const layerInfos = [];
+
+ const showPureTransformLayers = this.showPureTransformLayers_;
+
+ const visitedLayers = {};
+ function visitLayer(layer, depth, isMask, isReplica) {
+ if (visitedLayers[layer.layerId]) return;
+
+ visitedLayers[layer.layerId] = true;
+ const info = {layer,
+ depth};
+
+ if (layer.args.drawsContent) {
+ info.name = layer.objectInstance.name;
+ } else {
+ info.name = 'cc::LayerImpl';
+ }
+
+ if (layer.usingGpuRasterization) {
+ info.name += ' (G)';
+ }
+
+ info.isMaskLayer = isMask;
+ info.replicaLayer = isReplica;
+
+ if (showPureTransformLayers || layer.args.drawsContent) {
+ layerInfos.push(info);
+ }
+ }
+ tree.iterLayers(visitLayer);
+ return layerInfos;
+ },
+
+ updateContents_() {
+ if (this.renderPassQuads_) {
+ this.updateRenderPassContents_();
+ } else {
+ this.updateLayerContents_();
+ }
+ },
+
+ updateRenderPassContents_() {
+ this.itemList_.clear();
+
+ let selectedRenderPassId;
+ if (this.selection_ && this.selection_.associatedRenderPassId) {
+ selectedRenderPassId = this.selection_.associatedRenderPassId;
+ }
+
+ const renderPassInfos = this.getRenderPassInfos_();
+ renderPassInfos.forEach(function(renderPassInfo) {
+ const renderPass = renderPassInfo.renderPass;
+ const id = renderPassInfo.id;
+
+ const item = this.createElementWithDepth_(renderPassInfo.depth);
+ const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+
+ Polymer.dom(labelEl).textContent = renderPassInfo.name + ' ' + id;
+ item.renderPass = renderPass;
+ item.renderPassId = id;
+ Polymer.dom(this.itemList_).appendChild(item);
+
+ if (id === selectedRenderPassId) {
+ renderPass.selectionState =
+ tr.model.SelectionState.SELECTED;
+ }
+ }, this);
+ },
+
+ updateLayerContents_() {
+ this.changingItemSelection_ = true;
+ try {
+ this.itemList_.clear();
+
+ let selectedLayerId;
+ if (this.selection_ && this.selection_.associatedLayerId) {
+ selectedLayerId = this.selection_.associatedLayerId;
+ }
+
+ const layerInfos = this.getLayerInfos_();
+ layerInfos.forEach(function(layerInfo) {
+ const layer = layerInfo.layer;
+ const id = layer.layerId;
+
+ const item = this.createElementWithDepth_(layerInfo.depth);
+ const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+
+ Polymer.dom(labelEl).textContent = layerInfo.name + ' ' + id;
+
+ const notesEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+ if (layerInfo.isMaskLayer) {
+ Polymer.dom(notesEl).textContent += '(mask)';
+ }
+ if (layerInfo.isReplicaLayer) {
+ Polymer.dom(notesEl).textContent += '(replica)';
+ }
+
+ if ((layer.gpuMemoryUsageInBytes !== undefined) &&
+ (layer.gpuMemoryUsageInBytes > 0)) {
+ const gpuUsageStr = tr.b.Unit.byName.sizeInBytes.format(
+ layer.gpuMemoryUsageInBytes);
+ Polymer.dom(notesEl).textContent += ' (' + gpuUsageStr + ' MiB)';
+ }
+
+ item.layer = layer;
+ Polymer.dom(this.itemList_).appendChild(item);
+
+ if (layer.layerId === selectedLayerId) {
+ layer.selectionState = tr.model.SelectionState.SELECTED;
+ item.selected = true;
+ }
+ }, this);
+ } finally {
+ this.changingItemSelection_ = false;
+ }
+ },
+
+ createElementWithDepth_(depth) {
+ const item = document.createElement('div');
+
+ const indentEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+ indentEl.style.whiteSpace = 'pre';
+ for (let i = 0; i < depth; i++) {
+ Polymer.dom(indentEl).textContent =
+ Polymer.dom(indentEl).textContent + ' ';
+ }
+ return item;
+ },
+
+ onItemSelectionChanged_(e) {
+ if (this.changingItemSelection_) return;
+ if (this.renderPassQuads_) {
+ this.onRenderPassSelected_(e);
+ } else {
+ this.onLayerSelected_(e);
+ }
+ tr.b.dispatchSimpleEvent(this, 'selection-change', false);
+ },
+
+ onRenderPassSelected_(e) {
+ let selectedRenderPass;
+ let selectedRenderPassId;
+ if (this.itemList_.selectedElement) {
+ selectedRenderPass = this.itemList_.selectedElement.renderPass;
+ selectedRenderPassId =
+ this.itemList_.selectedElement.renderPassId;
+ }
+
+ if (selectedRenderPass) {
+ this.selection_ = new tr.ui.e.chrome.cc.RenderPassSelection(
+ selectedRenderPass, selectedRenderPassId);
+ } else {
+ this.selection_ = undefined;
+ }
+ },
+
+ onLayerSelected_(e) {
+ let selectedLayer;
+ if (this.itemList_.selectedElement) {
+ selectedLayer = this.itemList_.selectedElement.layer;
+ }
+
+ if (selectedLayer) {
+ this.selection_ = new tr.ui.e.chrome.cc.LayerSelection(selectedLayer);
+ } else {
+ this.selection_ = undefined;
+ }
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.selection_ === selection) return;
+ this.selection_ = selection;
+ this.updateContents_();
+ }
+ };
+
+ return {
+ LayerPicker,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html
new file mode 100644
index 00000000000..1aaee9d7fbc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_picker.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a LayerTreeHostImpl snapshot in a human readable form.
+ * @constructor
+ */
+ const LayerTreeHostImplSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-layer-tree-host-impl-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ LayerTreeHostImplSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-lthi-s-view');
+ this.style.display = 'flex';
+ this.style.flexDirection = 'row';
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.minWidth = 0;
+
+ this.selection_ = undefined;
+
+ this.layerPicker_ = new tr.ui.e.chrome.cc.LayerPicker();
+ this.layerPicker_.style.flexGrow = 0;
+ this.layerPicker_.style.flexShrink = 0;
+ this.layerPicker_.style.flexBasis = 'auto';
+ this.layerPicker_.style.minWidth = '200px';
+ this.layerPicker_.addEventListener(
+ 'selection-change',
+ this.onLayerPickerSelectionChanged_.bind(this));
+
+ this.layerView_ = new tr.ui.e.chrome.cc.LayerView();
+ this.layerView_.addEventListener(
+ 'selection-change',
+ this.onLayerViewSelectionChanged_.bind(this));
+ this.layerView_.style.flexGrow = 1;
+ this.layerView_.style.flexShrink = 1;
+ this.layerView_.style.flexBasis = 'auto';
+ this.layerView_.style.minWidth = 0;
+
+ this.dragHandle_ = document.createElement('tr-ui-b-drag-handle');
+ this.dragHandle_.style.flexGrow = 0;
+ this.dragHandle_.style.flexShrink = 0;
+ this.dragHandle_.style.flexBasis = 'auto';
+ this.dragHandle_.horizontal = false;
+ this.dragHandle_.target = this.layerPicker_;
+
+ Polymer.dom(this).appendChild(this.layerPicker_);
+ Polymer.dom(this).appendChild(this.dragHandle_);
+ Polymer.dom(this).appendChild(this.layerView_);
+
+ // Make sure we have the current values from layerView_ and layerPicker_,
+ // since those might have been created before we added the listener.
+ this.onLayerViewSelectionChanged_();
+ this.onLayerPickerSelectionChanged_();
+ },
+
+ get objectSnapshot() {
+ return this.objectSnapshot_;
+ },
+
+ set objectSnapshot(objectSnapshot) {
+ this.objectSnapshot_ = objectSnapshot;
+
+ const lthi = this.objectSnapshot;
+ let layerTreeImpl;
+ if (lthi) {
+ layerTreeImpl = lthi.getTree(this.layerPicker_.whichTree);
+ }
+
+ this.layerPicker_.lthiSnapshot = lthi;
+ this.layerView_.layerTreeImpl = layerTreeImpl;
+ this.layerView_.regenerateContent();
+
+ if (!this.selection_) return;
+
+ this.selection = this.selection_.findEquivalent(lthi);
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.selection_ === selection) return;
+
+ this.selection_ = selection;
+ this.layerPicker_.selection = selection;
+ this.layerView_.selection = selection;
+ tr.b.dispatchSimpleEvent(this, 'cc-selection-change');
+ },
+
+ onLayerPickerSelectionChanged_() {
+ this.selection_ = this.layerPicker_.selection;
+ this.layerView_.selection = this.selection;
+ this.layerView_.layerTreeImpl = this.layerPicker_.layerTreeImpl;
+ this.layerView_.isRenderPassQuads = this.layerPicker_.isRenderPassQuads;
+ this.layerView_.regenerateContent();
+ tr.b.dispatchSimpleEvent(this, 'cc-selection-change');
+ },
+
+ onLayerViewSelectionChanged_() {
+ this.selection_ = this.layerView_.selection;
+ this.layerPicker_.selection = this.selection;
+ tr.b.dispatchSimpleEvent(this, 'cc-selection-change');
+ },
+
+ get extraHighlightsByLayerId() {
+ return this.layerView_.extraHighlightsByLayerId;
+ },
+
+ set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+ this.layerView_.extraHighlightsByLayerId = extraHighlightsByLayerId;
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ LayerTreeHostImplSnapshotView, {typeName: 'cc::LayerTreeHostImpl'});
+
+ return {
+ LayerTreeHostImplSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html
new file mode 100644
index 00000000000..1831be24618
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const snapshot = instance.snapshots[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView();
+ view.style.width = '900px';
+ view.style.height = '400px';
+ view.objectSnapshot = snapshot;
+
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html
new file mode 100644
index 00000000000..2a7e5666f8b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html
@@ -0,0 +1,1200 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/extras/chrome/cc/debug_colors.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/extras/chrome/cc/render_pass.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<link rel="import" href="/tracing/ui/base/quad_stack_view.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id='tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template'>
+ <style>
+ #input-event {
+ background-image: url('./images/input-event.png');
+ display: none;
+ }
+ </style>
+ <img id='input-event'/>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Graphical view of LayerTreeImpl, with controls for
+ * type of layer content shown and info bar for content-loading warnings.
+ */
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const TILE_HEATMAP_TYPE = {};
+ TILE_HEATMAP_TYPE.NONE = 'none';
+ TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY = 'scheduledPriority';
+ TILE_HEATMAP_TYPE.USING_GPU_MEMORY = 'usingGpuMemory';
+
+ const cc = tr.ui.e.chrome.cc;
+
+ function createTileRectsSelectorBaseOptions() {
+ return [{label: 'None', value: 'none'},
+ {label: 'Coverage Rects', value: 'coverage'}];
+ }
+
+
+ /**
+ * @constructor
+ */
+ const LayerTreeQuadStackView =
+ tr.ui.b.define('tr-ui-e-chrome-cc-layer-tree-quad-stack-view');
+
+ LayerTreeQuadStackView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.flexDirection = 'column';
+ this.style.minHeight = 0;
+ this.style.display = 'flex';
+
+ this.isRenderPassQuads_ = false;
+ this.pictureAsImageData_ = {}; // Maps picture.guid to PictureAsImageData.
+ this.messages_ = [];
+ this.controls_ = document.createElement('top-controls');
+ this.controls_.style.flexGrow = 0;
+ this.controls_.style.flexShrink = 0;
+ this.controls_.style.flexBasis = 'auto';
+ this.controls_.style.backgroundImage =
+ '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))';
+ this.controls_.style.borderBottom = '1px solid #8e8e8e';
+ this.controls_.style.borderTop = '1px solid white';
+ this.controls_.style.display = 'flex';
+ this.controls_.style.flexDirection = 'row';
+ this.controls_.style.flexWrap = 'wrap';
+ this.controls_.style.fontSize = '14px';
+ this.controls_.style.paddingLeft = '2px';
+ this.controls_.style.overflow = 'hidden';
+ this.infoBar_ = document.createElement('tr-ui-b-info-bar');
+ this.quadStackView_ = new tr.ui.b.QuadStackView();
+ this.quadStackView_.addEventListener(
+ 'selectionchange', this.onQuadStackViewSelectionChange_.bind(this));
+ this.quadStackView_.style.flexGrow = 1;
+ this.quadStackView_.style.flexShrink = 1;
+ this.quadStackView_.style.flexBasis = 'auto';
+ this.quadStackView_.style.minWidth = '200px';
+
+ this.extraHighlightsByLayerId_ = undefined;
+ this.inputEventImageData_ = undefined;
+
+ const m = tr.ui.b.MOUSE_SELECTOR_MODE;
+ const mms = this.quadStackView_.mouseModeSelector;
+ mms.settingsKey = 'tr.e.cc.layerTreeQuadStackView.mouseModeSelector';
+ mms.setKeyCodeForMode(m.SELECTION, 'Z'.charCodeAt(0));
+ mms.setKeyCodeForMode(m.PANSCAN, 'X'.charCodeAt(0));
+ mms.setKeyCodeForMode(m.ZOOM, 'C'.charCodeAt(0));
+ mms.setKeyCodeForMode(m.ROTATE, 'V'.charCodeAt(0));
+
+ const node = tr.ui.b.instantiateTemplate(
+ '#tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template', THIS_DOC);
+ Polymer.dom(this).appendChild(node);
+ Polymer.dom(this).appendChild(this.controls_);
+ Polymer.dom(this).appendChild(this.infoBar_);
+ Polymer.dom(this).appendChild(this.quadStackView_);
+
+ this.tileRectsSelector_ = tr.ui.b.createSelector(
+ this, 'howToShowTiles',
+ 'layerView.howToShowTiles', 'none',
+ createTileRectsSelectorBaseOptions());
+ Polymer.dom(this.controls_).appendChild(this.tileRectsSelector_);
+
+ const tileHeatmapText = tr.ui.b.createSpan({
+ textContent: 'Tile heatmap:'
+ });
+ Polymer.dom(this.controls_).appendChild(tileHeatmapText);
+
+ const tileHeatmapSelector = tr.ui.b.createSelector(
+ this, 'tileHeatmapType',
+ 'layerView.tileHeatmapType', TILE_HEATMAP_TYPE.NONE,
+ [{label: 'None',
+ value: TILE_HEATMAP_TYPE.NONE},
+ {label: 'Scheduled Priority',
+ value: TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY},
+ {label: 'Is using GPU memory',
+ value: TILE_HEATMAP_TYPE.USING_GPU_MEMORY}
+ ]);
+ Polymer.dom(this.controls_).appendChild(tileHeatmapSelector);
+
+ const showOtherLayersCheckbox = tr.ui.b.createCheckBox(
+ this, 'showOtherLayers',
+ 'layerView.showOtherLayers', true,
+ 'Other layers/passes');
+ showOtherLayersCheckbox.title =
+ 'When checked, show all layers, selected or not.';
+ Polymer.dom(this.controls_).appendChild(showOtherLayersCheckbox);
+
+ const showInvalidationsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showInvalidations',
+ 'layerView.showInvalidations', true,
+ 'Invalidations');
+ showInvalidationsCheckbox.title =
+ 'When checked, compositing invalidations are highlighted in red';
+ Polymer.dom(this.controls_).appendChild(showInvalidationsCheckbox);
+
+ const showUnrecordedRegionCheckbox = tr.ui.b.createCheckBox(
+ this, 'showUnrecordedRegion',
+ 'layerView.showUnrecordedRegion', true,
+ 'Unrecorded area');
+ showUnrecordedRegionCheckbox.title =
+ 'When checked, unrecorded areas are highlighted in yellow';
+ Polymer.dom(this.controls_).appendChild(showUnrecordedRegionCheckbox);
+
+ const showBottlenecksCheckbox = tr.ui.b.createCheckBox(
+ this, 'showBottlenecks',
+ 'layerView.showBottlenecks', true,
+ 'Bottlenecks');
+ showBottlenecksCheckbox.title =
+ 'When checked, scroll bottlenecks are highlighted';
+ Polymer.dom(this.controls_).appendChild(showBottlenecksCheckbox);
+
+ const showLayoutRectsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showLayoutRects',
+ 'layerView.showLayoutRects', false,
+ 'Layout rects');
+ showLayoutRectsCheckbox.title =
+ 'When checked, shows rects for regions where layout happened';
+ Polymer.dom(this.controls_).appendChild(showLayoutRectsCheckbox);
+
+ const showContentsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showContents',
+ 'layerView.showContents', true,
+ 'Contents');
+ showContentsCheckbox.title =
+ 'When checked, show the rendered contents inside the layer outlines';
+ Polymer.dom(this.controls_).appendChild(showContentsCheckbox);
+
+ const showAnimationBoundsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showAnimationBounds',
+ 'layerView.showAnimationBounds', false,
+ 'Animation Bounds');
+ showAnimationBoundsCheckbox.title = 'When checked, show a border around' +
+ ' a layer showing the extent of its animation.';
+ Polymer.dom(this.controls_).appendChild(showAnimationBoundsCheckbox);
+
+ const showInputEventsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showInputEvents',
+ 'layerView.showInputEvents', true,
+ 'Input events');
+ showInputEventsCheckbox.title = 'When checked, input events are ' +
+ 'displayed as circles.';
+ Polymer.dom(this.controls_).appendChild(showInputEventsCheckbox);
+
+ this.whatRasterizedLink_ = document.createElement(
+ 'tr-ui-a-analysis-link');
+ this.whatRasterizedLink_.style.position = 'absolute';
+ this.whatRasterizedLink_.style.bottom = '15px';
+ this.whatRasterizedLink_.style.left = '10px';
+ this.whatRasterizedLink_.selection =
+ this.getWhatRasterizedEventSet_.bind(this);
+ Polymer.dom(this.quadStackView_).appendChild(this.whatRasterizedLink_);
+ },
+
+ get layerTreeImpl() {
+ return this.layerTreeImpl_;
+ },
+
+ set isRenderPassQuads(newValue) {
+ this.isRenderPassQuads_ = newValue;
+ },
+
+ set layerTreeImpl(layerTreeImpl) {
+ if (this.layerTreeImpl_ === layerTreeImpl) return;
+
+ // FIXME(pdr): We may want to clear pictureAsImageData_ here to save
+ // memory at the cost of performance. Note that
+ // pictureAsImageData_ will be cleared when this is
+ // destructed, but this view might live for several
+ // layerTreeImpls.
+ this.layerTreeImpl_ = layerTreeImpl;
+ this.selection = undefined;
+ },
+
+ get extraHighlightsByLayerId() {
+ return this.extraHighlightsByLayerId_;
+ },
+
+ set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+ this.extraHighlightsByLayerId_ = extraHighlightsByLayerId;
+ this.scheduleUpdateContents_();
+ },
+
+ get showOtherLayers() {
+ return this.showOtherLayers_;
+ },
+
+ set showOtherLayers(show) {
+ this.showOtherLayers_ = show;
+ this.updateContents_();
+ },
+
+ get showAnimationBounds() {
+ return this.showAnimationBounds_;
+ },
+
+ set showAnimationBounds(show) {
+ this.showAnimationBounds_ = show;
+ this.updateContents_();
+ },
+
+ get showInputEvents() {
+ return this.showInputEvents_;
+ },
+
+ set showInputEvents(show) {
+ this.showInputEvents_ = show;
+ this.updateContents_();
+ },
+
+ get showContents() {
+ return this.showContents_;
+ },
+
+ set showContents(show) {
+ this.showContents_ = show;
+ this.updateContents_();
+ },
+
+ get showInvalidations() {
+ return this.showInvalidations_;
+ },
+
+ set showInvalidations(show) {
+ this.showInvalidations_ = show;
+ this.updateContents_();
+ },
+
+ get showUnrecordedRegion() {
+ return this.showUnrecordedRegion_;
+ },
+
+ set showUnrecordedRegion(show) {
+ this.showUnrecordedRegion_ = show;
+ this.updateContents_();
+ },
+
+ get showBottlenecks() {
+ return this.showBottlenecks_;
+ },
+
+ set showBottlenecks(show) {
+ this.showBottlenecks_ = show;
+ this.updateContents_();
+ },
+
+ get showLayoutRects() {
+ return this.showLayoutRects_;
+ },
+
+ set showLayoutRects(show) {
+ this.showLayoutRects_ = show;
+ this.updateContents_();
+ },
+
+ get howToShowTiles() {
+ return this.howToShowTiles_;
+ },
+
+ set howToShowTiles(val) {
+ // Make sure val is something we expect.
+ if (val !== 'none' && val !== 'coverage' && isNaN(parseFloat(val))) {
+ throw new Error(
+ 'howToShowTiles requires "none" or "coverage" or a number');
+ }
+
+ this.howToShowTiles_ = val;
+ this.updateContents_();
+ },
+
+ get tileHeatmapType() {
+ return this.tileHeatmapType_;
+ },
+
+ set tileHeatmapType(val) {
+ this.tileHeatmapType_ = val;
+ this.updateContents_();
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.selection === selection) return;
+
+ this.selection_ = selection;
+ tr.b.dispatchSimpleEvent(this, 'selection-change');
+ this.updateContents_();
+ },
+
+ regenerateContent() {
+ this.updateTilesSelector_();
+ this.updateContents_();
+ },
+
+ loadDataForImageElement_(image, callback) {
+ const imageContent = window.getComputedStyle(image).backgroundImage;
+ if (!imageContent) {
+ // The style has not been applied because the view has not been added
+ // into the DOM tree yet. Try again in another cycle.
+ this.scheduleUpdateContents_();
+ return;
+ }
+ image.src = tr.ui.b.extractUrlString(imageContent);
+ image.onload = function() {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ canvas.width = image.width;
+ canvas.height = image.height;
+ ctx.drawImage(image, 0, 0);
+ const imageData = ctx.getImageData(
+ 0, 0, canvas.width, canvas.height);
+ callback(imageData);
+ };
+ },
+
+ onQuadStackViewSelectionChange_(e) {
+ const selectableQuads = e.quads.filter(function(q) {
+ return q.selectionToSetIfClicked !== undefined;
+ });
+ if (selectableQuads.length === 0) {
+ this.selection = undefined;
+ return;
+ }
+
+ // Sort the quads low to high on stackingGroupId.
+ selectableQuads.sort(function(x, y) {
+ const z = x.stackingGroupId - y.stackingGroupId;
+ if (z !== 0) return z;
+
+ return x.selectionToSetIfClicked.specicifity -
+ y.selectionToSetIfClicked.specicifity;
+ });
+
+ // TODO(nduca): Support selecting N things at once.
+ const quadToSelect = selectableQuads[selectableQuads.length - 1];
+ this.selection = quadToSelect.selectionToSetIfClicked;
+ },
+
+ scheduleUpdateContents_() {
+ if (this.updateContentsPending_) return;
+
+ this.updateContentsPending_ = true;
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContents_, this);
+ },
+
+ updateContents_() {
+ if (!this.layerTreeImpl_) {
+ this.quadStackView_.headerText = 'No tree';
+ this.quadStackView_.quads = [];
+ return;
+ }
+
+
+ const status = this.computePictureLoadingStatus_();
+ if (!status.picturesComplete) return;
+
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const lthiInstance = lthi.objectInstance;
+ const worldViewportRect = tr.b.math.Rect.fromXYWH(
+ 0, 0,
+ lthi.deviceViewportSize.width, lthi.deviceViewportSize.height);
+ this.quadStackView_.deviceRect = worldViewportRect;
+ if (this.isRenderPassQuads_) {
+ this.quadStackView_.quads = this.generateRenderPassQuads();
+ } else {
+ this.quadStackView_.quads = this.generateLayerQuads();
+ }
+
+ this.updateWhatRasterizedLinkState_();
+
+ let message = '';
+ if (lthi.tilesHaveGpuMemoryUsageInfo) {
+ const thisTreeUsageInBytes = this.layerTreeImpl_.gpuMemoryUsageInBytes;
+ const otherTreeUsageInBytes = lthi.gpuMemoryUsageInBytes -
+ thisTreeUsageInBytes;
+ message +=
+ tr.b.convertUnit(thisTreeUsageInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB on this tree';
+ if (otherTreeUsageInBytes) {
+ message += ', ' +
+ tr.b.convertUnit(otherTreeUsageInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB on the other tree';
+ }
+ } else {
+ if (this.layerTreeImpl_) {
+ const thisTreeUsageInBytes =
+ this.layerTreeImpl_.gpuMemoryUsageInBytes;
+ message +=
+ tr.b.convertUnit(thisTreeUsageInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB on this tree';
+
+ if (this.layerTreeImpl_.otherTree) {
+ // Older Chromes don't report enough data to know how much memory is
+ // being used across both trees. We know the memory consumed by each
+ // tree, but there is resource sharing *between the trees* so we
+ // can't simply sum up the per-tree costs. We need either the total
+ // plus one tree, to guess the unique on the other tree, etc. Newer
+ // chromes report memory per tile, which allows LTHI to compute the
+ // total tile memory usage, letting us figure things out properly.
+ message += ', ??? MiB on other tree. ';
+ }
+ }
+ }
+
+ if (lthi.args.tileManagerBasicState) {
+ const tmgs = lthi.args.tileManagerBasicState.globalState;
+ message += ' (softMax=' +
+ tr.b.convertUnit(tmgs.softMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB, hardMax=' +
+ tr.b.convertUnit(tmgs.hardMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' +
+ tmgs.memoryLimitPolicy + ')';
+ } else {
+ // Old Chromes do not have a globalState on the LTHI dump.
+ // But they do issue a DidManage event wiht the globalstate. Find that
+ // event so that we show some global state.
+ const thread = lthi.snapshottedOnThread;
+ const didManageTilesSlices = thread.sliceGroup.slices.filter(s => {
+ if (s.category !== 'tr.e.cc') return false;
+
+ if (s.title !== 'DidManage') return false;
+
+ if (s.end > lthi.ts) return false;
+
+ return true;
+ });
+ didManageTilesSlices.sort(function(x, y) {
+ return x.end - y.end;
+ });
+ if (didManageTilesSlices.length > 0) {
+ const newest = didManageTilesSlices[didManageTilesSlices.length - 1];
+ const tmgs = newest.args.state.global_state;
+ message += ' (softMax=' +
+ tr.b.convertUnit(tmgs.softMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB, hardMax=' +
+ tr.b.convertUnit(tmgs.hardMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' +
+ tmgs.memoryLimitPolicy + ')';
+ }
+ }
+
+ if (this.layerTreeImpl_.otherTree) {
+ message += ' (Another tree exists)';
+ }
+
+ if (message.length) {
+ this.quadStackView_.headerText = message;
+ } else {
+ this.quadStackView_.headerText = undefined;
+ }
+
+ this.updateInfoBar_(status.messages);
+ },
+
+ updateTilesSelector_() {
+ const data = createTileRectsSelectorBaseOptions();
+
+ if (this.layerTreeImpl_) {
+ // First get all of the scales information from LTHI.
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const scaleNames = lthi.getContentsScaleNames();
+ for (const scale in scaleNames) {
+ data.push({
+ label: 'Scale ' + scale + ' (' + scaleNames[scale] + ')',
+ value: scale
+ });
+ }
+ }
+
+ // Then create a new selector and replace the old one.
+ const newSelector = tr.ui.b.createSelector(
+ this, 'howToShowTiles',
+ 'layerView.howToShowTiles', 'none',
+ data);
+ this.controls_.replaceChild(newSelector, this.tileRectsSelector_);
+ this.tileRectsSelector_ = newSelector;
+ },
+
+ computePictureLoadingStatus_() {
+ // Figure out if we can draw the quads yet. While we're at it, figure out
+ // if we have any warnings we need to show.
+ const layers = this.layers;
+ const status = {
+ messages: [],
+ picturesComplete: true
+ };
+ if (this.showContents) {
+ let hasPendingRasterizeImage = false;
+ let firstPictureError = undefined;
+ let hasMissingLayerRect = false;
+ let hasUnresolvedPictureRef = false;
+ for (let i = 0; i < layers.length; i++) {
+ const layer = layers[i];
+ for (let ir = 0; ir < layer.pictures.length; ++ir) {
+ const picture = layer.pictures[ir];
+
+ if (picture.idRef) {
+ hasUnresolvedPictureRef = true;
+ continue;
+ }
+ if (!picture.layerRect) {
+ hasMissingLayerRect = true;
+ continue;
+ }
+
+ const pictureAsImageData = this.pictureAsImageData_[picture.guid];
+ if (!pictureAsImageData) {
+ hasPendingRasterizeImage = true;
+ this.pictureAsImageData_[picture.guid] =
+ tr.e.cc.PictureAsImageData.Pending(this);
+ picture.rasterize(
+ {stopIndex: undefined},
+ function(pictureImageData) {
+ const picture_ = pictureImageData.picture;
+ this.pictureAsImageData_[picture_.guid] = pictureImageData;
+ this.scheduleUpdateContents_();
+ }.bind(this));
+ continue;
+ }
+ if (pictureAsImageData.isPending()) {
+ hasPendingRasterizeImage = true;
+ continue;
+ }
+ if (pictureAsImageData.error) {
+ if (!firstPictureError) {
+ firstPictureError = pictureAsImageData.error;
+ }
+ break;
+ }
+ }
+ }
+ if (hasPendingRasterizeImage) {
+ status.picturesComplete = false;
+ } else {
+ if (hasUnresolvedPictureRef) {
+ status.messages.push({
+ header: 'Missing picture',
+ details: 'Your trace didn\'t have pictures for every layer. ' +
+ 'Old chrome versions had this problem'});
+ }
+ if (hasMissingLayerRect) {
+ status.messages.push({
+ header: 'Missing layer rect',
+ details: 'Your trace may be corrupt or from a very old ' +
+ 'Chrome revision.'});
+ }
+ if (firstPictureError) {
+ status.messages.push({
+ header: 'Cannot rasterize',
+ details: firstPictureError});
+ }
+ }
+ }
+ if (this.showInputEvents && this.layerTreeImpl.tracedInputLatencies &&
+ this.inputEventImageData_ === undefined) {
+ const image = Polymer.dom(this).querySelector('#input-event');
+ if (!image.src) {
+ this.loadDataForImageElement_(image, function(imageData) {
+ this.inputEventImageData_ = imageData;
+ this.updateContentsPending_ = false;
+ this.scheduleUpdateContents_();
+ }.bind(this));
+ }
+ status.picturesComplete = false;
+ }
+ return status;
+ },
+
+ get selectedRenderPass() {
+ if (this.selection) {
+ return this.selection.renderPass_;
+ }
+ },
+
+ get selectedLayer() {
+ if (this.selection) {
+ const selectedLayerId = this.selection.associatedLayerId;
+ return this.layerTreeImpl_.findLayerWithId(selectedLayerId);
+ }
+ },
+
+ get renderPasses() {
+ let renderPasses =
+ this.layerTreeImpl.layerTreeHostImpl.args.frame.renderPasses;
+ if (!this.showOtherLayers) {
+ const selectedRenderPass = this.selectedRenderPass;
+ if (selectedRenderPass) {
+ renderPasses = [selectedRenderPass];
+ }
+ }
+ return renderPasses;
+ },
+
+ get layers() {
+ let layers = this.layerTreeImpl.renderSurfaceLayerList;
+ if (!this.showOtherLayers) {
+ const selectedLayer = this.selectedLayer;
+ if (selectedLayer) {
+ layers = [selectedLayer];
+ }
+ }
+ return layers;
+ },
+
+ appendImageQuads_(quads, layer, layerQuad) {
+ // Generate image quads for the layer
+ for (let ir = 0; ir < layer.pictures.length; ++ir) {
+ const picture = layer.pictures[ir];
+ if (!picture.layerRect) continue;
+
+ const unitRect = picture.layerRect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+
+ const pictureData = this.pictureAsImageData_[picture.guid];
+ if (this.showContents && pictureData && pictureData.imageData) {
+ iq.imageData = pictureData.imageData;
+ iq.borderColor = 'rgba(0,0,0,0)';
+ } else {
+ iq.imageData = undefined;
+ }
+
+ iq.stackingGroupId = layerQuad.stackingGroupId;
+ quads.push(iq);
+ }
+ },
+
+ appendAnimationQuads_(quads, layer, layerQuad) {
+ if (!layer.animationBoundsRect) return;
+
+ const rect = layer.animationBoundsRect;
+ const abq = tr.b.math.Quad.fromRect(rect);
+
+ abq.backgroundColor = 'rgba(164,191,48,0.5)';
+ abq.borderColor = 'rgba(205,255,0,0.75)';
+ abq.borderWidth = 3.0;
+ abq.stackingGroupId = layerQuad.stackingGroupId;
+ abq.selectionToSetIfClicked = new cc.AnimationRectSelection(
+ layer, rect);
+ quads.push(abq);
+ },
+
+ appendInvalidationQuads_(quads, layer, layerQuad) {
+ if (layer.layerTreeImpl.hasSourceFrameBeenDrawnBefore) return;
+
+ // Generate the invalidation rect quads.
+ for (const rect of layer.invalidation.rects) {
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+ iq.backgroundColor = 'rgba(0, 255, 0, 0.1)';
+ if (rect.reason === 'appeared') {
+ iq.backgroundColor = 'rgba(0, 255, 128, 0.1)';
+ }
+ iq.borderColor = 'rgba(0, 255, 0, 1)';
+ iq.stackingGroupId = layerQuad.stackingGroupId;
+
+ let message = 'Invalidation rect';
+ if (rect.reason) {
+ message += ' (' + rect.reason + ')';
+ }
+ if (rect.client) {
+ message += ' for ' + rect.client;
+ }
+
+ iq.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, message, rect, rect);
+ quads.push(iq);
+ }
+ },
+
+ appendUnrecordedRegionQuads_(quads, layer, layerQuad) {
+ // Generate the unrecorded region quads.
+ for (let ir = 0; ir < layer.unrecordedRegion.rects.length; ir++) {
+ const rect = layer.unrecordedRegion.rects[ir];
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+ iq.backgroundColor = 'rgba(240, 230, 140, 0.3)';
+ iq.borderColor = 'rgba(240, 230, 140, 1)';
+ iq.stackingGroupId = layerQuad.stackingGroupId;
+ iq.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, 'Unrecorded area', rect, rect);
+ quads.push(iq);
+ }
+ },
+
+ appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId) {
+ function processRegion(region, label, borderColor) {
+ const backgroundColor = borderColor.clone();
+ backgroundColor.a = 0.4 * (borderColor.a || 1.0);
+
+ if (!region || !region.rects) return;
+
+ for (let ir = 0; ir < region.rects.length; ir++) {
+ const rect = region.rects[ir];
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+ iq.backgroundColor = backgroundColor.toString();
+ iq.borderColor = borderColor.toString();
+ iq.borderWidth = 4.0;
+ iq.stackingGroupId = stackingGroupId;
+ iq.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, label, rect, rect);
+ quads.push(iq);
+ }
+ }
+
+ processRegion(layer.touchEventHandlerRegion, 'Touch listener',
+ tr.b.Color.fromString('rgb(228, 226, 27)'));
+ processRegion(layer.wheelEventHandlerRegion, 'Wheel listener',
+ tr.b.Color.fromString('rgb(176, 205, 29)'));
+ processRegion(layer.nonFastScrollableRegion, 'Repaints on scroll',
+ tr.b.Color.fromString('rgb(213, 134, 32)'));
+ },
+
+ appendTileCoverageRectQuads_(
+ quads, layer, layerQuad, heatmapType) {
+ if (!layer.tileCoverageRects) return;
+
+ const tiles = [];
+ for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) {
+ const tile = layer.tileCoverageRects[ct].tile;
+ if (tile !== undefined) tiles.push(tile);
+ }
+
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const minMax =
+ this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType);
+ const heatmapResult =
+ this.computeHeatmapColors_(tiles, minMax, heatmapType);
+ let heatIndex = 0;
+
+ for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) {
+ let rect = layer.tileCoverageRects[ct].geometryRect;
+ rect = rect.scale(1.0 / layer.geometryContentsScale);
+
+ const tile = layer.tileCoverageRects[ct].tile;
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+ quad.stackingGroupId = layerQuad.stackingGroupId;
+ let type = tr.e.cc.tileTypes.missing;
+ if (tile) {
+ type = tile.getTypeForLayer(layer);
+ quad.backgroundColor = heatmapResult[heatIndex].color;
+ ++heatIndex;
+ }
+
+ quad.borderColor = tr.e.cc.tileBorder[type].color;
+ quad.borderWidth = tr.e.cc.tileBorder[type].width;
+ let label;
+ if (tile) {
+ label = 'coverageRect';
+ } else {
+ label = 'checkerboard coverageRect';
+ }
+ quad.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, label, rect, layer.tileCoverageRects[ct]);
+
+ quads.push(quad);
+ }
+ },
+
+ appendLayoutRectQuads_(quads, layer, layerQuad) {
+ if (!layer.layoutRects) {
+ return;
+ }
+
+ for (let ct = 0; ct < layer.layoutRects.length; ++ct) {
+ let rect = layer.layoutRects[ct].geometryRect;
+ rect = rect.scale(1.0 / layer.geometryContentsScale);
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+ quad.stackingGroupId = layerQuad.stackingGroupId;
+
+ quad.borderColor = 'rgba(0, 0, 200, 0.7)';
+ quad.borderWidth = 2;
+ const label = 'Layout rect';
+ quad.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, label, rect);
+
+ quads.push(quad);
+ }
+ },
+
+ getValueForHeatmap_(tile, heatmapType) {
+ if (heatmapType === TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY) {
+ return tile.scheduledPriority === 0 ?
+ undefined :
+ tile.scheduledPriority;
+ } else if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) {
+ if (tile.isSolidColor) return 0.5;
+ return tile.isUsingGpuMemory ? 0 : 1;
+ }
+ },
+
+ getMinMaxForHeatmap_(tiles, heatmapType) {
+ const range = new tr.b.math.Range();
+ if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) {
+ range.addValue(0);
+ range.addValue(1);
+ return range;
+ }
+
+ for (let i = 0; i < tiles.length; ++i) {
+ const value = this.getValueForHeatmap_(tiles[i], heatmapType);
+ if (value === undefined) continue;
+ range.addValue(value);
+ }
+ if (range.range === 0) {
+ range.addValue(1);
+ }
+ return range;
+ },
+
+ computeHeatmapColors_(tiles, minMax, heatmapType) {
+ const min = minMax.min;
+ const max = minMax.max;
+
+ const color = function(value) {
+ let hue = 120 * (1 - (value - min) / (max - min));
+ if (hue < 0) hue = 0;
+ return 'hsla(' + hue + ', 100%, 50%, 0.5)';
+ };
+
+ const values = [];
+ for (let i = 0; i < tiles.length; ++i) {
+ const tile = tiles[i];
+ const value = this.getValueForHeatmap_(tile, heatmapType);
+ const res = {
+ value,
+ color: value !== undefined ? color(value) : undefined
+ };
+ values.push(res);
+ }
+
+ return values;
+ },
+
+ appendTilesWithScaleQuads_(
+ quads, layer, layerQuad, scale, heatmapType) {
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+
+ const tiles = [];
+ for (let i = 0; i < lthi.activeTiles.length; ++i) {
+ const tile = lthi.activeTiles[i];
+
+ if (Math.abs(tile.contentsScale - scale) > 1e-6) {
+ continue;
+ }
+
+ // TODO(vmpstr): Make the stiching of tiles and layers a part of
+ // tile construction (issue 346)
+ if (layer.layerId !== tile.layerId) continue;
+
+ tiles.push(tile);
+ }
+
+ const minMax =
+ this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType);
+ const heatmapResult =
+ this.computeHeatmapColors_(tiles, minMax, heatmapType);
+
+ for (let i = 0; i < tiles.length; ++i) {
+ const tile = tiles[i];
+ const rect = tile.layerRect;
+ if (!tile.layerRect) continue;
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+ quad.stackingGroupId = layerQuad.stackingGroupId;
+
+ const type = tile.getTypeForLayer(layer);
+ quad.borderColor = tr.e.cc.tileBorder[type].color;
+ quad.borderWidth = tr.e.cc.tileBorder[type].width;
+
+ quad.backgroundColor = heatmapResult[i].color;
+ const data = {
+ tileType: type
+ };
+ if (heatmapType !== TILE_HEATMAP_TYPE.NONE) {
+ data[heatmapType] = heatmapResult[i].value;
+ }
+ quad.selectionToSetIfClicked = new cc.TileSelection(tile, data);
+ quads.push(quad);
+ }
+ },
+
+ appendHighlightQuadsForLayer_(
+ quads, layer, layerQuad, highlights) {
+ highlights.forEach(function(highlight) {
+ const rect = highlight.rect;
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ let colorId = ColorScheme.getColorIdForGeneralPurposeString(
+ highlight.colorKey);
+ const offset = ColorScheme.properties.brightenedOffsets[0];
+ colorId = ColorScheme.getVariantColorId(colorId, offset);
+
+ const color = ColorScheme.colors[colorId];
+
+ const quadForDrawing = quad.clone();
+ quadForDrawing.backgroundColor = color.withAlpha(0.5).toString();
+ quadForDrawing.borderColor = color.withAlpha(1.0).darken().toString();
+ quadForDrawing.stackingGroupId = layerQuad.stackingGroupId;
+ quads.push(quadForDrawing);
+ }, this);
+ },
+
+ generateRenderPassQuads() {
+ if (!this.layerTreeImpl.layerTreeHostImpl.args.frame) return [];
+ const renderPasses = this.renderPasses;
+ if (!renderPasses) return [];
+
+ const quads = [];
+ for (let i = 0; i < renderPasses.length; ++i) {
+ const quadList = renderPasses[i].quadList;
+ for (let j = 0; j < quadList.length; ++j) {
+ const drawQuad = quadList[j];
+ const quad = drawQuad.rectAsTargetSpaceQuad.clone();
+ quad.borderColor = 'rgb(170, 204, 238)';
+ quad.borderWidth = 2;
+ quad.stackingGroupId = i;
+ quads.push(quad);
+ }
+ }
+ return quads;
+ },
+
+ generateLayerQuads() {
+ this.updateContentsPending_ = false;
+
+ // Generate the quads for the view.
+ const layers = this.layers;
+ const quads = [];
+ let nextStackingGroupId = 0;
+ const alreadyVisitedLayerIds = {};
+
+
+ let selectionHighlightsByLayerId;
+ if (this.selection) {
+ selectionHighlightsByLayerId = this.selection.highlightsByLayerId;
+ } else {
+ selectionHighlightsByLayerId = {};
+ }
+
+ const extraHighlightsByLayerId = this.extraHighlightsByLayerId || {};
+
+ for (let i = 1; i <= layers.length; i++) {
+ // Generate quads back-to-front.
+ const layer = layers[layers.length - i];
+ alreadyVisitedLayerIds[layer.layerId] = true;
+ if (layer.objectInstance.name === 'cc::NinePatchLayerImpl') {
+ continue;
+ }
+
+ const layerQuad = layer.layerQuad.clone();
+ if (layer.usingGpuRasterization) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ layerQuad.borderWidth = 2.0 * pixelRatio;
+ layerQuad.borderColor = 'rgba(154,205,50,0.75)';
+ } else {
+ layerQuad.borderColor = 'rgba(0,0,0,0.75)';
+ }
+ layerQuad.stackingGroupId = nextStackingGroupId++;
+ layerQuad.selectionToSetIfClicked = new cc.LayerSelection(layer);
+ layerQuad.layer = layer;
+ if (this.showOtherLayers && this.selectedLayer === layer) {
+ layerQuad.upperBorderColor = 'rgb(156,189,45)';
+ }
+
+ if (this.showAnimationBounds) {
+ this.appendAnimationQuads_(quads, layer, layerQuad);
+ }
+
+ this.appendImageQuads_(quads, layer, layerQuad);
+ quads.push(layerQuad);
+
+
+ if (this.showInvalidations) {
+ this.appendInvalidationQuads_(quads, layer, layerQuad);
+ }
+ if (this.showUnrecordedRegion) {
+ this.appendUnrecordedRegionQuads_(quads, layer, layerQuad);
+ }
+ if (this.showBottlenecks) {
+ this.appendBottleneckQuads_(quads, layer, layerQuad,
+ layerQuad.stackingGroupId);
+ }
+ if (this.showLayoutRects) {
+ this.appendLayoutRectQuads_(quads, layer, layerQuad);
+ }
+
+ if (this.howToShowTiles === 'coverage') {
+ this.appendTileCoverageRectQuads_(
+ quads, layer, layerQuad, this.tileHeatmapType);
+ } else if (this.howToShowTiles !== 'none') {
+ this.appendTilesWithScaleQuads_(
+ quads, layer, layerQuad,
+ this.howToShowTiles, this.tileHeatmapType);
+ }
+
+ let highlights;
+ highlights = extraHighlightsByLayerId[layer.layerId];
+ if (highlights) {
+ this.appendHighlightQuadsForLayer_(
+ quads, layer, layerQuad, highlights);
+ }
+
+ highlights = selectionHighlightsByLayerId[layer.layerId];
+ if (highlights) {
+ this.appendHighlightQuadsForLayer_(
+ quads, layer, layerQuad, highlights);
+ }
+ }
+
+ this.layerTreeImpl.iterLayers(function(layer, depth, isMask, isReplica) {
+ if (!this.showOtherLayers && this.selectedLayer !== layer) return;
+ if (alreadyVisitedLayerIds[layer.layerId]) return;
+
+ const layerQuad = layer.layerQuad;
+ const stackingGroupId = nextStackingGroupId++;
+ if (this.showBottlenecks) {
+ this.appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId);
+ }
+ }, this);
+
+ const tracedInputLatencies = this.layerTreeImpl.tracedInputLatencies;
+ if (this.showInputEvents && tracedInputLatencies) {
+ for (let i = 0; i < tracedInputLatencies.length; i++) {
+ const coordinatesArray =
+ tracedInputLatencies[i].args.data.coordinates;
+ for (let j = 0; j < coordinatesArray.length; j++) {
+ const inputQuad = tr.b.math.Quad.fromXYWH(
+ coordinatesArray[j].x - 25,
+ coordinatesArray[j].y - 25,
+ 50,
+ 50);
+ inputQuad.borderColor = 'rgba(0, 0, 0, 0)';
+ inputQuad.imageData = this.inputEventImageData_;
+ quads.push(inputQuad);
+ }
+ }
+ }
+
+ return quads;
+ },
+
+ updateInfoBar_(infoBarMessages) {
+ if (infoBarMessages.length) {
+ this.infoBar_.removeAllButtons();
+ this.infoBar_.message = 'Some problems were encountered...';
+ this.infoBar_.addButton('More info...', function(e) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = '';
+ infoBarMessages.forEach(function(message) {
+ const title = document.createElement('h3');
+ Polymer.dom(title).textContent = message.header;
+
+ const details = document.createElement('div');
+ Polymer.dom(details).textContent = message.details;
+
+ Polymer.dom(overlay).appendChild(title);
+ Polymer.dom(overlay).appendChild(details);
+ });
+ overlay.visible = true;
+
+ e.stopPropagation();
+ return false;
+ });
+ this.infoBar_.visible = true;
+ } else {
+ this.infoBar_.removeAllButtons();
+ this.infoBar_.message = '';
+ this.infoBar_.visible = false;
+ }
+ },
+
+ getWhatRasterized_() {
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const renderProcess = lthi.objectInstance.parent;
+ const tasks = [];
+ for (const event of renderProcess.getDescendantEvents()) {
+ if (!(event instanceof tr.model.Slice)) continue;
+
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(event);
+ if (tile === undefined) continue;
+
+ if (tile.containingSnapshot === lthi) {
+ tasks.push(event);
+ }
+ }
+ return tasks;
+ },
+
+ updateWhatRasterizedLinkState_() {
+ const tasks = this.getWhatRasterized_();
+ if (tasks.length) {
+ Polymer.dom(this.whatRasterizedLink_).textContent =
+ tasks.length + ' raster tasks';
+ this.whatRasterizedLink_.style.display = '';
+ } else {
+ Polymer.dom(this.whatRasterizedLink_).textContent = '';
+ this.whatRasterizedLink_.style.display = 'none';
+ }
+ },
+
+ getWhatRasterizedEventSet_() {
+ return new tr.model.EventSet(this.getWhatRasterized_());
+ }
+ };
+
+ return {
+ LayerTreeQuadStackView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html
new file mode 100644
index 00000000000..66932ae785f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('tileCoverageRectCount', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ view.layerTreeImpl = lthi.activeTree;
+ view.howToShowTiles = 'none';
+ view.showInvalidations = false;
+ view.showContents = false;
+
+ // There should be some quads drawn with all "show" checkboxes off,
+ // but that number can change with new features added.
+ const aQuads = view.generateLayerQuads();
+ view.howToShowTiles = 'coverage';
+ const bQuads = view.generateLayerQuads();
+ const numCoverageRects = bQuads.length - aQuads.length;
+
+ // We know we have 5 coverage rects in lthi cats.
+ assert.strictEqual(numCoverageRects, 5);
+ });
+
+ test('inputEvent', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+ lthi.activeTree.tracedInputLatencies =
+ [{args: {data: {coordinates: [{x: 10, y: 20}, {x: 30, y: 40}]}}}];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ view.layerTreeImpl = lthi.activeTree;
+ view.showInputEvents = false;
+
+ const aQuads = view.generateLayerQuads();
+ view.showInputEvents = true;
+ const bQuads = view.generateLayerQuads();
+ const numInputEventRects = bQuads.length - aQuads.length;
+
+ assert.strictEqual(numInputEventRects, 2);
+
+ // We should not start loading the image until the view is added into the
+ // DOM tree.
+ const image = Polymer.dom(view).querySelector('#input-event');
+ assert.strictEqual(getComputedStyle(image).backgroundImage, '');
+ assert.strictEqual(image.src, '');
+
+ document.body.appendChild(view);
+ view.updateContents_();
+ assert.notEqual(getComputedStyle(image).backgroundImage, '');
+ assert.notEqual(image.src, '');
+ view.remove();
+ });
+
+ test('invalidation', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ view.layerTreeImpl = lthi.activeTree;
+ view.showInvalidations = false;
+
+ const aQuads = view.generateLayerQuads();
+ view.showInvalidations = true;
+ const bQuads = view.generateLayerQuads();
+ const numInvalidationRects = bQuads.length - aQuads.length;
+
+ // We know we have 3 invalidation rects.
+ assert.strictEqual(numInvalidationRects, 3);
+
+ const expectedRectTypes = [
+ 'Invalidation rect (appeared) for client1',
+ 'Invalidation rect (disappeared) for client2',
+ 'Invalidation rect' // The non-annotated rect.
+ ];
+ const found = [];
+ for (const quad of bQuads) {
+ const i = expectedRectTypes.indexOf(quad.selectionToSetIfClicked &&
+ quad.selectionToSetIfClicked.rectType_);
+ if (i !== -1) {
+ found[i] = true;
+ }
+ }
+ assert.deepEqual(found, [true, true, true]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html
new file mode 100644
index 00000000000..56ecf770ec1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview LayerView coordinates graphical and analysis views of layers.
+ */
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const constants = tr.e.cc.constants;
+
+ /**
+ * @constructor
+ */
+ const LayerView = tr.ui.b.define('tr-ui-e-chrome-cc-layer-view');
+
+ LayerView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.flexDirection = 'column';
+ this.style.display = 'flex';
+
+ this.layerTreeQuadStackView_ =
+ new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ this.dragBar_ = document.createElement('tr-ui-b-drag-handle');
+ this.analysisEl_ =
+ document.createElement('tr-ui-e-chrome-cc-layer-view-analysis');
+ this.analysisEl_.style.flexGrow = 0;
+ this.analysisEl_.style.flexShrink = 0;
+ this.analysisEl_.style.flexBasis = 'auto';
+ this.analysisEl_.style.height = '150px';
+ this.analysisEl_.style.overflow = 'auto';
+ this.analysisEl_.addEventListener('requestSelectionChange',
+ this.onRequestSelectionChangeFromAnalysisEl_.bind(this));
+
+ this.dragBar_.target = this.analysisEl_;
+
+ Polymer.dom(this).appendChild(this.layerTreeQuadStackView_);
+ Polymer.dom(this).appendChild(this.dragBar_);
+ Polymer.dom(this).appendChild(this.analysisEl_);
+
+ this.layerTreeQuadStackView_.addEventListener('selection-change',
+ function() {
+ this.layerTreeQuadStackViewSelectionChanged_();
+ }.bind(this));
+ this.layerTreeQuadStackViewSelectionChanged_();
+ },
+
+ get layerTreeImpl() {
+ return this.layerTreeQuadStackView_.layerTreeImpl;
+ },
+
+ set layerTreeImpl(newValue) {
+ return this.layerTreeQuadStackView_.layerTreeImpl = newValue;
+ },
+
+ set isRenderPassQuads(newValue) {
+ return this.layerTreeQuadStackView_.isRenderPassQuads = newValue;
+ },
+
+ get selection() {
+ return this.layerTreeQuadStackView_.selection;
+ },
+
+ set selection(newValue) {
+ this.layerTreeQuadStackView_.selection = newValue;
+ },
+
+ regenerateContent() {
+ this.layerTreeQuadStackView_.regenerateContent();
+ },
+
+ layerTreeQuadStackViewSelectionChanged_() {
+ const selection = this.layerTreeQuadStackView_.selection;
+ if (selection) {
+ this.dragBar_.style.display = '';
+ this.analysisEl_.style.display = '';
+ Polymer.dom(this.analysisEl_).textContent = '';
+
+ const layer = selection.layer;
+ if (tr.e.cc.PictureSnapshot.CanDebugPicture() &&
+ layer &&
+ layer.args &&
+ layer.args.pictures &&
+ layer.args.pictures.length) {
+ Polymer.dom(this.analysisEl_).appendChild(
+ this.createPictureBtn_(layer.args.pictures));
+ }
+
+ const analysis = selection.createAnalysis();
+ Polymer.dom(this.analysisEl_).appendChild(analysis);
+ for (const child of this.analysisEl_.children) {
+ child.style.userSelect = 'text';
+ }
+ } else {
+ this.dragBar_.style.display = 'none';
+ this.analysisEl_.style.display = 'none';
+ const analysis = Polymer.dom(this.analysisEl_).firstChild;
+ if (analysis) {
+ Polymer.dom(this.analysisEl_).removeChild(analysis);
+ }
+ this.layerTreeQuadStackView_.style.height =
+ window.getComputedStyle(this).height;
+ }
+ tr.b.dispatchSimpleEvent(this, 'selection-change');
+ },
+
+ createPictureBtn_(pictures) {
+ if (!(pictures instanceof Array)) {
+ pictures = [pictures];
+ }
+
+ const link = document.createElement('tr-ui-a-analysis-link');
+ link.selection = function() {
+ const layeredPicture = new tr.e.cc.LayeredPicture(pictures);
+ const snapshot = new tr.e.cc.PictureSnapshot(layeredPicture);
+ snapshot.picture = layeredPicture;
+
+ const selection = new tr.model.EventSet();
+ selection.push(snapshot);
+ return selection;
+ };
+ Polymer.dom(link).textContent = 'View in Picture Debugger';
+ return link;
+ },
+
+ onRequestSelectionChangeFromAnalysisEl_(e) {
+ if (!(e.selection instanceof tr.ui.e.chrome.cc.Selection)) {
+ return;
+ }
+
+ e.stopPropagation();
+ this.selection = e.selection;
+ },
+
+ get extraHighlightsByLayerId() {
+ return this.layerTreeQuadStackView_.extraHighlightsByLayerId;
+ },
+
+ set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+ this.layerTreeQuadStackView_.extraHighlightsByLayerId =
+ extraHighlightsByLayerId;
+ }
+ };
+
+ return {
+ LayerView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html
new file mode 100644
index 00000000000..ed3de7b87e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+ const numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+ const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+
+ const view = new tr.ui.e.chrome.cc.LayerView();
+ view.style.height = '500px';
+ view.layerTreeImpl = lthi.activeTree;
+ view.selection = new tr.ui.e.chrome.cc.LayerSelection(layer);
+
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_withTileHighlight', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+ const numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+ const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+ const tile = lthi.activeTiles[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerView();
+ view.style.height = '500px';
+ view.layerTreeImpl = lthi.activeTree;
+ view.selection = new tr.ui.e.chrome.cc.TileSelection(tile);
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html
new file mode 100644
index 00000000000..5dc62b1b4f6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html
@@ -0,0 +1,455 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import"
+ href="/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html">
+
+<template id="tr-ui-e-chrome-cc-picture-debugger-template">
+ <left-panel>
+ <picture-info>
+ <div>
+ <span class='title'>Skia Picture</span>
+ <span class='size'></span>
+ </div>
+ <div>
+ <input class='filename' type='text' value='skpicture.skp' />
+ <button class='export'>Export</button>
+ </div>
+ </picture-info>
+ </left-panel>
+ <right-panel>
+ <tr-ui-e-chrome-cc-picture-ops-chart-view>
+ </tr-ui-e-chrome-cc-picture-ops-chart-view>
+ <raster-area><canvas></canvas></raster-area>
+ </right-panel>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ /**
+ * PictureDebugger is a view of a PictureSnapshot for inspecting
+ * the picture in detail. (e.g., timing information, etc.)
+ *
+ * @constructor
+ */
+ const PictureDebugger = tr.ui.b.define('tr-ui-e-chrome-cc-picture-debugger');
+
+ PictureDebugger.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ const node = tr.ui.b.instantiateTemplate(
+ '#tr-ui-e-chrome-cc-picture-debugger-template', THIS_DOC);
+
+ Polymer.dom(this).appendChild(node);
+
+ this.style.display = 'flex';
+ this.style.flexDirection = 'row';
+
+ const title = this.querySelector('.title');
+ title.style.fontWeight = 'bold';
+ title.style.marginLeft = '5px';
+ title.style.marginRight = '5px';
+
+ this.pictureAsImageData_ = undefined;
+ this.showOverdraw_ = false;
+ this.zoomScaleValue_ = 1;
+
+ this.sizeInfo_ = Polymer.dom(this).querySelector('.size');
+ this.rasterArea_ = Polymer.dom(this).querySelector('raster-area');
+ this.rasterArea_.style.backgroundColor = '#ddd';
+ this.rasterArea_.style.minHeight = '100px';
+ this.rasterArea_.style.minWidth = '200px';
+ this.rasterArea_.style.overflow = 'auto';
+ this.rasterArea_.style.paddingLeft = '5px';
+ this.rasterCanvas_ = Polymer.dom(this.rasterArea_)
+ .querySelector('canvas');
+ this.rasterCtx_ = this.rasterCanvas_.getContext('2d');
+
+ this.filename_ = Polymer.dom(this).querySelector('.filename');
+ this.filename_.style.userSelect = 'text';
+ this.filename_.style.marginLeft = '5px';
+
+ this.drawOpsChartSummaryView_ =
+ new tr.ui.e.chrome.cc.PictureOpsChartSummaryView();
+ this.drawOpsChartView_ = new tr.ui.e.chrome.cc.PictureOpsChartView();
+ this.drawOpsChartView_.addEventListener(
+ 'selection-changed', this.onChartBarClicked_.bind(this));
+
+ this.exportButton_ = Polymer.dom(this).querySelector('.export');
+ this.exportButton_.addEventListener(
+ 'click', this.onSaveAsSkPictureClicked_.bind(this));
+
+ this.trackMouse_();
+
+ const overdrawCheckbox = tr.ui.b.createCheckBox(
+ this, 'showOverdraw',
+ 'pictureView.showOverdraw', false,
+ 'Show overdraw');
+
+ const chartCheckbox = tr.ui.b.createCheckBox(
+ this, 'showSummaryChart',
+ 'pictureView.showSummaryChart', false,
+ 'Show timing summary');
+
+ const pictureInfo = Polymer.dom(this).querySelector('picture-info');
+ pictureInfo.style.flexGrow = 0;
+ pictureInfo.style.flexShrink = 0;
+ pictureInfo.style.flexBasis = 'auto';
+ pictureInfo.style.paddingTop = '2px';
+ Polymer.dom(pictureInfo).appendChild(overdrawCheckbox);
+ Polymer.dom(pictureInfo).appendChild(chartCheckbox);
+
+ this.drawOpsView_ = new tr.ui.e.chrome.cc.PictureOpsListView();
+ this.drawOpsView_.flexGrow = 1;
+ this.drawOpsView_.flexShrink = 1;
+ this.drawOpsView_.flexBasis = 'auto';
+ this.drawOpsView_.addEventListener(
+ 'selection-changed', this.onChangeDrawOps_.bind(this));
+
+ const leftPanel = Polymer.dom(this).querySelector('left-panel');
+ leftPanel.style.flexDirection = 'column';
+ leftPanel.style.display = 'flex';
+ leftPanel.style.flexGrow = 0;
+ leftPanel.style.flexShrink = 0;
+ leftPanel.style.flexBasis = 'auto';
+ leftPanel.style.minWidth = '200px';
+ leftPanel.style.overflow = 'auto';
+ Polymer.dom(leftPanel).appendChild(this.drawOpsChartSummaryView_);
+ Polymer.dom(leftPanel).appendChild(this.drawOpsView_);
+
+ const middleDragHandle = document.createElement('tr-ui-b-drag-handle');
+ middleDragHandle.style.flexGrow = 0;
+ middleDragHandle.style.flexShrink = 0;
+ middleDragHandle.style.flexBasis = 'auto';
+ middleDragHandle.horizontal = false;
+ middleDragHandle.target = leftPanel;
+
+ const rightPanel = Polymer.dom(this).querySelector('right-panel');
+ rightPanel.style.flexGrow = 1;
+ rightPanel.style.flexShrink = 1;
+ rightPanel.style.flexBasis = 'auto';
+ rightPanel.style.minWidth = 0;
+ rightPanel.style.flexDirection = 'column';
+ rightPanel.style.display = 'flex';
+
+ const chartView = Polymer.dom(rightPanel).querySelector(
+ 'tr-ui-e-chrome-cc-picture-ops-chart-view');
+ this.drawOpsChartView_.style.flexGrow = 0;
+ this.drawOpsChartView_.style.flexShrink = 0;
+ this.drawOpsChartView_.style.flexBasis = 'auto';
+ this.drawOpsChartView_.style.minWidth = 0;
+ this.drawOpsChartView_.style.overflowX = 'auto';
+ this.drawOpsChartView_.style.overflowY = 'hidden';
+ rightPanel.replaceChild(this.drawOpsChartView_, chartView);
+
+ this.infoBar_ = document.createElement('tr-ui-b-info-bar');
+ Polymer.dom(this.rasterArea_).appendChild(this.infoBar_);
+
+ Polymer.dom(this).insertBefore(middleDragHandle, rightPanel);
+
+ this.picture_ = undefined;
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ thisArg: this,
+ keyCode: 'h'.charCodeAt(0),
+ callback(e) {
+ this.moveSelectedOpBy(-1);
+ e.stopPropagation();
+ }
+ }));
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ thisArg: this,
+ keyCode: 'l'.charCodeAt(0),
+ callback(e) {
+ this.moveSelectedOpBy(1);
+ e.stopPropagation();
+ }
+ }));
+ Polymer.dom(this).appendChild(hkc);
+ },
+
+ onSaveAsSkPictureClicked_() {
+ // Decode base64 data into a String
+ const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData());
+
+ // Convert this String into an Uint8Array
+ const length = rawData.length;
+ const arrayBuffer = new ArrayBuffer(length);
+ const uint8Array = new Uint8Array(arrayBuffer);
+ for (let c = 0; c < length; c++) {
+ uint8Array[c] = rawData.charCodeAt(c);
+ }
+
+ // Create a blob URL from the binary array.
+ const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
+ const blobUrl = window.webkitURL.createObjectURL(blob);
+
+ // Create a link and click on it. BEST API EVAR!
+ const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+ link.href = blobUrl;
+ link.download = this.filename_.value;
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(
+ 'click', true, false, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ link.dispatchEvent(event);
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.drawOpsView_.picture = picture;
+ this.drawOpsChartView_.picture = picture;
+ this.drawOpsChartSummaryView_.picture = picture;
+ this.picture_ = picture;
+
+ this.exportButton_.disabled = !this.picture_.canSave;
+
+ if (picture) {
+ const size = this.getRasterCanvasSize_();
+ this.rasterCanvas_.width = size.width;
+ this.rasterCanvas_.height = size.height;
+ }
+
+ const bounds = this.rasterArea_.getBoundingClientRect();
+ const selectorBounds = this.mouseModeSelector_.getBoundingClientRect();
+ this.mouseModeSelector_.pos = {
+ x: (bounds.right - selectorBounds.width - 10),
+ y: bounds.top
+ };
+
+ this.rasterize_();
+
+ this.scheduleUpdateContents_();
+ },
+
+ getRasterCanvasSize_() {
+ const style = window.getComputedStyle(this.rasterArea_);
+ const width =
+ Math.max(parseInt(style.width), this.picture_.layerRect.width);
+ const height =
+ Math.max(parseInt(style.height), this.picture_.layerRect.height);
+
+ return {
+ width,
+ height
+ };
+ },
+
+ scheduleUpdateContents_() {
+ if (this.updateContentsPending_) return;
+
+ this.updateContentsPending_ = true;
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContents_.bind(this)
+ );
+ },
+
+ updateContents_() {
+ this.updateContentsPending_ = false;
+
+ if (this.picture_) {
+ Polymer.dom(this.sizeInfo_).textContent = '(' +
+ this.picture_.layerRect.width + ' x ' +
+ this.picture_.layerRect.height + ')';
+ }
+
+ this.drawOpsChartView_.updateChartContents();
+ this.drawOpsChartView_.scrollSelectedItemIntoViewIfNecessary();
+
+ // Return if picture hasn't finished rasterizing.
+ if (!this.pictureAsImageData_) return;
+
+ this.infoBar_.visible = false;
+ this.infoBar_.removeAllButtons();
+ if (this.pictureAsImageData_.error) {
+ this.infoBar_.message = 'Cannot rasterize...';
+ this.infoBar_.addButton('More info...', function(e) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = this.pictureAsImageData_.error;
+ overlay.visible = true;
+ e.stopPropagation();
+ return false;
+ }.bind(this));
+ this.infoBar_.visible = true;
+ }
+
+ this.drawPicture_();
+ },
+
+ drawPicture_() {
+ const size = this.getRasterCanvasSize_();
+ if (size.width !== this.rasterCanvas_.width) {
+ this.rasterCanvas_.width = size.width;
+ }
+ if (size.height !== this.rasterCanvas_.height) {
+ this.rasterCanvas_.height = size.height;
+ }
+
+ this.rasterCtx_.clearRect(0, 0, size.width, size.height);
+
+ if (!this.pictureAsImageData_.imageData) return;
+
+ const imgCanvas = this.pictureAsImageData_.asCanvas();
+ const w = imgCanvas.width;
+ const h = imgCanvas.height;
+ this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h,
+ 0, 0, w * this.zoomScaleValue_,
+ h * this.zoomScaleValue_);
+ },
+
+ rasterize_() {
+ if (this.picture_) {
+ this.picture_.rasterize(
+ {
+ stopIndex: this.drawOpsView_.selectedOpIndex,
+ showOverdraw: this.showOverdraw_
+ },
+ this.onRasterComplete_.bind(this));
+ }
+ },
+
+ onRasterComplete_(pictureAsImageData) {
+ this.pictureAsImageData_ = pictureAsImageData;
+ this.scheduleUpdateContents_();
+ },
+
+ moveSelectedOpBy(increment) {
+ if (this.selectedOpIndex === undefined) {
+ this.selectedOpIndex = 0;
+ return;
+ }
+ this.selectedOpIndex = tr.b.math.clamp(
+ this.selectedOpIndex + increment,
+ 0, this.numOps);
+ },
+
+ get numOps() {
+ return this.drawOpsView_.numOps;
+ },
+
+ get selectedOpIndex() {
+ return this.drawOpsView_.selectedOpIndex;
+ },
+
+ set selectedOpIndex(index) {
+ this.drawOpsView_.selectedOpIndex = index;
+ this.drawOpsChartView_.selectedOpIndex = index;
+ },
+
+ onChartBarClicked_(e) {
+ this.drawOpsView_.selectedOpIndex =
+ this.drawOpsChartView_.selectedOpIndex;
+ },
+
+ onChangeDrawOps_(e) {
+ this.rasterize_();
+ this.scheduleUpdateContents_();
+
+ this.drawOpsChartView_.selectedOpIndex =
+ this.drawOpsView_.selectedOpIndex;
+ },
+
+ set showOverdraw(v) {
+ this.showOverdraw_ = v;
+ this.rasterize_();
+ },
+
+ set showSummaryChart(chartShouldBeVisible) {
+ if (chartShouldBeVisible) {
+ this.drawOpsChartSummaryView_.show();
+ } else {
+ this.drawOpsChartSummaryView_.hide();
+ }
+ },
+
+ trackMouse_() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this.rasterArea_;
+ Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_);
+
+ this.mouseModeSelector_.supportedModeMask =
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector';
+
+ this.mouseModeSelector_.addEventListener('beginzoom',
+ this.onBeginZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatezoom',
+ this.onUpdateZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('endzoom',
+ this.onEndZoom_.bind(this));
+ },
+
+ onBeginZoom_(e) {
+ this.isZooming_ = true;
+
+ this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+
+ e.preventDefault();
+ },
+
+ onUpdateZoom_(e) {
+ if (!this.isZooming_) return;
+
+ const currentMouseViewPos = this.extractRelativeMousePosition_(e);
+
+ // Take the distance the mouse has moved and we want to zoom at about
+ // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned
+ // more if people feel it's too slow.
+ this.zoomScaleValue_ +=
+ ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001);
+ this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1);
+
+ this.drawPicture_();
+
+ this.lastMouseViewPos_ = currentMouseViewPos;
+ },
+
+ onEndZoom_(e) {
+ this.lastMouseViewPos_ = undefined;
+ this.isZooming_ = false;
+ e.preventDefault();
+ },
+
+ extractRelativeMousePosition_(e) {
+ return {
+ x: e.clientX - this.rasterArea_.offsetLeft,
+ y: e.clientY - this.rasterArea_.offsetTop
+ };
+ }
+ };
+
+ return {
+ PictureDebugger,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html
new file mode 100644
index 00000000000..e89e6b355e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const picture = new tr.e.cc.PictureSnapshot({id: '31415'}, 10, {
+ 'params': {
+ 'opaque_rect': [-15, -15, 0, 0],
+ 'layer_rect': [-15, -15, 46, 833]
+ },
+ 'skp64': 'DAAAAHYEAADzAQAABwAAAAFkYWVy8AAAAAgAAB4DAAAADAAAIAAAgD8AAIA/CAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADoAAAACAAAHgMAAAAMAAAjAAAAAAAAAAAMAAAjAAAAAAAAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADkAAAAGAAAFQEAAAAAAAAAAAAAAADAjkQAgPlDGAAAFQIAAAAAAAAAAAAAAADAjkQAgPlDCAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADgAAAAGAAAFQMAAAAAAKBAAACgQAAAgEIAAIBCBAAAHAQAABwEAAAcBAAAHHRjYWYBAAAADVNrU3JjWGZlcm1vZGVjZnB0AAAAAHlhcmGgAAAAIHRucAMAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgEAAAP//ADABAAAAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgED/////AjABAAAAAAAAAAAAAAAAAAEAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEEAAIA/AAAAAAAAgD8AAAAAAACAQP8AAP8AMAEAAAAAACBmb2U=' // @suppress longLineCheck
+ });
+ picture.preInitialize();
+ picture.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.PictureDebugger();
+ this.addHTMLOutput(dbg);
+ dbg.picture = picture;
+ dbg.style.border = '1px solid black';
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html
new file mode 100644
index 00000000000..55a8685aee0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html
@@ -0,0 +1,458 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const OPS_TIMING_ITERATIONS = 3;
+ const CHART_PADDING_LEFT = 65;
+ const CHART_PADDING_RIGHT = 40;
+ const AXIS_PADDING_LEFT = 60;
+ const AXIS_PADDING_RIGHT = 35;
+ const AXIS_PADDING_TOP = 25;
+ const AXIS_PADDING_BOTTOM = 45;
+ const AXIS_LABEL_PADDING = 5;
+ const AXIS_TICK_SIZE = 10;
+ const LABEL_PADDING = 5;
+ const LABEL_INTERLEAVE_OFFSET = 15;
+ const BAR_PADDING = 5;
+ const VERTICAL_TICKS = 5;
+ const HUE_CHAR_CODE_ADJUSTMENT = 5.7;
+
+ /**
+ * Provides a chart showing the cumulative time spent in Skia operations
+ * during picture rasterization.
+ *
+ * @constructor
+ */
+ const PictureOpsChartSummaryView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-picture-ops-chart-summary-view');
+
+ PictureOpsChartSummaryView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.flexGrow = 0;
+ this.style.flexShrink = 0;
+ this.style.flexBasis = 'auto';
+ this.style.fontSize = 0;
+ this.style.margin = 0;
+ this.style.minHeight = '200px';
+ this.style.minWidth = '200px';
+ this.style.overflow = 'hidden';
+ this.style.padding = 0;
+
+ this.picture_ = undefined;
+ this.pictureDataProcessed_ = false;
+
+ this.chartScale_ = window.devicePixelRatio;
+
+ this.chart_ = document.createElement('canvas');
+ this.chartCtx_ = this.chart_.getContext('2d');
+ Polymer.dom(this).appendChild(this.chart_);
+
+ this.opsTimingData_ = [];
+
+ this.chartWidth_ = 0;
+ this.chartHeight_ = 0;
+ this.requiresRedraw_ = true;
+
+ this.currentBarMouseOverTarget_ = null;
+
+ this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+ new ResizeObserver(this.onResize_.bind(this)).observe(this);
+ },
+
+ get requiresRedraw() {
+ return this.requiresRedraw_;
+ },
+
+ set requiresRedraw(requiresRedraw) {
+ this.requiresRedraw_ = requiresRedraw;
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ this.pictureDataProcessed_ = false;
+
+ if (Polymer.dom(this).classList.contains('hidden')) return;
+
+ this.processPictureData_();
+ this.requiresRedraw = true;
+ this.updateChartContents();
+ },
+
+ hide() {
+ Polymer.dom(this).classList.add('hidden');
+ this.style.display = 'none';
+ },
+
+ show() {
+ Polymer.dom(this).classList.remove('hidden');
+ this.style.display = '';
+
+ if (!this.pictureDataProcessed_) {
+ this.processPictureData_();
+ }
+ this.requiresRedraw = true;
+ this.updateChartContents();
+ },
+
+ onMouseMove_(e) {
+ const lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
+ this.currentBarMouseOverTarget_ = null;
+
+ const x = e.offsetX;
+ const y = e.offsetY;
+
+ const chartLeft = CHART_PADDING_LEFT;
+ const chartRight = this.chartWidth_ - CHART_PADDING_RIGHT;
+ const chartTop = AXIS_PADDING_TOP;
+ const chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM;
+ const chartInnerWidth = chartRight - chartLeft;
+
+ if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
+ this.currentBarMouseOverTarget_ = Math.floor(
+ (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
+
+ this.currentBarMouseOverTarget_ = tr.b.math.clamp(
+ this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
+ }
+
+ if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) return;
+
+ this.drawChartContents_();
+ },
+
+ onResize_() {
+ this.requiresRedraw = true;
+ this.updateChartContents();
+ },
+
+ updateChartContents() {
+ if (this.requiresRedraw) {
+ this.updateChartDimensions_();
+ }
+
+ this.drawChartContents_();
+ },
+
+ updateChartDimensions_() {
+ this.chartWidth_ = this.offsetWidth;
+ this.chartHeight_ = this.offsetHeight;
+
+ // Scale up the canvas according to the devicePixelRatio, then reduce it
+ // down again via CSS. Finally we apply a scale to the canvas so that
+ // things are drawn at the correct size.
+ this.chart_.width = this.chartWidth_ * this.chartScale_;
+ this.chart_.height = this.chartHeight_ * this.chartScale_;
+
+ this.chart_.style.width = this.chartWidth_ + 'px';
+ this.chart_.style.height = this.chartHeight_ + 'px';
+
+ this.chartCtx_.scale(this.chartScale_, this.chartScale_);
+ },
+
+ processPictureData_() {
+ this.resetOpsTimingData_();
+ this.pictureDataProcessed_ = true;
+
+ if (!this.picture_) return;
+
+ let ops = this.picture_.getOps();
+ if (!ops) return;
+
+ ops = this.picture_.tagOpsWithTimings(ops);
+
+ // Check that there are valid times.
+ if (ops[0].cmd_time === undefined) return;
+
+ this.collapseOpsToTimingBuckets_(ops);
+ },
+
+ drawChartContents_() {
+ this.clearChartContents_();
+
+ if (this.opsTimingData_.length === 0) {
+ this.showNoTimingDataMessage_();
+ return;
+ }
+
+ this.drawChartAxes_();
+ this.drawBars_();
+ this.drawLineAtBottomOfChart_();
+
+ if (this.currentBarMouseOverTarget_ === null) return;
+
+ this.drawTooltip_();
+ },
+
+ drawLineAtBottomOfChart_() {
+ this.chartCtx_.strokeStyle = '#AAA';
+ this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
+ this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
+ this.chartCtx_.stroke();
+ },
+
+ drawTooltip_() {
+ const tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
+ const tooltipTitle = tooltipData.cmd_string;
+ const tooltipTime = tooltipData.cmd_time.toFixed(4);
+
+ const tooltipWidth = 110;
+ const tooltipHeight = 40;
+ const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
+ CHART_PADDING_LEFT;
+ const barWidth = chartInnerWidth / this.opsTimingData_.length;
+ const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
+
+ const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
+ barWidth - tooltipOffset;
+ const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.shadowOffsetX = 0;
+ this.chartCtx_.shadowOffsetY = 5;
+ this.chartCtx_.shadowBlur = 4;
+ this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
+
+ this.chartCtx_.strokeStyle = '#888';
+ this.chartCtx_.fillStyle = '#EEE';
+ this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.shadowColor = 'transparent';
+ this.chartCtx_.translate(0.5, 0.5);
+ this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.restore();
+
+ this.chartCtx_.fillStyle = '#222';
+ this.chartCtx_.textBaseline = 'top';
+ this.chartCtx_.font = '800 12px Arial';
+ this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
+
+ this.chartCtx_.fillStyle = '#555';
+ this.chartCtx_.textBaseline = 'top';
+ this.chartCtx_.font = '400 italic 10px Arial';
+ this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms',
+ left + 8, top + 22);
+ },
+
+ drawBars_() {
+ const len = this.opsTimingData_.length;
+ const max = this.opsTimingData_[0].cmd_time;
+ const min = this.opsTimingData_[len - 1].cmd_time;
+
+ const width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT;
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+ const barWidth = Math.floor(width / len);
+
+ let opData;
+ let opTiming;
+ let opHeight;
+ let opLabel;
+ let barLeft;
+
+ for (let b = 0; b < len; b++) {
+ opData = this.opsTimingData_[b];
+ opTiming = opData.cmd_time / max;
+
+ opHeight = Math.round(Math.max(1, opTiming * height));
+ opLabel = opData.cmd_string;
+ barLeft = CHART_PADDING_LEFT + b * barWidth;
+
+ this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
+
+ this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
+ height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
+ }
+ },
+
+ getOpColor_(opName) {
+ const characters = opName.split('');
+ const hue = characters.reduce(this.reduceNameToHue, 0) % 360;
+
+ return 'hsl(' + hue + ', 30%, 50%)';
+ },
+
+ reduceNameToHue(previousValue, currentValue, index, array) {
+ // Get the char code and apply a magic adjustment value so we get
+ // pretty colors from around the rainbow.
+ return Math.round(previousValue + currentValue.charCodeAt(0) *
+ HUE_CHAR_CODE_ADJUSTMENT);
+ },
+
+ drawChartAxes_() {
+ const len = this.opsTimingData_.length;
+ const max = this.opsTimingData_[0].cmd_time;
+ const min = this.opsTimingData_[len - 1].cmd_time;
+
+ const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+
+ const totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
+ CHART_PADDING_RIGHT;
+ const barWidth = Math.floor(totalBarWidth / len);
+ const tickYInterval = height / (VERTICAL_TICKS - 1);
+ let tickYPosition = 0;
+ const tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
+ let tickVal = 0;
+
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.strokeStyle = '#777';
+ this.chartCtx_.save();
+
+ // Translate half a pixel to avoid blurry lines.
+ this.chartCtx_.translate(0.5, 0.5);
+
+ // Sides.
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+ this.chartCtx_.moveTo(0, 0);
+ this.chartCtx_.lineTo(0, height);
+ this.chartCtx_.lineTo(width, height);
+
+ // Y-axis ticks.
+ this.chartCtx_.font = '10px Arial';
+ this.chartCtx_.textAlign = 'right';
+ this.chartCtx_.textBaseline = 'middle';
+
+ for (let t = 0; t < VERTICAL_TICKS; t++) {
+ tickYPosition = Math.round(t * tickYInterval);
+ tickVal = (max - t * tickValInterval).toFixed(4);
+
+ this.chartCtx_.moveTo(0, tickYPosition);
+ this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
+ this.chartCtx_.fillText(tickVal,
+ -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
+ }
+
+ this.chartCtx_.stroke();
+
+ this.chartCtx_.restore();
+
+
+ // Labels.
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
+ AXIS_PADDING_TOP + height + LABEL_PADDING);
+
+ this.chartCtx_.font = '10px Arial';
+ this.chartCtx_.textAlign = 'center';
+ this.chartCtx_.textBaseline = 'top';
+
+ let labelTickLeft;
+ let labelTickBottom;
+ for (let l = 0; l < len; l++) {
+ labelTickLeft = Math.round(l * barWidth);
+ labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
+
+ this.chartCtx_.save();
+ this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING);
+ this.chartCtx_.lineTo(labelTickLeft, labelTickBottom);
+ this.chartCtx_.stroke();
+ this.chartCtx_.restore();
+
+ this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
+ labelTickLeft, labelTickBottom);
+ }
+
+ this.chartCtx_.restore();
+
+ this.chartCtx_.restore();
+ },
+
+ clearChartContents_() {
+ this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
+ },
+
+ showNoTimingDataMessage_() {
+ this.chartCtx_.font = '800 italic 14px Arial';
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.textAlign = 'center';
+ this.chartCtx_.textBaseline = 'middle';
+ this.chartCtx_.fillText('No timing data available.',
+ this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
+ },
+
+ collapseOpsToTimingBuckets_(ops) {
+ const opsTimingDataIndexHash_ = {};
+ const timingData = this.opsTimingData_;
+ let op;
+ let opIndex;
+
+ for (let i = 0; i < ops.length; i++) {
+ op = ops[i];
+
+ if (op.cmd_time === undefined) continue;
+
+ // Try to locate the entry for the current operation
+ // based on its name. If that fails, then create one for it.
+ opIndex = opsTimingDataIndexHash_[op.cmd_string] || null;
+
+ if (opIndex === null) {
+ timingData.push({
+ cmd_time: 0,
+ cmd_string: op.cmd_string
+ });
+
+ opIndex = timingData.length - 1;
+ opsTimingDataIndexHash_[op.cmd_string] = opIndex;
+ }
+
+ timingData[opIndex].cmd_time += op.cmd_time;
+ }
+
+ timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
+
+ this.collapseTimingBucketsToOther_(4);
+ },
+
+ collapseTimingBucketsToOther_(count) {
+ const timingData = this.opsTimingData_;
+ const otherSource = timingData.splice(count, timingData.length - count);
+ let otherDestination = null;
+
+ if (!otherSource.length) return;
+
+ timingData.push({
+ cmd_time: 0,
+ cmd_string: 'Other'
+ });
+
+ otherDestination = timingData[timingData.length - 1];
+ for (let i = 0; i < otherSource.length; i++) {
+ otherDestination.cmd_time += otherSource[i].cmd_time;
+ }
+ },
+
+ sortTimingBucketsByOpTimeDescending_(a, b) {
+ return b.cmd_time - a.cmd_time;
+ },
+
+ resetOpsTimingData_() {
+ this.opsTimingData_.length = 0;
+ }
+ };
+
+ return {
+ PictureOpsChartSummaryView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html
new file mode 100644
index 00000000000..413998847aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html
@@ -0,0 +1,505 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const BAR_PADDING = 1;
+ const BAR_WIDTH = 5;
+ const CHART_PADDING_LEFT = 65;
+ const CHART_PADDING_RIGHT = 30;
+ const CHART_PADDING_BOTTOM = 35;
+ const CHART_PADDING_TOP = 20;
+ const AXIS_PADDING_LEFT = 55;
+ const AXIS_PADDING_RIGHT = 30;
+ const AXIS_PADDING_BOTTOM = 35;
+ const AXIS_PADDING_TOP = 20;
+ const AXIS_TICK_SIZE = 5;
+ const AXIS_LABEL_PADDING = 5;
+ const VERTICAL_TICKS = 5;
+ const HUE_CHAR_CODE_ADJUSTMENT = 5.7;
+
+ /**
+ * Provides a chart showing the cumulative time spent in Skia operations
+ * during picture rasterization.
+ *
+ * @constructor
+ */
+ const PictureOpsChartView =
+ tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view');
+
+ PictureOpsChartView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.display = 'block';
+ this.style.height = '180px';
+ this.style.margin = 0;
+ this.style.padding = 0;
+ this.style.position = 'relative';
+
+ this.picture_ = undefined;
+ this.pictureOps_ = undefined;
+ this.opCosts_ = undefined;
+
+ this.chartScale_ = window.devicePixelRatio;
+
+ this.chart_ = document.createElement('canvas');
+ this.chartCtx_ = this.chart_.getContext('2d');
+ Polymer.dom(this).appendChild(this.chart_);
+
+ this.selectedOpIndex_ = undefined;
+ this.chartWidth_ = 0;
+ this.chartHeight_ = 0;
+ this.dimensionsHaveChanged_ = true;
+
+ this.currentBarMouseOverTarget_ = undefined;
+
+ this.ninetyFifthPercentileCost_ = 0;
+ this.totalOpCost_ = 0;
+
+ this.chart_.addEventListener('click', this.onClick_.bind(this));
+ this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+ new ResizeObserver(this.onResize_.bind(this)).observe(this);
+
+ this.usePercentileScale_ = false;
+ this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox(
+ this, 'usePercentileScale',
+ 'PictureOpsChartView.usePercentileScale', false,
+ 'Limit to 95%-ile');
+ Polymer.dom(this.usePercentileScaleCheckbox_).classList.add(
+ 'use-percentile-scale');
+ this.usePercentileScaleCheckbox_.style.position = 'absolute';
+ this.usePercentileScaleCheckbox_.style.left = 0;
+ this.usePercentileScaleCheckbox_.style.top = 0;
+ Polymer.dom(this).appendChild(this.usePercentileScaleCheckbox_);
+ },
+
+ get dimensionsHaveChanged() {
+ return this.dimensionsHaveChanged_;
+ },
+
+ set dimensionsHaveChanged(dimensionsHaveChanged) {
+ this.dimensionsHaveChanged_ = dimensionsHaveChanged;
+ },
+
+ get usePercentileScale() {
+ return this.usePercentileScale_;
+ },
+
+ set usePercentileScale(usePercentileScale) {
+ this.usePercentileScale_ = usePercentileScale;
+ this.drawChartContents_();
+ },
+
+ get numOps() {
+ return this.opCosts_.length;
+ },
+
+ get selectedOpIndex() {
+ return this.selectedOpIndex_;
+ },
+
+ set selectedOpIndex(selectedOpIndex) {
+ if (selectedOpIndex < 0) throw new Error('Invalid index');
+ if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
+
+ this.selectedOpIndex_ = selectedOpIndex;
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps());
+ this.currentBarMouseOverTarget_ = undefined;
+ this.processPictureData_();
+ this.dimensionsHaveChanged = true;
+ },
+
+ processPictureData_() {
+ if (this.pictureOps_ === undefined) return;
+
+ let totalOpCost = 0;
+
+ // Take a copy of the picture ops data for sorting.
+ this.opCosts_ = this.pictureOps_.map(function(op) {
+ totalOpCost += op.cmd_time;
+ return op.cmd_time;
+ });
+ this.opCosts_.sort();
+
+ const ninetyFifthPercentileCostIndex = Math.floor(
+ this.opCosts_.length * 0.95);
+ this.ninetyFifthPercentileCost_ =
+ this.opCosts_[ninetyFifthPercentileCostIndex];
+ this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
+
+ this.totalOpCost_ = totalOpCost;
+ },
+
+ extractBarIndex_(e) {
+ let index = undefined;
+
+ if (this.pictureOps_ === undefined ||
+ this.pictureOps_.length === 0) {
+ return index;
+ }
+
+ const x = e.offsetX;
+ const y = e.offsetY;
+
+ const totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
+
+ const chartLeft = CHART_PADDING_LEFT;
+ const chartTop = 0;
+ const chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
+ const chartRight = chartLeft + totalBarWidth;
+
+ if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom) {
+ return index;
+ }
+
+ index = Math.floor((x - chartLeft) / totalBarWidth *
+ this.pictureOps_.length);
+
+ index = tr.b.math.clamp(index, 0, this.pictureOps_.length - 1);
+
+ return index;
+ },
+
+ onClick_(e) {
+ const barClicked = this.extractBarIndex_(e);
+
+ if (barClicked === undefined) return;
+
+ // If we click on the already selected item we should deselect.
+ if (barClicked === this.selectedOpIndex) {
+ this.selectedOpIndex = undefined;
+ } else {
+ this.selectedOpIndex = barClicked;
+ }
+
+ e.preventDefault();
+
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ },
+
+ onMouseMove_(e) {
+ const lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
+ this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
+
+ if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) {
+ return;
+ }
+
+ this.drawChartContents_();
+ },
+
+ onResize_() {
+ this.dimensionsHaveChanged = true;
+ this.updateChartContents();
+ },
+
+ scrollSelectedItemIntoViewIfNecessary() {
+ if (this.selectedOpIndex === undefined) {
+ return;
+ }
+
+ const width = this.offsetWidth;
+ const left = this.scrollLeft;
+ const right = left + width;
+ const targetLeft = CHART_PADDING_LEFT +
+ (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
+
+ if (targetLeft > left && targetLeft < right) {
+ return;
+ }
+
+ this.scrollLeft = (targetLeft - width * 0.5);
+ },
+
+ updateChartContents() {
+ if (this.dimensionsHaveChanged) {
+ this.updateChartDimensions_();
+ }
+
+ this.drawChartContents_();
+ },
+
+ updateChartDimensions_() {
+ if (!this.pictureOps_) return;
+
+ let width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
+ ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
+
+ if (width < this.offsetWidth) {
+ width = this.offsetWidth;
+ }
+
+ // Allow the element to be its natural size as set by flexbox, then lock
+ // the width in before we set the width of the canvas.
+ this.chartWidth_ = width;
+ this.chartHeight_ = this.getBoundingClientRect().height;
+
+ // Scale up the canvas according to the devicePixelRatio, then reduce it
+ // down again via CSS. Finally we apply a scale to the canvas so that
+ // things are drawn at the correct size.
+ this.chart_.width = this.chartWidth_ * this.chartScale_;
+ this.chart_.height = this.chartHeight_ * this.chartScale_;
+
+ this.chart_.style.width = this.chartWidth_ + 'px';
+ this.chart_.style.height = this.chartHeight_ + 'px';
+
+ this.chartCtx_.scale(this.chartScale_, this.chartScale_);
+
+ this.dimensionsHaveChanged = false;
+ },
+
+ drawChartContents_() {
+ this.clearChartContents_();
+
+ if (this.pictureOps_ === undefined ||
+ this.pictureOps_.length === 0 ||
+ this.pictureOps_[0].cmd_time === undefined) {
+ this.showNoTimingDataMessage_();
+ return;
+ }
+
+ this.drawSelection_();
+ this.drawBars_();
+ this.drawChartAxes_();
+ this.drawLinesAtTickMarks_();
+ this.drawLineAtBottomOfChart_();
+
+ if (this.currentBarMouseOverTarget_ === undefined) {
+ return;
+ }
+
+ this.drawTooltip_();
+ },
+
+ drawSelection_() {
+ if (this.selectedOpIndex === undefined) {
+ return;
+ }
+
+ const width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
+ this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
+ this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
+ width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
+ },
+
+ drawChartAxes_() {
+ const min = this.opCosts_[0];
+ const max = this.opCosts_[this.opCosts_.length - 1];
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+
+ const tickYInterval = height / (VERTICAL_TICKS - 1);
+ let tickYPosition = 0;
+ const tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
+ let tickVal = 0;
+
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.strokeStyle = '#777';
+ this.chartCtx_.save();
+
+ // Translate half a pixel to avoid blurry lines.
+ this.chartCtx_.translate(0.5, 0.5);
+
+ // Sides.
+ this.chartCtx_.beginPath();
+ this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+ this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
+ AXIS_PADDING_BOTTOM);
+ this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
+ this.chartHeight_ - AXIS_PADDING_BOTTOM);
+ this.chartCtx_.stroke();
+ this.chartCtx_.closePath();
+
+ // Y-axis ticks.
+ this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+
+ this.chartCtx_.font = '10px Arial';
+ this.chartCtx_.textAlign = 'right';
+ this.chartCtx_.textBaseline = 'middle';
+
+ this.chartCtx_.beginPath();
+ for (let t = 0; t < VERTICAL_TICKS; t++) {
+ tickYPosition = Math.round(t * tickYInterval);
+ tickVal = (max - t * tickValInterval).toFixed(4);
+
+ this.chartCtx_.moveTo(0, tickYPosition);
+ this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
+ this.chartCtx_.fillText(tickVal,
+ -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
+ }
+
+ this.chartCtx_.stroke();
+ this.chartCtx_.closePath();
+
+ this.chartCtx_.restore();
+ },
+
+ drawLinesAtTickMarks_() {
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+ const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
+ const tickYInterval = height / (VERTICAL_TICKS - 1);
+ let tickYPosition = 0;
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
+ this.chartCtx_.beginPath();
+ this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
+
+ for (let t = 0; t < VERTICAL_TICKS; t++) {
+ tickYPosition = Math.round(t * tickYInterval);
+
+ this.chartCtx_.moveTo(0, tickYPosition);
+ this.chartCtx_.lineTo(width, tickYPosition);
+ this.chartCtx_.stroke();
+ }
+
+ this.chartCtx_.restore();
+ this.chartCtx_.closePath();
+ },
+
+ drawLineAtBottomOfChart_() {
+ this.chartCtx_.strokeStyle = '#AAA';
+ this.chartCtx_.beginPath();
+ this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
+ this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
+ this.chartCtx_.stroke();
+ this.chartCtx_.closePath();
+ },
+
+ drawTooltip_() {
+ const tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
+ const tooltipTitle = tooltipData.cmd_string;
+ const tooltipTime = tooltipData.cmd_time.toFixed(4);
+ const toolTipTimePercentage =
+ ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
+
+ const tooltipWidth = 120;
+ const tooltipHeight = 40;
+ const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
+ CHART_PADDING_LEFT;
+ const barWidth = BAR_WIDTH + BAR_PADDING;
+ const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
+
+ const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
+ barWidth - tooltipOffset;
+ const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.shadowOffsetX = 0;
+ this.chartCtx_.shadowOffsetY = 5;
+ this.chartCtx_.shadowBlur = 4;
+ this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
+
+ this.chartCtx_.strokeStyle = '#888';
+ this.chartCtx_.fillStyle = '#EEE';
+ this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.shadowColor = 'transparent';
+ this.chartCtx_.translate(0.5, 0.5);
+ this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.restore();
+
+ this.chartCtx_.fillStyle = '#222';
+ this.chartCtx_.textAlign = 'left';
+ this.chartCtx_.textBaseline = 'top';
+ this.chartCtx_.font = '800 12px Arial';
+ this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
+
+ this.chartCtx_.fillStyle = '#555';
+ this.chartCtx_.font = '400 italic 10px Arial';
+ this.chartCtx_.fillText(tooltipTime + 'ms (' +
+ toolTipTimePercentage + '%)', left + 8, top + 22);
+ },
+
+ drawBars_() {
+ let op;
+ let opColor = 0;
+ let opHeight = 0;
+ const opWidth = BAR_WIDTH + BAR_PADDING;
+ let opHover = false;
+
+ const bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
+ const maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
+ CHART_PADDING_TOP;
+
+ let maxValue;
+ if (this.usePercentileScale) {
+ maxValue = this.ninetyFifthPercentileCost_;
+ } else {
+ maxValue = this.maxCost_;
+ }
+
+ for (let b = 0; b < this.pictureOps_.length; b++) {
+ op = this.pictureOps_[b];
+ opHeight = Math.round(
+ (op.cmd_time / maxValue) * maxHeight);
+ opHeight = Math.max(opHeight, 1);
+ opHover = (b === this.currentBarMouseOverTarget_);
+ opColor = this.getOpColor_(op.cmd_string, opHover);
+
+ if (b === this.selectedOpIndex) {
+ this.chartCtx_.fillStyle = '#FFFF00';
+ } else {
+ this.chartCtx_.fillStyle = opColor;
+ }
+
+ this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
+ bottom - opHeight, BAR_WIDTH, opHeight);
+ }
+ },
+
+ getOpColor_(opName, hover) {
+ const characters = opName.split('');
+
+ const hue = characters.reduce(this.reduceNameToHue, 0) % 360;
+ const saturation = 30;
+ const lightness = hover ? '75%' : '50%';
+
+ return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
+ },
+
+ reduceNameToHue(previousValue, currentValue, index, array) {
+ // Get the char code and apply a magic adjustment value so we get
+ // pretty colors from around the rainbow.
+ return Math.round(previousValue + currentValue.charCodeAt(0) *
+ HUE_CHAR_CODE_ADJUSTMENT);
+ },
+
+ clearChartContents_() {
+ this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
+ },
+
+ showNoTimingDataMessage_() {
+ this.chartCtx_.font = '800 italic 14px Arial';
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.textAlign = 'center';
+ this.chartCtx_.textBaseline = 'middle';
+ this.chartCtx_.fillText('No timing data available.',
+ this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
+ }
+ };
+
+ return {
+ PictureOpsChartView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html
new file mode 100644
index 00000000000..2e45be58c33
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const OPS_TIMING_ITERATIONS = 3; // Iterations to average op timing info over.
+ const ANNOTATION = 'Comment';
+ const BEGIN_ANNOTATION = 'BeginCommentGroup';
+ const END_ANNOTATION = 'EndCommentGroup';
+ const ANNOTATION_ID = 'ID: ';
+ const ANNOTATION_CLASS = 'CLASS: ';
+ const ANNOTATION_TAG = 'TAG: ';
+
+ const constants = tr.e.cc.constants;
+
+ /**
+ * @constructor
+ */
+ const PictureOpsListView =
+ tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-list-view');
+
+ PictureOpsListView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.borderTop = '1px solid grey';
+ this.style.overflow = 'auto';
+ this.opsList_ = new tr.ui.b.ListView();
+ Polymer.dom(this).appendChild(this.opsList_);
+
+ this.selectedOp_ = undefined;
+ this.selectedOpIndex_ = undefined;
+ this.opsList_.addEventListener(
+ 'selection-changed', this.onSelectionChanged_.bind(this));
+
+ this.picture_ = undefined;
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.opsList_.clear();
+
+ if (!this.picture_) return;
+
+ let ops = this.picture_.getOps();
+ if (!ops) return;
+
+ ops = this.picture_.tagOpsWithTimings(ops);
+
+ ops = this.opsTaggedWithAnnotations_(ops);
+
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ const item = document.createElement('div');
+ item.opIndex = op.opIndex;
+ Polymer.dom(item).textContent = i + ') ' + op.cmd_string;
+
+ // Display the element info associated with the op, if available.
+ if (op.elementInfo.tag || op.elementInfo.id || op.elementInfo.class) {
+ const elementInfo = document.createElement('span');
+ Polymer.dom(elementInfo).classList.add('elementInfo');
+ elementInfo.style.color = 'purple';
+ elementInfo.style.fontSize = 'small';
+ elementInfo.style.fontWeight = 'bold';
+ elementInfo.style.color = '#777';
+ const tag = op.elementInfo.tag ? op.elementInfo.tag : 'unknown';
+ const id = op.elementInfo.id ? 'id=' + op.elementInfo.id : undefined;
+ const className = op.elementInfo.class ? 'class=' +
+ op.elementInfo.class : undefined;
+ Polymer.dom(elementInfo).textContent =
+ '<' + tag + (id ? ' ' : '') +
+ (id ? id : '') + (className ? ' ' : '') +
+ (className ? className : '') + '>';
+ Polymer.dom(item).appendChild(elementInfo);
+ }
+
+ // Display the Skia params.
+ // FIXME: now that we have structured data, we should format it.
+ // (https://github.com/google/trace-viewer/issues/782)
+ if (op.info.length > 0) {
+ const infoItem = document.createElement('div');
+ Polymer.dom(infoItem).textContent = JSON.stringify(op.info);
+ infoItem.style.fontSize = 'x-small';
+ infoItem.style.color = '#777';
+ Polymer.dom(item).appendChild(infoItem);
+ }
+
+ // Display the op timing, if available.
+ if (op.cmd_time && op.cmd_time >= 0.0001) {
+ const time = document.createElement('span');
+ Polymer.dom(time).classList.add('time');
+ const rounded = op.cmd_time.toFixed(4);
+ Polymer.dom(time).textContent = '(' + rounded + 'ms)';
+ time.style.fontSize = 'x-small';
+ time.style.color = 'rgb(136, 0, 0)';
+ Polymer.dom(item).appendChild(time);
+ }
+
+ item.style.borderBottom = '1px solid #555';
+ item.style.fontSize = 'small';
+ item.style.fontWeight = 'bold';
+ item.style.paddingBottom = '5px';
+ item.style.paddingLeft = '5px';
+ item.style.cursor = 'pointer';
+
+ for (const child of item.children) {
+ child.style.fontWeight = 'normal';
+ child.style.marginLeft = '1em';
+ child.style.maxWidth = '300px';
+ }
+
+ Polymer.dom(this.opsList_).appendChild(item);
+ }
+ },
+
+ onSelectionChanged_(e) {
+ let beforeSelectedOp = true;
+
+ // Deselect on re-selection.
+ if (this.opsList_.selectedElement === this.selectedOp_) {
+ this.opsList_.selectedElement = undefined;
+ beforeSelectedOp = false;
+ this.selectedOpIndex_ = undefined;
+ }
+
+ this.selectedOp_ = this.opsList_.selectedElement;
+
+ // Set selection on all previous ops.
+ const ops = this.opsList_.children;
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ if (op === this.selectedOp_) {
+ beforeSelectedOp = false;
+ this.selectedOpIndex_ = op.opIndex;
+ } else if (beforeSelectedOp) {
+ Polymer.dom(op).setAttribute('beforeSelection', 'beforeSelection');
+ op.style.backgroundColor = 'rgb(103, 199, 165)';
+ } else {
+ Polymer.dom(op).removeAttribute('beforeSelection');
+ op.style.backgroundColor = '';
+ }
+ }
+
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ },
+
+ get numOps() {
+ return this.opsList_.children.length;
+ },
+
+ get selectedOpIndex() {
+ return this.selectedOpIndex_;
+ },
+
+ set selectedOpIndex(s) {
+ this.selectedOpIndex_ = s;
+
+ if (s === undefined) {
+ this.opsList_.selectedElement = this.selectedOp_;
+ this.onSelectionChanged_();
+ } else {
+ if (s < 0) throw new Error('Invalid index');
+ if (s >= this.numOps) throw new Error('Invalid index');
+ this.opsList_.selectedElement = this.opsList_.getElementByIndex(s + 1);
+ tr.ui.b.scrollIntoViewIfNeeded(this.opsList_.selectedElement);
+ }
+ },
+
+ /**
+ * Return Skia operations tagged by annotation.
+ *
+ * The ops returned from Picture.getOps() contain both Skia ops and
+ * annotations threaded together. This function removes all annotations
+ * from the list and tags each op with the associated annotations.
+ * Additionally, the last {tag, id, class} is stored as elementInfo on
+ * each op.
+ *
+ * @param {Array} ops Array of Skia operations and annotations.
+ * @return {Array} Skia ops where op.annotations contains the associated
+ * annotations for a given op.
+ */
+ opsTaggedWithAnnotations_(ops) {
+ // This algorithm works by walking all the ops and pushing any
+ // annotations onto a stack. When a non-annotation op is found, the
+ // annotations stack is traversed and stored with the op.
+ const annotationGroups = [];
+ const opsWithoutAnnotations = [];
+ for (let opIndex = 0; opIndex < ops.length; opIndex++) {
+ const op = ops[opIndex];
+ op.opIndex = opIndex;
+ switch (op.cmd_string) {
+ case BEGIN_ANNOTATION:
+ annotationGroups.push([]);
+ break;
+ case END_ANNOTATION:
+ annotationGroups.pop();
+ break;
+ case ANNOTATION:
+ annotationGroups[annotationGroups.length - 1].push(op);
+ break;
+ default: {
+ const annotations = [];
+ let elementInfo = {};
+ annotationGroups.forEach(function(annotationGroup) {
+ elementInfo = {};
+ annotationGroup.forEach(function(annotation) {
+ annotation.info.forEach(function(info) {
+ if (info.includes(ANNOTATION_TAG)) {
+ elementInfo.tag = info.substring(
+ info.indexOf(ANNOTATION_TAG) +
+ ANNOTATION_TAG.length).toLowerCase();
+ } else if (info.includes(ANNOTATION_ID)) {
+ elementInfo.id = info.substring(
+ info.indexOf(ANNOTATION_ID) +
+ ANNOTATION_ID.length);
+ } else if (info.includes(ANNOTATION_CLASS)) {
+ elementInfo.class = info.substring(
+ info.indexOf(ANNOTATION_CLASS) +
+ ANNOTATION_CLASS.length);
+ }
+
+ annotations.push(info);
+ });
+ });
+ });
+ op.annotations = annotations;
+ op.elementInfo = elementInfo;
+ opsWithoutAnnotations.push(op);
+ }
+ }
+ }
+
+ return opsWithoutAnnotations;
+ }
+ };
+
+ return {
+ PictureOpsListView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html
new file mode 100644
index 00000000000..b58c1568f4f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const PictureOpsListView = tr.ui.e.chrome.cc.PictureOpsListView;
+
+ test('instantiate', function() {
+ if (!tr.e.cc.PictureSnapshot.CanRasterize()) return;
+
+ const m = new tr.Model(g_catLTHIEvents);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+ const snapshot = instance.snapshots[0];
+
+ const view = new PictureOpsListView();
+ view.picture = snapshot;
+ assert.strictEqual(view.opsList_.children.length, 142);
+ });
+
+ test('selection', function() {
+ if (!tr.e.cc.PictureSnapshot.CanRasterize()) return;
+
+ const m = new tr.Model(g_catLTHIEvents);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+ const snapshot = instance.snapshots[0];
+
+ const view = new PictureOpsListView();
+ view.picture = snapshot;
+ let didSelectionChange = 0;
+ view.addEventListener('selection-changed', function() {
+ didSelectionChange = true;
+ });
+ assert.isFalse(didSelectionChange);
+ view.opsList_.selectedElement = view.opsList_.children[3];
+ assert.isTrue(didSelectionChange);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html
new file mode 100644
index 00000000000..a9db575773f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a picture snapshot in a human readable form.
+ * @constructor
+ */
+ const PictureSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-picture-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ PictureSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add(
+ 'tr-ui-e-chrome-cc-picture-snapshot-view');
+ this.style.display = 'flex';
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.minWidth = 0;
+ this.pictureDebugger_ = new tr.ui.e.chrome.cc.PictureDebugger();
+ this.pictureDebugger_.style.flexGrow = 1;
+ this.pictureDebugger_.style.flexShrink = 1;
+ this.pictureDebugger_.style.flexBasis = 'auto';
+ this.pictureDebugger_.style.minWidth = 0;
+ Polymer.dom(this).appendChild(this.pictureDebugger_);
+ },
+
+ updateContents() {
+ if (this.objectSnapshot_ && this.pictureDebugger_) {
+ this.pictureDebugger_.picture = this.objectSnapshot_;
+ }
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ PictureSnapshotView,
+ {
+ typeNames: ['cc::Picture', 'cc::LayeredPicture'],
+ showInstances: false
+ });
+
+ return {
+ PictureSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html
new file mode 100644
index 00000000000..6b1a7cb7df0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /**
+ * @constructor
+ */
+ function RasterTaskSelection(selection) {
+ tr.ui.e.chrome.cc.Selection.call(this);
+ const whySupported = RasterTaskSelection.whySuported(selection);
+ if (!whySupported.ok) {
+ throw new Error('Fail: ' + whySupported.why);
+ }
+ this.slices_ = Array.from(selection);
+ this.tiles_ = this.slices_.map(function(slice) {
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(slice);
+ if (tile === undefined) {
+ throw new Error('This should never happen due to .supports check.');
+ }
+ return tile;
+ });
+ }
+
+ RasterTaskSelection.whySuported = function(selection) {
+ if (!(selection instanceof tr.model.EventSet)) {
+ return {ok: false, why: 'Must be selection'};
+ }
+
+ if (selection.length === 0) {
+ return {ok: false, why: 'Selection must be non empty'};
+ }
+
+ let referenceSnapshot = undefined;
+ for (const event of selection) {
+ if (!(event instanceof tr.model.Slice)) {
+ return {ok: false, why: 'Not a slice'};
+ }
+
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(event);
+ if (tile === undefined) {
+ return {ok: false, why: 'No tile found'};
+ }
+
+ if (!referenceSnapshot) {
+ referenceSnapshot = tile.containingSnapshot;
+ } else {
+ if (tile.containingSnapshot !== referenceSnapshot) {
+ return {
+ ok: false,
+ why: 'Raster tasks are from different compositor instances'
+ };
+ }
+ }
+ }
+ return {ok: true};
+ };
+
+ RasterTaskSelection.supports = function(selection) {
+ return RasterTaskSelection.whySuported(selection).ok;
+ };
+
+ RasterTaskSelection.prototype = {
+ __proto__: tr.ui.e.chrome.cc.Selection.prototype,
+
+ get specicifity() {
+ return 3;
+ },
+
+ get associatedLayerId() {
+ const tile0 = this.tiles_[0];
+ const allSameLayer = this.tiles_.every(function(tile) {
+ tile.layerId === tile0.layerId;
+ });
+ if (allSameLayer) {
+ return tile0.layerId;
+ }
+ return undefined;
+ },
+
+ get extraHighlightsByLayerId() {
+ const highlights = {};
+ this.tiles_.forEach(function(tile, i) {
+ if (highlights[tile.layerId] === undefined) {
+ highlights[tile.layerId] = [];
+ }
+ const slice = this.slices_[i];
+ highlights[tile.layerId].push({
+ colorKey: slice.title,
+ rect: tile.layerRect
+ });
+ }, this);
+ return highlights;
+ },
+
+ createAnalysis() {
+ const sel = new tr.model.EventSet();
+ this.slices_.forEach(function(slice) {
+ sel.push(slice);
+ });
+
+ let analysis;
+ if (sel.length === 1) {
+ analysis = document.createElement('tr-ui-a-single-event-sub-view');
+ } else {
+ analysis = document.createElement('tr-ui-e-chrome-cc-raster-task-view');
+ }
+ analysis.selection = sel;
+ return analysis;
+ },
+
+ findEquivalent(lthi) {
+ // Raster tasks are only valid in one LTHI.
+ return undefined;
+ },
+
+ // RasterTaskSelection specific stuff follows.
+ get containingSnapshot() {
+ return this.tiles_[0].containingSnapshot;
+ }
+ };
+
+ return {
+ RasterTaskSelection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html
new file mode 100644
index 00000000000..d95a7135d37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+ const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) {
+ return slice.title === 'RasterTask';
+ });
+
+ let selection = new tr.model.EventSet();
+ selection.push(rasterTasks[0]);
+ selection.push(rasterTasks[1]);
+
+ assert.isTrue(tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection));
+ selection = new tr.ui.e.chrome.cc.RasterTaskSelection(selection);
+ const highlights = selection.extraHighlightsByLayerId;
+ assert.lengthOf(Object.keys(highlights), 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html
new file mode 100644
index 00000000000..a5f7f5d806c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-e-chrome-cc-raster-task-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #heading {
+ flex: 0 0 auto;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+
+ <div id="heading">
+ Rasterization costs in
+ <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link>
+ </div>
+ <tr-ui-b-table id="content"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-chrome-cc-raster-task-view',
+
+ created() {
+ this.selection_ = undefined;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+
+ this.updateContents_();
+ },
+
+ updateColumns_(hadCpuDurations) {
+ const timeSpanConfig = {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ ownerDocument: this.ownerDocument
+ };
+
+ const columns = [
+ {
+ title: 'Layer',
+ value(row) {
+ if (row.isTotals) return 'Totals';
+ if (row.layer) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(
+ function() {
+ return new tr.ui.e.chrome.cc.LayerSelection(row.layer);
+ },
+ 'Layer ' + row.layerId);
+ return linkEl;
+ }
+ return 'Layer ' + row.layerId;
+ },
+ width: '250px'
+ },
+ {
+ title: 'Num Tiles',
+ value(row) { return row.numTiles; },
+ cmp(a, b) { return a.numTiles - b.numTiles; }
+ },
+ {
+ title: 'Num Analysis Tasks',
+ value(row) { return row.numAnalysisTasks; },
+ cmp(a, b) {
+ return a.numAnalysisTasks - b.numAnalysisTasks;
+ }
+ },
+ {
+ title: 'Num Raster Tasks',
+ value(row) { return row.numRasterTasks; },
+ cmp(a, b) { return a.numRasterTasks - b.numRasterTasks; }
+ },
+ {
+ title: 'Wall Duration (ms)',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.duration, timeSpanConfig);
+ },
+ cmp(a, b) { return a.duration - b.duration; }
+ }
+ ];
+
+ if (hadCpuDurations) {
+ columns.push({
+ title: 'CPU Duration (ms)',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.cpuDuration, timeSpanConfig);
+ },
+ cmp(a, b) { return a.cpuDuration - b.cpuDuration; }
+ });
+ }
+
+ let colWidthPercentage;
+ if (columns.length === 1) {
+ colWidthPercentage = '100%';
+ } else {
+ colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+ }
+
+ for (let i = 1; i < columns.length; i++) {
+ columns[i].width = colWidthPercentage;
+ }
+
+ this.$.content.tableColumns = columns;
+ this.$.content.sortColumnIndex = columns.length - 1;
+ },
+
+ updateContents_() {
+ const table = this.$.content;
+
+ if (this.selection_.length === 0) {
+ this.$.link.setSelectionAndContent(undefined, '');
+ table.tableRows = [];
+ table.footerRows = [];
+ table.rebuild();
+ return;
+ }
+ // LTHI link.
+ const lthi = tr.e.cc.getTileFromRasterTaskSlice(
+ tr.b.getFirstElement(this.selection_)).containingSnapshot;
+ this.$.link.setSelectionAndContent(function() {
+ return new tr.model.EventSet(lthi);
+ }, lthi.userFriendlyName);
+
+ // Get costs by layer.
+ const costsByLayerId = {};
+ function getCurrentCostsForLayerId(tile) {
+ const layerId = tile.layerId;
+ const lthi = tile.containingSnapshot;
+ let layer;
+ if (lthi.activeTree) {
+ layer = lthi.activeTree.findLayerWithId(layerId);
+ }
+ if (layer === undefined && lthi.pendingTree) {
+ layer = lthi.pendingTree.findLayerWithId(layerId);
+ }
+ if (costsByLayerId[layerId] === undefined) {
+ costsByLayerId[layerId] = {
+ layerId,
+ layer,
+ numTiles: 0,
+ numAnalysisTasks: 0,
+ numRasterTasks: 0,
+ duration: 0,
+ cpuDuration: 0
+ };
+ }
+ return costsByLayerId[layerId];
+ }
+
+ let totalDuration = 0;
+ let totalCpuDuration = 0;
+ let totalNumAnalyzeTasks = 0;
+ let totalNumRasterizeTasks = 0;
+ let hadCpuDurations = false;
+
+ const tilesThatWeHaveSeen = {};
+
+ this.selection_.forEach(function(slice) {
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(slice);
+ const curCosts = getCurrentCostsForLayerId(tile);
+
+ if (!tilesThatWeHaveSeen[tile.objectInstance.id]) {
+ tilesThatWeHaveSeen[tile.objectInstance.id] = true;
+ curCosts.numTiles += 1;
+ }
+
+ if (tr.e.cc.isSliceDoingAnalysis(slice)) {
+ curCosts.numAnalysisTasks += 1;
+ totalNumAnalyzeTasks += 1;
+ } else {
+ curCosts.numRasterTasks += 1;
+ totalNumRasterizeTasks += 1;
+ }
+ curCosts.duration += slice.duration;
+ totalDuration += slice.duration;
+ if (slice.cpuDuration !== undefined) {
+ curCosts.cpuDuration += slice.cpuDuration;
+ totalCpuDuration += slice.cpuDuration;
+ hadCpuDurations = true;
+ }
+ });
+
+ // Apply to the table.
+ this.updateColumns_(hadCpuDurations);
+ table.tableRows = Object.values(costsByLayerId);
+ table.rebuild();
+
+ // Footer.
+ table.footerRows = [
+ {
+ isTotals: true,
+ numTiles: Object.keys(tilesThatWeHaveSeen).length,
+ numAnalysisTasks: totalNumAnalyzeTasks,
+ numRasterTasks: totalNumRasterizeTasks,
+ duration: totalDuration,
+ cpuDuration: totalCpuDuration
+ }
+ ];
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html
new file mode 100644
index 00000000000..56767cfcc89
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createSelection() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+ const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) {
+ return slice.title === 'RasterTask' || slice.title === 'AnalyzeTask';
+ });
+
+ const selection = new tr.model.EventSet();
+ selection.model = m;
+
+ selection.push(rasterTasks[0]);
+ selection.push(rasterTasks[1]);
+ return selection;
+ }
+
+ test('basic', function() {
+ const selection = createSelection();
+ const view = document.createElement('tr-ui-e-chrome-cc-raster-task-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+
+ test('analysisViewIntegration', function() {
+ const selection = createSelection();
+
+ const timelineView = {model: selection.model};
+ const brushingStateController =
+ new tr.c.BrushingStateController(timelineView);
+
+ const analysisEl = document.createElement('tr-ui-a-analysis-view');
+ analysisEl.brushingStateController = brushingStateController;
+ brushingStateController.changeSelectionFromTimeline(selection);
+
+ assert.isDefined(Polymer.dom(analysisEl).querySelector(
+ 'tr-ui-e-chrome-cc-raster-task-view'));
+
+ const sv = tr.ui.b.findDeepElementMatching(
+ analysisEl, 'tr-ui-a-multi-thread-slice-sub-view');
+ assert.isTrue(sv.requiresTallView);
+ this.addHTMLOutput(analysisEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html
new file mode 100644
index 00000000000..2794540e115
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html
@@ -0,0 +1,304 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ function Selection() {
+ this.selectionToSetIfClicked = undefined;
+ }
+ Selection.prototype = {
+ /**
+ * When two things are picked in the UI, one must occasionally tie-break
+ * between them to decide what was really clicked. Things with higher
+ * specicifity will win.
+ */
+ get specicifity() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * If a selection is related to a specific layer, then this returns the
+ * layerId of that layer. If the selection is not related to a layer, for
+ * example if the device viewport is selected, then this returns undefined.
+ */
+ get associatedLayerId() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * If a selection is related to a specific render pass, then this returns
+ * the layerId of that layer. If the selection is not related to a layer,
+ * for example if the device viewport is selected, then this returns
+ * undefined.
+ */
+ get associatedRenderPassId() {
+ throw new Error('Not implemented');
+ },
+
+
+ get highlightsByLayerId() {
+ return {};
+ },
+
+ /**
+ * Called when the selection is made active in the layer view. Must return
+ * an HTMLElement that explains this selection in detail.
+ */
+ createAnalysis() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Should try to create the equivalent selection in the provided LTHI,
+ * or undefined if it can't be done.
+ */
+ findEquivalent(lthi) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function RenderPassSelection(renderPass, renderPassId) {
+ if (!renderPass || (renderPassId === undefined)) {
+ throw new Error('Render pass (with id) is required');
+ }
+ this.renderPass_ = renderPass;
+ this.renderPassId_ = renderPassId;
+ }
+
+ RenderPassSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 1;
+ },
+
+ get associatedLayerId() {
+ return undefined;
+ },
+
+ get associatedRenderPassId() {
+ return this.renderPassId_;
+ },
+
+ get renderPass() {
+ return this.renderPass_;
+ },
+
+ createAnalysis() {
+ const dataView = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ dataView.label = 'RenderPass ' + this.renderPassId_;
+ dataView.object = this.renderPass_.args;
+ return dataView;
+ },
+
+ get title() {
+ return this.renderPass_.objectInstance.typeName;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function LayerSelection(layer) {
+ if (!layer) {
+ throw new Error('Layer is required');
+ }
+ this.layer_ = layer;
+ }
+
+ LayerSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 1;
+ },
+
+ get associatedLayerId() {
+ return this.layer_.layerId;
+ },
+
+ get associatedRenderPassId() {
+ return undefined;
+ },
+
+ get layer() {
+ return this.layer_;
+ },
+
+ createAnalysis() {
+ const dataView = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ dataView.label = 'Layer ' + this.layer_.layerId;
+ if (this.layer_.usingGpuRasterization) {
+ dataView.label += ' (GPU-rasterized)';
+ }
+ dataView.object = this.layer_.args;
+ return dataView;
+ },
+
+ get title() {
+ return this.layer_.objectInstance.typeName;
+ },
+
+ findEquivalent(lthi) {
+ const layer = lthi.activeTree.findLayerWithId(this.layer_.layerId) ||
+ lthi.pendingTree.findLayerWithId(this.layer_.layerId);
+ if (!layer) return undefined;
+ return new LayerSelection(layer);
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function TileSelection(tile, opt_data) {
+ this.tile_ = tile;
+ this.data_ = opt_data || {};
+ }
+
+ TileSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 2;
+ },
+
+ get associatedLayerId() {
+ return this.tile_.layerId;
+ },
+
+ get highlightsByLayerId() {
+ const highlights = {};
+ highlights[this.tile_.layerId] = [
+ {
+ colorKey: this.tile_.objectInstance.typeName,
+ rect: this.tile_.layerRect
+ }
+ ];
+ return highlights;
+ },
+
+ createAnalysis() {
+ const analysis = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ analysis.label = 'Tile ' + this.tile_.objectInstance.id + ' on layer ' +
+ this.tile_.layerId;
+ if (this.data_) {
+ analysis.object = {
+ moreInfo: this.data_,
+ tileArgs: this.tile_.args
+ };
+ } else {
+ analysis.object = this.tile_.args;
+ }
+ return analysis;
+ },
+
+ findEquivalent(lthi) {
+ const tileInstance = this.tile_.tileInstance;
+ if (lthi.ts < tileInstance.creationTs ||
+ lthi.ts >= tileInstance.deletionTs) {
+ return undefined;
+ }
+ const tileSnapshot = tileInstance.getSnapshotAt(lthi.ts);
+ if (!tileSnapshot) return undefined;
+ return new TileSelection(tileSnapshot);
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function LayerRectSelection(layer, rectType, rect, opt_data) {
+ this.layer_ = layer;
+ this.rectType_ = rectType;
+ this.rect_ = rect;
+ this.data_ = opt_data !== undefined ? opt_data : rect;
+ }
+
+ LayerRectSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 2;
+ },
+
+ get associatedLayerId() {
+ return this.layer_.layerId;
+ },
+
+
+ get highlightsByLayerId() {
+ const highlights = {};
+ highlights[this.layer_.layerId] = [
+ {
+ colorKey: this.rectType_,
+ rect: this.rect_
+ }
+ ];
+ return highlights;
+ },
+
+ createAnalysis() {
+ const analysis = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ analysis.label = this.rectType_ + ' on layer ' + this.layer_.layerId;
+ analysis.object = this.data_;
+ return analysis;
+ },
+
+ findEquivalent(lthi) {
+ return undefined;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function AnimationRectSelection(layer, rect) {
+ this.layer_ = layer;
+ this.rect_ = rect;
+ }
+
+ AnimationRectSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 0;
+ },
+
+ get associatedLayerId() {
+ return this.layer_.layerId;
+ },
+
+ createAnalysis() {
+ const analysis = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ analysis.label = 'Animation Bounds of layer ' + this.layer_.layerId;
+ analysis.object = this.rect_;
+ return analysis;
+ }
+ };
+
+ return {
+ Selection,
+ RenderPassSelection,
+ LayerSelection,
+ TileSelection,
+ LayerRectSelection,
+ AnimationRectSelection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html
new file mode 100644
index 00000000000..ad1f633f334
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+
+<script>
+
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a tile in a human readable form.
+ * @constructor
+ */
+ const TileSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-tile-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ TileSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-tile-snapshot-view');
+ this.layerTreeView_ =
+ new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView();
+ Polymer.dom(this).appendChild(this.layerTreeView_);
+ },
+
+ updateContents() {
+ const tile = this.objectSnapshot_;
+ const layerTreeHostImpl = tile.containingSnapshot;
+ if (!layerTreeHostImpl) return;
+
+ this.layerTreeView_.objectSnapshot = layerTreeHostImpl;
+ this.layerTreeView_.selection = new tr.ui.e.chrome.cc.TileSelection(tile);
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ TileSnapshotView,
+ {
+ typeName: 'cc::Tile',
+ showInTrackView: false
+ });
+
+ return {
+ TileSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html
new file mode 100644
index 00000000000..af6c79447bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<dom-module id='tr-ui-e-chrome-codesearch'>
+ <template>
+ <style>
+ :host {
+ white-space: nowrap;
+ }
+ #codesearchLink {
+ font-size: x-small;
+ margin-left: 20px;
+ text-decoration: none;
+ }
+ </style>
+ <a id="codesearchLink" target=_blank on-click="onClick">&#x1F50D;</a>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome', function() {
+ Polymer({
+ is: 'tr-ui-e-chrome-codesearch',
+
+ set searchPhrase(phrase) {
+ const link = Polymer.dom(this.$.codesearchLink);
+ const codeSearchURL =
+ 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=';
+ link.setAttribute('href', codeSearchURL + encodeURIComponent(phrase));
+ },
+
+ onClick(clickEvent) {
+ // Let the event trigger the default action of following the link. Stop
+ // the propagation of the event here, so that subsequent handlers do not
+ // intercept the clicks.
+ clickEvent.stopPropagation();
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html
new file mode 100644
index 00000000000..ec7991b6640
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/gpu/gpu_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/gpu/state.html">
+<link rel="import" href="/tracing/ui/extras/chrome/gpu/state_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png
new file mode 100644
index 00000000000..8ea9bc726bb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css
new file mode 100644
index 00000000000..7c2c34787dc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css
@@ -0,0 +1,15 @@
+/* Copyright (c) 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.
+ */
+
+.tr-ui-e-chrome-gpu-state-snapshot-view {
+ background: url('./images/checkerboard.png');
+ display: flex;
+ overflow: auto;
+}
+
+.tr-ui-e-chrome-gpu-state-snapshot-view img {
+ display: block;
+ margin: 16px auto 16px auto;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html
new file mode 100644
index 00000000000..ba6c345be5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/extras/chrome/gpu/state_view.css">
+
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.gpu', function() {
+ /*
+ * Displays a GPU state snapshot in a human readable form.
+ * @constructor
+ */
+ const StateSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-gpu-state-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ StateSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-chrome-gpu-state-snapshot-view');
+ this.screenshotImage_ = document.createElement('img');
+ Polymer.dom(this).appendChild(this.screenshotImage_);
+ },
+
+ updateContents() {
+ if (this.objectSnapshot_ && this.objectSnapshot_.screenshot) {
+ this.screenshotImage_.src = 'data:image/png;base64,' +
+ this.objectSnapshot_.screenshot;
+ }
+ }
+ };
+ tr.ui.analysis.ObjectSnapshotView.register(
+ StateSnapshotView,
+ {typeName: 'gpu::State'});
+
+ return {
+ StateSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html
new file mode 100644
index 00000000000..db80ef75afa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/layout_tree.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+
+<dom-module id='tr-ui-a-layout-tree-sub-view'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <div id="content"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ Polymer({
+ is: 'tr-ui-a-layout-tree-sub-view',
+ behaviors: ['tr-ui-a-sub-view'],
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ updateContents_() {
+ this.set('$.content.textContent', '');
+ if (!this.currentSelection_) return;
+
+ const columns = [
+ {
+ title: 'Tag/Name',
+ value(layoutObject) {
+ return layoutObject.tag || ':' + layoutObject.name;
+ }
+ },
+
+ {
+ title: 'htmlId',
+ value(layoutObject) {
+ return layoutObject.htmlId || '';
+ }
+ },
+
+ {
+ title: 'classNames',
+ value(layoutObject) {
+ return layoutObject.classNames || '';
+ }
+ },
+
+ {
+ title: 'reasons',
+ value(layoutObject) {
+ return layoutObject.needsLayoutReasons.join(', ');
+ }
+ },
+
+ {
+ title: 'width',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.width;
+ }
+ },
+
+ {
+ title: 'height',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.height;
+ }
+ },
+
+ {
+ title: 'absX',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.left;
+ }
+ },
+
+ {
+ title: 'absY',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.top;
+ }
+ },
+
+ {
+ title: 'relX',
+ value(layoutObject) {
+ return layoutObject.relativeRect.left;
+ }
+ },
+
+ {
+ title: 'relY',
+ value(layoutObject) {
+ return layoutObject.relativeRect.top;
+ }
+ },
+
+ {
+ title: 'float',
+ value(layoutObject) {
+ return layoutObject.isFloat ? 'float' : '';
+ }
+ },
+
+ {
+ title: 'positioned',
+ value(layoutObject) {
+ return layoutObject.isPositioned ? 'positioned' : '';
+ }
+ },
+
+ {
+ title: 'relative',
+ value(layoutObject) {
+ return layoutObject.isRelativePositioned ? 'relative' : '';
+ }
+ },
+
+ {
+ title: 'sticky',
+ value(layoutObject) {
+ return layoutObject.isStickyPositioned ? 'sticky' : '';
+ }
+ },
+
+ {
+ title: 'anonymous',
+ value(layoutObject) {
+ return layoutObject.isAnonymous ? 'anonymous' : '';
+ }
+ },
+
+ {
+ title: 'row',
+ value(layoutObject) {
+ if (layoutObject.tableRow === undefined) {
+ return '';
+ }
+ return layoutObject.tableRow;
+ }
+ },
+
+ {
+ title: 'col',
+ value(layoutObject) {
+ if (layoutObject.tableCol === undefined) {
+ return '';
+ }
+ return layoutObject.tableCol;
+ }
+ },
+
+ {
+ title: 'rowSpan',
+ value(layoutObject) {
+ if (layoutObject.tableRowSpan === undefined) {
+ return '';
+ }
+ return layoutObject.tableRowSpan;
+ }
+ },
+
+ {
+ title: 'colSpan',
+ value(layoutObject) {
+ if (layoutObject.tableColSpan === undefined) {
+ return '';
+ }
+ return layoutObject.tableColSpan;
+ }
+ },
+
+ {
+ title: 'address',
+ value(layoutObject) {
+ return layoutObject.id.toString(16);
+ }
+ }
+ ];
+
+ const table = this.ownerDocument.createElement('tr-ui-b-table');
+ table.defaultExpansionStateCallback = function(
+ layoutObject, parentLayoutObject) {
+ return true;
+ };
+ table.subRowsPropertyName = 'childLayoutObjects';
+ table.tableColumns = columns;
+ table.tableRows = this.currentSelection_.map(function(snapshot) {
+ return snapshot.rootLayoutObject;
+ });
+ table.rebuild();
+ Polymer.dom(this.$.content).appendChild(table);
+ },
+ });
+
+ return {};
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-layout-tree-sub-view',
+ tr.e.chrome.LayoutTreeSnapshot,
+ {
+ multi: false,
+ title: 'Layout Tree',
+ });
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-layout-tree-sub-view',
+ tr.e.chrome.LayoutTreeSnapshot,
+ {
+ multi: true,
+ title: 'Layout Trees',
+ });
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html
new file mode 100644
index 00000000000..cea42a2d78c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!--
+The chrome config is heavily used:
+ - chrome://tracing,
+ - trace2html, which in turn implies
+ - adb_profile_chrome
+ - telemetry
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the
+same way that all non-UI files depend on tracing/base/base.html. Enforce this
+dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/extras/chrome_config.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/codesearch.html">
+<link rel="import" href="/tracing/ui/extras/chrome/gpu/gpu.html">
+<link rel="import" href="/tracing/ui/extras/chrome/layout_tree_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html">
+<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html">
+<link rel="import" href="/tracing/ui/extras/v8_config.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html
new file mode 100644
index 00000000000..cb3d41a88a0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<!--
+This class tries to (simply) copy the telemetry Results object, but outputs
+directly to an HTML table. It takes things that look like Telemetry values,
+and updates the table internally.
+-->
+<dom-module id='tr-ui-e-deep-reports-html-results'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-deep-reports-html-results',
+
+ created() {
+ this.hasColumnNamed_ = {};
+ this.pageToRowMap_ = new WeakMap();
+ },
+
+ ready() {
+ const table = this.$.table;
+ table.tableColumns = [
+ {
+ title: 'Label',
+ value(row) { return row.label; },
+ width: '350px'
+ }
+ ];
+ this.clear();
+ },
+
+ clear() {
+ this.$.table.tableRows = [];
+ },
+
+ addColumnIfNeeded_(columnName) {
+ if (this.hasColumnNamed_[columnName]) return;
+
+ this.hasColumnNamed_[columnName] = true;
+
+ const column = {
+ title: columnName,
+ value(row) {
+ if (row[columnName] === undefined) return '';
+ return row[columnName];
+ }
+ };
+
+ const columns = this.$.table.tableColumns;
+ columns.push(column);
+
+ // Update widths.
+ let colWidthPercentage;
+ if (columns.length === 1) {
+ colWidthPercentage = '100%';
+ } else {
+ colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+ }
+
+ for (let i = 1; i < columns.length; i++) {
+ columns[i].width = colWidthPercentage;
+ }
+
+ this.$.table.tableColumns = columns;
+ },
+
+ getRowForPage_(page) {
+ if (!this.pageToRowMap_.has(page)) {
+ const i = page.url.lastIndexOf('/');
+ const baseName = page.url.substring(i + 1);
+
+ const link = document.createElement('a');
+ link.href = 'trace_viewer.html#' + page.url;
+ Polymer.dom(link).textContent = baseName;
+
+ const row = {
+ label: link,
+ value: '',
+ subRows: [],
+ isExpanded: true
+ };
+ this.$.table.tableRows.push(row);
+ this.pageToRowMap_.set(page, row);
+
+ // Kick table rebuild.
+ this.$.table.tableRows = this.$.table.tableRows;
+ }
+ return this.pageToRowMap_.get(page);
+ },
+
+ addValue(value) {
+ /* Value is expected to be a scalar telemetry-style Value. */
+ if (value.type !== 'scalar') {
+ throw new Error('wat');
+ }
+
+ this.addColumnIfNeeded_(value.name);
+ const rowForPage = this.getRowForPage_(value.page);
+ rowForPage[value.name] = value.value;
+
+ // Kick table rebuild.
+ this.$.table.tableRows = this.$.table.tableRows;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html
new file mode 100644
index 00000000000..9cf2d7f6c79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/ui/extras/deep_reports/scalar_value.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.deep_reports', function() {
+ /**
+ * Runs deep reports on the provided files, and pushes telemetry-style
+ * values to the results object.
+ */
+ function main(results, filesInDir) {
+ let lastP = new Promise(function(resolve) { resolve(); });
+
+ filesInDir.forEach(function(filename) {
+ // TODO(nduca): Make this like telemetry page.
+ const page = {
+ url: filename
+ };
+ lastP = lastP.then(function() {
+ return loadModelFromFileAsync(filename);
+ });
+ lastP = lastP.then(function(model) {
+ processModel(results, page, model);
+ });
+ });
+ return lastP;
+ }
+
+ function loadModelFromFileAsync(filename) {
+ return tr.b.getAsync(filename).then(function(trace) {
+ const io = new tr.ImportOptions();
+ io.shiftWorldToZero = true;
+ io.pruneEmptyContainers = false;
+
+ const m = new tr.Model();
+ try {
+ m.importTraces([trace], io);
+ } catch (e) {
+ throw new Error('While loading ' + filename + ' got: ' + e.toString());
+ }
+ return m;
+ });
+ }
+
+ function processModel(results, page, model) {
+ results.addValue(
+ new tr.ui.e.deep_reports.ScalarValue(
+ page, 'numRailIRs', 'ms', model.userModel.expectations.length));
+ }
+
+ return {
+ main
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html
new file mode 100644
index 00000000000..cb550c35b4b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.deep_reports', function() {
+ function ScalarValue(page, name, units, value,
+ opt_important, opt_description) {
+ this.type = 'scalar';
+ this.page = page;
+ this.name = name;
+ this.units = units;
+ this.value = value;
+ this.important = opt_important !== undefined ? opt_important : false;
+ this.description = opt_description || '';
+ }
+ ScalarValue.fromDict = function(page, dict) {
+ if (dict.type !== 'scalar') {
+ throw new Error('wat');
+ }
+ const v = new ScalarValue(page, dict.name, dict.units, dict.value);
+ v.important = dict.important;
+ v.description = dict.description;
+ v.value = dict.value;
+ return v;
+ };
+
+ ScalarValue.prototype = {
+
+ };
+
+ return {
+ ScalarValue,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html
new file mode 100644
index 00000000000..1748dd12b88
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<dom-module id='tr-ui-e-drive-comment-element'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ #comment-area {
+ display: flex;
+ flex-direction: column;
+ border-top: 1px solid #e8e8e8;
+ background-color: white;
+ padding: 6px;
+ margin-bottom: 4px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
+ border-radius: 2px;
+ font-size: small;
+ }
+ #comment-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 8px;
+ }
+ #comment-header-text {
+ display: flex;
+ flex-direction: column;
+ padding-left: 10px;
+ }
+ #comment-img {
+ width: 32px;
+ height: 32px;
+ }
+ #comment-text-author {
+ padding-bottom: 2px;
+ }
+ #comment-date {
+ color: #777;
+ font-size: 11px;
+ }
+ #comment-content {
+ word-wrap: break-word;
+ }
+ </style>
+ <div id="comment-area">
+ <div id="comment-header">
+ <img id="comment-img" src="{{ comment.author.picture.url }}" />
+ <div id="comment-header-text">
+ <div id="comment-text-author">{{ comment.author.displayName }}</div>
+ <div id="comment-date">{{ createdDate }}</div>
+ </div>
+ </div>
+ <div id="comment-content">{{_computeCommentContentPrefix( comment)}}
+ {{ comment.content }}</div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-drive-comment-element',
+
+ properties: {
+ comment: {
+ type: String,
+ observer: '_commentChanged'
+ }
+ },
+
+ _commentChanged() {
+ this.createdDate = new Date(this.comment.createdDate).toLocaleString();
+ },
+
+ _computeCommentContentPrefix(comment) {
+ return comment.anchor ? '&#9875;&nbsp;' : '';
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html
new file mode 100644
index 00000000000..8f52839029d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/extras/drive/comment_element.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-e-drive-comments-side-panel'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ display: flex;
+ width: 290px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ background-color: #eee;
+ }
+ toolbar {
+ flex: 0 0 auto;
+ border-bottom: 1px solid black;
+ display: flex;
+ }
+ result-area {
+ flex: 1 1 auto;
+ display: block;
+ min-height: 0;
+ padding: 4px;
+ }
+ #comments-textarea-container {
+ display: flex;
+ }
+ #commentinput {
+ width: 100%;
+ }
+ </style>
+
+ <toolbar id='toolbar'></toolbar>
+ <result-area id='result_area'>
+ <template is="dom-repeat" items="{{comments_}}" repeat="{{ comment in comments_ }}">
+ <tr-ui-e-drive-comment-element comment="{{comment}}"
+ on-click="commentClick">
+ </tr-ui-e-drive-comment-element>
+ </template>
+ <div id="comments-textarea-container">
+ <textarea id="commentinput" on-focus='textAreaFocus'
+ on-blur='textAreaBlur'
+ on-keypress="textareaKeypress"></textarea>
+ </div>
+ </result-area>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-drive-comments-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.selection_ = undefined;
+ this.comments_ = [];
+ this.annotationFromComment_ = undefined;
+ this.textAreaFocused = false;
+ },
+
+ setCommentProvider(commentProvider) {
+ this.commentProvider_ = commentProvider;
+ },
+
+ attached() {
+ if (this.commentProvider_ === undefined) {
+ this.commentProvider_ =
+ new tr.ui.e.drive.analysis.DefaultCommentProvider();
+ }
+ this.commentProvider_.attachToElement(this);
+ },
+
+ detached() {
+ this.commentProvider_.detachFromElement();
+ },
+
+ commentClick(event) {
+ const anchor = event.currentTarget.comment.anchor;
+ if (!anchor) return;
+
+ const uiState =
+ JSON.parse(anchor).a[0][tr.ui.e.drive.constants.ANCHOR_NAME];
+
+ const myEvent = new CustomEvent('navigateToUIState', { detail:
+ new tr.ui.b.UIState(new tr.model.Location(uiState.location.xWorld,
+ uiState.location.yComponents),
+ uiState.scaleX)
+ });
+ document.dispatchEvent(myEvent);
+
+ if (this.annotationFromComment_) {
+ this.model.removeAnnotation(this.annotationFromComment_);
+ }
+ const loc = new tr.model.Location(uiState.location.xWorld,
+ uiState.location.yComponents);
+
+ const text = sender.comment.author.displayName + ': ' +
+ sender.comment.content;
+ this.annotationFromComment_ =
+ new tr.model.CommentBoxAnnotation(loc, text);
+ this.model.addAnnotation(this.annotationFromComment_);
+ },
+
+ textareaKeypress(event) {
+ // Check for return key.
+ if (event.keyCode === 13 && !event.ctrlKey) {
+ this.commentProvider_.addComment(this.$.commentinput.value);
+ this.$.commentinput.value = '';
+ }
+ event.stopPropagation();
+ return true;
+ },
+
+ textAreaFocus(event) {
+ this.textAreaFocused = true;
+ },
+
+ textAreaBlur(event) {
+ this.textAreaFocused = false;
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ this.updateContents_();
+ },
+
+ get currentRangeOfInterest() {
+ if (this.rangeOfInterest_.isEmpty) {
+ return this.model_.bounds;
+ }
+ return this.rangeOfInterest_;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ },
+
+ updateContents_() {
+ this.commentProvider_.updateComments();
+ },
+
+ supportsModel(m) {
+ if (m === undefined) {
+ return {
+ supported: false,
+ reason: 'Unknown tracing model'
+ };
+ }
+ return {
+ supported: true
+ };
+ },
+
+ get textLabel() {
+ return 'Comments';
+ }
+});
+
+tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-drive-comments-side-panel');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html
new file mode 100644
index 00000000000..639c1b9b597
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function StubCommentProvider() {
+ this.addDummyComment('Lorem ipsum dolor sit amet');
+ this.addDummyComment('consectetur adipiscing elit');
+ this.addDummyComment('sed do eiusmod tempor incididunt ut labore et ' +
+ 'dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' +
+ 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' +
+ 'consequat. Duis aute irure dolor in reprehenderit in voluptate ' +
+ 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ' +
+ 'occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' +
+ 'mollit anim id est laborum.');
+ }
+
+ StubCommentProvider.prototype = {
+ comments_: [],
+
+ attachToElement(attachedElement) {
+ this.attachedElement_ = attachedElement;
+ this.updateComments();
+ },
+
+ detachFromElement() {
+ },
+
+ updateComments() {
+ this.attachedElement_.comments_ = this.comments_;
+ },
+
+ addDummyComment(content) {
+ const newComment = {
+ author: {
+ displayName: 'Casper the Friendly Ghost',
+ picture: {
+ url: 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/' +
+ 'AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/s128/photo.jpg'
+ }
+ },
+ createdDate: Date.now(),
+ anchor: (this.comments_.length) % 2 ? 1 : 0,
+ content
+ };
+
+ this.comments_.push(newComment);
+ },
+
+ addComment(body) {
+ this.addDummyComment(body);
+ this.updateComments();
+ }
+ };
+
+ test('instantiate', function() {
+ const panel = document.createElement('tr-ui-e-drive-comments-side-panel');
+ panel.setCommentProvider(new StubCommentProvider);
+ this.addHTMLOutput(panel);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html
new file mode 100644
index 00000000000..42d3706f854
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/comment_box_annotation.html">
+
+<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+
+<script>
+'use strict';
+
+(function() {
+ function addDriveCommentWithUIState_(text, uiState) {
+ gapi.client.load('drive', 'v2', function() {
+ const request = gapi.client.drive.revisions.get({
+ 'fileId': tr.ui.e.drive.getDriveFileId(),
+ 'revisionId': 'head'
+ });
+ request.execute(function(resp) {
+ const anchorObject = {};
+ anchorObject[tr.ui.e.drive.constants.ANCHOR_NAME] = uiState;
+ let anchor = {
+ 'r': resp.id,
+ 'a': [anchorObject]
+ };
+ anchor = JSON.stringify(anchor);
+ gapi.client.load('drive', 'v2', function() {
+ const request = gapi.client.drive.comments.insert({
+ 'fileId': tr.ui.e.drive.getDriveFileId(),
+ 'resource': {'content': text, anchor}
+ });
+ request.execute();
+ });
+ });
+ });
+ }
+
+ function onCommentWithUIState(e) {
+ addDriveCommentWithUIState_(e.detail.name, e.detail.location);
+ }
+
+ document.addEventListener('commentWithUIState',
+ onCommentWithUIState.bind(this));
+}());
+
+tr.exportTo('tr.ui.e.drive.analysis', function() {
+ function DefaultCommentProvider() { }
+
+ DefaultCommentProvider.prototype = {
+ attachToElement(attachedElement) {
+ this.attachedElement_ = attachedElement;
+ this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this),
+ 5000);
+ },
+
+ detachFromElement() {
+ clearTimeout(this.commentsCheckTimer_);
+ },
+
+ checkForComments_() {
+ this.updateComments();
+ this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this),
+ 5000);
+ },
+
+ updateComments() {
+ gapi.client.load('drive', 'v2', () => {
+ const request = gapi.client.drive.comments.list({
+ 'fileId': tr.ui.e.drive.getDriveFileId()
+ });
+ request.execute(results => {
+ this.attachedElement_.comments_ = results.items;
+ });
+ });
+ },
+
+ addComment(body) {
+ gapi.client.load('drive', 'v2', () => {
+ const request = gapi.client.drive.comments.insert({
+ 'fileId': tr.ui.e.drive.getDriveFileId(),
+ 'resource': {'content': body}
+ });
+ request.execute(resp => {
+ this.updateComments();
+ });
+ });
+ }
+ };
+
+ return {
+ DefaultCommentProvider,
+ };
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html
new file mode 100644
index 00000000000..270dcccf2fc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html
@@ -0,0 +1,463 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+ <script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
+
+ <link rel="import" href="/components/polymer/polymer.html">
+ <link rel="import" href="/tracing/ui/extras/drive/drive_comment_provider.html">
+ <link rel="import" href="/tracing/ui/extras/full_config.html">
+ <link rel="import" href="/tracing/ui/timeline_view.html">
+
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+ body > x-timeline-view {
+ flex: 1 1 auto;
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ body > x-timeline-view:focus {
+ outline: none;
+ }
+ nav {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ }
+ #navbar button {
+ height: 24px;
+ padding-bottom: 3px;
+ vertical-align: middle;
+ box-shadow: none;
+ background-color: #4d90fe;
+ background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed);
+ border: 1px solid #3079ed;
+ color: #fff;
+ border-radius: 2px;
+ cursor: default;
+ font-size: 11px;
+ font-weight: bold;
+ text-align: center;
+ white-space: nowrap;
+ line-height: 27px;
+ min-width: 54px;
+ outline: 0px;
+ padding: 0 8px;
+ font: normal 13px arial,sans-serif;
+ margin: 5px;
+ }
+ #collabs {
+ display: flex;
+ flex-direction: row;
+ }
+ .collaborator-div {
+ display: inline-block;
+ vertical-align: middle;
+ min-height: 0;
+ width: 100px;
+ font-size: 11px;
+ font-weight: bold;
+ font: normal 13px arial,sans-serif;
+ margin: 10px;
+ }
+ .collaborator-img {
+ margin: 2px;
+ }
+ .collaborator-tooltip {
+ z-index: 10000;
+ transition: visibility 0,opacity .13s ease-in;
+ background-color: #2a2a2a;
+ border: 1px solid #fff;
+ color: #fff;
+ cursor: default;
+ display: block;
+ font-family: arial, sans-serif;
+ font-size: 11px;
+ font-weight: bold;
+ margin-left: -1px;
+ opacity: 1;
+ padding: 7px 9px;
+ word-break: break-word;
+ position: absolute;
+ }
+ .collaborator-tooltip-content {
+ color: #fff;
+ }
+ .collaborator-tooltip-arrow {
+ position: absolute;
+ top: -6px;
+ }
+ .collaborator-tooltip-arrow-before {
+ border-color: #fff transparent !important;
+ left: -6px;
+ border: 6px solid;
+ border-top-width: 0;
+ content: '';
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+ }
+ .collaborator-tooltip-arrow-after {
+ top: 1px;
+ border-color: #2a2a2a transparent !important;
+ left: -5px;
+ border: 5px solid;
+ border-top-width: 0;
+ content: '';
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+ }
+
+ </style>
+ <title>Trace Viewer</title>
+</head>
+<body>
+ <nav id="navbar">
+ <div id="collabs"></div>
+ <button id="x-drive-save-to-disk">Save to disk</button>
+ <button id="x-drive-save-to-drive">Save to Drive</button>
+ <button id="x-drive-load-from-drive">Load from Drive</button>
+ <button id="x-drive-share">Share</button>
+ </nav>
+ <x-timeline-view>
+ </x-timeline-view>
+
+ <script>
+ 'use strict';
+
+ // Needs to be global as it's passed through the Google API as a
+ // GET parameter.
+ let onAPIClientLoaded_ = null;
+
+ (function() {
+ tr.exportTo('tr.ui.e.drive', function() {
+ const appId = '239864068844';
+ const constants = {
+ APP_ID: appId,
+ ANCHOR_NAME: appId + '.trace_viewer',
+ DEVELOPER_KEY: 'AIzaSyDR-6_wL9vHg1_oz4JHk8IQAkv2_Y0Y8-M',
+ CLIENT_ID: '239864068844-c7gefbfdcp0j6grltulh2r88tsvl18c1.apps.' +
+ 'googleusercontent.com',
+ SCOPE: [
+ 'https://www.googleapis.com/auth/drive',
+ 'https://www.googleapis.com/auth/drive.install',
+ 'https://www.googleapis.com/auth/drive.file',
+ 'profile'
+ ]
+ };
+
+ return {
+ getDriveFileId() { return driveFileId_; },
+ constants
+ };
+ });
+
+
+ let pickerApiLoaded_ = false;
+ let oauthToken_ = null;
+
+ let timelineViewEl_ = null;
+ let driveDocument_ = null;
+ let shareClient_ = null;
+ let fileIdToLoad_ = null;
+ let driveFileId_ = null;
+
+ function parseGETParameter(val) {
+ let result = null;
+ let tmp = [];
+ location.search.substr(1).split('&').forEach(function(item) {
+ tmp = item.split('=');
+ if (tmp[0] === val) {
+ result = decodeURIComponent(tmp[1]);
+ }
+ });
+ return result;
+ }
+
+ // Use the Google API Loader script to load the google.picker script.
+ onAPIClientLoaded_ = function() {
+ const driveState = parseGETParameter('state');
+ if (driveState !== null) {
+ const driveStateJson = JSON.parse(driveState);
+ fileIdToLoad_ = String(driveStateJson.ids);
+ }
+
+ gapi.load('picker', {'callback': onPickerApiLoad});
+ gapi.load('auth', {'callback'() {
+ onAuthApiLoad(true, onAuthResultSuccess);
+ return tr.b.timeout(30e3)
+ .then(() => onAuthApiLoad(true, function() {}))
+ .then(() => tr.b.timeout(30e3))
+ .then(() => onRepeatAuthApiLoad);
+ }});
+ };
+
+ function onAuthApiLoad(tryImmediate, resultCallback) {
+ window.gapi.auth.authorize(
+ {'client_id': tr.ui.e.drive.constants.CLIENT_ID,
+ 'scope': tr.ui.e.drive.constants.SCOPE, 'immediate': tryImmediate},
+ function(authResult) {
+ handleAuthResult(authResult, tryImmediate, resultCallback);
+ });
+ }
+
+ function onPickerApiLoad() {
+ pickerApiLoaded_ = true;
+ if (fileIdToLoad_ === null) {
+ createPicker();
+ }
+ }
+
+ function onAuthResultSuccess() {
+ if (fileIdToLoad_ === null) {
+ createPicker();
+ } else {
+ loadFileFromDrive(fileIdToLoad_);
+ }
+ }
+
+ function handleAuthResult(authResult, wasImmediate, resultCallback) {
+ if (authResult && !authResult.error) {
+ oauthToken_ = authResult.access_token;
+ resultCallback();
+ } else if (wasImmediate) {
+ onAuthApiLoad(false);
+ }
+ }
+
+ function createPicker() {
+ if (pickerApiLoaded_ && oauthToken_) {
+ const view = new google.picker.View(google.picker.ViewId.DOCS);
+ view.setMimeTypes('application/json,application/octet-stream');
+ const picker = new google.picker.PickerBuilder()
+ .enableFeature(google.picker.Feature.NAV_HIDDEN)
+ .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
+ .setAppId(tr.ui.e.drive.constants.APP_ID)
+ .setOAuthToken(oauthToken_)
+ .addView(view)
+ .addView(new google.picker.DocsUploadView())
+ .setDeveloperKey(tr.ui.e.drive.constants.DEVELOPER_KEY)
+ .setCallback(pickerCallback)
+ .build();
+ picker.setVisible(true);
+ }
+ }
+
+ function pickerCallback(data) {
+ if (data.action === google.picker.Action.PICKED) {
+ loadFileFromDrive(data.docs[0].id);
+ }
+ }
+
+ function initShareButton() {
+ shareClient_ = new gapi.drive.share.ShareClient(
+ tr.ui.e.drive.constants.APP_ID);
+ shareClient_.setItemIds([driveFileId_]);
+ }
+
+ function loadFileFromDrive(fileId) {
+ gapi.client.load('drive', 'v2', function() {
+ const request = gapi.client.drive.files.get({fileId});
+ request.execute(function(resp) { downloadFile(resp); });
+ driveFileId_ = fileId;
+ gapi.load('drive-share', initShareButton);
+ });
+ }
+
+ function downloadFile(file) {
+ if (file.downloadUrl) {
+ const downloadingOverlay = tr.ui.b.Overlay();
+ downloadingOverlay.title = 'Downloading...';
+ downloadingOverlay.userCanClose = false;
+ downloadingOverlay.msgEl = document.createElement('div');
+ Polymer.dom(downloadingOverlay).appendChild(downloadingOverlay.msgEl);
+ downloadingOverlay.msgEl.style.margin = '20px';
+ downloadingOverlay.update = function(msg) {
+ Polymer.dom(this.msgEl).textContent = msg;
+ };
+ downloadingOverlay.visible = true;
+
+ const accessToken = gapi.auth.getToken().access_token;
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', file.downloadUrl);
+ xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
+ xhr.onload = function() {
+ downloadingOverlay.visible = false;
+ onDownloaded(file.title, xhr.responseText);
+ };
+ xhr.onprogress = function(evt) {
+ downloadingOverlay.update(
+ Math.floor(evt.position * 100 / file.fileSize) + '% complete');
+ };
+ xhr.onerror = function() { alert('Failed downloading!'); };
+ xhr.send();
+ } else {
+ alert('No URL!');
+ }
+ }
+
+ function displayAllCollaborators() {
+ const allCollaborators = driveDocument_.getCollaborators();
+ const collaboratorCount = allCollaborators.length;
+ const collabspan = document.getElementById('collabs');
+ Polymer.dom(collabspan).innerHTML = '';
+ const imageList = [];
+ for (let i = 0; i < collaboratorCount; i++) {
+ const user = allCollaborators[i];
+
+ const img = document.createElement('img');
+ img.src = user.photoUrl;
+ img.alt = user.displayName;
+ img.height = 30;
+ img.width = 30;
+ img.className = 'collaborator-img';
+ Polymer.dom(collabspan).appendChild(img);
+ imageList.push({'image': img, 'name': user.displayName});
+ }
+ for (i = 0; i < imageList.length; i++) {
+ const collabTooltip = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip'
+ });
+ const collabTooltipContent = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-content'
+ });
+ Polymer.dom(collabTooltipContent).textContent = imageList[i].name;
+ Polymer.dom(collabTooltip).appendChild(collabTooltipContent);
+ Polymer.dom(collabspan).appendChild(collabTooltip);
+ const collabTooltipArrow = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-arrow'});
+ Polymer.dom(collabTooltip).appendChild(collabTooltipArrow);
+ const collabTooltipArrowBefore = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-arrow-before'});
+ Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowBefore);
+ const collabTooltipArrowAfter = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-arrow-after'});
+ Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowAfter);
+
+ const rect = imageList[i].image.getBoundingClientRect();
+ collabTooltip.style.top = (rect.bottom - 6) + 'px';
+ collabTooltip.style.left =
+ (rect.left + 16 - (collabTooltip.offsetWidth / 2)) + 'px';
+ collabTooltipArrow.style.left = (collabTooltip.offsetWidth / 2) + 'px';
+ collabTooltip.style.visibility = 'hidden';
+ function visibilityDelegate(element, visibility) {
+ return function() {
+ element.style.visibility = visibility;
+ };
+ }
+ imageList[i].image.addEventListener(
+ 'mouseover', visibilityDelegate(collabTooltip, 'visible'));
+ imageList[i].image.addEventListener(
+ 'mouseout', visibilityDelegate(collabTooltip, 'hidden'));
+ }
+ }
+
+ function onRealtimeFileLoaded(doc) {
+ if (driveDocument_) {
+ driveDocument_.close();
+ }
+ driveDocument_ = doc;
+ doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED,
+ displayAllCollaborators);
+ doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT,
+ displayAllCollaborators);
+
+ displayAllCollaborators(doc);
+ }
+
+ function onRealtimeError(e) {
+ alert('Error loading realtime: ' + e);
+ }
+
+ function onDownloaded(filename, content) {
+ gapi.load('auth:client,drive-realtime,drive-share', function() {
+ gapi.drive.realtime.load(driveFileId_,
+ onRealtimeFileLoaded,
+ null,
+ onRealtimeError);
+ });
+
+ const traces = [];
+ const filenames = [];
+ filenames.push(filename);
+ traces.push(content);
+ createViewFromTraces(filenames, traces);
+ }
+
+ function createViewFromTraces(filenames, traces) {
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m);
+ const p = i.importTracesWithProgressDialog(traces);
+ p.then(
+ function() {
+ timelineViewEl_.model = m;
+ timelineViewEl_.updateDocumentFavicon();
+ timelineViewEl_.globalMode = true;
+ timelineViewEl_.viewTitle = '';
+ },
+ function(err) {
+ const downloadingOverlay = new tr.ui.b.Overlay();
+ Polymer.dom(downloadingOverlay).textContent =
+ tr.b.normalizeException(err).message;
+ downloadingOverlay.title = 'Import error';
+ downloadingOverlay.visible = true;
+ });
+ }
+
+ function onSaveToDiskClicked() {
+ throw new Error('Not implemented');
+ }
+
+ function onSaveToDriveClicked() {
+ throw new Error('Not implemented');
+ }
+
+ function onLoadFromDriveClicked() {
+ createPicker();
+ }
+
+ function onLoad() {
+ timelineViewEl_ = Polymer.dom(document).querySelector('x-timeline-view');
+ timelineViewEl_.globalMode = true;
+ const navbar = document.getElementById('navbar');
+ timelineViewEl_.style.top = navbar.offsetHeight + 'px';
+ tr.ui.b.decorate(timelineViewEl_, tr.ui.TimelineView);
+ }
+
+ window.addEventListener('load', onLoad);
+
+ document.getElementById('x-drive-save-to-disk').onclick =
+ onSaveToDiskClicked;
+ document.getElementById('x-drive-save-to-drive').onclick =
+ onSaveToDriveClicked;
+ document.getElementById('x-drive-load-from-drive').onclick =
+ onLoadFromDriveClicked;
+ document.getElementById('x-drive-share').onclick = function() {
+ shareClient_.showSettingsDialog();
+ };
+ }());
+
+ </script>
+ <script type="text/javascript"
+ src="https://apis.google.com/js/client.js?onload=onAPIClientLoaded_">
+ </script>
+</body>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html
new file mode 100644
index 00000000000..6d1e29d4e20
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the
+same way that all non-UI files depend on tracing/base/base.html. Enforce this
+dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<!-- The full config is all the configs slammed together. -->
+<link rel="import" href="/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html">
+<link rel="import" href="/tracing/ui/extras/chrome_config.html">
+<link rel="import" href="/tracing/ui/extras/lean_config.html">
+<link rel="import" href="/tracing/ui/extras/systrace_config.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html
new file mode 100644
index 00000000000..8d66352f140
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the same way that
+all non-UI files depend on tracing/base/base.html. Enforce this dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/extras/lean_config.html" data-suppress-import-order>
+
+<!--
+The lean config is just enough to import uncompressed, trace-event-formatted
+json blobs.
+-->
+<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html
new file mode 100644
index 00000000000..0971d7f5409
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-e-s-alerts-side-panel'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ width: 250px;
+ }
+ #content {
+ flex-direction: column;
+ display: flex;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+
+ <div id='content'>
+ <toolbar id='toolbar'></toolbar>
+ <result-area id='result_area'></result-area>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-s-alerts-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+
+ ready() {
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.selection_ = undefined;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ },
+
+ set selection(selection) {
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ },
+
+ /**
+ * Fires a selection event selecting all alerts of the specified
+ * type.
+ */
+ selectAlertsOfType(alertTypeString) {
+ const alertsOfType = this.model_.alerts.filter(function(alert) {
+ return alert.title === alertTypeString;
+ });
+
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = new tr.model.EventSet(alertsOfType);
+ this.dispatchEvent(event);
+ },
+
+ /**
+ * Returns a map for the specified alerts where each key is the
+ * alert type string and each value is a list of alerts with that
+ * type.
+ */
+ alertsByType_(alerts) {
+ const alertsByType = {};
+ alerts.forEach(function(alert) {
+ if (!alertsByType[alert.title]) {
+ alertsByType[alert.title] = [];
+ }
+
+ alertsByType[alert.title].push(alert);
+ });
+ return alertsByType;
+ },
+
+ alertsTableRows_(alertsByType) {
+ return Object.keys(alertsByType).map(function(key) {
+ return {
+ alertType: key,
+ count: alertsByType[key].length
+ };
+ });
+ },
+
+ alertsTableColumns_() {
+ return [
+ {
+ title: 'Alert type',
+ value(row) { return row.alertType; },
+ width: '180px'
+ },
+ {
+ title: 'Count',
+ width: '100%',
+ value(row) { return row.count; }
+ }
+ ];
+ },
+
+ createAlertsTable_(alerts) {
+ const alertsByType = this.alertsByType_(alerts);
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = this.alertsTableColumns_();
+ table.tableRows = this.alertsTableRows_(alertsByType);
+ table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ table.addEventListener('selection-changed', function(e) {
+ const row = table.selectedTableRow;
+ if (row) {
+ this.selectAlertsOfType(row.alertType);
+ }
+ }.bind(this));
+
+ return table;
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.result_area).textContent = '';
+ if (this.model_ === undefined) return;
+
+ const panel = this.createAlertsTable_(this.model_.alerts);
+ Polymer.dom(this.$.result_area).appendChild(panel);
+ },
+
+ supportsModel(m) {
+ if (m === undefined) {
+ return {
+ supported: false,
+ reason: 'Unknown tracing model'
+ };
+ } else if (m.alerts.length === 0) {
+ return {
+ supported: false,
+ reason: 'No alerts in tracing model'
+ };
+ }
+
+ return {
+ supported: true
+ };
+ },
+
+ get textLabel() {
+ return 'Alerts';
+ }
+});
+
+tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-s-alerts-side-panel');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html
new file mode 100644
index 00000000000..c4cb9825d1e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert');
+ const ALERT_INFO_2 = new tr.model.EventInfo(
+ 'Alert 2', 'Warning alert');
+
+ test('instantiate', function() {
+ const panel = document.createElement('tr-ui-e-s-alerts-side-panel');
+ panel.model = createModelWithAlerts([
+ new tr.model.Alert(ALERT_INFO_1, 5),
+ new tr.model.Alert(ALERT_INFO_2, 35)
+ ]);
+ panel.style.height = '100px';
+
+ this.addHTMLOutput(panel);
+ });
+
+ test('selectAlertsOfType', function() {
+ const panel = document.createElement('tr-ui-e-s-alerts-side-panel');
+ const alerts = [
+ new tr.model.Alert(ALERT_INFO_1, 1),
+ new tr.model.Alert(ALERT_INFO_1, 2),
+ new tr.model.Alert(ALERT_INFO_2, 3)
+ ];
+
+ const predictedAlerts = new tr.model.EventSet([alerts[0], alerts[1]]);
+ panel.model = createModelWithAlerts(alerts);
+ panel.style.height = '100px';
+ this.addHTMLOutput(panel);
+
+ let selectionChanged = false;
+ panel.addEventListener('requestSelectionChange', function(e) {
+ selectionChanged = true;
+ assert.isTrue(e.selection.equals(predictedAlerts));
+ });
+ panel.selectAlertsOfType(ALERT_INFO_1.title);
+
+ assert.isTrue(selectionChanged);
+ });
+
+ function createModelWithAlerts(alerts) {
+ const m = new tr.Model();
+ m.alerts = alerts;
+ return m;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html
new file mode 100644
index 00000000000..e5fd7689479
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html
@@ -0,0 +1,347 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-e-s-frame-data-side-panel'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ width: 600px;
+ flex-direction: column;
+ }
+ table-container {
+ display: flex;
+ overflow: auto;
+ font-size: 12px;
+ }
+ </style>
+ <div>
+ Organize by:
+ <select id="select">
+ <option value="none">None</option>
+ <option value="tree">Frame Tree</option>
+ </select>
+ </div>
+ <table-container>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </table-container>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.e.s', function() {
+ const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
+ const FrameTreeNodeSnapshot = tr.e.chrome.FrameTreeNodeSnapshot;
+ const RenderFrameSnapshot = tr.e.chrome.RenderFrameSnapshot;
+ const TopLevelSnapshot = tr.e.chrome.TopLevelSnapshot;
+
+ const BlameContextInstance = tr.e.chrome.BlameContextInstance;
+ const FrameTreeNodeInstance = tr.e.chrome.FrameTreeNodeInstance;
+ const RenderFrameInstance = tr.e.chrome.RenderFrameInstance;
+ const TopLevelInstance = tr.e.chrome.TopLevelInstance;
+
+ /**
+ * @constructor
+ * If |context| is provided, creates a row for the given context.
+ * Otherwise, creates an empty Row template which can be used for aggregating
+ * data from a group of subrows.
+ */
+ function Row(context) {
+ this.subRows = undefined;
+ this.contexts = [];
+ this.type = undefined;
+ this.renderer = 'N/A';
+ this.url = undefined;
+ this.time = 0;
+ this.eventsOfInterest = new tr.model.EventSet();
+
+ if (context === undefined) return;
+
+ this.type = context.objectInstance.blameContextType;
+ this.contexts.push(context);
+ if (context instanceof FrameTreeNodeSnapshot) {
+ if (context.renderFrame) {
+ this.contexts.push(context.renderFrame);
+ this.renderer = context.renderFrame.objectInstance.parent.pid;
+ }
+ } else if (context instanceof RenderFrameSnapshot) {
+ if (context.frameTreeNode) {
+ this.contexts.push(context.frameTreeNode);
+ }
+ this.renderer = context.objectInstance.parent.pid;
+ } else if (context instanceof TopLevelSnapshot) {
+ this.renderer = context.objectInstance.parent.pid;
+ } else {
+ throw new Error('Unknown context type');
+ }
+ this.eventsOfInterest.addEventSet(this.contexts);
+
+ // TODO(xiaochengh): Handle the case where a subframe has a trivial url
+ // (e.g., about:blank), but inherits the origin of its parent. This is not
+ // needed now, but will be required if we want to group rows by origin.
+ this.url = context.url;
+ }
+
+ const groupFunctions = {
+ none: rows => rows,
+
+ // Group the rows according to the frame tree structure.
+ // Example: consider frame tree a(b, c(d)), where each frame has 1ms time
+ // attributed to it. The resulting table should look like:
+ // Type | Time | URL
+ // --------------+------+-----
+ // Frame Tree | 4 | a
+ // +- Frame | 1 | a
+ // +- Subframe | 1 | b
+ // +- Frame Tree | 2 | c
+ // +- Frame | 1 | c
+ // +- Subframe | 1 | d
+ tree(rows, rowMap) {
+ // Finds the parent of a specific row. When there is conflict between the
+ // browser's dump of the frame tree and the renderers', use the browser's.
+ const getParentRow = function(row) {
+ let pivot;
+ row.contexts.forEach(function(context) {
+ if (context instanceof tr.e.chrome.FrameTreeNodeSnapshot) {
+ pivot = context;
+ }
+ });
+ if (pivot && pivot.parentContext) {
+ return rowMap[pivot.parentContext.guid];
+ }
+ return undefined;
+ };
+
+ const rootRows = [];
+ rows.forEach(function(row) {
+ const parentRow = getParentRow(row);
+ if (parentRow === undefined) {
+ rootRows.push(row);
+ return;
+ }
+ if (parentRow.subRows === undefined) {
+ parentRow.subRows = [];
+ }
+ parentRow.subRows.push(row);
+ });
+
+ const aggregateAllDescendants = function(row) {
+ if (!row.subRows) {
+ if (getParentRow(row)) {
+ row.type = 'Subframe';
+ }
+ return row;
+ }
+ const result = new Row();
+ result.type = 'Frame Tree';
+ result.renderer = row.renderer;
+ result.url = row.url;
+ result.subRows = [row];
+ row.subRows.forEach(
+ subRow => result.subRows.push(aggregateAllDescendants(subRow)));
+ result.subRows.forEach(function(subRow) {
+ result.time += subRow.time;
+ result.eventsOfInterest.addEventSet(subRow.eventsOfInterest);
+ });
+ row.subRows = undefined;
+ return result;
+ };
+
+ return rootRows.map(rootRow => aggregateAllDescendants(rootRow));
+ }
+
+ // TODO(xiaochengh): Add grouping by site and probably more...
+ };
+
+ Polymer({
+ is: 'tr-ui-e-s-frame-data-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.model_ = undefined;
+ this.rangeOfInterest_ = new tr.b.math.Range();
+
+ this.$.table.showHeader = true;
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.tableColumns = this.createFrameDataTableColumns_();
+
+ this.$.table.addEventListener('selection-changed', function(e) {
+ this.selectEventSet_(this.$.table.selectedTableRow.eventsOfInterest);
+ }.bind(this));
+
+ this.$.select.addEventListener('change', function(e) {
+ this.updateContents_();
+ }.bind(this));
+ },
+
+ selectEventSet_(eventSet) {
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = eventSet;
+ this.dispatchEvent(event);
+ },
+
+ createFrameDataTableColumns_() {
+ return [
+ {
+ title: 'Renderer',
+ value: row => row.renderer,
+ cmp: (a, b) => a.renderer - b.renderer
+ },
+ {
+ title: 'Type',
+ value: row => row.type
+ },
+ // TODO(xiaochengh): Decide what details to show in the table:
+ // - URL seems necessary, but we may also want origin instead/both.
+ // - Distinguish between browser time and renderer time?
+ // - Distinguish between CPU time and wall clock time?
+ // - Memory? Network? ...
+ {
+ title: 'Time',
+ value: row => tr.v.ui.createScalarSpan(row.time, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ }),
+ cmp: (a, b) => a.time - b.time
+ },
+ {
+ title: 'URL',
+ value: row => row.url,
+ cmp: (a, b) => (a.url || '').localeCompare(b.url || '')
+ }
+ ];
+ },
+
+ createFrameDataTableRows_() {
+ if (!this.model_) return [];
+
+ // Gather contexts into skeletons of rows.
+ const rows = [];
+ const rowMap = {};
+ for (const proc of Object.values(this.model_.processes)) {
+ proc.objects.iterObjectInstances(function(objectInstance) {
+ if (!(objectInstance instanceof BlameContextInstance)) {
+ return;
+ }
+ objectInstance.snapshots.forEach(function(snapshot) {
+ if (rowMap[snapshot.guid]) return;
+
+ const row = new Row(snapshot);
+ row.contexts.forEach(context => rowMap[context.guid] = row);
+ rows.push(row);
+ }, this);
+ }, this);
+ }
+
+ // Find slices attributed to each row.
+ // TODO(xiaochengh): We should implement a getter
+ // BlameContextSnapshot.attributedEvents, instead of process the model in
+ // a UI component.
+ for (const proc of Object.values(this.model_.processes)) {
+ for (const thread of Object.values(proc.threads)) {
+ thread.sliceGroup.iterSlicesInTimeRange(function(topLevelSlice) {
+ topLevelSlice.contexts.forEach(function(context) {
+ if (!context.snapshot.guid || !rowMap[context.snapshot.guid]) {
+ return;
+ }
+ const row = rowMap[context.snapshot.guid];
+ row.eventsOfInterest.push(topLevelSlice);
+ row.time += topLevelSlice.selfTime || 0;
+ });
+ }, this.currentRangeOfInterest.min, this.currentRangeOfInterest.max);
+ }
+ }
+
+ // Apply grouping to rows.
+ const select = this.$.select;
+ const groupOption = select.options[select.selectedIndex].value;
+ const groupFunction = groupFunctions[groupOption];
+ return groupFunction(rows, rowMap);
+ },
+
+ updateContents_() {
+ this.$.table.tableRows = this.createFrameDataTableRows_();
+ this.$.table.rebuild();
+ },
+
+ supportsModel(m) {
+ if (!m) {
+ return {
+ supported: false,
+ reason: 'No model available.'
+ };
+ }
+
+ const ans = {supported: false};
+ for (const proc of Object.values(m.processes)) {
+ proc.objects.iterObjectInstances(function(instance) {
+ if (instance instanceof BlameContextInstance) {
+ ans.supported = true;
+ }
+ });
+ }
+
+ if (!ans.supported) {
+ ans.reason = 'No frame data available';
+ }
+ return ans;
+ },
+
+ get currentRangeOfInterest() {
+ if (this.rangeOfInterest_.isEmpty) {
+ return this.model_.bounds;
+ }
+ return this.rangeOfInterest_;
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ this.updateContents_();
+ },
+
+ get selection() {
+ // Not applicable.
+ },
+
+ set selection(_) {
+ // Not applicable.
+ },
+
+ get textLabel() {
+ return 'Frame Data';
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-s-frame-data-side-panel');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html
new file mode 100644
index 00000000000..298afe05d42
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+
+ function topLevelOptions(pid, id) {
+ return {
+ pid,
+ id,
+ cat: 'blink',
+ scope: 'PlatformThread',
+ name: 'TopLevel'
+ };
+ }
+
+ function renderFrameOptions(pid, id, parent) {
+ return {
+ pid,
+ id,
+ cat: 'blink',
+ scope: 'RenderFrame',
+ name: 'RenderFrame',
+ args: {parent: {
+ id_ref: parent.id,
+ scope: parent.scope
+ }}
+ };
+ }
+
+ function frameTreeNodeOptions(pid, id, opt_renderFrame, opt_parentId) {
+ const ans = {
+ pid,
+ id,
+ cat: 'navigation',
+ scope: 'FrameTreeNode',
+ name: 'FrameTreeNode',
+ args: {}
+ };
+ if (opt_renderFrame) {
+ ans.args.renderFrame = {
+ id_ref: opt_renderFrame.id,
+ pid_ref: opt_renderFrame.pid,
+ scope: 'RenderFrame'
+ };
+ }
+ if (opt_parentId) {
+ ans.args.parent = {
+ id_ref: opt_parentId,
+ scope: 'FrameTreeNode'
+ };
+ }
+ return ans;
+ }
+
+ /**
+ * Creates some independent contexts. Checks if all are present in the panel.
+ */
+ test('basic', function() {
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ TestUtils.newSnapshot(model, renderFrameOptions(
+ 1, '0x2', {id: '0x1', scope: 'PlatformThread'}));
+ TestUtils.newSnapshot(model, frameTreeNodeOptions(
+ 2, '0x3'));
+ });
+ assert.lengthOf(panel.$.table.tableRows, 3);
+
+ this.addHTMLOutput(panel);
+ });
+
+ /**
+ * Creates a FrameTreeNode in the browser process and a RenderFrame in a
+ * renderer process that are the same frame. Checks if they are merged into
+ * one row in the panel.
+ */
+ test('mergeCrossProcessFrameBlameContexts', function() {
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ TestUtils.newSnapshot(model, renderFrameOptions(
+ 1, '0x2', {id: '0x1', scope: 'PlatformThread'}));
+ TestUtils.newSnapshot(model, frameTreeNodeOptions(
+ 2, '0x3', {id: '0x2', pid: 1}));
+ });
+ assert.lengthOf(panel.$.table.tableRows, 2);
+
+ this.addHTMLOutput(panel);
+ });
+
+ function newAttributedSlice(model, pid, start, duration, context) {
+ const slice = TestUtils.newSliceEx({start, duration});
+ slice.contexts = [{type: 'FrameBlameContext', snapshot: context}];
+ model.getOrCreateProcess(pid).getOrCreateThread(1).sliceGroup.pushSlice(
+ slice);
+ return slice;
+ }
+
+ /**
+ * Changes the range of interest. Checks if the panel updates correspondingly.
+ */
+ test('respondToRangeOfInterest', function() {
+ let topLevel;
+ let slice1;
+ let slice2;
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ slice1 = newAttributedSlice(model, 1, 1500, 500, topLevel);
+ slice2 = newAttributedSlice(model, 1, 2500, 500, topLevel);
+ });
+
+ // The default range of interest contains both slices.
+ assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals(
+ new tr.model.EventSet([topLevel, slice1, slice2])));
+
+ // The new range of interest contains only slice2.
+ panel.rangeOfInterest = tr.b.math.Range.fromExplicitRange(slice2.start,
+ slice2.end);
+ assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals(
+ new tr.model.EventSet([topLevel, slice2])));
+
+ this.addHTMLOutput(panel);
+ });
+
+ /**
+ * Selects a row in the panel. Checks if the context(s) of the row and the
+ * slices attributed to the row are selected.
+ */
+ test('selectAttributedEvents', function() {
+ let topLevel;
+ let slice;
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ slice = newAttributedSlice(model, 1, 1500, 500, topLevel);
+ });
+
+ let selectionChanged = false;
+ panel.addEventListener('requestSelectionChange', function(e) {
+ selectionChanged = true;
+ assert.isTrue(
+ e.selection.equals(new tr.model.EventSet([topLevel, slice])));
+ });
+ panel.$.table.selectedTableRow = panel.$.table.tableRows[0];
+ assert.isTrue(selectionChanged);
+
+ this.addHTMLOutput(panel);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html
new file mode 100644
index 00000000000..14e33919922
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html
@@ -0,0 +1,334 @@
+<!DOCTYPE html>
+<!--
+Copyright 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-e-s-input-latency-side-panel'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ display: flex;
+ }
+ toolbar {
+ flex: 0 0 auto;
+ border-bottom: 1px solid black;
+ display: flex;
+ }
+ result-area {
+ flex: 1 1 auto;
+ display: block;
+ min-height: 0;
+ overflow-y: auto;
+ }
+ </style>
+
+ <toolbar id='toolbar'></toolbar>
+ <result-area id='result_area'></result-area>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-s-input-latency-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+
+ ready() {
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.frametimeType_ = tr.model.helpers.IMPL_FRAMETIME_TYPE;
+ this.latencyChart_ = undefined;
+ this.frametimeChart_ = undefined;
+ this.selectedProcessId_ = undefined;
+ this.mouseDownIndex_ = undefined;
+ this.curMouseIndex_ = undefined;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ if (this.model_) {
+ this.modelHelper_ = this.model_.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ } else {
+ this.modelHelper_ = undefined;
+ }
+
+ this.updateToolbar_();
+ this.updateContents_();
+ },
+
+ get frametimeType() {
+ return this.frametimeType_;
+ },
+
+ set frametimeType(type) {
+ if (this.frametimeType_ === type) return;
+
+ this.frametimeType_ = type;
+ this.updateContents_();
+ },
+
+ get selectedProcessId() {
+ return this.selectedProcessId_;
+ },
+
+ set selectedProcessId(process) {
+ if (this.selectedProcessId_ === process) return;
+
+ this.selectedProcessId_ = process;
+ this.updateContents_();
+ },
+
+ set selection(selection) {
+ if (this.latencyChart_ === undefined) return;
+
+ this.latencyChart_.brushedRange = selection.bounds;
+ },
+
+ // This function is for testing purpose.
+ setBrushedIndices(mouseDownIndex, curIndex) {
+ this.mouseDownIndex_ = mouseDownIndex;
+ this.curMouseIndex_ = curIndex;
+ this.updateBrushedRange_();
+ },
+
+ updateBrushedRange_() {
+ if (this.latencyChart_ === undefined) return;
+
+ let r = new tr.b.math.Range();
+ if (this.mouseDownIndex_ === undefined) {
+ this.latencyChart_.brushedRange = r;
+ return;
+ }
+ r = this.latencyChart_.computeBrushRangeFromIndices(
+ this.mouseDownIndex_, this.curMouseIndex_);
+ this.latencyChart_.brushedRange = r;
+
+ // Based on the brushed range, update the selection of LatencyInfo in
+ // the timeline view by sending a selectionChange event.
+ let latencySlices = [];
+ for (const thread of this.model_.getAllThreads()) {
+ for (const event of thread.getDescendantEvents()) {
+ if (event.title.indexOf('InputLatency:') === 0) {
+ latencySlices.push(event);
+ }
+ }
+ }
+ latencySlices = tr.model.helpers.getSlicesIntersectingRange(
+ r, latencySlices);
+
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = new tr.model.EventSet(latencySlices);
+ this.latencyChart_.dispatchEvent(event);
+ },
+
+ registerMouseEventForLatencyChart_() {
+ this.latencyChart_.addEventListener('item-mousedown', function(e) {
+ this.mouseDownIndex_ = e.index;
+ this.curMouseIndex_ = e.index;
+ this.updateBrushedRange_();
+ }.bind(this));
+
+ this.latencyChart_.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+
+ this.curMouseIndex_ = e.index;
+ this.updateBrushedRange_();
+ }.bind(this));
+
+ this.latencyChart_.addEventListener('item-mouseup', function(e) {
+ this.curMouseIndex = e.index;
+ this.updateBrushedRange_();
+ }.bind(this));
+ },
+
+ updateToolbar_() {
+ const browserProcess = this.modelHelper_.browserProcess;
+ const labels = [];
+
+ if (browserProcess !== undefined) {
+ const labelStr = 'Browser: ' + browserProcess.pid;
+ labels.push({label: labelStr, value: browserProcess.pid});
+ }
+
+ for (const rendererHelper of
+ Object.values(this.modelHelper_.rendererHelpers)) {
+ const rendererProcess = rendererHelper.process;
+ const labelStr = 'Renderer: ' + rendererProcess.userFriendlyName;
+ labels.push({label: labelStr, value: rendererProcess.userFriendlyName});
+ }
+
+ if (labels.length === 0) return;
+
+ this.selectedProcessId_ = labels[0].value;
+ const toolbarEl = this.$.toolbar;
+ Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector(
+ this, 'frametimeType',
+ 'inputLatencySidePanel.frametimeType', this.frametimeType_,
+ [{label: 'Main Thread Frame Times',
+ value: tr.model.helpers.MAIN_FRAMETIME_TYPE},
+ {label: 'Impl Thread Frame Times',
+ value: tr.model.helpers.IMPL_FRAMETIME_TYPE}
+ ]));
+ Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector(
+ this, 'selectedProcessId',
+ 'inputLatencySidePanel.selectedProcessId',
+ this.selectedProcessId_,
+ labels));
+ },
+
+ // TODO(charliea): Delete this function in favor of rangeOfInterest.
+ get currentRangeOfInterest() {
+ if (this.rangeOfInterest_.isEmpty) {
+ return this.model_.bounds;
+ }
+ return this.rangeOfInterest_;
+ },
+
+ createLatencyLineChart(data, title, parentNode) {
+ const chart = new tr.ui.b.LineChart();
+ Polymer.dom(parentNode).appendChild(chart);
+ let width = 600;
+ if (document.body.clientWidth !== undefined) {
+ width = document.body.clientWidth * 0.5;
+ }
+ chart.graphWidth = width;
+ chart.chartTitle = title;
+ chart.data = data;
+ return chart;
+ },
+
+ updateContents_() {
+ const resultArea = this.$.result_area;
+ this.latencyChart_ = undefined;
+ this.frametimeChart_ = undefined;
+ Polymer.dom(resultArea).textContent = '';
+
+ if (this.modelHelper_ === undefined) return;
+
+ const rangeOfInterest = this.currentRangeOfInterest;
+
+ let chromeProcess;
+ if (this.modelHelper_.rendererHelpers[this.selectedProcessId_]) {
+ chromeProcess = this.modelHelper_.rendererHelpers[
+ this.selectedProcessId_
+ ];
+ } else {
+ chromeProcess = this.modelHelper_.browserHelper;
+ }
+
+ const frameEvents = chromeProcess.getFrameEventsInRange(
+ this.frametimeType, rangeOfInterest);
+
+ const frametimeData = tr.model.helpers.getFrametimeDataFromEvents(
+ frameEvents);
+ const averageFrametime = tr.b.math.Statistics.mean(frametimeData, d =>
+ d.frametime
+ );
+
+ const latencyEvents = this.modelHelper_.browserHelper.
+ getLatencyEventsInRange(
+ rangeOfInterest);
+
+ const latencyData = [];
+ latencyEvents.forEach(function(event) {
+ if (event.inputLatency === undefined) return;
+
+ latencyData.push({
+ x: event.start,
+ latency: event.inputLatency / 1000
+ });
+ });
+
+ const averageLatency = tr.b.math.Statistics.mean(latencyData, function(d) {
+ return d.latency;
+ });
+
+ // Create summary.
+ const latencySummaryText = document.createElement('div');
+ Polymer.dom(latencySummaryText).appendChild(tr.ui.b.createSpan({
+ textContent: 'Average Latency ' + averageLatency + ' ms',
+ bold: true}));
+ Polymer.dom(resultArea).appendChild(latencySummaryText);
+
+ const frametimeSummaryText = document.createElement('div');
+ Polymer.dom(frametimeSummaryText).appendChild(tr.ui.b.createSpan({
+ textContent: 'Average Frame Time ' + averageFrametime + ' ms',
+ bold: true}));
+ Polymer.dom(resultArea).appendChild(frametimeSummaryText);
+
+ if (latencyData.length !== 0) {
+ this.latencyChart_ = this.createLatencyLineChart(
+ latencyData, 'Latency Over Time', resultArea);
+ this.registerMouseEventForLatencyChart_();
+ }
+
+ if (frametimeData.length !== 0) {
+ this.frametimeChart_ = this.createLatencyLineChart(
+ frametimeData, 'Frame Times', resultArea);
+ }
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ this.updateContents_();
+ },
+
+ supportsModel(m) {
+ if (m === undefined) {
+ return {
+ supported: false,
+ reason: 'Unknown tracing model'
+ };
+ }
+
+ if (!tr.model.helpers.ChromeModelHelper.supportsModel(m)) {
+ return {
+ supported: false,
+ reason: 'No Chrome browser or renderer process found'
+ };
+ }
+
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ if (modelHelper.browserHelper &&
+ modelHelper.browserHelper.hasLatencyEvents) {
+ return {
+ supported: true
+ };
+ }
+
+ return {
+ supported: false,
+ reason: 'No InputLatency events trace. Consider enabling ' +
+ 'benchmark" and "input" category when recording the trace'
+ };
+ },
+
+ get textLabel() {
+ return 'Input Latency';
+ }
+});
+
+tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-s-input-latency-side-panel');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html
new file mode 100644
index 00000000000..de225416faa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const latencyData = [
+ {
+ x: 1000,
+ latency: 16
+ },
+ {
+ x: 2000,
+ latency: 17
+ },
+ {
+ x: 3000,
+ latency: 14
+ },
+ {
+ x: 4000,
+ latency: 23
+ }
+ ];
+ let lc = document.createElement('tr-ui-e-s-input-latency-side-panel');
+ let container = document.createElement('div');
+ this.addHTMLOutput(container);
+ const latencyChart = lc.createLatencyLineChart(
+ latencyData, 'latency', container);
+
+ const frametimeData = [
+ {
+ x: 1000,
+ frametime: 16
+ },
+ {
+ x: 2000,
+ frametime: 17
+ },
+ {
+ x: 3000,
+ frametime: 14
+ },
+ {
+ x: 4000,
+ frametime: 23
+ }
+ ];
+ lc = document.createElement('tr-ui-e-s-input-latency-side-panel');
+ container = document.createElement('div');
+ this.addHTMLOutput(container);
+ const frametimeChart = lc.createLatencyLineChart(
+ frametimeData, 'frametime', container);
+ });
+
+ test('brushedRangeChange', function() {
+ const events = [];
+ for (let i = 0; i < 10; i++) {
+ const startTs = i * 10000;
+ const endTs = startTs + 1000 * (i % 2);
+ events.push(
+ {
+ 'cat': 'benchmark',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': startTs,
+ 'ph': 'S',
+ 'name': 'InputLatency',
+ 'id': i
+ });
+ events.push(
+ {
+ 'cat': 'benchmark',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': endTs,
+ 'ph': 'T',
+ 'name': 'InputLatency',
+ 'args': {'step': 'GestureScrollUpdate'},
+ 'id': i
+ });
+ events.push(
+ {
+ 'cat': 'benchmark',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': endTs,
+ 'ph': 'F',
+ 'name': 'InputLatency',
+ 'args': {
+ 'data': {
+ 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT': {
+ 'time': startTs
+ },
+ 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT': {
+ 'time': endTs
+ }
+ }
+ },
+ 'id': i
+ });
+ }
+ events.push({'cat': '__metadata',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': 0,
+ 'ph': 'M',
+ 'name': 'thread_name',
+ 'args': {'name': 'CrBrowserMain'}});
+
+ const panel = document.createElement('tr-ui-e-s-input-latency-side-panel');
+ this.addHTMLOutput(panel);
+
+ let selectionChanged = false;
+
+ panel.model = tr.c.TestUtils.newModelWithEvents([events]);
+ function listener(e) {
+ selectionChanged = true;
+ assert.strictEqual(e.selection.length, 3);
+ const predictedStarts = [20, 31, 40];
+ let i = 0;
+ for (const event of e.selection) {
+ assert.strictEqual(event.start, predictedStarts[i++]);
+ }
+ }
+ panel.ownerDocument.addEventListener('requestSelectionChange', listener);
+ try {
+ panel.setBrushedIndices(2, 4);
+ } finally {
+ panel.ownerDocument.removeEventListener(
+ 'requestSelectionChange', listener);
+ }
+ assert.isTrue(selectionChanged);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html
new file mode 100644
index 00000000000..31bc1dbd997
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/system_stats/system_stats_snapshot.html">
+<link rel="import"
+ href="/tracing/ui/extras/system_stats/system_stats_instance_track.html">
+<link rel="import"
+ href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css
new file mode 100644
index 00000000000..40096f5497c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css
@@ -0,0 +1,15 @@
+/* Copyright (c) 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.
+ */
+
+.tr-ui-e-system-stats-instance-track {
+ height: 500px;
+}
+
+.tr-ui-e-system-stats-instance-track ul {
+ list-style: none;
+ list-style-position: outside;
+ margin: 0;
+ overflow: hidden;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html
new file mode 100644
index 00000000000..7695086660c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet"
+ href="/tracing/ui/extras/system_stats/system_stats_instance_track.css">
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+<link rel="import" href="/tracing/ui/tracks/stacked_bars_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.system_stats', function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ let statCount;
+
+ const excludedStats = {'meminfo': {
+ 'pswpin': 0,
+ 'pswpout': 0,
+ 'pgmajfault': 0},
+ 'diskinfo': {
+ 'io': 0,
+ 'io_time': 0,
+ 'read_time': 0,
+ 'reads': 0,
+ 'reads_merged': 0,
+ 'sectors_read': 0,
+ 'sectors_written': 0,
+ 'weighted_io_time': 0,
+ 'write_time': 0,
+ 'writes': 0,
+ 'writes_merged': 0},
+ 'swapinfo': {},
+ 'perfinfo': {
+ 'idle_time': 0,
+ 'read_transfer_count': 0,
+ 'write_transfer_count': 0,
+ 'other_transfer_count': 0,
+ 'read_operation_count': 0,
+ 'write_operation_count': 0,
+ 'other_operation_count': 0,
+ 'pagefile_pages_written': 0,
+ 'pagefile_pages_write_ios': 0,
+ 'available_pages': 0,
+ 'pages_read': 0,
+ 'page_read_ios': 0}
+ };
+
+ /**
+ * Tracks that display system stats data.
+ *
+ * @constructor
+ * @extends {StackedBarsTrack}
+ */
+
+ const SystemStatsInstanceTrack = tr.ui.b.define(
+ 'tr-ui-e-system-stats-instance-track', tr.ui.tracks.StackedBarsTrack);
+
+ const kPageSizeWindows = 4096;
+
+ SystemStatsInstanceTrack.prototype = {
+
+ __proto__: tr.ui.tracks.StackedBarsTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.StackedBarsTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('tr-ui-e-system-stats-instance-track');
+ this.objectInstance_ = null;
+ },
+
+ set objectInstances(objectInstances) {
+ if (!objectInstances) {
+ this.objectInstance_ = [];
+ return;
+ }
+ if (objectInstances.length !== 1) {
+ throw new Error('Bad object instance count.');
+ }
+ this.objectInstance_ = objectInstances[0];
+ if (this.objectInstance_ !== null) {
+ this.computeRates_(this.objectInstance_.snapshots);
+ this.maxStats_ = this.computeMaxStats_(
+ this.objectInstance_.snapshots);
+ }
+ },
+
+ computeRates_(snapshots) {
+ for (let i = 0; i < snapshots.length; i++) {
+ const snapshot = snapshots[i];
+ const stats = snapshot.getStats();
+ let prevSnapshot;
+
+ if (i === 0) {
+ // Deltas will be zero.
+ prevSnapshot = snapshots[0];
+ } else {
+ prevSnapshot = snapshots[i - 1];
+ }
+ const prevStats = prevSnapshot.getStats();
+ let timeIntervalSeconds = (snapshot.ts - prevSnapshot.ts) / 1000;
+ // Prevent divide by zero.
+ if (timeIntervalSeconds === 0) {
+ timeIntervalSeconds = 1;
+ }
+
+ this.computeRatesRecursive_(prevStats, stats,
+ timeIntervalSeconds);
+ }
+ },
+
+ computeRatesRecursive_(prevStats, stats,
+ timeIntervalSeconds) {
+ for (const statName in stats) {
+ if (stats[statName] instanceof Object) {
+ this.computeRatesRecursive_(prevStats[statName],
+ stats[statName],
+ timeIntervalSeconds);
+ } else {
+ if (statName === 'sectors_read') {
+ stats.bytes_read_per_sec = (stats.sectors_read -
+ prevStats.sectors_read) *
+ 512 / timeIntervalSeconds;
+ }
+ if (statName === 'sectors_written') {
+ stats.bytes_written_per_sec =
+ (stats.sectors_written -
+ prevStats.sectors_written) *
+ 512 / timeIntervalSeconds;
+ }
+ if (statName === 'pgmajfault') {
+ stats.pgmajfault_per_sec = (stats.pgmajfault -
+ prevStats.pgmajfault) /
+ timeIntervalSeconds;
+ }
+ if (statName === 'pswpin') {
+ stats.bytes_swpin_per_sec = (stats.pswpin -
+ prevStats.pswpin) *
+ 1000 / timeIntervalSeconds;
+ }
+ if (statName === 'pswpout') {
+ stats.bytes_swpout_per_sec = (stats.pswpout -
+ prevStats.pswpout) *
+ 1000 / timeIntervalSeconds;
+ }
+
+ // All the stats below are available only on Windows:
+
+ if (statName === 'idle_time') {
+ // Total amount of idle_time, in unit of 100 nanoseconds.
+ const units = tr.b.convertUnit(100.,
+ tr.b.UnitScale.TIME.NANO_SEC, tr.b.UnitScale.TIME.SEC);
+ const idleTile = (stats.idle_time - prevStats.idle_time) * units;
+ stats.idle_time_per_sec = idleTile / timeIntervalSeconds;
+ }
+ if (statName === 'read_transfer_count') {
+ const bytesRead = stats.read_transfer_count -
+ prevStats.read_transfer_count;
+ stats.bytes_read_per_sec = bytesRead / timeIntervalSeconds;
+ }
+ if (statName === 'write_transfer_count') {
+ const bytesWritten = stats.write_transfer_count -
+ prevStats.write_transfer_count;
+ stats.bytes_written_per_sec = bytesWritten / timeIntervalSeconds;
+ }
+ if (statName === 'other_transfer_count') {
+ const bytesTransfer = stats.other_transfer_count -
+ prevStats.other_transfer_count;
+ stats.bytes_other_per_sec = bytesTransfer / timeIntervalSeconds;
+ }
+ if (statName === 'read_operation_count') {
+ const readOperation = stats.read_operation_count -
+ prevStats.read_operation_count;
+ stats.read_operation_per_sec = readOperation / timeIntervalSeconds;
+ }
+ if (statName === 'write_operation_count') {
+ const writeOperation = stats.write_operation_count -
+ prevStats.write_operation_count;
+ stats.write_operation_per_sec =
+ writeOperation / timeIntervalSeconds;
+ }
+ if (statName === 'other_operation_count') {
+ const otherOperation = stats.other_operation_count -
+ prevStats.other_operation_count;
+ stats.other_operation_per_sec =
+ otherOperation / timeIntervalSeconds;
+ }
+ if (statName === 'pagefile_pages_written') {
+ const pageFileBytesWritten =
+ (stats.pagefile_pages_written -
+ prevStats.pagefile_pages_written) * kPageSizeWindows;
+ stats.pagefile_bytes_written_per_sec =
+ pageFileBytesWritten / timeIntervalSeconds;
+ }
+ if (statName === 'pagefile_pages_write_ios') {
+ const pagefileWriteOperation =
+ stats.pagefile_pages_write_ios -
+ prevStats.pagefile_pages_write_ios;
+ stats.pagefile_write_operation_per_sec =
+ pagefileWriteOperation / timeIntervalSeconds;
+ }
+ if (statName === 'available_pages') {
+ // Nothing to do here for now.
+ stats.available_pages_in_bytes =
+ stats.available_pages * kPageSizeWindows;
+ // TODO(sebmarchand): Add a available_pages_field that tracks the
+ // variation of this metric?
+ }
+ if (statName === 'pages_read') {
+ const pagesBytesRead =
+ (stats.pages_read - prevStats.pages_read) * kPageSizeWindows;
+ stats.bytes_read_per_sec = pagesBytesRead / timeIntervalSeconds;
+ }
+ if (statName === 'page_read_ios') {
+ const pagesBytesReadOperations =
+ stats.page_read_ios - prevStats.page_read_ios;
+ stats.pagefile_write_operation_per_sec =
+ pagesBytesReadOperations / timeIntervalSeconds;
+ }
+ }
+ }
+ },
+
+ computeMaxStats_(snapshots) {
+ const maxStats = {};
+ statCount = 0;
+
+ for (let i = 0; i < snapshots.length; i++) {
+ const snapshot = snapshots[i];
+ const stats = snapshot.getStats();
+
+ this.computeMaxStatsRecursive_(stats, maxStats,
+ excludedStats);
+ }
+
+ return maxStats;
+ },
+
+ computeMaxStatsRecursive_(stats, maxStats, excludedStats) {
+ for (const statName in stats) {
+ if (stats[statName] instanceof Object) {
+ if (!(statName in maxStats)) {
+ maxStats[statName] = {};
+ }
+
+ let excludedNested;
+ if (excludedStats && statName in excludedStats) {
+ excludedNested = excludedStats[statName];
+ } else {
+ excludedNested = null;
+ }
+
+ this.computeMaxStatsRecursive_(stats[statName],
+ maxStats[statName],
+ excludedNested);
+ } else {
+ if (excludedStats && statName in excludedStats) {
+ continue;
+ }
+ if (!(statName in maxStats)) {
+ maxStats[statName] = 0;
+ statCount++;
+ }
+ if (stats[statName] > maxStats[statName]) {
+ maxStats[statName] = stats[statName];
+ }
+ }
+ }
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawStatBars_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawStatBars_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const width = bounds.width * pixelRatio;
+ const height = (bounds.height * pixelRatio) / statCount;
+
+ // Culling parameters.
+ const vp = this.viewport.currentDisplayTransform;
+
+ // Scale by the size of the largest snapshot.
+ const maxStats = this.maxStats_;
+
+ const objectSnapshots = this.objectInstance_.snapshots;
+ let lowIndex = tr.b.findLowIndexInSortedArray(
+ objectSnapshots,
+ function(snapshot) {
+ return snapshot.ts;
+ },
+ viewLWorld);
+
+ // Assure that the stack with the left edge off screen still gets drawn
+ if (lowIndex > 0) lowIndex -= 1;
+
+ for (let i = lowIndex; i < objectSnapshots.length; ++i) {
+ const snapshot = objectSnapshots[i];
+ const trace = snapshot.getStats();
+ const currentY = height;
+
+ const left = snapshot.ts;
+ if (left > viewRWorld) break;
+
+ let leftView = vp.xWorldToView(left);
+ if (leftView < 0) leftView = 0;
+
+ // Compute the edges for the column graph bar.
+ let right;
+ if (i !== objectSnapshots.length - 1) {
+ right = objectSnapshots[i + 1].ts;
+ } else {
+ // If this is the last snapshot of multiple snapshots, use the width
+ // of the previous snapshot for the width.
+ if (objectSnapshots.length > 1) {
+ right = objectSnapshots[i].ts + (objectSnapshots[i].ts -
+ objectSnapshots[i - 1].ts);
+ } else {
+ // If there's only one snapshot, use max bounds as the width.
+ right = this.objectInstance_.parent.model.bounds.max;
+ }
+ }
+
+ let rightView = vp.xWorldToView(right);
+ if (rightView > width) {
+ rightView = width;
+ }
+
+ // Floor the bounds to avoid a small gap between stacks.
+ leftView = Math.floor(leftView);
+ rightView = Math.floor(rightView);
+
+ // Descend into nested stats.
+ this.drawStatBarsRecursive_(snapshot,
+ leftView,
+ rightView,
+ height,
+ trace,
+ maxStats,
+ currentY);
+
+ if (i === lowIndex) {
+ this.drawStatNames_(leftView, height, currentY, '', maxStats);
+ }
+ }
+ ctx.lineWidth = 1;
+ },
+
+ drawStatBarsRecursive_(snapshot,
+ leftView,
+ rightView,
+ height,
+ stats,
+ maxStats,
+ currentY) {
+ const ctx = this.context();
+
+ for (const statName in maxStats) {
+ if (stats[statName] instanceof Object) {
+ // Use the y-position returned from the recursive call.
+ currentY = this.drawStatBarsRecursive_(snapshot,
+ leftView,
+ rightView,
+ height,
+ stats[statName],
+ maxStats[statName],
+ currentY);
+ } else {
+ const maxStat = maxStats[statName];
+
+ // Draw a bar for the stat. The height of the bar is scaled
+ // against the largest value of the stat across all snapshots.
+ ctx.fillStyle = EventPresenter.getBarSnapshotColor(
+ snapshot, Math.round(currentY / height));
+
+ let barHeight;
+ if (maxStat > 0) {
+ barHeight = height * Math.max(stats[statName], 0) / maxStat;
+ } else {
+ barHeight = 0;
+ }
+
+ ctx.fillRect(leftView, currentY - barHeight,
+ Math.max(rightView - leftView, 1), barHeight);
+
+ currentY += height;
+ }
+ }
+
+ // Return the updated y-position.
+ return currentY;
+ },
+
+ drawStatNames_(leftView, height, currentY, prefix, maxStats) {
+ const ctx = this.context();
+
+ ctx.textAlign = 'end';
+ ctx.font = '12px Arial';
+ ctx.fillStyle = '#000000';
+ for (const statName in maxStats) {
+ if (maxStats[statName] instanceof Object) {
+ currentY = this.drawStatNames_(leftView, height, currentY,
+ statName, maxStats[statName]);
+ } else {
+ let fullname = statName;
+
+ if (prefix !== '') {
+ fullname = prefix + ' :: ' + statName;
+ }
+
+ ctx.fillText(fullname, leftView - 10, currentY - height / 4);
+ currentY += height;
+ }
+ }
+
+ return currentY;
+ }
+ };
+
+ tr.ui.tracks.ObjectInstanceTrack.register(
+ SystemStatsInstanceTrack,
+ {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+ return {
+ SystemStatsInstanceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html
new file mode 100644
index 00000000000..8dc4bc28264
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SystemStatsInstanceTrack =
+ tr.ui.e.system_stats.SystemStatsInstanceTrack;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createObjects = function() {
+ const objectInstance = new tr.model.ObjectInstance({});
+ const snapshots = [];
+
+ const stats1 = {};
+ const stats2 = {};
+
+ stats1.committed_memory = 2000000;
+ stats2.committed_memory = 3000000;
+
+ stats1.meminfo = {};
+ stats1.meminfo.free = 10000;
+ stats2.meminfo = {};
+ stats2.meminfo.free = 20000;
+
+ stats1.perfinfo = {};
+ stats1.perfinfo.idle_time = 10;
+ stats1.perfinfo.read_transfer_count = 20;
+ stats1.perfinfo.write_transfer_count = 30;
+ stats1.perfinfo.other_transfer_count = 40;
+ stats1.perfinfo.read_operation_count = 2;
+ stats1.perfinfo.write_operation_count = 3;
+ stats1.perfinfo.other_operation_count = 4;
+ stats1.perfinfo.pagefile_pages_written = 5;
+ stats1.perfinfo.pagefile_pages_write_ios = 6;
+
+ stats2.perfinfo = {};
+ stats2.perfinfo.idle_time = 110;
+ stats2.perfinfo.read_transfer_count = 120;
+ stats2.perfinfo.write_transfer_count = 130;
+ stats2.perfinfo.other_transfer_count = 140;
+ stats2.perfinfo.read_operation_count = 102;
+ stats2.perfinfo.write_operation_count = 103;
+ stats2.perfinfo.other_operation_count = 104;
+ stats2.perfinfo.pagefile_pages_written = 105;
+ stats2.perfinfo.pagefile_pages_write_ios = 106;
+
+ snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance,
+ 10, stats1));
+ snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance,
+ 20, stats2));
+
+ objectInstance.snapshots = snapshots;
+
+ return objectInstance;
+ };
+
+ test('instantiate', function() {
+ const objectInstances = [];
+ objectInstances.push(createObjects());
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new SystemStatsInstanceTrack(viewport);
+ track.objectInstances = objectInstances;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ const snapshot1 = track.objectInstance_.snapshots[1];
+ const stats1 = snapshot1.getStats();
+
+ // Raw counters should not move.
+ assert.strictEqual(stats1.perfinfo.idle_time, 110);
+ assert.strictEqual(stats1.perfinfo.read_operation_count, 102);
+ assert.strictEqual(stats1.perfinfo.write_operation_count, 103);
+ assert.strictEqual(stats1.perfinfo.other_operation_count, 104);
+ assert.strictEqual(stats1.perfinfo.read_transfer_count, 120);
+ assert.strictEqual(stats1.perfinfo.write_transfer_count, 130);
+ assert.strictEqual(stats1.perfinfo.other_transfer_count, 140);
+ assert.strictEqual(stats1.perfinfo.pagefile_pages_written, 105);
+ assert.strictEqual(stats1.perfinfo.pagefile_pages_write_ios, 106);
+
+ // Rates should be computed.
+ assert.strictEqual(stats1.perfinfo.idle_time_per_sec, 0.001);
+ assert.strictEqual(stats1.perfinfo.bytes_read_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.bytes_written_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.bytes_other_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.read_operation_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.write_operation_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.other_operation_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.pagefile_bytes_written_per_sec,
+ 40960000);
+ assert.strictEqual(stats1.perfinfo.pagefile_write_operation_per_sec, 10000);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasic';
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css
new file mode 100644
index 00000000000..e698b15aa70
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css
@@ -0,0 +1,28 @@
+/* Copyright (c) 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.
+ */
+
+.tr-ui-e-system-stats-snapshot-view .subhead {
+ font-size: small;
+ padding-bottom: 10px;
+}
+
+.tr-ui-e-system-stats-snapshot-view ul {
+ background-position: 0 5px;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ font-family: monospace;
+ list-style: none;
+ margin: 0;
+ padding-left: 15px;
+}
+
+.tr-ui-e-system-stats-snapshot-view li {
+ background-position: 0 5px;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ list-style: none;
+ margin: 0;
+ padding-left: 15px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html
new file mode 100644
index 00000000000..54f4b869f4a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet"
+ href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.css">
+
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.system_stats', function() {
+ /*
+ * Displays a system stats snapshot in a human readable form. @constructor
+ */
+ 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;
+ }
+ // Clear old snapshot view.
+ Polymer.dom(this).textContent = '';
+
+ const stats = snapshot.getStats();
+ Polymer.dom(this).appendChild(this.buildList_(stats));
+ },
+
+ isFloat(n) {
+ return typeof n === 'number' && n % 1 !== 0;
+ },
+
+ /**
+ * Creates nested lists.
+ *
+ * @param {Object} stats The current trace system stats entry.
+ * @return {Element} A ul list element.
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html
new file mode 100644
index 00000000000..fcc410b754a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the
+same way that all non-UI files depend on tracing/base/base.html. Enforce this
+dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/extras/systrace_config.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html
new file mode 100644
index 00000000000..bc3247d9289
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html
@@ -0,0 +1,728 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-e-v8-gc-objects-stats-table'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ flex: 0 0 auto;
+ align-self: stretch;
+ margin-top: 1em;
+ font-size: 12px;
+ }
+ .diff {
+ display: inline-block;
+ margin-top: 1em;
+ margin-left: 0.8em;
+ }
+ </style>
+ <div class="diff" id="diffOption">
+ Diff
+ </div>
+ <tr-ui-b-table id="diffTable"></tr-ui-b-table>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.v8', function() {
+ // Instance types that should not be part of the overview as they are either
+ // double-attributed (e.g. also part of some other instance type) or do not
+ // make any sense in memory profiling.
+ const IGNORED_ENTRIES = {
+ // Ignore code aging entries as they are already accounted in their
+ // respective code instance types.
+ match: full => full.startsWith('*CODE_AGE_')
+ };
+
+ // Groups are matched on a first-matched basis, i.e., once a group matches we
+ // are done with an entry.
+ // Requires properties:
+ // - match(full): Return true iff |full| should be part of the group and
+ // false otherwise.
+ // - keyToName(key): Returns the human readable name for |key|.
+ // - nameToKey(name): Returns the key for |name|.
+ // Optional properties:
+ // - realEntry: A string representing the actual entry in the trace. If this
+ // entry is present an additional entry UNKNOWN will be created holding all
+ // the unaccounted data.
+ 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;
+ }
+
+ class DiffEntry {
+ constructor(originalEntry, diffEntry) {
+ this.originalEntry_ = originalEntry;
+ this.diffEntry_ = diffEntry;
+ }
+ get title() {
+ return this.diffEntry_.title;
+ }
+ get overall() {
+ return this.diffEntry_.overall;
+ }
+ get overAllocated() {
+ return this.diffEntry_.overAllocated;
+ }
+ get count() {
+ return this.diffEntry_.count;
+ }
+ get overallPercent() {
+ return this.diffEntry_.overallPercent;
+ }
+ get overAllocatedPercent() {
+ return this.diffEntry_.overAllocatedPercent;
+ }
+ get origin() {
+ return this.originalEntry_;
+ }
+ get diff() {
+ return this.diffEntry_;
+ }
+ get subRows() {
+ return this.diffEntry_.subRows;
+ }
+ }
+
+ class Entry {
+ constructor(title, count, overall, overAllocated, histogram,
+ overAllocatedHistogram) {
+ this.title_ = title;
+ this.overall_ = overall;
+ this.count_ = count;
+ this.overAllocated_ = overAllocated;
+ this.histogram_ = histogram;
+ this.overAllocatedHistogram_ = overAllocatedHistogram;
+ this.bucketSize_ = this.histogram_.length;
+ this.overallPercent_ = 100;
+ this.overAllocatedPercent_ = 100;
+ }
+
+ get title() {
+ return this.title_;
+ }
+
+ get overall() {
+ return this.overall_;
+ }
+
+ get count() {
+ return this.count_;
+ }
+
+ get overAllocated() {
+ return this.overAllocated_;
+ }
+
+ get histogram() {
+ return this.histogram_;
+ }
+
+ get overAllocatedHistogram() {
+ return this.overAllocatedHistogram_;
+ }
+
+ get bucketSize() {
+ return this.bucketSize_;
+ }
+
+ get overallPercent() {
+ return this.overallPercent_;
+ }
+
+ set overallPercent(value) {
+ this.overallPercent_ = value;
+ }
+
+ get overAllocatedPercent() {
+ return this.overAllocatedPercent_;
+ }
+
+ set overAllocatedPercent(value) {
+ this.overAllocatedPercent_ = value;
+ }
+
+ setFromObject(obj) {
+ this.count_ = obj.count;
+ // Calculate memory in KB.
+ this.overall_ = obj.overall / 1024;
+ this.overAllocated_ = obj.over_allocated / 1024;
+ this.histogram_ = obj.histogram;
+ this.overAllocatedHistogram_ = obj.over_allocated_histogram;
+ }
+
+ diff(other) {
+ const entry = new Entry(this.title_, other.count_ - this.count,
+ other.overall_ - this.overall,
+ other.overAllocated_ - this.overAllocated, [], []);
+ entry.overallPercent = computePercentage(entry.overall, this.overall);
+ entry.overAllocatedPercent = computePercentage(entry.overAllocated,
+ this.overAllocated);
+ return new DiffEntry(this, entry);
+ }
+ }
+
+ class GroupedEntry extends Entry {
+ constructor(title, count, overall, overAllocated, histogram,
+ overAllocatedHistogram) {
+ super(title, count, overall, overAllocated, histogram,
+ overAllocatedHistogram);
+ this.histogram_.fill(0);
+ this.overAllocatedHistogram_.fill(0);
+ this.entries_ = new Map();
+ }
+
+ get title() {
+ return this.title_;
+ }
+
+ set title(value) {
+ this.title_ = value;
+ }
+
+ get subRows() {
+ return Array.from(this.entries_.values());
+ }
+
+ getEntryFromTitle(title) {
+ return this.entries_.get(title);
+ }
+
+ add(entry) {
+ this.count_ += entry.count;
+ this.overall_ += entry.overall;
+ this.overAllocated_ += entry.overAllocated;
+ if (this.bucketSize_ === entry.bucketSize) {
+ for (let i = 0; i < this.bucketSize_; ++i) {
+ this.histogram_[i] += entry.histogram[i];
+ this.overAllocatedHistogram_[i] += entry.overAllocatedHistogram[i];
+ }
+ }
+ this.entries_.set(entry.title, entry);
+ }
+
+ accumulateUnknown(title) {
+ let unknownCount = this.count_;
+ let unknownOverall = this.overall_;
+ let unknownOverAllocated = this.overAllocated_;
+ const unknownHistogram = tr.b.deepCopy(this.histogram_);
+ const unknownOverAllocatedHistogram =
+ tr.b.deepCopy(this.overAllocatedHistogram_);
+ for (const entry of this.entries_.values()) {
+ unknownCount -= entry.count;
+ unknownOverall -= entry.overall;
+ unknownOverAllocated -= entry.overAllocated;
+ for (let i = 0; i < this.bucketSize_; ++i) {
+ unknownHistogram[i] -= entry.histogram[i];
+ unknownOverAllocatedHistogram[i] -= entry.overAllocatedHistogram[i];
+ }
+ }
+ unknownOverAllocated =
+ unknownOverAllocated < 0 ? 0 : unknownOverAllocated;
+ this.entries_.set(title, new Entry(title, unknownCount, unknownOverall,
+ unknownOverAllocated, unknownHistogram,
+ unknownOverAllocatedHistogram));
+ }
+
+ calculatePercentage() {
+ for (const entry of this.entries_.values()) {
+ entry.overallPercent = computePercentage(entry.overall, this.overall_);
+ entry.overAllocatedPercent =
+ computePercentage(entry.overAllocated, this.overAllocated_);
+
+ if (entry instanceof GroupedEntry) entry.calculatePercentage();
+ }
+ }
+
+ diff(other) {
+ let newTitle = '';
+ if (this.title_.startsWith('Isolate')) {
+ newTitle = 'Total';
+ } else {
+ newTitle = this.title_;
+ }
+ const result = new GroupedEntry(newTitle, 0, 0, 0, [], []);
+ for (const entry of this.entries_) {
+ const otherEntry = other.getEntryFromTitle(entry[0]);
+ if (otherEntry === undefined) continue;
+ result.add(entry[1].diff(otherEntry));
+ }
+ result.overallPercent = computePercentage(result.overall, this.overall);
+ result.overAllocatedPercent = computePercentage(result.overAllocated,
+ this.overAllocated);
+ return new DiffEntry(this, result);
+ }
+ }
+
+ function createSelector(targetEl, defaultValue, items, callback) {
+ const selectorEl = document.createElement('select');
+ selectorEl.addEventListener('change', callback.bind(targetEl));
+ const defaultOptionEl = document.createElement('option');
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ const optionEl = document.createElement('option');
+ Polymer.dom(optionEl).textContent = item.label;
+ optionEl.targetPropertyValue = item.value;
+ optionEl.item = item;
+ Polymer.dom(selectorEl).appendChild(optionEl);
+ }
+ selectorEl.__defineGetter__('selectedValue', function(v) {
+ if (selectorEl.children[selectorEl.selectedIndex] === undefined) {
+ return undefined;
+ }
+ return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue;
+ });
+ selectorEl.__defineGetter__('selectedItem', function(v) {
+ if (selectorEl.children[selectorEl.selectedIndex] === undefined) {
+ return undefined;
+ }
+ return selectorEl.children[selectorEl.selectedIndex].item;
+ });
+ selectorEl.__defineSetter__('selectedValue', function(v) {
+ for (let i = 0; i < selectorEl.children.length; i++) {
+ const value = selectorEl.children[i].targetPropertyValue;
+ if (value === v) {
+ const changed = selectorEl.selectedIndex !== i;
+ if (changed) {
+ selectorEl.selectedIndex = i;
+ callback();
+ }
+ return;
+ }
+ }
+ throw new Error('Not a valid value');
+ });
+ selectorEl.selectedIndex = -1;
+
+ return selectorEl;
+ }
+
+ function plusMinus(value, toFixed = 3) {
+ return (value > 0 ? '+' : '') + value.toFixed(toFixed);
+ }
+
+ function addArrow(value) {
+ if (value === 0) return value;
+ if (value === Number.NEGATIVE_INFINITY) return '\u2193\u221E';
+ if (value === Number.POSITIVE_INFINITY) return '\u2191\u221E';
+ return (value > 0 ? '\u2191' : '\u2193') + Math.abs(value.toFixed(3));
+ }
+
+ Polymer({
+ is: 'tr-ui-e-v8-gc-objects-stats-table',
+
+ ready() {
+ this.$.diffOption.style.display = 'none';
+ this.isolateEntries_ = [];
+ this.selector1_ = undefined;
+ this.selector2_ = undefined;
+ },
+
+ constructDiffTable_(table) {
+ this.$.diffTable.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.diffTable.tableColumns = [
+ {
+ title: 'Component',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.title;
+ return typeEl;
+ },
+ showExpandButtons: true
+ },
+ {
+ title: 'Overall Memory(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = row.origin.overall.toFixed(3);
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.origin.overall - b.origin.overall;
+ }
+ },
+ {
+ title: 'diff(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = plusMinus(row.overall);
+ if (row.overall > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overall < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'diff(%)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = addArrow(row.overallPercent);
+ if (row.overall > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overall < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'Over Allocated Memory(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = row.origin.overAllocated.toFixed(3);
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.origin.overAllocated - b.origin.overAllocated;
+ }
+ },
+ {
+ title: 'diff(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = plusMinus(row.overAllocated);
+ if (row.overAllocated > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overAllocated < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ },
+ {
+ title: 'diff(%)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = addArrow(row.overAllocatedPercent);
+ if (row.overAllocated > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overAllocated < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ },
+ {
+ title: 'Count',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = row.origin.count;
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.origin.count - b.origin.count;
+ }
+ },
+ {
+ title: 'diff',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = plusMinus(row.count, 0);
+ if (row.count > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.count < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.count - b.count;
+ }
+ },
+ ];
+ },
+
+ buildOptions_() {
+ const items = [];
+ for (const isolateEntry of this.isolateEntries_) {
+ items.push({
+ label: isolateEntry.title,
+ value: isolateEntry
+ });
+ }
+ this.$.diffOption.style.display = 'inline-block';
+ this.selector1_ = createSelector(
+ this, '', items, this.diffOptionChanged_);
+ Polymer.dom(this.$.diffOption).appendChild(this.selector1_);
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = ' VS ';
+ Polymer.dom(this.$.diffOption).appendChild(spanEl);
+ this.selector2_ = createSelector(
+ this, '', items, this.diffOptionChanged_);
+ Polymer.dom(this.$.diffOption).appendChild(this.selector2_);
+ },
+
+ diffOptionChanged_() {
+ const isolateEntry1 = this.selector1_.selectedValue;
+ const isolateEntry2 = this.selector2_.selectedValue;
+ if (isolateEntry1 === undefined || isolateEntry2 === undefined) {
+ return;
+ }
+ if (isolateEntry1 === isolateEntry2) {
+ this.$.diffTable.tableRows = [];
+ this.$.diffTable.rebuild();
+ return;
+ }
+ this.$.diffTable.tableRows = [isolateEntry1.diff(isolateEntry2)];
+ this.$.diffTable.rebuild();
+ },
+
+ constructTable_() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.tableColumns = [
+ {
+ title: 'Component',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.title;
+ return typeEl;
+ },
+ showExpandButtons: true
+ },
+ {
+ title: 'Overall Memory (KB)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overall.toFixed(3);
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'Over Allocated Memory (KB)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overAllocated.toFixed(3);
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ },
+ {
+ title: 'Overall Count',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.count;
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.count - b.count;
+ }
+ },
+ {
+ title: 'Overall Memory Percent',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overallPercent.toFixed(3) + '%';
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'Overall Allocated Memory Percent',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overAllocatedPercent.toFixed(3) + '%';
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ }
+ ];
+
+ this.$.table.sortColumnIndex = 1;
+ this.$.table.sortDescending = true;
+ },
+
+ buildSubEntry_(objects, groupEntry, keyToName) {
+ const typeGroup = INSTANCE_TYPE_GROUPS[groupEntry.title];
+ for (const instanceType of typeGroup) {
+ const e = objects[instanceType];
+ if (e === undefined) continue;
+ delete objects[instanceType];
+ let title = instanceType;
+ if (keyToName !== undefined) title = keyToName(title);
+ // Represent memery in KB unit.
+ groupEntry.add(new Entry(title, e.count, e.overall / 1024,
+ e.over_allocated / 1024, e.histogram,
+ e.over_allocated_histogram));
+ }
+ },
+
+ buildUnGroupedEntries_(objects, objectEntry, bucketSize) {
+ for (const title of Object.getOwnPropertyNames(objects)) {
+ const obj = objects[title];
+ const groupedEntry = new GroupedEntry(title, 0, 0, 0,
+ new Array(bucketSize),
+ new Array(bucketSize));
+ groupedEntry.setFromObject(obj);
+ objectEntry.add(groupedEntry);
+ }
+ },
+
+ createGroupEntries_(groupEntries, objects, bucketSize) {
+ for (const groupName of Object.getOwnPropertyNames(
+ INSTANCE_TYPE_GROUPS)) {
+ const groupEntry = new GroupedEntry(groupName, 0, 0, 0,
+ new Array(bucketSize),
+ new Array(bucketSize));
+ if (INSTANCE_TYPE_GROUPS[groupName].realEntry !== undefined) {
+ groupEntry.savedRealEntry =
+ objects[INSTANCE_TYPE_GROUPS[groupName].realEntry];
+ delete objects[INSTANCE_TYPE_GROUPS[groupName].realEntry];
+ }
+ groupEntries[groupName] = groupEntry;
+ }
+ },
+
+ buildGroupEntries_(groupEntries, objectEntry) {
+ for (const groupName of Object.getOwnPropertyNames(groupEntries)) {
+ const groupEntry = groupEntries[groupName];
+ if (groupEntry.savedRealEntry !== undefined) {
+ groupEntry.setFromObject(groupEntry.savedRealEntry);
+ groupEntry.accumulateUnknown('UNKNOWN');
+ delete groupEntry.savedRealEntry;
+ }
+ objectEntry.add(groupEntry);
+ }
+ },
+
+ buildSubEntriesForGroups_(groupEntries, objects) {
+ for (const instanceType of Object.getOwnPropertyNames(objects)) {
+ if (IGNORED_ENTRIES.match(instanceType)) {
+ delete objects[instanceType];
+ continue;
+ }
+ const e = objects[instanceType];
+ for (const name of Object.getOwnPropertyNames(INSTANCE_TYPE_GROUPS)) {
+ const group = INSTANCE_TYPE_GROUPS[name];
+ if (group.match(instanceType)) {
+ groupEntries[name].add(new Entry(
+ group.keyToName(instanceType), e.count, e.overall / 1024,
+ e.over_allocated / 1024, e.histogram,
+ e.over_allocated_histogram));
+ delete objects[instanceType];
+ }
+ }
+ }
+ },
+
+ build_(objects, objectEntry, bucketSize) {
+ delete objects.END;
+ const groupEntries = {};
+ this.createGroupEntries_(groupEntries, objects, bucketSize);
+ this.buildSubEntriesForGroups_(groupEntries, objects);
+ this.buildGroupEntries_(groupEntries, objectEntry);
+ this.buildUnGroupedEntries_(objects, objectEntry, bucketSize);
+ },
+
+ set selection(slices) {
+ slices.sortEvents(function(a, b) {
+ return b.start - a.start;
+ });
+ const previous = undefined;
+ for (const slice of slices) {
+ if (!slice instanceof tr.e.v8.V8GCStatsThreadSlice) continue;
+ const liveObjects = slice.liveObjects;
+ const deadObjects = slice.deadObjects;
+ const isolate = liveObjects.isolate;
+
+ const isolateEntry =
+ new GroupedEntry(
+ 'Isolate_' + isolate + ' at ' + slice.start.toFixed(3) + ' ms',
+ 0, 0, 0, [], []);
+ const liveEntry = new GroupedEntry('live objects', 0, 0, 0, [], []);
+ const deadEntry = new GroupedEntry('dead objects', 0, 0, 0, [], []);
+
+ const liveBucketSize = liveObjects.bucket_sizes.length;
+ const deadBucketSize = deadObjects.bucket_sizes.length;
+
+ this.build_(tr.b.deepCopy(liveObjects.type_data), liveEntry,
+ liveBucketSize);
+ isolateEntry.add(liveEntry);
+
+ this.build_(tr.b.deepCopy(deadObjects.type_data), deadEntry,
+ deadBucketSize);
+ isolateEntry.add(deadEntry);
+
+ isolateEntry.calculatePercentage();
+ this.isolateEntries_.push(isolateEntry);
+ }
+ this.updateTable_();
+
+ if (slices.length > 1) {
+ this.buildOptions_();
+ this.constructDiffTable_();
+ }
+ },
+
+ updateTable_() {
+ this.constructTable_();
+ this.$.table.tableRows = this.isolateEntries_;
+ this.$.table.rebuild();
+ },
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html
new file mode 100644
index 00000000000..42d904d94c8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx({
+ title: 'V8.GC_Objects_Stats',
+ start: 1,
+ end: 1,
+ type: tr.e.v8.V8GCStatsThreadSlice,
+ cat: 'disabled-by-default-v8.gc_stats',
+ args: {
+ // eslint-disable-next-line
+ live:'{"isolate":"0x00000000001","id":1,"time":111,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_OBJECT_TYPE":{"type":4,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_TYPED_ARRAY_TYPE":{"type":5,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"CODE_TYPE":{"type":4,"overall":6,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_BYTECODE_HANDLER":{"type":7,"overall":5,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_AGE_Quadragenarian":{"type":8,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}',
+ // eslint-disable-next-line
+ dead:'{"isolate":"0x00000000001","id":2,"time":112,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":3,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}'
+ }
+ })
+ );
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx({
+ title: 'V8.GC_Objects_Stats',
+ start: 2,
+ end: 2,
+ type: tr.e.v8.V8GCStatsThreadSlice,
+ cat: 'disabled-by-default-v8.gc_stats',
+ args: {
+ // eslint-disable-next-line
+ live:'{"isolate":"0x00000000001","id":1,"time":113,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":3,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":6,"count":7,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}',
+ // eslint-disable-next-line
+ dead:'{"isolate":"0x00000000001","id":2,"time":114,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":4,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}'
+ }
+ })
+ );
+ m.s3 = m.t2.sliceGroup.pushSlice(
+ newSliceEx({
+ title: 'V8.GC_Objects_Stats',
+ start: 3,
+ end: 3,
+ type: tr.e.v8.V8GCStatsThreadSlice,
+ cat: 'disabled-by-default-v8.gc_stats',
+ args: {
+ // eslint-disable-next-line
+ live:'{"isolate":"0x00000000001","id":1,"time":115,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP1":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP2":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[0,1,0,0],"over_allocated_histogram":[0,0,0,0]}}}',
+ // eslint-disable-next-line
+ dead:'{"isolate":"0x00000000001","id":2,"time":116,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}'
+ }
+ })
+ );
+ });
+ return m;
+ }
+
+ test('GCObjectTableSingleSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s1);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ const row = rows[0];
+ assert.strictEqual(row.overall, 0.0263671875);
+ assert.strictEqual(row.count, 21);
+ assert.strictEqual(row.overAllocated, 0);
+ const subRows = row.subRows;
+ const live = subRows[0];
+ assert.strictEqual(live.overall, 0.0224609375);
+ assert.strictEqual(live.count, 17);
+ assert.strictEqual(live.overAllocated, 0);
+ const dead = subRows[1];
+ assert.strictEqual(dead.overall, 0.00390625);
+ assert.strictEqual(dead.count, 4);
+ assert.strictEqual(dead.overAllocated, 0);
+ });
+
+ test('GCObjectTableMultiSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s1);
+ eventSet.push(m.s2);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+
+ let row = rows[0];
+ assert.strictEqual(row.overall, 0.0146484375);
+ assert.strictEqual(row.count, 17);
+ assert.strictEqual(row.overAllocated, 0);
+ let subRows = row.subRows;
+ let live = subRows[0];
+ assert.strictEqual(live.overall, 0.0087890625);
+ assert.strictEqual(live.count, 11);
+ assert.strictEqual(live.overAllocated, 0);
+ let dead = subRows[1];
+ assert.strictEqual(dead.overall, 0.005859375);
+ assert.strictEqual(dead.count, 6);
+ assert.strictEqual(dead.overAllocated, 0);
+
+ row = rows[1];
+ assert.strictEqual(row.overall, 0.0263671875);
+ assert.strictEqual(row.count, 21);
+ assert.strictEqual(row.overAllocated, 0);
+ subRows = row.subRows;
+ live = subRows[0];
+ assert.strictEqual(live.overall, 0.0224609375);
+ assert.strictEqual(live.count, 17);
+ assert.strictEqual(live.overAllocated, 0);
+ dead = subRows[1];
+ assert.strictEqual(dead.overall, 0.00390625);
+ assert.strictEqual(dead.count, 4);
+ assert.strictEqual(dead.overAllocated, 0);
+ });
+
+ test('GCObjectTableDiff', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s1);
+ eventSet.push(m.s2);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+ const diffEntry = rows[0].diff(rows[1]);
+
+ assert.strictEqual(diffEntry.origin.overall.toFixed(3), '0.015');
+ assert.strictEqual(diffEntry.origin.overAllocated, 0);
+ assert.strictEqual(diffEntry.overall.toFixed(3), '-0.004');
+ assert.strictEqual(diffEntry.overAllocated, 0);
+ assert.strictEqual(diffEntry.overallPercent.toFixed(3), '-26.667');
+ assert.strictEqual(diffEntry.overAllocatedPercent, 0);
+ });
+
+ test('GCObjectTableGroupEntryWithoutGroupDefined', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s3);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ const row = rows[0];
+ assert.strictEqual(row.overall, 0.013671875);
+ assert.strictEqual(row.count, 18);
+ assert.strictEqual(row.overAllocated, 0);
+ const subRows = row.subRows;
+
+ const live = subRows[0];
+ assert.strictEqual(live.overall, 0.0087890625);
+ assert.strictEqual(live.count, 12);
+ assert.strictEqual(live.overAllocated, 0);
+
+ // ungrouped entry should be top level entry.
+ const unGrouped1 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP1');
+ assert.isDefined(unGrouped1);
+ assert.strictEqual(unGrouped1.overall, 0.001953125);
+ assert.strictEqual(unGrouped1.count, 3);
+ assert.strictEqual(unGrouped1.overAllocated, 0);
+
+ const unGrouped2 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP2');
+ assert.isDefined(unGrouped2);
+ assert.strictEqual(unGrouped2.overall, 0.001953125);
+ assert.strictEqual(unGrouped2.count, 3);
+ assert.strictEqual(unGrouped2.overAllocated, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html
new file mode 100644
index 00000000000..e19a367cfca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/v8/ic_stats_entry.html">
+<link rel="import" href="/tracing/extras/v8/v8_ic_stats_thread_slice.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-e-v8-ic-stats-table'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ flex: 0 0 auto;
+ align-self: stretch;
+ margin-top: 1em;
+ font-size: 12px;
+ }
+ #total {
+ margin-top: 1em;
+ margin-left: 0.8em;
+ }
+ #groupOption {
+ display: inline-block;
+ margin-top: 1em;
+ margin-left: 0.8em;
+ }
+ </style>
+ <div style="padding-right: 200px">
+ <div style="float:right; border-style: solid; border-width: 1px; padding:20px">
+ 0 uninitialized<br>
+ . premonomorphic<br>
+ 1 monomorphic<br>
+ ^ recompute handler<br>
+ P polymorphic<br>
+ N megamorphic<br>
+ G generic
+ </div>
+ </div>
+ <div id="total">
+ </div>
+ <div id="groupOption">
+ Group Key
+ </div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.v8', function() {
+ const PROPERTIES = tr.e.v8.IC_STATS_PROPERTIES.map(
+ x => {return {label: x, value: x};});
+ const ICStatsEntry = tr.e.v8.ICStatsEntry;
+ const ICStatsEntryGroup = tr.e.v8.ICStatsEntryGroup;
+ const ICStatsCollection = tr.e.v8.ICStatsCollection;
+
+ Polymer({
+ is: 'tr-ui-e-v8-ic-stats-table',
+
+ ready() {
+ this.icStatsCollection_ = new ICStatsCollection();
+ this.groupKey_ = PROPERTIES[0].value;
+ this.selector_ = tr.ui.b.createSelector(this, 'groupKey',
+ 'v8ICStatsGroupKey',
+ this.groupKey_, PROPERTIES);
+ Polymer.dom(this.$.groupOption).appendChild(this.selector_);
+ },
+
+ get groupKey() {
+ return this.groupKey_;
+ },
+
+ set groupKey(key) {
+ this.groupKey_ = key;
+ if (this.icStatsCollection_.length === 0) return;
+ this.updateTable_(this.groupKey_);
+ },
+
+ constructTable_(table, groupKey) {
+ table.tableColumns = [
+ {
+ title: '',
+ value: row => {
+ let expanded = false;
+ const buttonEl = tr.ui.b.createButton('details', function() {
+ const previousSibling = Polymer.dom(this).parentNode.parentNode;
+ const parentNode = previousSibling.parentNode;
+ if (expanded) {
+ const trEls = parentNode.getElementsByClassName('subTable');
+ Array.from(trEls).map(x => x.parentNode.removeChild(x));
+ expanded = false;
+ return;
+ }
+ expanded = true;
+ const subGroups = row.createSubGroup();
+ const tr = document.createElement('tr');
+ tr.classList.add('subTable');
+ tr.appendChild(document.createElement('td'));
+ const td = document.createElement('td');
+ td.colSpan = 3;
+ for (const subGroup of subGroups) {
+ const property = subGroup[0];
+ const all = Array.from(subGroup[1].values());
+ const group = all.slice(0, 20);
+ const divEl = document.createElement('div');
+ const spanEl = document.createElement('span');
+ const subTableEl = document.createElement('tr-ui-b-table');
+
+ spanEl.innerText = `Top 20 out of ${all.length}`;
+ spanEl.style.fontWeight = 'bold';
+ spanEl.style.fontSize = '14px';
+ divEl.appendChild(spanEl);
+
+ this.constructTable_(subTableEl, property);
+ subTableEl.tableRows = group;
+ subTableEl.rebuild();
+ divEl.appendChild(subTableEl);
+ td.appendChild(divEl);
+ }
+ tr.appendChild(td);
+ parentNode.insertBefore(tr, previousSibling.nextSibling);
+ });
+ return buttonEl;
+ }
+ },
+ {
+ title: 'Percentage',
+ value(row) {
+ const spanEl = document.createElement('span');
+ spanEl.innerText = (row.percentage * 100).toFixed(3) + '%';
+ return spanEl;
+ },
+ cmp: (a, b) => a.percentage - b.percentage
+ },
+ {
+ title: 'Count',
+ value(row) {
+ const spanEl = document.createElement('span');
+ spanEl.innerText = row.length;
+ return spanEl;
+ },
+ cmp: (a, b) => a.length - b.length
+ },
+ {
+ title: groupKey,
+ value(row) {
+ const spanEl = document.createElement('span');
+ spanEl.innerText = row.key ? row.key : '';
+ return spanEl;
+ }
+ }
+ ];
+
+ table.sortColumnIndex = 1;
+ table.sortDescending = true;
+ },
+
+ updateTable_(groupKey) {
+ this.constructTable_(this.$.table, groupKey);
+ this.$.table.tableRows = this.icStatsCollection_.groupBy(groupKey);
+ this.$.table.rebuild();
+ },
+
+ set selection(slices) {
+ for (const slice of slices) {
+ for (const icStatsObj of slice.icStats) {
+ const entry = new ICStatsEntry(icStatsObj);
+ this.icStatsCollection_.add(entry);
+ }
+ }
+ this.$.total.innerText = 'Total items: ' + this.icStatsCollection_.length;
+ this.updateTable_(this.selector_.selectedValue);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..eded2c44a66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+
+<dom-module id='tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view'>
+ <template>
+ <style>
+ </style>
+ <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats">
+ </tr-ui-e-v8-gc-objects-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.gcObjectsStats.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view',
+ tr.e.v8.V8GCStatsThreadSlice,
+ {
+ multi: true,
+ title: 'V8 GC Stats slices'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..36b14cb7be0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html">
+
+<dom-module id='tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view'>
+ <template>
+ <tr-ui-e-v8-ic-stats-table id="table">
+ </tr-ui-e-v8-ic-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.table.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view',
+ tr.e.v8.V8ICStatsThreadSlice,
+ {
+ multi: true,
+ title: 'V8 IC stats slices'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html
new file mode 100644
index 00000000000..48c1f05b274
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+
+<dom-module id='tr-ui-e-multi-v8-thread-slice-sub-view'>
+ <template>
+ <tr-ui-a-multi-thread-slice-sub-view id="content"></tr-ui-a-multi-thread-slice-sub-view>
+ <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-multi-v8-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.runtimeCallStats.slices = selection;
+ this.$.content.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-multi-v8-thread-slice-sub-view',
+ tr.e.v8.V8ThreadSlice,
+ {
+ multi: true,
+ title: 'V8 slices'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..d0d62bed87d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ CompileFullCode: [3, 345],
+ LoadIC_Miss: [5, 567],
+ ParseLazy: [8, 890]
+ }}}));
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ HandleApiCall: [1, 123],
+ OptimizeCode: [7, 789]
+ }}}));
+ });
+ return m;
+ }
+
+ test('selectMultiV8ThreadSlices', function() {
+ const m = createModel();
+
+ const viewEl =
+ document.createElement('tr-ui-e-multi-v8-thread-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(m.s1);
+ selection.push(m.s2);
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ const rows = viewEl.$.runtimeCallStats.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 2714,
+ 567,
+ 0,
+ 789,
+ 0,
+ 345,
+ 0,
+ 890,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 123
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html
new file mode 100644
index 00000000000..27bf0f5ef72
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-e-v8-runtime-call-stats-table'>
+ <template>
+ <style>
+ #table, #blink_rcs_table {
+ flex: 0 0 auto;
+ align-self: stretch;
+ margin-top: 1em;
+ font-size: 12px;
+ }
+
+ #v8_rcs_heading, #blink_rcs_heading {
+ padding-top: 1em;
+ font-size: 18px;
+ }
+ </style>
+ <h1 id="v8_rcs_heading"></h1>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ <h1 id="blink_rcs_heading"></h1>
+ <tr-ui-b-table id="blink_rcs_table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.v8', function() {
+ const codeSearchURL_ = 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=';
+
+ function removeBlinkPrefix_(name) {
+ if (name.startsWith('Blink_')) name = name.substring(6);
+ return name;
+ }
+
+ function handleCodeSearchForV8_(event) {
+ if (event.target.parentNode === undefined) return;
+ let name = event.target.parentNode.entryName;
+ if (name.startsWith('API_')) name = name.substring(4);
+ const url = codeSearchURL_ + encodeURIComponent(name) + '+file:src/v8/src';
+ window.open(url, '_blank');
+ }
+
+ function handleCodeSearchForBlink_(event) {
+ if (event.target.parentNode === undefined) return;
+ const name = event.target.parentNode.entryName;
+ const url = codeSearchURL_ +
+ encodeURIComponent('RuntimeCallStats::CounterId::k' + name) +
+ '+file:src/third_party/WebKit/|src/out/Debug/';
+ window.open(url, '_blank');
+ }
+
+ function createCodeSearchEl_(handleCodeSearch) {
+ const codeSearchEl = document.createElement('span');
+ codeSearchEl.innerText = '?';
+ codeSearchEl.style.float = 'right';
+ codeSearchEl.style.borderRadius = '5px';
+ codeSearchEl.style.backgroundColor = '#EEE';
+ codeSearchEl.addEventListener('click',
+ handleCodeSearch.bind(this));
+ return codeSearchEl;
+ }
+
+ const timeColumn_ = {
+ title: 'Time',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = (row.time / 1000.0).toFixed(3) + ' ms';
+ return typeEl;
+ },
+ width: '100px',
+ cmp(a, b) {
+ return a.time - b.time;
+ }
+ };
+
+ const countColumn_ = {
+ title: 'Count',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.count;
+ return typeEl;
+ },
+ width: '100px',
+ cmp(a, b) {
+ return a.count - b.count;
+ }
+ };
+
+ function percentColumn_(title, totalTime) {
+ return {
+ title,
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = (row.time / totalTime * 100).toFixed(3) + '%';
+ return typeEl;
+ },
+ width: '100px',
+ cmp(a, b) {
+ return a.time - b.time;
+ }
+ };
+ }
+
+ function nameColumn_(handleCodeSearch, modifyName) {
+ return {
+ title: 'Name',
+ value(row) {
+ const typeEl = document.createElement('span');
+ let name = row.name;
+ if (modifyName) name = modifyName(name);
+ typeEl.innerText = name;
+ if (!(row instanceof tr.e.v8.RuntimeStatsGroup)) {
+ typeEl.title = 'click ? for code search';
+ typeEl.entryName = name;
+ const codeSearchEl = createCodeSearchEl_(handleCodeSearch);
+ typeEl.appendChild(codeSearchEl);
+ }
+ return typeEl;
+ },
+ width: '200px',
+ showExpandButtons: true
+ };
+ }
+
+ function initializeCommonOptions_(table) {
+ table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ table.sortColumnIndex = 1;
+ table.sortDescending = true;
+ table.subRowsPropertyName = 'values';
+ }
+
+ Polymer({
+ is: 'tr-ui-e-v8-runtime-call-stats-table',
+
+ ready() {
+ this.table_ = this.$.table;
+ this.blink_rcs_table_ = this.$.blink_rcs_table;
+ this.totalTime_ = 0;
+ },
+
+ constructV8RCSTable_(totalTime) {
+ this.table_.tableColumns = [
+ nameColumn_(handleCodeSearchForV8_),
+ timeColumn_,
+ countColumn_,
+ percentColumn_('Percent', totalTime)
+ ];
+
+ initializeCommonOptions_(this.table_);
+ },
+
+ constructBlinkRCSTable_(blinkCppTotalTime) {
+ this.blink_rcs_table_.tableColumns = [
+ nameColumn_(handleCodeSearchForBlink_, removeBlinkPrefix_),
+ timeColumn_,
+ countColumn_,
+ percentColumn_('Percent (of \'Blink C++\' + \'API\')',
+ blinkCppTotalTime)
+ ];
+
+ initializeCommonOptions_(this.blink_rcs_table_);
+ },
+
+ set slices(slices) {
+ const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection();
+ runtimeGroupCollection.addSlices(slices);
+ if (runtimeGroupCollection.totalTime > 0) {
+ this.$.v8_rcs_heading.textContent = 'V8 Runtime Call Stats';
+ this.constructV8RCSTable_(runtimeGroupCollection.totalTime);
+ this.table_.tableRows = runtimeGroupCollection.runtimeGroups;
+ this.table_.rebuild();
+ }
+
+ const blinkRCSGroupCollection =
+ runtimeGroupCollection.blinkRCSGroupCollection;
+ if (runtimeGroupCollection.blinkCppTotalTime > 0 &&
+ blinkRCSGroupCollection.totalTime > 0) {
+ this.$.blink_rcs_heading.textContent = 'Blink Runtime Call Stats';
+ this.constructBlinkRCSTable_(runtimeGroupCollection.blinkCppTotalTime);
+ this.blink_rcs_table_.tableRows = blinkRCSGroupCollection.runtimeGroups;
+ this.blink_rcs_table_.rebuild();
+ }
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html
new file mode 100644
index 00000000000..06698d03c4a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const apiObjectGet = [1, 123];
+ const functionCallback = [2, 234];
+ const compileFullCode = [3, 345];
+ const allocateInTargetSpace = [4, 456];
+ const loadIcMiss = [5, 567];
+ const jsExecution = [6, 678];
+ const optimizeCode = [7, 789];
+ const parseLazy = [8, 890];
+ const handleApiCall = [9, 901];
+ const compileBackground = [1, 101];
+ const parseBackground = [2, 202];
+ const optimizeCodeBackground = [3, 303];
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ JS_Execution: jsExecution,
+ HandleApiCall: handleApiCall,
+ CompileFullCode: compileFullCode,
+ LoadIC_Miss: loadIcMiss,
+ ParseLazy: parseLazy,
+ RecompileConcurrent: optimizeCode,
+ OptimizeCode: optimizeCode,
+ FunctionCallback: functionCallback,
+ AllocateInTargetSpace: allocateInTargetSpace,
+ API_Object_Get: apiObjectGet,
+ CompileBackgroundIgnition: compileBackground,
+ ParseBackgroundFunctionLiteral: parseBackground,
+ RecompileConcurrent: optimizeCodeBackground
+ }}}));
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ JS_Execution: jsExecution,
+ HandleApiCall: handleApiCall,
+ CompileFullCode: compileFullCode,
+ LoadIC_Miss: loadIcMiss,
+ ParseLazy: parseLazy,
+ OptimizeCode: optimizeCode,
+ FunctionCallback: functionCallback,
+ AllocateInTargetSpace: allocateInTargetSpace,
+ API_Object_Get: apiObjectGet
+ }}}));
+ m.s3 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ LoadIC_LoadCallback: [1, 111],
+ StoreIC_StoreCallback: [2, 222],
+ }}}));
+ });
+ return m;
+ }
+
+ test('SingleSliceSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-e-v8-runtime-call-stats-table');
+ viewEl.slices = [m.s1];
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 5589,
+ loadIcMiss[1],
+ optimizeCodeBackground[1],
+ optimizeCode[1],
+ compileBackground[1],
+ compileFullCode[1],
+ parseBackground[1],
+ parseLazy[1],
+ functionCallback[1],
+ apiObjectGet[1],
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ allocateInTargetSpace[1],
+ jsExecution[1],
+ handleApiCall[1]
+ ]);
+ });
+
+ test('MultiSliceSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-e-v8-runtime-call-stats-table');
+ viewEl.slices = [m.s1, m.s2];
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 10572,
+ loadIcMiss[1] * 2,
+ optimizeCodeBackground[1],
+ optimizeCode[1] * 2,
+ compileBackground[1],
+ compileFullCode[1] * 2,
+ parseBackground[1],
+ parseLazy[1] * 2,
+ functionCallback[1] * 2,
+ apiObjectGet[1] * 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ allocateInTargetSpace[1] * 2,
+ jsExecution[1] * 2,
+ handleApiCall[1] * 2
+ ]);
+
+ assert.deepEqual(rows.map(r => r.entries_.size), [
+ 0,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 1,
+ 1
+ ]);
+ });
+
+ test('groupCorrectly', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-e-v8-runtime-call-stats-table');
+ viewEl.slices = [m.s3];
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 333,
+ 333,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]);
+
+ assert.deepEqual(rows.map(r => r.entries_.size), [
+ 0,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..6a8d5f15b73
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+
+<dom-module id='tr-ui-e-single-v8-gc-stats-thread-slice-sub-view'>
+ <template>
+ <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view>
+ <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats"></tr-ui-e-v8-gc-objects-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ this.$.gcObjectsStats.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view',
+ tr.e.v8.V8GCStatsThreadSlice,
+ {
+ multi: false,
+ title: 'V8 GC stats slice'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..eeaf407eab1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html">
+
+<dom-module id='tr-ui-e-single-v8-ic-stats-thread-slice-sub-view'>
+ <template>
+ <tr-ui-e-v8-ic-stats-table id="table">
+ </tr-ui-e-v8-ic-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.table.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view',
+ tr.e.v8.V8ICStatsThreadSlice,
+ {
+ multi: false,
+ title: 'V8 IC stats slice'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html
new file mode 100644
index 00000000000..a9b1189fc76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+
+<dom-module id='tr-ui-e-single-v8-thread-slice-sub-view'>
+ <template>
+ <tr-ui-a-single-thread-slice-sub-view id="content"></tr-ui-a-single-thread-slice-sub-view>
+ <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-single-v8-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.runtimeCallStats.slices = selection;
+ this.$.content.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-single-v8-thread-slice-sub-view',
+ tr.e.v8.V8ThreadSlice,
+ {
+ multi: false,
+ title: 'V8 slice'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..9e12aa6e044
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ CompileFullCode: [3, 345],
+ LoadIC_Miss: [5, 567],
+ ParseLazy: [8, 890]
+ }}}));
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ HandleApiCall: [1, 123],
+ OptimizeCode: [7, 789]
+ }}}));
+ });
+ return m;
+ }
+
+ test('selectV8ThreadSlice', function() {
+ const m = createModel();
+
+ const viewEl =
+ document.createElement('tr-ui-e-single-v8-thread-slice-sub-view');
+ const selection1 = new tr.model.EventSet();
+ selection1.push(m.s1);
+ viewEl.selection = selection1;
+ this.addHTMLOutput(viewEl);
+ let rows = viewEl.$.runtimeCallStats.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 1802,
+ 567,
+ 0,
+ 0,
+ 0,
+ 345,
+ 0,
+ 890,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]);
+
+ const selection2 = new tr.model.EventSet();
+ selection2.push(m.s2);
+ viewEl.selection = selection2;
+ rows = viewEl.$.runtimeCallStats.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 912,
+ 0,
+ 0,
+ 789,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 123
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html
new file mode 100644
index 00000000000..f78005c2b54
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+
+<link rel="import" href="/tracing/extras/v8_config.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_control.html b/chromium/third_party/catapult/tracing/tracing/ui/find_control.html
new file mode 100644
index 00000000000..daaa0f59777
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_control.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/find_controller.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<dom-module id='tr-ui-find-control'>
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ display: flex;
+ position: relative;
+ }
+ input {
+ -webkit-user-select: auto;
+ background-color: #f8f8f8;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ width: 170px;
+ }
+ input:focus {
+ background-color: white;
+ }
+ tr-ui-b-toolbar-button {
+ border-left: none;
+ margin: 0;
+ }
+ #hitCount {
+ left: 0;
+ opacity: 0.25;
+ pointer-events: none;
+ position: absolute;
+ text-align: right;
+ top: 2px;
+ width: 167px;
+ z-index: 1;
+ }
+ #spinner {
+ visibility: hidden;
+ width: 8px;
+ height: 8px;
+ left: 154px;
+ pointer-events: none;
+ position: absolute;
+ top: 4px;
+ z-index: 1;
+
+ border: 2px solid transparent;
+ border-bottom: 2px solid rgba(0, 0, 0, 0.5);
+ border-right: 2px solid rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ }
+ @keyframes spin { 100% { transform: rotate(360deg); } }
+ </style>
+
+ <input type='text' id='filter'
+ on-input="filterTextChanged"
+ on-keydown="filterKeyDown"
+ on-blur="filterBlur"
+ on-focus="filterFocus"
+ on-mouseup="filterMouseUp" />
+ <div id="spinner"></div>
+ <tr-ui-b-toolbar-button on-click="findPrevious">
+ &larr;
+ </tr-ui-b-toolbar-button>
+ <tr-ui-b-toolbar-button on-click="findNext">
+ &rarr;
+ </tr-ui-b-toolbar-button>
+ <div id="hitCount">0 of 0</div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-find-control',
+
+ filterKeyDown(e) {
+ if (e.keyCode === 27) {
+ const hkc = tr.b.getHotkeyControllerForElement(this);
+ if (hkc) {
+ hkc.childRequestsBlur(this);
+ } else {
+ this.blur();
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ } else if (e.keyCode === 13) {
+ if (e.shiftKey) {
+ this.findPrevious();
+ } else {
+ this.findNext();
+ }
+ }
+ },
+
+ filterBlur(e) {
+ this.updateHitCountEl();
+ },
+
+ filterFocus(e) {
+ this.$.filter.select();
+ },
+
+ // Prevent that the input text is deselected after focusing the find
+ // control with the mouse.
+ filterMouseUp(e) {
+ e.preventDefault();
+ },
+
+ get controller() {
+ return this.controller_;
+ },
+
+ set controller(c) {
+ this.controller_ = c;
+ this.updateHitCountEl();
+ },
+
+ focus() {
+ this.$.filter.focus();
+ },
+
+ get hasFocus() {
+ return this === document.activeElement;
+ },
+
+ filterTextChanged() {
+ Polymer.dom(this.$.hitCount).textContent = '';
+ this.$.spinner.style.visibility = 'visible';
+ this.$.spinner.style.animation = 'spin 1s linear infinite';
+ this.controller.startFiltering(this.$.filter.value).then(function() {
+ this.$.spinner.style.visibility = 'hidden';
+ this.$.spinner.style.animation = '';
+ this.updateHitCountEl();
+ }.bind(this));
+ },
+
+ findNext() {
+ if (this.controller) {
+ this.controller.findNext();
+ }
+ this.updateHitCountEl();
+ },
+
+ findPrevious() {
+ if (this.controller) {
+ this.controller.findPrevious();
+ }
+ this.updateHitCountEl();
+ },
+
+ updateHitCountEl() {
+ if (!this.controller || this.$.filter.value.length === 0) {
+ Polymer.dom(this.$.hitCount).textContent = '';
+ return;
+ }
+
+ const n = this.controller.filterHits.length;
+ const i = n === 0 ? -1 : this.controller.currentHitIndex;
+ Polymer.dom(this.$.hitCount).textContent = (i + 1) + ' of ' + n;
+ },
+
+ setText(string) {
+ this.$.filter.value = string;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html b/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html
new file mode 100644
index 00000000000..e3af0ec9c6f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/find_control.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const ctl = document.createElement('tr-ui-find-control');
+ ctl.controller = {
+ findNext() { },
+ findPrevious() { },
+ reset() {},
+
+ filterHits: ['a', 'b'],
+
+ currentHitIndex: 0
+ };
+
+ this.addHTMLOutput(ctl);
+ });
+
+ test('updateHitCountEl_twoResults', function() {
+ const ctl = document.createElement('tr-ui-find-control');
+ ctl.controller = {
+ findNext() { },
+ findPrevious() { },
+ reset() {},
+
+ filterHits: ['a', 'b'],
+
+ currentHitIndex: 0
+ };
+
+ this.addHTMLOutput(ctl);
+ ctl.$.filter.value = 'test';
+ ctl.updateHitCountEl();
+ assert.strictEqual(ctl.$.hitCount.textContent, '1 of 2');
+ });
+
+ test('updateHitCountEl_emptyFilter', function() {
+ const ctl = document.createElement('tr-ui-find-control');
+ ctl.controller = {
+ findNext() { },
+ findPrevious() { },
+ reset() {},
+
+ filterHits: ['a', 'b'],
+
+ currentHitIndex: 0
+ };
+
+ this.addHTMLOutput(ctl);
+ ctl.$.filter.value = '';
+ ctl.updateHitCountEl();
+ assert.strictEqual(ctl.$.hitCount.textContent, '');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html
new file mode 100644
index 00000000000..926915b19fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview FindController.
+ */
+tr.exportTo('tr.ui', function() {
+ const Task = tr.b.Task;
+
+ function FindController(brushingStateController) {
+ this.brushingStateController_ = brushingStateController;
+ this.filterHits_ = [];
+ this.currentHitIndex_ = -1;
+ this.activePromise_ = Promise.resolve();
+ this.activeTask_ = undefined;
+ }
+
+ FindController.prototype = {
+ __proto__: Object.prototype,
+
+ get model() {
+ return this.brushingStateController_.model;
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ enqueueOperation_(operation) {
+ let task;
+ if (operation instanceof tr.b.Task) {
+ task = operation;
+ } else {
+ task = new tr.b.Task(operation, this);
+ }
+ if (this.activeTask_) {
+ this.activeTask_ = this.activeTask_.enqueue(task);
+ } else {
+ // We're enqueuing the first task, schedule it.
+ this.activeTask_ = task;
+ this.activePromise_ = Task.RunWhenIdle(this.activeTask_);
+ this.activePromise_.then(function() {
+ this.activePromise_ = undefined;
+ this.activeTask_ = undefined;
+ }.bind(this));
+ }
+ },
+
+ /**
+ * Updates the filter hits based on the provided |filterText|. Returns a
+ * promise which resolves when |filterHits| has been refreshed.
+ */
+ startFiltering(filterText) {
+ const sc = this.brushingStateController_;
+ if (!sc) return;
+
+ // TODO(beaudoin): Cancel anything left in the task queue, without
+ // invalidating the promise.
+ this.enqueueOperation_(function() {
+ this.filterHits_ = [];
+ this.currentHitIndex_ = -1;
+ }.bind(this));
+
+ // Try constructing a UIState from the filterText.
+ // UIState.fromUserFriendlyString will throw an error only if the string
+ // is syntactically correct to a UI state string but with invalid values.
+ // It will return undefined if there is no syntactic match.
+ let stateFromString;
+ try {
+ stateFromString = sc.uiStateFromString(filterText);
+ } catch (e) {
+ this.enqueueOperation_(function() {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = e.message;
+ overlay.title = 'UI State Navigation Error';
+ overlay.visible = true;
+ });
+ return this.activePromise_;
+ }
+
+ if (stateFromString !== undefined) {
+ this.enqueueOperation_(
+ sc.navToPosition.bind(this, stateFromString, true));
+ } else {
+ // filterText is not a navString here -- proceed with find and filter.
+ if (filterText.length === 0) {
+ this.enqueueOperation_(sc.findTextCleared.bind(sc));
+ } else {
+ const filter = new tr.c.FullTextFilter(filterText);
+ const filterHitSet = new tr.model.EventSet();
+ this.enqueueOperation_(sc.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, filterHitSet));
+ this.enqueueOperation_(function() {
+ this.filterHits_ = filterHitSet.toArray();
+ sc.findTextChangedTo(filterHitSet);
+ }.bind(this));
+ }
+ }
+ return this.activePromise_;
+ },
+
+ /**
+ * Returns the most recent filter hits as an array. Call
+ * |startFiltering| to ensure this is up to date after the filter settings
+ * have been changed.
+ */
+ get filterHits() {
+ return this.filterHits_;
+ },
+
+ get currentHitIndex() {
+ return this.currentHitIndex_;
+ },
+
+ find_(dir) {
+ const firstHit = this.currentHitIndex_ === -1;
+ if (firstHit && dir < 0) {
+ this.currentHitIndex_ = 0;
+ }
+
+ const N = this.filterHits.length;
+ this.currentHitIndex_ = (this.currentHitIndex_ + dir + N) % N;
+
+ if (!this.brushingStateController_) return;
+
+ this.brushingStateController_.findFocusChangedTo(
+ new tr.model.EventSet(this.filterHits[this.currentHitIndex]));
+ },
+
+ findNext() {
+ this.find_(1);
+ },
+
+ findPrevious() {
+ this.find_(-1);
+ }
+ };
+
+ return {
+ FindController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html
new file mode 100644
index 00000000000..76f3362899b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html
@@ -0,0 +1,366 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/find_controller.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Task = tr.b.Task;
+
+ /*
+ * Just enough of the BrushingStateController to support the tests below.
+ */
+ function FakeBrushingStateController() {
+ this.addAllEventsMatchingFilterToSelectionReturnValue = [];
+
+ this.viewport = undefined;
+ this.model = undefined;
+ this.selection = new tr.model.EventSet();
+ this.findMatches = new tr.model.EventSet();
+ }
+
+ FakeBrushingStateController.prototype = {
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ return new Task(function() {
+ const n = this.addAllEventsMatchingFilterToSelectionReturnValue.length;
+ for (let i = 0; i < n; i++) {
+ selection.push(
+ this.addAllEventsMatchingFilterToSelectionReturnValue[i]);
+ }
+ }, this);
+ },
+
+ uiStateFromString(string) {
+ return undefined;
+ },
+
+ findTextChangedTo(selection) {
+ this.findMatches = selection;
+ this.selection = new tr.model.EventSet();
+ },
+
+ findFocusChangedTo(selection) {
+ this.selection = selection;
+ },
+
+ findTextCleared(selection) {
+ this.selection = new tr.model.EventSet();
+ this.findMatches = new tr.model.EventSet();
+ }
+ };
+
+ test('findControllerNoModel', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+ controller.findNext();
+ controller.findPrevious();
+ });
+
+ test('findControllerEmptyHit', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ brushingStateController.selection = new tr.model.EventSet();
+ brushingStateController.findMatches = new tr.model.EventSet();
+ controller.findNext();
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 0);
+ controller.findPrevious();
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 0);
+ });
+
+ test('findControllerOneHit', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdf').then(function() {
+ try {
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+
+ controller.findPrevious();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerMultipleHits', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ const s2 = {guid: 2};
+ const s3 = {guid: 3};
+
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1, s2, s3
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdf').then(function() {
+ try {
+ // Loop through hits then when we wrap, try moving backward.
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ let matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s2);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s3);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+
+ controller.findPrevious();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s3);
+
+ controller.findPrevious();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s2);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerChangeFilterAfterNext', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ const s2 = {guid: 2};
+ const s3 = {guid: 3};
+ const s4 = {guid: 4};
+
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1, s2, s3
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdf').then(function() {
+ // Loop through hits then when we wrap, try moving backward.
+ controller.findNext();
+ brushingStateController.
+ addAllEventsMatchingFilterToSelectionReturnValue = [s4];
+
+ controller.startFiltering('asdfsf').then(function() {
+ controller.findNext();
+ try {
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s4);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+ });
+
+ test('findControllerSelectsAllItemsFirst', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ const s2 = {guid: 2};
+ const s3 = {guid: 3};
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1, s2, s3
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdfsf').then(function() {
+ try {
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ let matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s2);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerWithRealTimeline', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ '', 'a', 0, 1, {}, 3));
+ model.t1 = t1;
+ });
+
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ const timeline = document.createElement('tr-ui-timeline-view');
+ Polymer.dom(timeline).appendChild(container);
+
+ // This is for testing only, have to make sure things link up right.
+ timeline.trackViewContainer_ = container;
+
+ timeline.model = model;
+
+ const brushingStateController = timeline.brushingStateController;
+ const controller = timeline.findCtl_.controller;
+
+ // Test find with no filterText.
+ controller.findNext();
+
+ // Test find with filter txt.
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('a').then(function() {
+ try {
+ assert.strictEqual(brushingStateController.selection.length, 0);
+ assert.deepEqual(Array.from(brushingStateController.findMatches),
+ model.t1.sliceGroup.slices);
+
+ controller.findNext();
+ assert.isTrue(brushingStateController.selection.equals(
+ new tr.model.EventSet(model.t1.sliceGroup.slices[0])));
+
+ controller.startFiltering('xxx').then(function() {
+ try {
+ assert.strictEqual(brushingStateController.findMatches.length, 0);
+ assert.strictEqual(brushingStateController.selection.length, 1);
+
+ controller.findNext();
+ assert.strictEqual(brushingStateController.selection.length, 0);
+
+ controller.findNext();
+ assert.strictEqual(brushingStateController.selection.length, 0);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerNavigation', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ let navToPositionCallCount = 0;
+ let findTextClearedCallCount = 0;
+ const fakeUIState = {};
+ brushingStateController.uiStateFromString = function(string) {
+ if (string === '') return undefined;
+
+ assert.strictEqual(string, '2000@1.2x7');
+ return fakeUIState;
+ };
+ brushingStateController.navToPosition = function(uiState) {
+ assert.strictEqual(uiState, fakeUIState);
+ navToPositionCallCount++;
+ };
+ brushingStateController.findTextCleared = function() {
+ findTextClearedCallCount++;
+ };
+
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('2000@1.2x7').then(function() {
+ assert.strictEqual(navToPositionCallCount, 1);
+ }).then(function() {
+ controller.startFiltering('').then(function() {
+ try {
+ assert.strictEqual(findTextClearedCallCount, 1);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+ });
+
+ test('findControllerClearAfterSet', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+ let findTextChangedToCalled = false;
+ brushingStateController.findTextChangedTo = function(selection) {
+ findTextChangedToCalled = true;
+ };
+ brushingStateController.findTextCleared = function() {
+ assert.strictEqual(findTextChangedToCalled, true);
+ };
+ controller.startFiltering('1');
+ controller.startFiltering('');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png
new file mode 100644
index 00000000000..8eef2bf7ecc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png
new file mode 100644
index 00000000000..c67e697de5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png
new file mode 100644
index 00000000000..834004a0f74
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png b/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png
new file mode 100644
index 00000000000..83d09179817
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html b/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html
new file mode 100644
index 00000000000..2f21d5156f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/extras/full_config.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/metrics/all_metrics.html">
+<link rel="import" href="/tracing/metrics/metric_map_function.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/file.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tracing-ui-metrics-debugger-app'>
+ <style>
+ pre {
+ overflow: auto;
+ }
+ #bar {
+ display: flex;
+ flex-direction: row;
+ padding: 1px 6px;
+ }
+ </style>
+
+ <template>
+ <top-left-controls id="top_left_controls"></top-left-controls>
+ <input id="load_trace" type="file"/>
+ <button id="run_metric">Run metric</button>
+ <div id="trace_info"></div>
+ <pre id="map_results">
+ </pre>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ Polymer({
+ is: 'tracing-ui-metrics-debugger-app',
+ created() {
+ this.metrics_ = [];
+ tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach(
+ function(m) {
+ this.metrics_.push({
+ label: m.constructor.name,
+ value: m.constructor.name
+ });
+ }, this);
+ this.activeTrace_ = undefined;
+ this.settingsKey_ = undefined;
+ this.currentMetricName_ = undefined;
+ this.settingsKey_ = 'metrics-debugger-app-metric-name';
+ },
+
+ ready() {
+ const metricSelector = tr.ui.b.createSelector(
+ this, 'currentMetricName_',
+ this.settingsKey_,
+ this.metrics_[0].value,
+ this.metrics_);
+ Polymer.dom(this.$.top_left_controls).appendChild(
+ metricSelector);
+
+ this.$.load_trace.addEventListener('change', function(event) {
+ const file = event.target.files[0];
+ this.onTraceFileSelected_(file);
+ }.bind(this));
+ this.$.run_metric.addEventListener(
+ 'click', function(event) {
+ event.stopPropagation();
+ this.onRunMetricClicked_();
+ }.bind(this));
+ },
+
+ onRunMetricClicked_() {
+ if (this.activeTrace_ === undefined) {
+ tr.ui.b.Overlay.showError('You must load a trace first!');
+ return;
+ }
+ const result = new tr.mre.MreResult();
+ const model = this.activeTrace_.model;
+ const options = {metrics: [this.currentMetricName_]};
+ try {
+ tr.metrics.metricMapFunction(result, model, options);
+ this.set(
+ '$.map_results.textContent',
+ 'Metric result:\n' + JSON.stringify(result.asDict(), undefined, 2));
+ } catch (err) {
+ tr.ui.b.Overlay.showError('Error running metric:\n' + err.stack);
+ }
+ },
+
+ onTraceFileSelected_(file) {
+ tr.ui.b.readFile(file).then(
+ function(data) {
+ this.setActiveTrace(file.name, data);
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Error while loading file: ' + err);
+ });
+ },
+
+ setActiveTrace(filename, data) {
+ const model = new tr.Model();
+ const importOptions = new tr.importer.ImportOptions();
+ importOptions.pruneEmptyContainers = false;
+ importOptions.showImportWarnings = true;
+ importOptions.trackDetailedModelStats = true;
+
+ const i = new tr.importer.Import(model, importOptions);
+ i.importTracesWithProgressDialog([data]).then(
+ function() {
+ this.activeTrace_ = {
+ filename,
+ model,
+ };
+ Polymer.dom(this.$.trace_info).textContent = 'Trace file ' +
+ filename + ' is loaded.';
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Trace import error: ' + err);
+ });
+ },
+ });
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html
new file mode 100644
index 00000000000..2ab621151db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/ui_state.html">
+<link rel="import" href="/tracing/ui/brushing_state.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ /*
+ * Some elements such as analysis-links require at least one of their
+ * ancestors to have a BrushingStateController.
+ * Some clients of such elements, such as histogram-set-view, do not have a
+ * timeline-view, which is required by the BrushingStateController.
+ * This class provides the API of BrushingStateController but not the
+ * implementation, unless there is a real BrushingStateController in the
+ * owning element's ancestor chain, in which case the implementation is
+ * delegated to the real BrushingStateController.
+ */
+ class NullBrushingStateController extends tr.c.BrushingStateController {
+ constructor() {
+ super(undefined);
+ this.parentController = undefined;
+ }
+
+ dispatchChangeEvent_() {
+ if (this.parentController) this.parentController.dispatchChangeEvent_();
+ }
+
+ get model() {
+ if (!this.parentController) return undefined;
+ return this.parentController.model;
+ }
+
+ get trackView() {
+ if (!this.parentController) return undefined;
+ return this.parentController.trackView;
+ }
+
+ get viewport() {
+ if (!this.parentController) return undefined;
+ return this.parentController.viewport;
+ }
+
+ get historyEnabled() {
+ if (!this.parentController) return undefined;
+ return this.parentController.historyEnabled;
+ }
+
+ set historyEnabled(historyEnabled) {
+ if (this.parentController) {
+ this.parentController.historyEnabled = historyEnabled;
+ }
+ }
+
+ modelWillChange() {
+ if (this.parentController) this.parentController.modelWillChange();
+ }
+
+ modelDidChange() {
+ if (this.parentController) this.parentController.modelDidChange();
+ }
+
+ onUserInitiatedSelectionChange_() {
+ if (this.parentController) {
+ this.parentController.onUserInitiatedSelectionChange_();
+ }
+ }
+
+ onPopState_(e) {
+ if (this.parentController) this.parentController.onPopState_(e);
+ }
+
+ get selection() {
+ if (!this.parentController) return undefined;
+ return this.parentController.selection;
+ }
+
+ get findMatches() {
+ if (!this.parentController) return undefined;
+ return this.parentController.findMatches;
+ }
+
+ get selectionOfInterest() {
+ if (!this.parentController) return undefined;
+ return this.parentController.selectionOfInterest;
+ }
+
+ get currentBrushingState() {
+ if (!this.parentController) return undefined;
+ return this.parentController.currentBrushingState;
+ }
+
+ set currentBrushingState(newBrushingState) {
+ if (this.parentController) {
+ this.parentController.currentBrushingState = newBrushingState;
+ }
+ }
+
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ if (this.parentController) {
+ this.parentController.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ }
+ }
+
+ findTextChangedTo(allPossibleMatches) {
+ if (this.parentController) {
+ this.parentController.findTextChangedTo(allPossibleMatches);
+ }
+ }
+
+ findFocusChangedTo(currentFocus) {
+ if (this.parentController) {
+ this.parentController.findFocusChangedTo(currentFocus);
+ }
+ }
+
+ findTextCleared() {
+ if (this.parentController) {
+ this.parentController.findTextCleared();
+ }
+ }
+
+ uiStateFromString(string) {
+ if (this.parentController) {
+ this.parentController.uiStateFromString(string);
+ }
+ }
+
+ navToPosition(uiState, showNavLine) {
+ if (this.parentController) {
+ this.parentController.navToPosition(uiState, showNavLine);
+ }
+ }
+
+ changeSelectionFromTimeline(selection) {
+ if (this.parentController) {
+ this.parentController.changeSelectionFromTimeline(selection);
+ }
+ }
+
+ showScriptControlSelection(selection) {
+ if (this.parentController) {
+ this.parentController.showScriptControlSelection(selection);
+ }
+ }
+
+ changeSelectionFromRequestSelectionChangeEvent(selection) {
+ if (this.parentController) {
+ this.parentController.changeSelectionFromRequestSelectionChangeEvent(
+ selection);
+ }
+ }
+
+ changeAnalysisViewRelatedEvents(eventSet) {
+ if (this.parentController && (eventSet instanceof tr.model.EventSet)) {
+ this.parentController.changeAnalysisViewRelatedEvents(eventSet);
+ }
+ }
+
+ changeAnalysisLinkHoveredEvents(eventSet) {
+ if (this.parentController && (eventSet instanceof tr.model.EventSet)) {
+ this.parentController.changeAnalysisLinkHoveredEvents(eventSet);
+ }
+ }
+
+ getViewSpecificBrushingState(viewId) {
+ if (this.parentController) {
+ this.parentController.getViewSpecificBrushingState(viewId);
+ }
+ }
+
+ changeViewSpecificBrushingState(viewId, newState) {
+ if (this.parentController) {
+ this.parentController.changeViewSpecificBrushingState(viewId, newState);
+ }
+ }
+ }
+
+ return {
+ NullBrushingStateController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html
new file mode 100644
index 00000000000..a91a81e05ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/extras/tquery/tquery.html">
+
+<dom-module id='tr-ui-scripting-control'>
+ <template>
+ <style>
+ :host {
+ flex: 1 1 auto;
+ }
+ .root {
+ font-family: monospace;
+ cursor: text;
+
+ padding: 2px;
+ margin: 2px;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ background: white;
+
+ height: 100px;
+ overflow-y: auto;
+
+ transition-property: opacity, height, padding, margin;
+ transition-duration: .2s;
+ transition-timing-function: ease-out;
+ }
+ .hidden {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ height: 0px;
+ opacity: 0;
+ }
+ .focused {
+ outline: auto 5px -webkit-focus-ring-color;
+ }
+ #history {
+ -webkit-user-select: text;
+ color: #777;
+ }
+ #promptContainer {
+ display: flex;
+ }
+ #promptMark {
+ width: 1em;
+ color: #468;
+ }
+ #prompt {
+ flex: 1;
+ width: 100%;
+ border: none !important;
+ background-color: inherit !important;
+ font: inherit !important;
+ text-overflow: clip !important;
+ text-decoration: none !important;
+ }
+ #prompt:focus {
+ outline: none;
+ }
+ </style>
+
+ <div id="root" class="root hidden" tabindex="0"
+ on-focus="onConsoleFocus">
+ <div id='history'></div>
+ <div id='promptContainer'>
+ <span id='promptMark'>&gt;</span>
+ <input id='prompt' type='text'
+ on-keypress="promptKeyPress"
+ on-keydown="promptKeyDown"
+ on-blur="onConsoleBlur">
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-scripting-control',
+
+ isEnterKey_(event) {
+ // Check if in IME.
+ // Remove keyIdentifier after reference build rolls past M51 when
+ // KeyboardEvent.key was added.
+ return event.keyCode !== 229 &&
+ (event.key === 'Enter' || event.keyIdentifier === 'Enter');
+ },
+
+ setFocus_(focused) {
+ const promptEl = this.$.prompt;
+ if (focused) {
+ promptEl.focus();
+ Polymer.dom(this.$.root).classList.add('focused');
+ // Move cursor to the end of any existing text.
+ if (promptEl.value.length > 0) {
+ const sel = window.getSelection();
+ sel.collapse(
+ Polymer.dom(promptEl).firstChild, promptEl.value.length);
+ }
+ } else {
+ promptEl.blur();
+ Polymer.dom(this.$.root).classList.remove('focused');
+ // Workaround for crbug.com/89026 to ensure the prompt doesn't retain
+ // keyboard focus.
+ const parent = promptEl.parentElement;
+ const nextEl = Polymer.dom(promptEl).nextSibling;
+ promptEl.remove();
+ Polymer.dom(parent).insertBefore(promptEl, nextEl);
+ }
+ },
+
+ onConsoleFocus(e) {
+ e.stopPropagation();
+ this.setFocus_(true);
+ },
+
+ onConsoleBlur(e) {
+ e.stopPropagation();
+ this.setFocus_(false);
+ },
+
+ promptKeyDown(e) {
+ e.stopPropagation();
+ if (!this.isEnterKey_(e)) return;
+
+ e.preventDefault();
+ const promptEl = this.$.prompt;
+ const command = promptEl.value;
+ if (command.length === 0) return;
+
+ promptEl.value = '';
+ this.addLine_(String.fromCharCode(187) + ' ' + command);
+
+ let result;
+ try {
+ result = this.controller_.executeCommand(command);
+ } catch (e) {
+ result = e.stack || e.stackTrace;
+ }
+
+ if (result instanceof tr.e.tquery.TQuery) {
+ // TODO(skyostil): Show a cool spinner.
+ result.ready().then(function(selection) {
+ this.addLine_(selection.length + ' matches');
+ this.controller_.brushingStateController.
+ showScriptControlSelection(selection);
+ }.bind(this));
+ } else {
+ this.addLine_(result);
+ }
+ promptEl.scrollIntoView();
+ },
+
+ addLine_(line) {
+ const historyEl = this.$.history;
+ if (historyEl.innerText.length !== 0) {
+ historyEl.innerText += '\n';
+ }
+ historyEl.innerText += line;
+ },
+
+ promptKeyPress(e) {
+ e.stopPropagation();
+ },
+
+ toggleVisibility() {
+ const root = this.$.root;
+ if (!this.visible) {
+ Polymer.dom(root).classList.remove('hidden');
+ this.setFocus_(true);
+ } else {
+ Polymer.dom(root).classList.add('hidden');
+ this.setFocus_(false);
+ }
+ },
+
+ get hasFocus() {
+ return this === document.activeElement;
+ },
+
+ get visible() {
+ const root = this.$.root;
+ return !Polymer.dom(root).classList.contains('hidden');
+ },
+
+ get controller() {
+ return this.controller_;
+ },
+
+ set controller(c) {
+ this.controller_ = c;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html
new file mode 100644
index 00000000000..69e8b2b9de7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/scripting_control.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const ctl = document.createElement('tr-ui-scripting-control');
+ this.addHTMLOutput(ctl);
+ ctl.toggleVisibility();
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html
new file mode 100644
index 00000000000..b2611f4ac3a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/grouping_table.html">
+<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-sp-file-size-stats-side-panel'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ toolbar {
+ align-items: center;
+ background-color: rgb(236, 236, 236);
+ border-bottom: 1px solid #8e8e8e;
+ display: flex;
+ flex-direction: row;
+ flex-direction: row;
+ flex: 0 0 auto;
+ font-size: 12px;
+ padding: 0 10px 0 10px;
+ }
+ table-container {
+ display: flex;
+ min-height: 0px;
+ overflow-y: auto;
+ }
+ </style>
+
+ <toolbar>
+ <span><b>Group by:</b></span>
+ <tr-ui-b-grouping-table-groupby-picker id="picker">
+ </tr-ui-b-grouping-table-groupby-picker>
+ </toolbar>
+ <table-container>
+ <tr-ui-b-grouping-table id="table"></tr-ui-b-grouping-table>
+ </table-container>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+(function() {
+ Polymer({
+ is: 'tr-ui-sp-file-size-stats-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.model_ = undefined;
+ this.selection_ = new tr.model.EventSet();
+ this.$.picker.settingsKey = 'tr-ui-sp-file-size-stats-side-panel-picker';
+ this.$.picker.possibleGroups = [
+ {
+ key: 'phase', label: 'Event Type',
+ dataFn(eventStat) { return eventStat.phase; }
+ },
+ {
+ key: 'category', label: 'Category',
+ dataFn(eventStat) { return eventStat.category; }
+ },
+ {
+ key: 'title', label: 'Title',
+ dataFn(eventStat) { return eventStat.title; }
+ }
+ ];
+ // If the picker did not restore currentGroupKeys from Settings,
+ // then set default currentGroupKeys.
+ if (this.$.picker.currentGroupKeys.length === 0) {
+ this.$.picker.currentGroupKeys = ['phase', 'title'];
+ }
+ this.$.picker.addEventListener('current-groups-changed',
+ this.updateContents_.bind(this));
+ },
+
+ get textLabel() {
+ return 'File Size Stats';
+ },
+
+ supportsModel(m) {
+ if (!m) {
+ return {
+ supported: false,
+ reason: 'No stats were collected for this file.'
+ };
+ }
+
+ if (m.stats.allTraceEventStats.length === 0) {
+ return {
+ supported: false,
+ reason: 'No stats were collected for this file.'
+ };
+ }
+ return {
+ supported: true
+ };
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ },
+
+ createColumns_(stats) {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) {
+ const titleEl = document.createElement('span');
+ Polymer.dom(titleEl).textContent = row.title;
+ titleEl.style.textOverflow = 'ellipsis';
+ return titleEl;
+ },
+ cmp(a, b) {
+ return a.title.localeCompare(b.title);
+ },
+ width: '400px'
+ },
+ {
+ title: 'Num Events',
+ align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT,
+ value(row) {
+ return row.rowStats.numEvents;
+ },
+ cmp(a, b) {
+ return a.rowStats.numEvents - b.rowStats.numEvents;
+ },
+ width: '80px'
+ }
+ ];
+
+ if (stats && stats.hasEventSizesinBytes) {
+ columns.push({
+ title: 'Bytes',
+ value(row) {
+ const value = new tr.b.Scalar(tr.b.Unit.byName.sizeInBytes,
+ row.rowStats.totalEventSizeinBytes);
+ const spanEl = tr.v.ui.createScalarSpan(value);
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.rowStats.totalEventSizeinBytes -
+ b.rowStats.totalEventSizeinBytes;
+ },
+ width: '80px'
+ });
+ }
+ return columns;
+ },
+
+ updateContents_() {
+ const table = this.$.table;
+
+ const columns = this.createColumns_(this.model.stats);
+ table.rowStatsConstructor = function ModelStatsRowStats(row) {
+ const sum = tr.b.math.Statistics.sum(row.data, function(x) {
+ return x.numEvents;
+ });
+ const totalEventSizeinBytes = tr.b.math.Statistics.sum(row.data, x =>
+ x.totalEventSizeinBytes
+ );
+ return {
+ numEvents: sum,
+ totalEventSizeinBytes
+ };
+ };
+ table.tableColumns = columns;
+ table.sortColumnIndex = 1;
+ table.sortDescending = true;
+ table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+
+ table.groupBy = this.$.picker.currentGroups.map(function(group) {
+ return group.dataFn;
+ });
+
+ if (!this.model) {
+ table.dataToGroup = [];
+ } else {
+ table.dataToGroup = this.model.stats.allTraceEventStats;
+ }
+ this.$.table.rebuild();
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-sp-file-size-stats-side-panel');
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html
new file mode 100644
index 00000000000..a764387427a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+
+ function createModel(opt_customizeModelCallback) {
+ return TestUtils.newModel(function(model) {
+ const modelStats = model.stats;
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title3');
+ modelStats.willProcessBasicTraceEvent('Y', 'cat3', 'title3');
+ });
+ }
+
+ test('instantiate', function() {
+ const panel = document.createElement('tr-ui-sp-file-size-stats-side-panel');
+ panel.model = createModel();
+ panel.style.height = '200px';
+ this.addHTMLOutput(panel);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html
new file mode 100644
index 00000000000..4508a4347a9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/metrics/metric_map_function.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<dom-module id="tr-ui-sp-metrics-side-panel">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ div#error {
+ color: red;
+ }
+ #results {
+ font-size: 12px;
+ }
+ </style>
+
+ <top-left-controls id="top_left_controls"></top-left-controls>
+
+ <tr-v-ui-histogram-set-view id="results"></tr-v-ui-histogram-set-view>
+
+ <div id="error"></div>
+ </template>
+</dom-module>
+
+<script>
+'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_();
+ },
+
+ /**
+ * Return an estimate of how many milliseconds it would take to re-run the
+ * metric. If the metric has not been run, return undefined.
+ *
+ * @return {undefined|number}
+ */
+ 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() {
+ // Not applicable to metrics.
+ },
+
+ set selection(_) {
+ // Not applicable to metrics.
+ },
+
+ /**
+ * @return {undefined|!tr.b.math.Range}
+ */
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ /**
+ * This may be called rapidly as the mouse is moved.
+ * If the metric supportsRangeOfInterest and takes less than 100ms, then it
+ * will be re-run immediately; otherwise, the Recompute button will be
+ * enabled.
+ *
+ * @param {!tr.b.math.Range} range
+ */
+ 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();
+ }
+
+ this.recomputeButton_.style.background = '';
+
+ await this.$.results.build(histograms);
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-sp-metrics-side-panel');
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html
new file mode 100644
index 00000000000..dc90ab54930
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ const browserProcess = m.getOrCreateProcess(1);
+ const browserMain = browserProcess.getOrCreateThread(2);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+ browserMain.sliceGroup.endSlice(10);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+ browserMain.sliceGroup.endSlice(30);
+ }
+ });
+ return m;
+ }
+
+ function testMetric(values, model) {
+ const hist = new tr.v.Histogram('test histogram', tr.b.Unit.byName.count);
+ hist.addSample(1);
+ values.addHistogram(hist);
+ }
+
+ tr.metrics.MetricRegistry.register(testMetric);
+
+ test('instantiateCollapsed', async function() {
+ const metricsPanel = document.createElement('tr-ui-sp-metrics-side-panel');
+ this.addHTMLOutput(metricsPanel);
+ metricsPanel.currentMetricName_ = 'testMetric';
+ await metricsPanel.build(createModel());
+
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ metricsPanel, elem => elem.textContent === 'test histogram'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html
new file mode 100644
index 00000000000..8b8e2f52b14
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.behaviors', function() {
+ const SidePanel = {
+
+ get rangeOfInterest() {
+ throw new Error('Not implemented');
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ throw new Error('Not implemented');
+ },
+
+ get selection() {
+ throw new Error('Not implemented');
+ },
+
+ set selection(selection) {
+ throw new Error('Not implemented');
+ },
+
+ get model() {
+ throw new Error('Not implemented');
+ },
+
+ set model(model) {
+ throw new Error('Not implemented');
+ },
+
+ supportsModel(m) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ SidePanel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html
new file mode 100644
index 00000000000..95be3103873
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-side-panel-container'>
+ <template>
+ <style>
+ :host {
+ align-items: stretch;
+ display: flex;
+ background-color: white;
+ }
+
+ :host([expanded]) > #side_panel_drag_handle,
+ :host([expanded]) > active-panel-container {
+ flex: 1 1 auto;
+ border-left: 1px solid black;
+ display: flex;
+ }
+
+ :host(:not([expanded])) > #side_panel_drag_handle,
+ :host(:not([expanded])) > active-panel-container {
+ display: none;
+ }
+
+ active-panel-container {
+ display: flex;
+ }
+
+ tab-strip {
+ flex: 0 0 auto;
+ flex-direction: column;
+ -webkit-user-select: none;
+ background-color: rgb(236, 236, 236);
+ border-left: 1px solid black;
+ cursor: default;
+ display: flex;
+ min-width: 18px; /* workaround for flexbox and writing-mode mixing bug */
+ padding: 10px 0 10px 0;
+ font-size: 12px;
+ }
+
+ tab-strip > tab-strip-label {
+ flex-shrink: 0;
+ -webkit-writing-mode: vertical-rl;
+ white-space: nowrap;
+ display: inline;
+ margin-right: 1px;
+ min-height: 20px;
+ padding: 15px 3px 15px 1px;
+ }
+
+ tab-strip >
+ tab-strip-label:not([enabled]) {
+ color: rgb(128, 128, 128);
+ }
+
+ tab-strip > tab-strip-label[selected] {
+ background-color: white;
+ border: 1px solid rgb(163, 163, 163);
+ border-left: none;
+ padding: 14px 2px 14px 1px;
+ }
+
+ #active_panel_container {
+ overflow: auto;
+ }
+ </style>
+
+ <tr-ui-b-drag-handle id="side_panel_drag_handle"></tr-ui-b-drag-handle>
+ <active-panel-container id='active_panel_container'>
+ </active-panel-container>
+ <tab-strip id='tab_strip'></tab-strip>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-side-panel-container',
+
+ ready() {
+ this.activePanelContainer_ = this.$.active_panel_container;
+ this.tabStrip_ = this.$.tab_strip;
+
+ this.dragHandle_ = this.$.side_panel_drag_handle;
+ this.dragHandle_.horizontal = false;
+ this.dragHandle_.target = this.activePanelContainer_;
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.brushingStateController_ = undefined;
+ this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
+ this.onModelChanged_ = this.onModelChanged_.bind(this);
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ set brushingStateController(brushingStateController) {
+ if (this.brushingStateController) {
+ this.brushingStateController_.removeEventListener(
+ 'change', this.onSelectionChanged_);
+ this.brushingStateController_.removeEventListener(
+ 'model-changed', this.onModelChanged_);
+ }
+ this.brushingStateController_ = brushingStateController;
+ if (this.brushingStateController) {
+ this.brushingStateController_.addEventListener(
+ 'change', this.onSelectionChanged_);
+ this.brushingStateController_.addEventListener(
+ 'model-changed', this.onModelChanged_);
+ if (this.model) {
+ this.onModelChanged_();
+ }
+ }
+ },
+
+ onSelectionChanged_() {
+ if (this.activePanel) {
+ this.activePanel.selection = this.selection;
+ }
+ },
+
+ get model() {
+ return this.brushingStateController_.model;
+ },
+
+ onModelChanged_() {
+ this.activePanelType_ = undefined;
+ this.updateContents_();
+ },
+
+ get expanded() {
+ this.hasAttribute('expanded');
+ },
+
+ get activePanel() {
+ return this.activePanelContainer_.children[0];
+ },
+
+ get activePanelType() {
+ return this.activePanelType_;
+ },
+
+ set activePanelType(panelType) {
+ if (this.model === undefined) {
+ throw new Error('Cannot activate panel without a model');
+ }
+
+ let panel = undefined;
+ if (panelType) {
+ panel = document.createElement(panelType);
+ }
+
+ if (panel !== undefined && !panel.supportsModel(this.model)) {
+ throw new Error('Cannot activate panel: does not support this model');
+ }
+
+ if (this.activePanelType) {
+ Polymer.dom(this.getLabelElementForPanelType_(
+ this.activePanelType)).removeAttribute('selected');
+ }
+
+ if (this.activePanelType) {
+ this.getLabelElementForPanelType_(
+ this.activePanelType).removeAttribute('selected');
+ }
+
+ if (this.activePanel) {
+ this.activePanelContainer_.removeChild(this.activePanel);
+ }
+
+ if (panelType === undefined) {
+ Polymer.dom(this).removeAttribute('expanded');
+ this.activePanelType_ = undefined;
+ return;
+ }
+
+ Polymer.dom(this.getLabelElementForPanelType_(panelType)).
+ setAttribute('selected', true);
+ Polymer.dom(this).setAttribute('expanded', true);
+
+ Polymer.dom(this.activePanelContainer_).appendChild(panel);
+ panel.rangeOfInterest = this.rangeOfInterest_;
+ panel.selection = this.selection_;
+ panel.model = this.model;
+
+ this.activePanelType_ = panelType;
+ },
+
+ getPanelTypeForConstructor_(constructor) {
+ for (let i = 0; i < this.tabStrip_.children.length; i++) {
+ if (this.tabStrip_.children[i].panelType.constructor === constructor) {
+ return this.tabStrip_.children[i].panelType;
+ }
+ }
+ },
+
+ getLabelElementForPanelType_(panelType) {
+ for (let i = 0; i < this.tabStrip_.children.length; i++) {
+ if (this.tabStrip_.children[i].panelType === panelType) {
+ return this.tabStrip_.children[i];
+ }
+ }
+ return undefined;
+ },
+
+ updateContents_() {
+ const previouslyActivePanelType = this.activePanelType;
+
+ Polymer.dom(this.tabStrip_).textContent = '';
+ const supportedPanelTypes = [];
+ const panelTypeInfos =
+ tr.ui.side_panel.SidePanelRegistry.getAllRegisteredTypeInfos();
+ const unsupportedLabelEls = [];
+
+ for (const panelTypeInfo of panelTypeInfos) {
+ const labelEl = document.createElement('tab-strip-label');
+ const panel = panelTypeInfo.constructor();
+ const panelType = panel.tagName;
+
+ Polymer.dom(labelEl).textContent = panel.textLabel;
+ labelEl.panelType = panelType;
+
+ const supported = panel.supportsModel(this.model);
+ if (this.model && supported.supported) {
+ supportedPanelTypes.push(panelType);
+ Polymer.dom(labelEl).setAttribute('enabled', true);
+ labelEl.addEventListener('click', function(panelType) {
+ this.activePanelType =
+ this.activePanelType === panelType ? undefined : panelType;
+ }.bind(this, panelType));
+ Polymer.dom(this.tabStrip_).appendChild(labelEl);
+ } else {
+ if (this.activePanel) {
+ this.activePanelContainer_.removeChild(this.activePanel);
+ }
+ this.removeAttribute('expanded');
+ unsupportedLabelEls.push(labelEl);
+ }
+ }
+
+ // Labels do not shrink, so when the user drags the analysis-view up, the
+ // bottom labels are obscured first.
+ // Append all unsupported panel labels after all supported panel labels so
+ // that unsupported panel labels are obscured first.
+ for (const labelEl of unsupportedLabelEls) {
+ Polymer.dom(this.tabStrip_).appendChild(labelEl);
+ }
+
+ // Restore the active panel, or collapse
+ if (previouslyActivePanelType &&
+ supportedPanelTypes.includes(previouslyActivePanelType)) {
+ this.activePanelType = previouslyActivePanelType;
+ Polymer.dom(this).setAttribute('expanded', true);
+ } else {
+ if (this.activePanel) {
+ Polymer.dom(this.activePanelContainer_).removeChild(this.activePanel);
+ }
+ Polymer.dom(this).removeAttribute('expanded');
+ }
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(range) {
+ if (range === undefined) {
+ throw new Error('Must not be undefined');
+ }
+ this.rangeOfInterest_ = range;
+ if (this.activePanel) {
+ this.activePanel.rangeOfInterest = range;
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html
new file mode 100644
index 00000000000..a51be620426
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_container.html">
+
+<dom-module id="tr-ui-sp-disabled-side-panel"></dom-module>
+<dom-module id="tr-ui-sp-enabled-side-panel"></dom-module>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function FakeBrushingStateController() {
+ this.addAllEventsMatchingFilterToSelectionReturnValue = [];
+
+ this.viewport = undefined;
+ this.model = undefined;
+ this.selection = new tr.model.EventSet();
+ this.highlight = new tr.model.EventSet();
+ }
+
+ FakeBrushingStateController.prototype = {
+ addEventListener(name, cb) {
+ }
+ };
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ const browserProcess = m.getOrCreateProcess(1);
+ const browserMain = browserProcess.getOrCreateThread(2);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+ browserMain.sliceGroup.endSlice(10);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+ browserMain.sliceGroup.endSlice(30);
+ }
+ });
+ return m;
+ }
+
+ Polymer({
+ is: 'tr-ui-sp-disabled-test-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+ supportsModel(m) {
+ return {supported: false};
+ },
+ get textLabel() {
+ return 'Disabled';
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function disabled() {
+ return document.createElement('tr-ui-sp-disabled-test-panel');
+ });
+
+ Polymer({
+ is: 'tr-ui-sp-enabled-test-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+ supportsModel(m) {
+ return {supported: true};
+ },
+ get textLabel() {
+ return 'Enabled';
+ },
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function enabled() {
+ return document.createElement('tr-ui-sp-enabled-test-panel');
+ });
+
+ test('instantiateCollapsed', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ brushingStateController.model = createModel();
+
+ const container = document.createElement('tr-ui-side-panel-container');
+ container.brushingStateController = brushingStateController;
+ this.addHTMLOutput(container);
+
+ // The Enabled tab should appear first in the tab strip even though the
+ // disabled side panel was registered first.
+ // There may be other side panels.
+ const labels = tr.ui.b.findDeepElementsMatching(container,
+ 'TAB-STRIP-LABEL').map(e => e.textContent);
+ assert.isBelow(labels.indexOf('Enabled'), labels.indexOf('Disabled'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html
new file mode 100644
index 00000000000..0ec139f2225
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+
+<script>
+'use strict';
+
+// TODO(charliea): This can probably be cleaned up so that we don't have to
+// manually wrap the Polymer element names with a function and
+// `document.createElement` at each of the registration sites by creating a
+// new "Polymer" registration mode.
+tr.exportTo('tr.ui.side_panel', function() {
+ /**
+ * SidePanelRegistry is an entity for side panel Polymer elements to register
+ * on so that they'll render a side panel if the model has the correct data.
+ *
+ * Example usage:
+ *
+ * SidePanelRegistry.register(function() {
+ * return document.createElement('my-side-panel');
+ * });
+ *
+ * @constructor
+ */
+ function SidePanelRegistry() {}
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(SidePanelRegistry, options);
+
+ return {
+ SidePanelRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html
new file mode 100644
index 00000000000..c174d5eb005
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SidePanelRegistry = tr.ui.side_panel.SidePanelRegistry;
+
+ const testOptions = {
+ setUp() {
+ SidePanelRegistry.pushCleanStateBeforeTest();
+ },
+
+ tearDown() {
+ SidePanelRegistry.popCleanStateAfterTest();
+ },
+ };
+
+ test('register', function() {
+ SidePanelRegistry.register(function() {
+ return document.createElement('div');
+ });
+ SidePanelRegistry.register(function() {
+ return document.createElement('span');
+ });
+
+ const typeInfos = SidePanelRegistry.getAllRegisteredTypeInfos();
+ assert.strictEqual(typeInfos[0].constructor().tagName, 'DIV');
+ assert.strictEqual(typeInfos[1].constructor().tagName, 'SPAN');
+ assert.lengthOf(typeInfos, 2);
+ }, testOptions);
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html
new file mode 100644
index 00000000000..2aefa6d6de4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ function TimelineDisplayTransform(opt_that) {
+ if (opt_that) {
+ this.set(opt_that);
+ return;
+ }
+ this.scaleX = 1;
+ this.panX = 0;
+ this.panY = 0;
+ }
+
+ TimelineDisplayTransform.prototype = {
+ set(that) {
+ this.scaleX = that.scaleX;
+ this.panX = that.panX;
+ this.panY = that.panY;
+ },
+
+ clone() {
+ return new TimelineDisplayTransform(this);
+ },
+
+ equals(that) {
+ let eq = true;
+ if (that === undefined || that === null) {
+ return false;
+ }
+ eq &= this.panX === that.panX;
+ eq &= this.panY === that.panY;
+ eq &= this.scaleX === that.scaleX;
+ return !!eq;
+ },
+
+ almostEquals(that) {
+ let eq = true;
+ if (that === undefined || that === null) {
+ return false;
+ }
+ eq &= Math.abs(this.panX - that.panX) < 0.001;
+ eq &= Math.abs(this.panY - that.panY) < 0.001;
+ eq &= Math.abs(this.scaleX - that.scaleX) < 0.001;
+ return !!eq;
+ },
+
+ incrementPanXInViewUnits(xDeltaView) {
+ this.panX += this.xViewVectorToWorld(xDeltaView);
+ },
+
+ xPanWorldPosToViewPos(worldX, viewX, viewWidth) {
+ if (typeof viewX === 'string') {
+ if (viewX === 'left') {
+ viewX = 0;
+ } else if (viewX === 'center') {
+ viewX = viewWidth / 2;
+ } else if (viewX === 'right') {
+ viewX = viewWidth - 1;
+ } else {
+ throw new Error('viewX must be left|center|right or number.');
+ }
+ }
+ this.panX = (viewX / this.scaleX) - worldX;
+ },
+
+ xPanWorldBoundsIntoView(worldMin, worldMax, viewWidth) {
+ if (this.xWorldToView(worldMin) < 0) {
+ this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth);
+ } else if (this.xWorldToView(worldMax) > viewWidth) {
+ this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth);
+ }
+ },
+
+ xSetWorldBounds(worldMin, worldMax, viewWidth) {
+ const worldWidth = worldMax - worldMin;
+ const scaleX = viewWidth / worldWidth;
+ const panX = -worldMin;
+ this.setPanAndScale(panX, scaleX);
+ },
+
+ setPanAndScale(p, s) {
+ this.scaleX = s;
+ this.panX = p;
+ },
+
+ xWorldToView(x) {
+ return (x + this.panX) * this.scaleX;
+ },
+
+ xWorldVectorToView(x) {
+ return x * this.scaleX;
+ },
+
+ xViewToWorld(x) {
+ return (x / this.scaleX) - this.panX;
+ },
+
+ xViewVectorToWorld(x) {
+ return x / this.scaleX;
+ }
+ };
+
+ return {
+ TimelineDisplayTransform,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html
new file mode 100644
index 00000000000..a632a6dc4fd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/animation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ const kDefaultPanAnimationDurationMs = 100.0;
+ const lerp = tr.b.math.lerp;
+
+ /**
+ * Pans a TimelineDisplayTransform by a given amount.
+ * @constructor
+ * @extends {tr.ui.b.Animation}
+ * @param {Number} deltaX The total amount of change to the transform's panX.
+ * @param {Number} deltaY The total amount of change to the transform's panY.
+ * @param {Number=} opt_durationMs How long the pan animation should run.
+ * Defaults to kDefaultPanAnimationDurationMs.
+ */
+ function TimelineDisplayTransformPanAnimation(
+ deltaX, deltaY, opt_durationMs) {
+ this.deltaX = deltaX;
+ this.deltaY = deltaY;
+ if (opt_durationMs === undefined) {
+ this.durationMs = kDefaultPanAnimationDurationMs;
+ } else {
+ this.durationMs = opt_durationMs;
+ }
+
+ this.startPanX = undefined;
+ this.startPanY = undefined;
+ this.startTimeMs = undefined;
+ }
+
+ TimelineDisplayTransformPanAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+ get affectsPanY() {
+ return this.deltaY !== 0;
+ },
+
+ canTakeOverFor(existingAnimation) {
+ return existingAnimation instanceof TimelineDisplayTransformPanAnimation;
+ },
+
+ takeOverFor(existing, timestamp, target) {
+ const remainingDeltaXOnExisting = existing.goalPanX - target.panX;
+ const remainingDeltaYOnExisting = existing.goalPanY - target.panY;
+ let remainingTimeOnExisting = timestamp - (
+ existing.startTimeMs + existing.durationMs);
+ remainingTimeOnExisting = Math.max(remainingTimeOnExisting, 0);
+
+ this.deltaX += remainingDeltaXOnExisting;
+ this.deltaY += remainingDeltaYOnExisting;
+ this.durationMs += remainingTimeOnExisting;
+ },
+
+ start(timestamp, target) {
+ this.startTimeMs = timestamp;
+ this.startPanX = target.panX;
+ this.startPanY = target.panY;
+ },
+
+ tick(timestamp, target) {
+ let percentDone = (timestamp - this.startTimeMs) / this.durationMs;
+ percentDone = tr.b.math.clamp(percentDone, 0, 1);
+
+ target.panX = lerp(percentDone, this.startPanX, this.goalPanX);
+ if (this.affectsPanY) {
+ target.panY = lerp(percentDone, this.startPanY, this.goalPanY);
+ }
+ return timestamp >= this.startTimeMs + this.durationMs;
+ },
+
+ get goalPanX() {
+ return this.startPanX + this.deltaX;
+ },
+
+ get goalPanY() {
+ return this.startPanY + this.deltaY;
+ }
+ };
+
+ /**
+ * Zooms in/out on a specified location in the world.
+ *
+ * Zooming in and out is all about keeping the area under the mouse cursor,
+ * here called the "focal point" in the same place under the zoom. If one
+ * simply changes the scale, the area under the mouse cursor will change. To
+ * keep the focal point from moving during the zoom, the pan needs to change
+ * in order to compensate. Thus, a ZoomTo animation is given both a focal
+ * point in addition to the amount by which to zoom.
+ *
+ * @constructor
+ * @extends {tr.ui.b.Animation}
+ * @param {Number} goalFocalPointXWorld The X coordinate in the world which is
+ * of interest.
+ * @param {Number} goalFocalPointXView Where on the screen the
+ * goalFocalPointXWorld should stay centered during the zoom.
+ * @param {Number} goalFocalPointY Where the panY should be when the zoom
+ * completes.
+ * @param {Number} zoomInRatioX The ratio of the current scaleX to the goal
+ * scaleX.
+ */
+ function TimelineDisplayTransformZoomToAnimation(
+ goalFocalPointXWorld,
+ goalFocalPointXView,
+ goalFocalPointY,
+ zoomInRatioX,
+ opt_durationMs) {
+ this.goalFocalPointXWorld = goalFocalPointXWorld;
+ this.goalFocalPointXView = goalFocalPointXView;
+ this.goalFocalPointY = goalFocalPointY;
+ this.zoomInRatioX = zoomInRatioX;
+ if (opt_durationMs === undefined) {
+ this.durationMs = kDefaultPanAnimationDurationMs;
+ } else {
+ this.durationMs = opt_durationMs;
+ }
+
+ this.startTimeMs = undefined;
+ this.startScaleX = undefined;
+ this.goalScaleX = undefined;
+ this.startPanY = undefined;
+ }
+
+ TimelineDisplayTransformZoomToAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+ get affectsPanY() {
+ return this.startPanY !== this.goalFocalPointY;
+ },
+
+ canTakeOverFor(existingAnimation) {
+ return false;
+ },
+
+ takeOverFor(existingAnimation, timestamp, target) {
+ this.goalScaleX = target.scaleX * this.zoomInRatioX;
+ },
+
+ start(timestamp, target) {
+ this.startTimeMs = timestamp;
+ this.startScaleX = target.scaleX;
+ this.goalScaleX = this.zoomInRatioX * target.scaleX;
+ this.startPanY = target.panY;
+ },
+
+ tick(timestamp, target) {
+ let percentDone = (timestamp - this.startTimeMs) / this.durationMs;
+ percentDone = tr.b.math.clamp(percentDone, 0, 1);
+
+ target.scaleX = lerp(percentDone, this.startScaleX, this.goalScaleX);
+ if (this.affectsPanY) {
+ target.panY = lerp(percentDone, this.startPanY, this.goalFocalPointY);
+ }
+
+ target.xPanWorldPosToViewPos(
+ this.goalFocalPointXWorld, this.goalFocalPointXView);
+ return timestamp >= this.startTimeMs + this.durationMs;
+ }
+ };
+
+ return {
+ TimelineDisplayTransformPanAnimation,
+ TimelineDisplayTransformZoomToAnimation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html
new file mode 100644
index 00000000000..215a8863885
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/animation_controller.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const TimelineDisplayTransformPanAnimation =
+ tr.ui.TimelineDisplayTransformPanAnimation;
+ const TimelineDisplayTransformZoomToAnimation =
+ tr.ui.TimelineDisplayTransformZoomToAnimation;
+
+ test('panBasic', function() {
+ const target = new TimelineDisplayTransform();
+ target.cloneAnimationState = function() {
+ return this.clone();
+ };
+
+ const a = new TimelineDisplayTransformPanAnimation(10, 20, 100);
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+ controller.queueAnimation(a, 0);
+
+ assert.isTrue(a.affectsPanY);
+ tr.b.forcePendingRAFTasksToRun(50);
+ assert.isAbove(target.panX, 0);
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.strictEqual(target.panX, 10);
+ assert.strictEqual(target.panY, 20);
+ });
+
+ test('zoomBasic', function() {
+ const target = new TimelineDisplayTransform();
+ target.panY = 30;
+ target.cloneAnimationState = function() {
+ return this.clone();
+ };
+
+ const a = new TimelineDisplayTransformZoomToAnimation(10, 20, 30, 5, 100);
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+ controller.queueAnimation(a, 0);
+
+ assert.isFalse(a.affectsPanY);
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.strictEqual(target.scaleX, 5);
+ });
+
+ test('panTakeover', function() {
+ const target = new TimelineDisplayTransform();
+ target.cloneAnimationState = function() {
+ return this.clone();
+ };
+
+ const b = new TimelineDisplayTransformPanAnimation(10, 0, 100);
+ const a = new TimelineDisplayTransformPanAnimation(10, 0, 100);
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+ controller.queueAnimation(a, 0);
+
+ tr.b.forcePendingRAFTasksToRun(50);
+ controller.queueAnimation(b, 50);
+
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.isTrue(controller.hasActiveAnimation);
+
+ tr.b.forcePendingRAFTasksToRun(150);
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.strictEqual(target.panX, 20);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html
new file mode 100644
index 00000000000..d0df289c34e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+
+ test('basics', function() {
+ const a = new TimelineDisplayTransform();
+ a.panX = 0;
+ a.panY = 0;
+ a.scaleX = 1;
+
+ const b = new TimelineDisplayTransform();
+ b.panX = 10;
+ b.panY = 0;
+ b.scaleX = 1;
+
+ assert.isFalse(a.equals(b));
+ assert.isFalse(a.almostEquals(b));
+
+ const c = b.clone();
+ assert.isTrue(b.equals(c));
+ assert.isTrue(b.almostEquals(c));
+
+ c.set(a);
+ assert.isTrue(a.equals(c));
+ assert.isTrue(a.almostEquals(c));
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html
new file mode 100644
index 00000000000..36126f898db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ /**
+ * @constructor
+ */
+ function SnapIndicator(y, height) {
+ this.y = y;
+ this.height = height;
+ }
+
+ /**
+ * The interesting part of the world.
+ *
+ * @constructor
+ */
+ function TimelineInterestRange(vp) {
+ this.viewport_ = vp;
+
+ this.range_ = new tr.b.math.Range();
+
+ this.leftSelected_ = false;
+ this.rightSelected_ = false;
+
+ this.leftSnapIndicator_ = undefined;
+ this.rightSnapIndicator_ = undefined;
+ }
+
+ TimelineInterestRange.prototype = {
+ get isEmpty() {
+ return this.range_.isEmpty;
+ },
+
+ reset() {
+ this.range_.reset();
+ this.leftSelected_ = false;
+ this.rightSelected_ = false;
+ this.leftSnapIndicator_ = undefined;
+ this.rightSnapIndicator_ = undefined;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get min() {
+ return this.range_.min;
+ },
+
+ set min(min) {
+ this.range_.min = min;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get max() {
+ return this.range_.max;
+ },
+
+ set max(max) {
+ this.range_.max = max;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ set(range) {
+ this.range_.reset();
+ this.range_.addRange(range);
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ setMinAndMax(min, max) {
+ this.range_.min = min;
+ this.range_.max = max;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get range() {
+ return this.range_.range;
+ },
+
+ asRangeObject() {
+ const range = new tr.b.math.Range();
+ range.addRange(this.range_);
+ return range;
+ },
+
+ get leftSelected() {
+ return this.leftSelected_;
+ },
+
+ set leftSelected(leftSelected) {
+ if (this.leftSelected_ === leftSelected) return;
+
+ this.leftSelected_ = leftSelected;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get rightSelected() {
+ return this.rightSelected_;
+ },
+
+ set rightSelected(rightSelected) {
+ if (this.rightSelected_ === rightSelected) return;
+
+ this.rightSelected_ = rightSelected;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get leftSnapIndicator() {
+ return this.leftSnapIndicator_;
+ },
+
+ set leftSnapIndicator(leftSnapIndicator) {
+ this.leftSnapIndicator_ = leftSnapIndicator;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get rightSnapIndicator() {
+ return this.rightSnapIndicator_;
+ },
+
+ set rightSnapIndicator(rightSnapIndicator) {
+ this.rightSnapIndicator_ = rightSnapIndicator;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ draw(ctx, viewLWorld, viewRWorld, viewHeight) {
+ if (this.range_.isEmpty) return;
+
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const markerLWorld = this.min;
+ const markerRWorld = this.max;
+
+ const markerLView = Math.round(dt.xWorldToView(markerLWorld));
+ const markerRView = Math.round(dt.xWorldToView(markerRWorld));
+
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+ if (markerLWorld > viewLWorld) {
+ ctx.fillRect(dt.xWorldToView(viewLWorld), 0,
+ markerLView, viewHeight);
+ }
+
+ if (markerRWorld < viewRWorld) {
+ ctx.fillRect(markerRView, 0,
+ dt.xWorldToView(viewRWorld), viewHeight);
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ ctx.lineWidth = Math.round(pixelRatio);
+ if (this.range_.range > 0) {
+ this.drawLine_(ctx, viewLWorld, viewRWorld,
+ viewHeight, this.min, this.leftSelected_);
+ this.drawLine_(ctx, viewLWorld, viewRWorld,
+ viewHeight, this.max, this.rightSelected_);
+ } else {
+ this.drawLine_(ctx, viewLWorld, viewRWorld,
+ viewHeight, this.min,
+ this.leftSelected_ || this.rightSelected_);
+ }
+ ctx.lineWidth = 1;
+ },
+
+ drawLine_(ctx, viewLWorld, viewRWorld, height, ts, selected) {
+ if (ts < viewLWorld || ts >= viewRWorld) return;
+
+ const dt = this.viewport_.currentDisplayTransform;
+ const viewX = Math.round(dt.xWorldToView(ts));
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, viewX, 0, viewX, height);
+ if (selected) {
+ ctx.strokeStyle = 'rgb(255, 0, 0)';
+ } else {
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ }
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ drawIndicators(ctx, viewLWorld, viewRWorld) {
+ if (this.leftSnapIndicator_) {
+ this.drawIndicator_(ctx, viewLWorld, viewRWorld,
+ this.range_.min,
+ this.leftSnapIndicator_,
+ this.leftSelected_);
+ }
+ if (this.rightSnapIndicator_) {
+ this.drawIndicator_(ctx, viewLWorld, viewRWorld,
+ this.range_.max,
+ this.rightSnapIndicator_,
+ this.rightSelected_);
+ }
+ },
+
+ drawIndicator_(ctx, viewLWorld, viewRWorld,
+ xWorld, si, selected) {
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const viewX = Math.round(dt.xWorldToView(xWorld));
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const viewY = si.y * devicePixelRatio;
+ const viewHeight = si.height * devicePixelRatio;
+ const arrowSize = 4 * pixelRatio;
+
+ if (selected) {
+ ctx.fillStyle = 'rgb(255, 0, 0)';
+ } else {
+ ctx.fillStyle = 'rgb(0, 0, 0)';
+ }
+ tr.ui.b.drawTriangle(ctx,
+ viewX - arrowSize * 0.75, viewY,
+ viewX + arrowSize * 0.75, viewY,
+ viewX, viewY + arrowSize);
+ ctx.fill();
+ tr.ui.b.drawTriangle(ctx,
+ viewX - arrowSize * 0.75, viewY + viewHeight,
+ viewX + arrowSize * 0.75, viewY + viewHeight,
+ viewX, viewY + viewHeight - arrowSize);
+ ctx.fill();
+
+ ctx.restore();
+ }
+ };
+
+ return {
+ SnapIndicator,
+ TimelineInterestRange,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html
new file mode 100644
index 00000000000..f6087abf74c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html
@@ -0,0 +1,1179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/x_marker_annotation.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/timing_tool.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/model_track.html">
+<link rel="import" href="/tracing/ui/tracks/x_axis_track.html">
+
+<!--
+ Interactive visualizaiton of Model objects based loosely on gantt charts.
+ Each thread in the Model is given a set of Tracks, one per subrow in the
+ thread. The TimelineTrackView class acts as a controller, creating the
+ individual tracks, while Tracks do actual drawing.
+
+ Visually, the TimelineTrackView produces (prettier) visualizations like the
+ following:
+ Thread1: AAAAAAAAAA AAAAA
+ BBBB BB
+ Thread2: CCCCCC CCCCC
+-->
+<dom-module id='tr-ui-timeline-track-view'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ display: flex;
+ position: relative;
+ }
+
+ :host ::content * {
+ -webkit-user-select: none;
+ cursor: default;
+ }
+
+ #drag_box {
+ background-color: rgba(0, 0, 255, 0.25);
+ border: 1px solid rgb(0, 0, 96);
+ font-size: 75%;
+ position: fixed;
+ }
+
+ #hint_text {
+ position: absolute;
+ bottom: 6px;
+ right: 6px;
+ font-size: 8pt;
+ }
+ </style>
+ <slot></slot>
+
+ <div id='drag_box'></div>
+ <div id='hint_text'></div>
+
+ <tv-ui-b-hotkey-controller id='hotkey_controller'>
+ </tv-ui-b-hotkey-controller>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-track-view',
+
+ ready() {
+ this.displayTransform_ = new tr.ui.TimelineDisplayTransform();
+ this.model_ = undefined;
+
+ this.timelineView_ = undefined;
+ this.pollIfViewportAttachedInterval_ = undefined;
+
+ this.viewport_ = new tr.ui.TimelineViewport(this);
+ this.viewportDisplayTransformAtMouseDown_ = undefined;
+ this.brushingStateController_ = undefined;
+
+ this.rulerTrackContainer_ =
+ new tr.ui.tracks.DrawingContainer(this.viewport_);
+ Polymer.dom(this).appendChild(this.rulerTrackContainer_);
+ this.rulerTrackContainer_.invalidate();
+ this.rulerTrackContainer_.style.overflowY = 'hidden';
+ this.rulerTrackContainer_.style.flexShrink = '0';
+
+ this.rulerTrack_ = new tr.ui.tracks.XAxisTrack(this.viewport_);
+ Polymer.dom(this.rulerTrackContainer_).appendChild(this.rulerTrack_);
+
+ this.upperModelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_);
+ this.upperModelTrack_.upperMode = true;
+ Polymer.dom(this.rulerTrackContainer_).appendChild(this.upperModelTrack_);
+
+ this.modelTrackContainer_ =
+ new tr.ui.tracks.DrawingContainer(this.viewport_);
+ Polymer.dom(this).appendChild(this.modelTrackContainer_);
+ this.modelTrackContainer_.style.display = 'block';
+ this.modelTrackContainer_.style.flexGrow = '1';
+ this.modelTrackContainer_.invalidate();
+
+ this.viewport_.modelTrackContainer = this.modelTrackContainer_;
+
+ this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_);
+ Polymer.dom(this.modelTrackContainer_).appendChild(this.modelTrack_);
+
+ this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this);
+
+ this.initMouseModeSelector();
+
+ this.hideDragBox_();
+
+ this.initHintText_();
+
+ this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
+
+ this.onDblClick_ = this.onDblClick_.bind(this);
+ this.addEventListener('dblclick', this.onDblClick_);
+
+ this.onMouseWheel_ = this.onMouseWheel_.bind(this);
+ this.addEventListener('mousewheel', this.onMouseWheel_);
+
+ this.onMouseDown_ = this.onMouseDown_.bind(this);
+ this.addEventListener('mousedown', this.onMouseDown_);
+
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.addEventListener('mousemove', this.onMouseMove_);
+
+ this.onTouchStart_ = this.onTouchStart_.bind(this);
+ this.addEventListener('touchstart', this.onTouchStart_);
+
+ this.onTouchMove_ = this.onTouchMove_.bind(this);
+ this.addEventListener('touchmove', this.onTouchMove_);
+
+ this.onTouchEnd_ = this.onTouchEnd_.bind(this);
+ this.addEventListener('touchend', this.onTouchEnd_);
+
+
+ this.addHotKeys_();
+
+ this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
+ this.lastMouseViewPos_ = {x: 0, y: 0};
+
+ this.lastTouchViewPositions_ = [];
+
+ this.alert_ = undefined;
+
+ this.isPanningAndScanning_ = false;
+ this.isZooming_ = false;
+ },
+
+ initMouseModeSelector() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this;
+ Polymer.dom(this).appendChild(this.mouseModeSelector_);
+
+ this.mouseModeSelector_.addEventListener('beginpan',
+ this.onBeginPanScan_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatepan',
+ this.onUpdatePanScan_.bind(this));
+ this.mouseModeSelector_.addEventListener('endpan',
+ this.onEndPanScan_.bind(this));
+
+ this.mouseModeSelector_.addEventListener('beginselection',
+ this.onBeginSelection_.bind(this));
+ this.mouseModeSelector_.addEventListener('updateselection',
+ this.onUpdateSelection_.bind(this));
+ this.mouseModeSelector_.addEventListener('endselection',
+ this.onEndSelection_.bind(this));
+
+ this.mouseModeSelector_.addEventListener('beginzoom',
+ this.onBeginZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatezoom',
+ this.onUpdateZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('endzoom',
+ this.onEndZoom_.bind(this));
+
+ this.mouseModeSelector_.addEventListener('entertiming',
+ this.timingTool_.onEnterTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('begintiming',
+ this.timingTool_.onBeginTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('updatetiming',
+ this.timingTool_.onUpdateTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('endtiming',
+ this.timingTool_.onEndTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('exittiming',
+ this.timingTool_.onExitTiming.bind(this.timingTool_));
+
+ const m = tr.ui.b.MOUSE_SELECTOR_MODE;
+ this.mouseModeSelector_.supportedModeMask =
+ m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING;
+ this.mouseModeSelector_.settingsKey =
+ 'timelineTrackView.mouseModeSelector';
+ this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0));
+ this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0));
+ this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0));
+ this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0));
+
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ m.SELECTION, tr.ui.b.MODIFIER.SHIFT);
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ m.PANSCAN, tr.ui.b.MODIFIER.SPACE);
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ set brushingStateController(brushingStateController) {
+ if (this.brushingStateController_) {
+ this.brushingStateController_.removeEventListener('change',
+ this.onSelectionChanged_);
+ }
+ this.brushingStateController_ = brushingStateController;
+ if (this.brushingStateController_) {
+ this.brushingStateController_.addEventListener('change',
+ this.onSelectionChanged_);
+ }
+ },
+
+ set timelineView(view) {
+ this.timelineView_ = view;
+ },
+
+ get processViews() {
+ return this.modelTrack_.processViews;
+ },
+
+ onSelectionChanged_() {
+ this.showHintText_('Press \'m\' to mark current selection');
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ set selection(selection) {
+ throw new Error('DO NOT CALL THIS');
+ },
+
+ set highlight(highlight) {
+ throw new Error('DO NOT CALL THIS');
+ },
+
+ detach() {
+ this.modelTrack_.detach();
+ this.upperModelTrack_.detach();
+
+ if (this.pollIfViewportAttachedInterval_) {
+ window.clearInterval(this.pollIfViewportAttachedInterval_);
+ this.pollIfViewportAttachedInterval_ = undefined;
+ }
+ this.viewport_.detach();
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ if (!model) {
+ throw new Error('Model cannot be undefined');
+ }
+
+ const modelInstanceChanged = this.model_ !== model;
+ this.model_ = model;
+ this.modelTrack_.model = model;
+ this.upperModelTrack_.model = model;
+
+ // Set up a reasonable viewport.
+ if (modelInstanceChanged) {
+ // The following code uses an interval to detect when the parent element
+ // is attached to the document. That is a trigger to run the setup
+ // function and install a resize listener.
+ this.pollIfViewportAttachedInterval_ = window.setInterval(
+ this.pollIfViewportAttached_.bind(this), 250);
+ }
+ },
+
+ get hasVisibleContent() {
+ return this.modelTrack_.hasVisibleContent ||
+ this.upperModelTrack_.hasVisibleContent;
+ },
+
+ /**
+ * Checks whether the parentNode is attached to the document.
+ * When it is, the method installs the iframe-based resize detection hook
+ * and then runs setInitialViewport_, if present.
+ */
+ pollIfViewportAttached_() {
+ if (!this.viewport_.isAttachedToDocumentOrInTestMode ||
+ this.viewport_.clientWidth === 0) {
+ return;
+ }
+ window.addEventListener(
+ 'resize', this.viewport_.dispatchChangeEvent);
+ window.clearInterval(this.pollIfViewportAttachedInterval_);
+ this.pollIfViewportAttachedInterval_ = undefined;
+
+ this.setInitialViewport_();
+ },
+
+ setInitialViewport_() {
+ // We need the canvas size to be up-to-date at this point. We maybe in
+ // here before the raf fires, so the size may have not been updated since
+ // the canvas was resized.
+ this.modelTrackContainer_.updateCanvasSizeIfNeeded_();
+ const w = this.modelTrackContainer_.canvas.width;
+
+ let min;
+ let range;
+
+ if (this.model_.bounds.isEmpty) {
+ min = 0;
+ range = 1000;
+ } else if (this.model_.bounds.range === 0) {
+ min = this.model_.bounds.min;
+ range = 1000;
+ } else {
+ min = this.model_.bounds.min;
+ range = this.model_.bounds.range;
+ }
+
+ const boost = range * 0.15;
+ this.displayTransform_.set(this.viewport_.currentDisplayTransform);
+ this.displayTransform_.xSetWorldBounds(
+ min - boost, min + range + boost, w);
+ this.viewport_.setDisplayTransformImmediately(this.displayTransform_);
+ },
+
+ /**
+ * @param {Filter} filter The filter to use for finding matches.
+ * @param {Selection} selection The selection to add matches to.
+ * @return {Task} which performs the filtering.
+ */
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const modelTrack = this.modelTrack_;
+ const firstT = modelTrack.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ const lastT = firstT.after(function() {
+ this.upperModelTrack_.addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ }, this);
+ return firstT;
+ },
+
+ onMouseMove_(e) {
+ // Zooming requires the delta since the last mousemove so we need to avoid
+ // tracking it when the zoom interaction is active.
+ if (this.isZooming_) return;
+
+ this.storeLastMousePos_(e);
+ },
+
+ onTouchStart_(e) {
+ this.storeLastTouchPositions_(e);
+ this.focusElements_();
+ },
+
+ onTouchMove_(e) {
+ e.preventDefault();
+ this.onUpdateTransformForTouch_(e);
+ },
+
+ onTouchEnd_(e) {
+ this.storeLastTouchPositions_(e);
+ this.focusElements_();
+ },
+
+ addHotKeys_() {
+ this.addKeyDownHotKeys_();
+ this.addKeyPressHotKeys_();
+ },
+
+ addKeyPressHotKey(dict) {
+ dict.eventType = 'keypress';
+ dict.useCapture = false;
+ dict.thisArg = this;
+ const binding = new tr.ui.b.HotKey(dict);
+ this.$.hotkey_controller.addHotKey(binding);
+ },
+
+ addKeyPressHotKeys_() {
+ this.addKeyPressHotKey({
+ keyCodes: ['w'.charCodeAt(0), ','.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(1.5, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['s'.charCodeAt(0), 'o'.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(1 / 1.5, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'g'.charCodeAt(0),
+ callback(e) {
+ this.onGridToggle_(true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'G'.charCodeAt(0),
+ callback(e) {
+ this.onGridToggle_(false);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['W'.charCodeAt(0), '<'.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(10, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['S'.charCodeAt(0), 'O'.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(1 / 10, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'a'.charCodeAt(0),
+ callback(e) {
+ this.queueSmoothPan_(this.viewWidth_ * 0.3, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['d'.charCodeAt(0), 'e'.charCodeAt(0)],
+ callback(e) {
+ this.queueSmoothPan_(this.viewWidth_ * -0.3, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'A'.charCodeAt(0),
+ callback(e) {
+ this.queueSmoothPan_(viewWidth * 0.5, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'D'.charCodeAt(0),
+ callback(e) {
+ this.queueSmoothPan_(viewWidth * -0.5, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: '0'.charCodeAt(0),
+ callback(e) {
+ this.setInitialViewport_();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'f'.charCodeAt(0),
+ callback(e) {
+ this.zoomToSelection();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'm'.charCodeAt(0),
+ callback(e) {
+ this.setCurrentSelectionAsInterestRange_();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'p'.charCodeAt(0),
+ callback(e) {
+ this.selectPowerSamplesInCurrentTimeRange_();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'h'.charCodeAt(0),
+ callback(e) {
+ this.toggleHighDetails_();
+ e.stopPropagation();
+ }
+ });
+ },
+
+ get viewWidth_() {
+ return this.modelTrackContainer_.canvas.clientWidth;
+ },
+
+ addKeyDownHotKeys_() {
+ const addBinding = function(dict) {
+ dict.eventType = 'keydown';
+ dict.useCapture = false;
+ dict.thisArg = this;
+ const binding = new tr.ui.b.HotKey(dict);
+ this.$.hotkey_controller.addHotKey(binding);
+ }.bind(this);
+
+ addBinding({
+ keyCode: 37, // Left arrow.
+ callback(e) {
+ const curSel = this.brushingStateController_.selection;
+ const sel = this.viewport.getShiftedSelection(curSel, -1);
+
+ if (sel) {
+ this.brushingStateController.changeSelectionFromTimeline(sel);
+ this.panToSelection();
+ } else {
+ this.queueSmoothPan_(this.viewWidth_ * 0.3, 0);
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+
+ addBinding({
+ keyCode: 39, // Right arrow.
+ callback(e) {
+ const curSel = this.brushingStateController_.selection;
+ const sel = this.viewport.getShiftedSelection(curSel, 1);
+ if (sel) {
+ this.brushingStateController.changeSelectionFromTimeline(sel);
+ this.panToSelection();
+ } else {
+ this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0);
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+ },
+
+ onDblClick_(e) {
+ if (this.mouseModeSelector_.mode !==
+ tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) {
+ return;
+ }
+
+ const curSelection = this.brushingStateController_.selection;
+ if (!curSelection.length || !tr.b.getOnlyElement(curSelection).title) {
+ return;
+ }
+
+ const selection = new tr.model.EventSet();
+ const filter = new tr.c.ExactTitleFilter(
+ tr.b.getOnlyElement(curSelection).title);
+ this.modelTrack_.addAllEventsMatchingFilterToSelection(filter,
+ selection);
+
+ this.brushingStateController.changeSelectionFromTimeline(selection);
+ },
+
+ onMouseWheel_(e) {
+ if (!e.altKey) return;
+
+ const delta = e.wheelDelta / 120;
+ const zoomScale = Math.pow(1.5, delta);
+ this.zoomBy_(zoomScale);
+ e.preventDefault();
+ },
+
+ onMouseDown_(e) {
+ if (this.mouseModeSelector_.mode !==
+ tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) {
+ return;
+ }
+
+ // Mouse down must start on ruler track for crosshair guide lines to draw.
+ if (e.target !== this.rulerTrack_) return;
+
+ // Make sure we don't start a selection drag event here.
+ this.dragBeginEvent_ = undefined;
+
+ // Remove nav string marker if it exists, since we're clearing the
+ // find control box.
+ if (this.xNavStringMarker_) {
+ this.model.removeAnnotation(this.xNavStringMarker_);
+ this.xNavStringMarker_ = undefined;
+ }
+
+ const dt = this.viewport_.currentDisplayTransform;
+ tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler.
+ // If mouse event is on ruler, don't do anything.
+ if (e.target === this.rulerTrack_) return;
+
+ const relativePosition = this.extractRelativeMousePosition_(e);
+ const loc = tr.model.Location.fromViewCoordinates(
+ this.viewport_, relativePosition.x, relativePosition.y);
+ // Not all points on the timeline represents a valid location.
+ // ex. process header tracks, letter dot tracks.
+ if (!loc) return;
+
+ if (this.guideLineAnnotation_ === undefined) {
+ this.guideLineAnnotation_ =
+ new tr.model.XMarkerAnnotation(loc.xWorld);
+ this.model.addAnnotation(this.guideLineAnnotation_);
+ } else {
+ this.guideLineAnnotation_.timestamp = loc.xWorld;
+ this.modelTrackContainer_.invalidate();
+ }
+
+ // Set the findcontrol's text to nav string of current state.
+ const state = new tr.ui.b.UIState(loc,
+ this.viewport_.currentDisplayTransform.scaleX);
+ this.timelineView_.setFindCtlText(
+ state.toUserFriendlyString(this.viewport_));
+ }.bind(this),
+ undefined, // Mouse up handler.
+ function onKeyUpDuringDrag() {
+ if (this.dragBeginEvent_) {
+ this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
+ this.dragBoxXEnd_, this.dragBoxYEnd_);
+ }
+ }.bind(this));
+ },
+
+ queueSmoothPan_(viewDeltaX, deltaY) {
+ const deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
+ viewDeltaX);
+ const animation = new tr.ui.TimelineDisplayTransformPanAnimation(
+ deltaX, deltaY);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ },
+
+ /**
+ * Zoom in or out on the timeline by the given scale factor.
+ * @param {Number} scale The scale factor to apply. If <1, zooms out.
+ * @param {boolean} Whether to change the zoom level smoothly.
+ */
+ zoomBy_(scale, smooth) {
+ if (scale <= 0) {
+ return;
+ }
+
+ smooth = !!smooth;
+ const vp = this.viewport_;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
+ const goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
+ goalFocalPointXView);
+ if (smooth) {
+ const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
+ goalFocalPointXWorld, goalFocalPointXView,
+ vp.currentDisplayTransform.panY,
+ scale);
+ vp.queueDisplayTransformAnimation(animation);
+ } else {
+ this.displayTransform_.set(vp.currentDisplayTransform);
+ this.displayTransform_.scaleX *= scale;
+ this.displayTransform_.xPanWorldPosToViewPos(
+ goalFocalPointXWorld, goalFocalPointXView, this.viewWidth_);
+ vp.setDisplayTransformImmediately(this.displayTransform_);
+ }
+ },
+
+ /**
+ * Zoom into the current selection.
+ */
+ zoomToSelection() {
+ if (!this.brushingStateController.selectionOfInterest.length) return;
+
+ const bounds = this.brushingStateController.selectionOfInterest.bounds;
+ if (!bounds.range) return;
+
+ const worldCenter = bounds.center;
+ const viewCenter = this.modelTrackContainer_.canvas.width / 2;
+ const adjustedWorldRange = bounds.range * 1.25;
+ const newScale = this.modelTrackContainer_.canvas.width /
+ adjustedWorldRange;
+ const zoomInRatio = newScale /
+ this.viewport_.currentDisplayTransform.scaleX;
+
+ const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
+ worldCenter, viewCenter,
+ this.viewport_.currentDisplayTransform.panY,
+ zoomInRatio);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ },
+
+ /**
+ * Pan the view so the current selection becomes visible.
+ */
+ panToSelection() {
+ if (!this.brushingStateController.selectionOfInterest.length) return;
+
+ const bounds = this.brushingStateController.selectionOfInterest.bounds;
+ const worldCenter = bounds.center;
+ const viewWidth = this.viewWidth_;
+
+ const dt = this.viewport_.currentDisplayTransform;
+ if (false && !bounds.range) {
+ if (dt.xWorldToView(bounds.center) < 0 ||
+ dt.xWorldToView(bounds.center) > viewWidth) {
+ this.displayTransform_.set(dt);
+ this.displayTransform_.xPanWorldPosToViewPos(
+ worldCenter, 'center', viewWidth);
+ const deltaX = this.displayTransform_.panX - dt.panX;
+ const animation = new tr.ui.TimelineDisplayTransformPanAnimation(
+ deltaX, 0);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ }
+ return;
+ }
+
+ this.displayTransform_.set(dt);
+ this.displayTransform_.xPanWorldBoundsIntoView(
+ bounds.min,
+ bounds.max,
+ viewWidth);
+ const deltaX = this.displayTransform_.panX - dt.panX;
+ const animation = new tr.ui.TimelineDisplayTransformPanAnimation(
+ deltaX, 0);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ },
+
+ navToPosition(uiState, showNavLine) {
+ const location = uiState.location;
+ const scaleX = uiState.scaleX;
+ const track = location.getContainingTrack(this.viewport_);
+
+ const worldCenter = location.xWorld;
+ const viewCenter = this.modelTrackContainer_.canvas.width / 5;
+ const zoomInRatio = scaleX /
+ this.viewport_.currentDisplayTransform.scaleX;
+
+ // Vertically scroll so track is in view.
+ track.scrollIntoViewIfNeeded();
+
+ // Perform zoom and panX animation.
+ const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
+ worldCenter, viewCenter,
+ this.viewport_.currentDisplayTransform.panY,
+ zoomInRatio);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+
+ if (!showNavLine) return;
+ // Add an X Marker Annotation at the specified timestamp.
+ if (this.xNavStringMarker_) {
+ this.model.removeAnnotation(this.xNavStringMarker_);
+ }
+ this.xNavStringMarker_ =
+ new tr.model.XMarkerAnnotation(worldCenter);
+ this.model.addAnnotation(this.xNavStringMarker_);
+ },
+
+ selectPowerSamplesInCurrentTimeRange_() {
+ const selectionBounds = this.brushingStateController_.selection.bounds;
+ if (this.model.device.powerSeries && !selectionBounds.empty) {
+ const events = this.model.device.powerSeries.getSamplesWithinRange(
+ selectionBounds.min, selectionBounds.max);
+ const selection = new tr.model.EventSet(events);
+ this.brushingStateController_.changeSelectionFromTimeline(selection);
+ }
+ },
+
+ setCurrentSelectionAsInterestRange_() {
+ const selectionBounds = this.brushingStateController_.selection.bounds;
+ if (selectionBounds.empty) {
+ this.viewport_.interestRange.reset();
+ return;
+ }
+
+ if (this.viewport_.interestRange.min === selectionBounds.min &&
+ this.viewport_.interestRange.max === selectionBounds.max) {
+ this.viewport_.interestRange.reset();
+ } else {
+ this.viewport_.interestRange.set(selectionBounds);
+ }
+ },
+
+ toggleHighDetails_() {
+ this.viewport_.highDetails = !this.viewport_.highDetails;
+ },
+
+ hideDragBox_() {
+ this.$.drag_box.style.left = '-1000px';
+ this.$.drag_box.style.top = '-1000px';
+ this.$.drag_box.style.width = 0;
+ this.$.drag_box.style.height = 0;
+ },
+
+ setDragBoxPosition_(xStart, yStart, xEnd, yEnd) {
+ const loY = Math.min(yStart, yEnd);
+ const hiY = Math.max(yStart, yEnd);
+ const loX = Math.min(xStart, xEnd);
+ const hiX = Math.max(xStart, xEnd);
+ const modelTrackRect = this.modelTrack_.getBoundingClientRect();
+ const dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY};
+
+ dragRect.right = dragRect.left + dragRect.width;
+ dragRect.bottom = dragRect.top + dragRect.height;
+
+ const modelTrackContainerRect =
+ this.modelTrackContainer_.getBoundingClientRect();
+ const clipRect = {
+ left: modelTrackContainerRect.left,
+ top: modelTrackContainerRect.top,
+ right: modelTrackContainerRect.right,
+ bottom: modelTrackContainerRect.bottom
+ };
+
+ const headingWidth = window.getComputedStyle(
+ Polymer.dom(this).querySelector('tr-ui-b-heading')).width;
+ const trackTitleWidth = parseInt(headingWidth);
+ clipRect.left = clipRect.left + trackTitleWidth;
+
+ const intersectRect_ = function(r1, r2) {
+ if (r2.left > r1.right || r2.right < r1.left ||
+ r2.top > r1.bottom || r2.bottom < r1.top) {
+ return false;
+ }
+
+ const results = {};
+ results.left = Math.max(r1.left, r2.left);
+ results.top = Math.max(r1.top, r2.top);
+ results.right = Math.min(r1.right, r2.right);
+ results.bottom = Math.min(r1.bottom, r2.bottom);
+ results.width = results.right - results.left;
+ results.height = results.bottom - results.top;
+ return results;
+ };
+
+ // TODO(dsinclair): intersectRect_ can return false (which should actually
+ // be undefined) but we use finalDragBox without checking the return value
+ // which could potentially blowup. Fix this .....
+ const finalDragBox = intersectRect_(clipRect, dragRect);
+
+ this.$.drag_box.style.left = finalDragBox.left + 'px';
+ this.$.drag_box.style.width = finalDragBox.width + 'px';
+ this.$.drag_box.style.top = finalDragBox.top + 'px';
+ this.$.drag_box.style.height = finalDragBox.height + 'px';
+ this.$.drag_box.style.whiteSpace = 'nowrap';
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const canv = this.modelTrackContainer_.canvas;
+ const dt = this.viewport_.currentDisplayTransform;
+ const loWX = dt.xViewToWorld(
+ (loX - canv.offsetLeft) * pixelRatio);
+ const hiWX = dt.xViewToWorld(
+ (hiX - canv.offsetLeft) * pixelRatio);
+
+ Polymer.dom(this.$.drag_box).textContent =
+ tr.b.Unit.byName.timeDurationInMs.format(hiWX - loWX);
+
+ const e = new tr.b.Event('selectionChanging');
+ e.loWX = loWX;
+ e.hiWX = hiWX;
+ this.dispatchEvent(e);
+ },
+
+ onGridToggle_(left) {
+ const selection = this.brushingStateController_.selection;
+ const tb = left ? selection.bounds.min : selection.bounds.max;
+
+ // Toggle the grid off if the grid is on, the marker position is the same
+ // and the same element is selected (same timebase).
+ if (this.viewport_.gridEnabled &&
+ this.viewport_.gridSide === left &&
+ this.viewport_.gridInitialTimebase === tb) {
+ this.viewport_.gridside = undefined;
+ this.viewport_.gridEnabled = false;
+ this.viewport_.gridInitialTimebase = undefined;
+ return;
+ }
+
+ // Shift the timebase left until its just left of model_.bounds.min.
+ const numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) /
+ this.viewport_.gridStep_);
+
+ this.viewport_.gridEnabled = true;
+ this.viewport_.gridSide = left;
+ this.viewport_.gridInitialTimebase = tb;
+ this.viewport_.gridTimebase = tb -
+ (numIntervalsSinceStart + 1) * this.viewport_.gridStep_;
+ },
+
+ storeLastMousePos_(e) {
+ this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+ },
+
+ storeLastTouchPositions_(e) {
+ this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e);
+ },
+
+ extractRelativeMousePosition_(e) {
+ const canv = this.modelTrackContainer_.canvas;
+ return {
+ x: e.clientX - canv.offsetLeft,
+ y: e.clientY - canv.offsetTop
+ };
+ },
+
+ extractRelativeTouchPositions_(e) {
+ const canv = this.modelTrackContainer_.canvas;
+
+ const touches = [];
+ for (let i = 0; i < e.touches.length; ++i) {
+ touches.push({
+ x: e.touches[i].clientX - canv.offsetLeft,
+ y: e.touches[i].clientY - canv.offsetTop
+ });
+ }
+ return touches;
+ },
+
+ storeInitialMouseDownPos_(e) {
+ const position = this.extractRelativeMousePosition_(e);
+
+ this.mouseViewPosAtMouseDown_.x = position.x;
+ this.mouseViewPosAtMouseDown_.y = position.y;
+ },
+
+ focusElements_() {
+ this.$.hotkey_controller.childRequestsGeneralFocus(this);
+ },
+
+ storeInitialInteractionPositionsAndFocus_(e) {
+ this.storeInitialMouseDownPos_(e);
+ this.storeLastMousePos_(e);
+
+ this.focusElements_();
+ },
+
+ onBeginPanScan_(e) {
+ const vp = this.viewport_;
+ this.viewportDisplayTransformAtMouseDown_ =
+ vp.currentDisplayTransform.clone();
+ this.isPanningAndScanning_ = true;
+
+ this.storeInitialInteractionPositionsAndFocus_(e);
+ e.preventDefault();
+ },
+
+ onUpdatePanScan_(e) {
+ if (!this.isPanningAndScanning_) return;
+
+ const viewWidth = this.viewWidth_;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
+ this.mouseViewPosAtMouseDown_.x);
+
+ const yDelta = this.lastMouseViewPos_.y -
+ this.mouseViewPosAtMouseDown_.y;
+
+ this.displayTransform_.set(this.viewportDisplayTransformAtMouseDown_);
+ this.displayTransform_.incrementPanXInViewUnits(xDeltaView);
+ this.displayTransform_.panY -= yDelta;
+ this.viewport_.setDisplayTransformImmediately(this.displayTransform_);
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.storeLastMousePos_(e);
+ },
+
+ onEndPanScan_(e) {
+ this.isPanningAndScanning_ = false;
+
+ this.storeLastMousePos_(e);
+
+ if (!e.isClick) {
+ e.preventDefault();
+ }
+ },
+
+ onBeginSelection_(e) {
+ const canv = this.modelTrackContainer_.canvas;
+ const rect = this.modelTrack_.getBoundingClientRect();
+ const canvRect = canv.getBoundingClientRect();
+
+ const inside = rect &&
+ e.clientX >= rect.left &&
+ e.clientX < rect.right &&
+ e.clientY >= rect.top &&
+ e.clientY < rect.bottom &&
+ e.clientX >= canvRect.left &&
+ e.clientX < canvRect.right;
+
+ if (!inside) return;
+
+ this.dragBeginEvent_ = e;
+
+ this.storeInitialInteractionPositionsAndFocus_(e);
+ e.preventDefault();
+ },
+
+ onUpdateSelection_(e) {
+ if (!this.dragBeginEvent_) return;
+
+ // Update the drag box
+ this.dragBoxXStart_ = this.dragBeginEvent_.clientX;
+ this.dragBoxXEnd_ = e.clientX;
+ this.dragBoxYStart_ = this.dragBeginEvent_.clientY;
+ this.dragBoxYEnd_ = e.clientY;
+ this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
+ this.dragBoxXEnd_, this.dragBoxYEnd_);
+ },
+
+ onEndSelection_(e) {
+ e.preventDefault();
+
+ if (!this.dragBeginEvent_) return;
+
+ // Stop the dragging.
+ this.hideDragBox_();
+ const eDown = this.dragBeginEvent_;
+ this.dragBeginEvent_ = undefined;
+
+ // Figure out extents of the drag.
+ const loY = Math.min(eDown.clientY, e.clientY);
+ const hiY = Math.max(eDown.clientY, e.clientY);
+ const loX = Math.min(eDown.clientX, e.clientX);
+ const hiX = Math.max(eDown.clientX, e.clientX);
+
+ // Convert to worldspace.
+ const canv = this.modelTrackContainer_.canvas;
+ const worldOffset = canv.getBoundingClientRect().left;
+ const loVX = loX - worldOffset;
+ const hiVX = hiX - worldOffset;
+
+ // Figure out what has been selected.
+ const selection = new tr.model.EventSet();
+ if (eDown.appendSelection) {
+ const previousSelection = this.brushingStateController_.selection;
+ if (previousSelection !== undefined) {
+ selection.addEventSet(previousSelection);
+ }
+ }
+ this.modelTrack_.addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection);
+
+ // Activate the new selection.
+ this.brushingStateController_.changeSelectionFromTimeline(selection);
+ },
+
+ onBeginZoom_(e) {
+ this.isZooming_ = true;
+
+ this.storeInitialInteractionPositionsAndFocus_(e);
+ e.preventDefault();
+ },
+
+ onUpdateZoom_(e) {
+ if (!this.isZooming_) return;
+
+ const newPosition = this.extractRelativeMousePosition_(e);
+
+ const zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
+ newPosition.y) * 0.01;
+
+ this.zoomBy_(zoomScaleValue, false);
+ this.storeLastMousePos_(e);
+ },
+
+ onEndZoom_(e) {
+ this.isZooming_ = false;
+
+ if (!e.isClick) {
+ e.preventDefault();
+ }
+ },
+
+ computeTouchCenter_(positions) {
+ let xSum = 0;
+ let ySum = 0;
+ for (let i = 0; i < positions.length; ++i) {
+ xSum += positions[i].x;
+ ySum += positions[i].y;
+ }
+ return {
+ x: xSum / positions.length,
+ y: ySum / positions.length
+ };
+ },
+
+ computeTouchSpan_(positions) {
+ let xMin = Number.MAX_VALUE;
+ let yMin = Number.MAX_VALUE;
+ let xMax = Number.MIN_VALUE;
+ let yMax = Number.MIN_VALUE;
+ for (let i = 0; i < positions.length; ++i) {
+ xMin = Math.min(xMin, positions[i].x);
+ yMin = Math.min(yMin, positions[i].y);
+ xMax = Math.max(xMax, positions[i].x);
+ yMax = Math.max(yMax, positions[i].y);
+ }
+ return Math.sqrt((xMin - xMax) * (xMin - xMax) +
+ (yMin - yMax) * (yMin - yMax));
+ },
+
+ onUpdateTransformForTouch_(e) {
+ const newPositions = this.extractRelativeTouchPositions_(e);
+ const currentPositions = this.lastTouchViewPositions_;
+
+ const newCenter = this.computeTouchCenter_(newPositions);
+ const currentCenter = this.computeTouchCenter_(currentPositions);
+
+ const newSpan = this.computeTouchSpan_(newPositions);
+ const currentSpan = this.computeTouchSpan_(currentPositions);
+
+ const vp = this.viewport_;
+ const viewWidth = this.viewWidth_;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const xDelta = pixelRatio * (newCenter.x - currentCenter.x);
+ const yDelta = newCenter.y - currentCenter.y;
+ const zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1;
+
+ const viewFocus = pixelRatio * newCenter.x;
+ const worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus);
+
+ this.displayTransform_.set(vp.currentDisplayTransform);
+ this.displayTransform_.scaleX *= zoomScaleValue;
+ this.displayTransform_.xPanWorldPosToViewPos(
+ worldFocus, viewFocus, viewWidth);
+ this.displayTransform_.incrementPanXInViewUnits(xDelta);
+ this.displayTransform_.panY -= yDelta;
+ vp.setDisplayTransformImmediately(this.displayTransform_);
+ this.storeLastTouchPositions_(e);
+ },
+
+ initHintText_() {
+ this.$.hint_text.style.display = 'none';
+
+ this.pendingHintTextClearTimeout_ = undefined;
+ },
+
+ showHintText_(text) {
+ if (this.pendingHintTextClearTimeout_) {
+ window.clearTimeout(this.pendingHintTextClearTimeout_);
+ this.pendingHintTextClearTimeout_ = undefined;
+ }
+ this.pendingHintTextClearTimeout_ = setTimeout(
+ this.hideHintText_.bind(this), 1000);
+ Polymer.dom(this.$.hint_text).textContent = text;
+ this.$.hint_text.style.display = '';
+ },
+
+ hideHintText_() {
+ this.pendingHintTextClearTimeout_ = undefined;
+ this.$.hint_text.style.display = 'none';
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html
new file mode 100644
index 00000000000..a84addbe9bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Task = tr.b.Task;
+
+ test('instantiate', function() {
+ const numThreads = 500;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ const p100 = model.getOrCreateProcess(100);
+ for (let i = 0; i < numThreads; i++) {
+ const t = p100.getOrCreateThread(101 + i);
+ if (i % 2 === 0) {
+ t.sliceGroup.beginSlice('cat', 'a', 100);
+ t.sliceGroup.endSlice(110);
+ } else {
+ t.sliceGroup.beginSlice('cat', 'b', 50);
+ t.sliceGroup.endSlice(120);
+ }
+ }
+ }
+ });
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+ timeline.style.maxHeight = '600px';
+ this.addHTMLOutput(timeline);
+ });
+
+ test('addAllEventsMatchingFilterToSelectionAsTask', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 1.1, {}, 2.8));
+
+ const t1asg = t1.asyncSliceGroup;
+ t1asg.slices.push(
+ tr.c.TestUtils.newAsyncSliceNamed('a', 0, 1, t1, t1));
+ t1asg.slices.push(
+ tr.c.TestUtils.newAsyncSliceNamed('b', 1, 2, t1, t1));
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ let expected = new tr.model.EventSet(
+ [t1asg.slices[0], t1.sliceGroup.slices[0]]);
+ let result = new tr.model.EventSet;
+ let filterTask = timeline.addAllEventsMatchingFilterToSelectionAsTask(
+ new tr.c.TitleOrCategoryFilter('a'), result);
+ Task.RunSynchronously(filterTask);
+ assert.isTrue(result.equals(expected));
+
+ expected = new tr.model.EventSet(
+ [t1asg.slices[1], t1.sliceGroup.slices[1]]);
+ result = new tr.model.EventSet();
+ filterTask = timeline.addAllEventsMatchingFilterToSelectionAsTask(
+ new tr.c.TitleOrCategoryFilter('b'), result);
+ Task.RunSynchronously(filterTask);
+ assert.isTrue(result.equals(expected));
+ });
+
+ test('emptyThreadsDeleted', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isFalse(timeline.hasVisibleContent);
+ });
+
+ test('filteredCounters', function() {
+ const model = new tr.Model();
+ const c1 = model.kernel.getOrCreateCpu(0);
+ c1.getOrCreateCounter('', 'b');
+
+ const p1 = model.getOrCreateProcess(1);
+ const ctr = p1.getOrCreateCounter('', 'a');
+ const series = new tr.model.CounterSeries('a', 0);
+ series.addCounterSample(0, 1);
+ ctr.addSeries(series);
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('filteredCpus', function() {
+ const model = new tr.Model();
+ const c1 = model.kernel.getOrCreateCpu(1);
+ c1.getOrCreateCounter('', 'a');
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('filteredProcesses', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ p1.getOrCreateCounter('', 'a');
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('filteredThreads', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(2);
+ t1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({start: 0, duration: 1}));
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('interestRange', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ const trackView = document.createElement('tr-ui-timeline-track-view');
+ trackView.model = model;
+ this.addHTMLOutput(trackView);
+
+ const slice = model.processes[52].threads[53].sliceGroup.slices[2];
+ trackView.viewport.interestRange.setMinAndMax(slice.start, slice.end);
+ });
+
+ test('emptyInterestRange', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ const trackView = document.createElement('tr-ui-timeline-track-view');
+ trackView.model = model;
+ this.addHTMLOutput(trackView);
+ trackView.viewport.interestRange.reset();
+ });
+
+
+ test('thinnestInterestRange', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ const trackView = document.createElement('tr-ui-timeline-track-view');
+ trackView.model = model;
+ this.addHTMLOutput(trackView);
+ trackView.viewport.interestRange.reset();
+
+ const slice = model.processes[52].threads[53].sliceGroup.slices[2];
+ trackView.viewport.interestRange.setMinAndMax(slice.start, slice.start);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html
new file mode 100644
index 00000000000..9a7a83f4a7b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html
@@ -0,0 +1,641 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/metrics/all_metrics.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+<link rel="import" href="/tracing/ui/base/favicons.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/toolbar_button.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/find_control.html">
+<link rel="import" href="/tracing/ui/find_controller.html">
+<link rel="import" href="/tracing/ui/scripting_control.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_container.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_view_help_overlay.html">
+<link rel="import" href="/tracing/ui/timeline_view_metadata_overlay.html">
+<link rel="import" href="/tracing/value/ui/preferred_display_unit.html">
+
+<dom-module id='tr-ui-timeline-view'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ cursor: default;
+ display: flex;
+ font-family: sans-serif;
+ padding: 0;
+ }
+
+ #control {
+ background-color: #e6e6e6;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%,
+ from(#E5E5E5), to(#D1D1D1));
+ flex: 0 0 auto;
+ overflow-x: auto;
+ }
+
+ #control::-webkit-scrollbar { height: 0px; }
+
+ #control > #bar {
+ font-size: 12px;
+ display: flex;
+ flex-direction: row;
+ margin: 1px;
+ }
+
+ #control > #bar > #title {
+ display: flex;
+ align-items: center;
+ padding-left: 8px;
+ padding-right: 8px;
+ flex: 1 1 auto;
+ }
+
+ #control > #bar > #left_controls,
+ #control > #bar > #right_controls {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ }
+
+ #control > #bar > #left_controls > * { margin-right: 2px; }
+ #control > #bar > #right_controls > * { margin-left: 2px; }
+ #control > #collapsing_controls { display: flex; }
+
+ middle-container {
+ flex: 1 1 auto;
+ flex-direction: row;
+ border-bottom: 1px solid #8e8e8e;
+ display: flex;
+ min-height: 0;
+ }
+
+ middle-container ::content track-view-container {
+ flex: 1 1 auto;
+ display: flex;
+ min-height: 0;
+ min-width: 0;
+ overflow-x: hidden;
+ }
+
+ middle-container ::content track-view-container > * { flex: 1 1 auto; }
+ middle-container > x-timeline-view-side-panel-container { flex: 0 0 auto; }
+ tr-ui-b-drag-handle { flex: 0 0 auto; }
+ tr-ui-a-analysis-view { flex: 0 0 auto; }
+
+ #view_options_dropdown, #process_filter_dropdown {
+ --dropdown-button: {
+ -webkit-appearance: none;
+ align-items: normal;
+ background-color: rgb(248, 248, 248);
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-sizing: content-box;
+ color: rgba(0, 0, 0, 0.8);
+ font-family: sans-serif;
+ font-size: 12px;
+ padding: 2px 5px;
+ }
+ }
+ </style>
+
+ <tv-ui-b-hotkey-controller id="hkc"></tv-ui-b-hotkey-controller>
+ <div id="control">
+ <div id="bar">
+ <div id="left_controls"></div>
+ <div id="title">^_^</div>
+ <div id="right_controls">
+ <tr-ui-b-dropdown id="process_filter_dropdown" label="Processes"></tr-ui-b-dropdown>
+ <tr-ui-b-toolbar-button id="view_metadata_button">
+ M
+ </tr-ui-b-toolbar-button>
+ <tr-ui-b-dropdown id="view_options_dropdown" label="View Options"></tr-ui-b-dropdown>
+ <tr-ui-find-control id="view_find_control"></tr-ui-find-control>
+ <tr-ui-b-toolbar-button id="view_console_button">
+ &#187;
+ </tr-ui-b-toolbar-button>
+ <tr-ui-b-toolbar-button id="view_help_button">
+ ?
+ </tr-ui-b-toolbar-button>
+ </div>
+ </div>
+ <div id="collapsing_controls"></div>
+ <tr-ui-b-info-bar-group id="import-warnings">
+ </tr-ui-b-info-bar-group>
+ </div>
+ <middle-container>
+ <slot></slot>
+
+ <tr-ui-side-panel-container id="side_panel_container">
+ </tr-ui-side-panel-container>
+ </middle-container>
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
+ <tr-ui-a-analysis-view id="analysis"></tr-ui-a-analysis-view>
+
+ <tr-v-ui-preferred-display-unit id="display_unit">
+ </tr-v-ui-preferred-display-unit>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-view',
+
+ created() {
+ this.trackViewContainer_ = undefined;
+
+ this.queuedModel_ = undefined;
+
+ this.builtPromise_ = undefined;
+ this.doneBuilding_ = undefined;
+ },
+
+ attached() {
+ this.async(function() {
+ this.trackViewContainer_ = Polymer.dom(this).querySelector(
+ '#track_view_container');
+ if (!this.trackViewContainer_) {
+ throw new Error('missing trackviewContainer');
+ }
+
+ if (this.queuedModel_) this.updateContents_();
+ });
+ },
+
+ ready() {
+ this.tabIndex = 0; // Let the timeline able to receive key events.
+
+ this.titleEl_ = this.$.title;
+ this.leftControlsEl_ = this.$.left_controls;
+ this.rightControlsEl_ = this.$.right_controls;
+ this.collapsingControlsEl_ = this.$.collapsing_controls;
+ this.sidePanelContainer_ = this.$.side_panel_container;
+
+ this.brushingStateController_ = new tr.c.BrushingStateController(this);
+
+ this.findCtl_ = this.$.view_find_control;
+ this.findCtl_.controller = new tr.ui.FindController(
+ this.brushingStateController_);
+
+ this.scriptingCtl_ = document.createElement('tr-ui-scripting-control');
+ this.scriptingCtl_.controller = new tr.c.ScriptingController(
+ this.brushingStateController_);
+
+ this.sidePanelContainer_.brushingStateController =
+ this.brushingStateController_;
+
+ if (window.tr.metrics && window.tr.metrics.sh &&
+ window.tr.metrics.sh.SystemHealthMetric) {
+ this.railScoreSpan_ = document.createElement(
+ 'tr-metrics-ui-sh-system-health-span');
+ Polymer.dom(this.rightControls).appendChild(this.railScoreSpan_);
+ } else {
+ this.railScoreSpan_ = undefined;
+ }
+
+ this.processFilter_ = this.$.process_filter_dropdown;
+
+ this.optionsDropdown_ = this.$.view_options_dropdown;
+ Polymer.dom(this.optionsDropdown_.iconElement).textContent = 'View Options';
+
+ this.showFlowEvents_ = false;
+ Polymer.dom(this.optionsDropdown_).appendChild(tr.ui.b.createCheckBox(
+ this, 'showFlowEvents',
+ 'tr.ui.TimelineView.showFlowEvents', false,
+ 'Flow events'));
+ this.highlightVSync_ = false;
+ this.highlightVSyncCheckbox_ = tr.ui.b.createCheckBox(
+ this, 'highlightVSync',
+ 'tr.ui.TimelineView.highlightVSync', false,
+ 'Highlight VSync');
+ Polymer.dom(this.optionsDropdown_).appendChild(
+ this.highlightVSyncCheckbox_);
+
+ this.initMetadataButton_();
+ this.initConsoleButton_();
+ this.initHelpButton_();
+
+ Polymer.dom(this.collapsingControls).appendChild(this.scriptingCtl_);
+
+ this.dragEl_ = this.$.drag_handle;
+
+ this.analysisEl_ = this.$.analysis;
+ this.analysisEl_.brushingStateController = this.brushingStateController_;
+
+ this.addEventListener(
+ 'requestSelectionChange',
+ function(e) {
+ const sc = this.brushingStateController_;
+ sc.changeSelectionFromRequestSelectionChangeEvent(e.selection);
+ }.bind(this));
+
+ // Bookkeeping.
+ this.onViewportChanged_ = this.onViewportChanged_.bind(this);
+ this.bindKeyListeners_();
+
+ this.dragEl_.target = this.analysisEl_;
+ },
+
+ get globalMode() {
+ return this.hotkeyController.globalMode;
+ },
+
+ set globalMode(globalMode) {
+ globalMode = !!globalMode;
+ this.brushingStateController_.historyEnabled = globalMode;
+ this.hotkeyController.globalMode = globalMode;
+ },
+
+ get hotkeyController() {
+ return this.$.hkc;
+ },
+
+ updateDocumentFavicon() {
+ let hue;
+ if (!this.model) {
+ hue = 'blue';
+ } else {
+ hue = this.model.faviconHue;
+ }
+
+ let faviconData = tr.ui.b.FaviconsByHue[hue];
+ if (faviconData === undefined) {
+ faviconData = tr.ui.b.FaviconsByHue.blue;
+ }
+
+ // Find link if its there
+ let link = Polymer.dom(document.head).querySelector(
+ 'link[rel="shortcut icon"]');
+ if (!link) {
+ link = document.createElement('link');
+ link.rel = 'shortcut icon';
+ Polymer.dom(document.head).appendChild(link);
+ }
+ link.href = faviconData;
+ },
+
+ get showFlowEvents() {
+ return this.showFlowEvents_;
+ },
+
+ set showFlowEvents(showFlowEvents) {
+ this.showFlowEvents_ = showFlowEvents;
+ if (!this.trackView_) return;
+
+ this.trackView_.viewport.showFlowEvents = showFlowEvents;
+ },
+
+ get highlightVSync() {
+ return this.highlightVSync_;
+ },
+
+ set highlightVSync(highlightVSync) {
+ this.highlightVSync_ = highlightVSync;
+ if (!this.trackView_) return;
+
+ this.trackView_.viewport.highlightVSync = highlightVSync;
+ },
+
+ initHelpButton_() {
+ const helpButtonEl = this.$.view_help_button;
+
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'Chrome Tracing Help';
+ dlg.visible = false;
+ dlg.appendChild(
+ document.createElement('tr-ui-timeline-view-help-overlay'));
+
+ function onClick(e) {
+ dlg.visible = !dlg.visible;
+ // Stop event so it doesn't trigger new click listener on document.
+ e.stopPropagation();
+ }
+
+ helpButtonEl.addEventListener('click', onClick.bind(this));
+ },
+
+ initConsoleButton_() {
+ const toggleEl = this.$.view_console_button;
+
+ function onClick(e) {
+ this.scriptingCtl_.toggleVisibility();
+ e.stopPropagation();
+ return false;
+ }
+ toggleEl.addEventListener('click', onClick.bind(this));
+ },
+
+ initMetadataButton_() {
+ const showEl = this.$.view_metadata_button;
+
+ function onClick(e) {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'Metadata for trace';
+
+ const metadataOverlay = document.createElement(
+ 'tr-ui-timeline-view-metadata-overlay');
+ metadataOverlay.metadata = this.model.metadata;
+
+ Polymer.dom(dlg).appendChild(metadataOverlay);
+ dlg.visible = true;
+
+ e.stopPropagation();
+ return false;
+ }
+ showEl.addEventListener('click', onClick.bind(this));
+
+ this.updateMetadataButtonVisibility_();
+ },
+
+ updateMetadataButtonVisibility_() {
+ const showEl = this.$.view_metadata_button;
+ showEl.style.display =
+ (this.model && this.model.metadata.length) ? '' : 'none';
+ },
+
+ updateProcessList_() {
+ const dropdown = Polymer.dom(this.processFilter_);
+ while (dropdown.firstChild) {
+ dropdown.removeChild(dropdown.firstChild);
+ }
+ if (!this.model) return;
+
+ const trackView =
+ this.trackViewContainer_.querySelector('tr-ui-timeline-track-view');
+ const processViews = trackView.processViews;
+ const cboxes = [];
+ const updateAll = (checked) => {
+ for (const cbox of cboxes) {
+ cbox.checked = checked;
+ }
+ };
+
+ dropdown.appendChild(tr.ui.b.createButton('All', () => updateAll(true)));
+ dropdown.appendChild(tr.ui.b.createButton('None', () => updateAll(false)));
+
+ for (const view of processViews) {
+ const cbox = tr.ui.b.createCheckBox(undefined, undefined, undefined,
+ true, view.processBase.userFriendlyName,
+ () => view.visible = cbox.checked);
+ cbox.checked = view.visible;
+ cboxes.push(cbox);
+ view.addEventListener('visibility', () => cbox.checked = view.visible);
+ dropdown.appendChild(cbox);
+ }
+ },
+
+ get leftControls() {
+ return this.leftControlsEl_;
+ },
+
+ get rightControls() {
+ return this.rightControlsEl_;
+ },
+
+ get collapsingControls() {
+ return this.collapsingControlsEl_;
+ },
+
+ get viewTitle() {
+ return Polymer.dom(this.titleEl_).textContent.substring(
+ Polymer.dom(this.titleEl_).textContent.length - 2);
+ },
+
+ set viewTitle(text) {
+ if (text === undefined) {
+ Polymer.dom(this.titleEl_).textContent = '';
+ this.titleEl_.hidden = true;
+ return;
+ }
+ this.titleEl_.hidden = false;
+ Polymer.dom(this.titleEl_).textContent = text;
+ },
+
+ get model() {
+ if (this.trackView_) {
+ return this.trackView_.model;
+ }
+ return undefined;
+ },
+
+ set model(model) {
+ this.build(model);
+ },
+
+ async build(model) {
+ this.queuedModel_ = model;
+ this.builtPromise_ = new Promise((resolve, reject) => {
+ this.doneBuilding_ = resolve;
+ });
+ if (this.trackViewContainer_) await this.updateContents_();
+ },
+
+ get builtPromise() {
+ return this.builtPromise_;
+ },
+
+ async updateContents_() {
+ if (this.trackViewContainer_ === undefined) {
+ throw new Error(
+ 'timeline-view.updateContents_ requires trackViewContainer_');
+ }
+
+ const model = this.queuedModel_;
+ this.queuedModel_ = undefined;
+
+ const modelInstanceChanged = model !== this.model;
+ const modelValid = model && !model.bounds.isEmpty;
+
+ const importWarningsEl = Polymer.dom(this.root).querySelector(
+ '#import-warnings');
+ Polymer.dom(importWarningsEl).textContent = '';
+
+ // Remove old trackView if the model has completely changed.
+ if (modelInstanceChanged) {
+ if (this.railScoreSpan_) {
+ this.railScoreSpan_.model = undefined;
+ }
+ Polymer.dom(this.trackViewContainer_).textContent = '';
+ if (this.trackView_) {
+ this.trackView_.viewport.removeEventListener(
+ 'change', this.onViewportChanged_);
+ this.trackView_.brushingStateController = undefined;
+ this.trackView_.detach();
+ this.trackView_ = undefined;
+ }
+ this.brushingStateController_.modelWillChange();
+ }
+
+ // Create new trackView if needed.
+ if (modelValid && !this.trackView_) {
+ this.trackView_ = document.createElement('tr-ui-timeline-track-view');
+ this.trackView_.timelineView = this;
+
+ this.trackView.brushingStateController = this.brushingStateController_;
+
+ Polymer.dom(this.trackViewContainer_).appendChild(this.trackView_);
+ this.trackView_.viewport.addEventListener(
+ 'change', this.onViewportChanged_);
+ }
+
+ // Set the model.
+ if (modelValid) {
+ this.trackView_.model = model;
+ this.trackView_.viewport.showFlowEvents = this.showFlowEvents;
+ this.trackView_.viewport.highlightVSync = this.highlightVSync;
+ if (this.railScoreSpan_) {
+ this.railScoreSpan_.model = model;
+ }
+
+ this.$.display_unit.preferredTimeDisplayMode = model.intrinsicTimeUnit;
+ }
+
+ if (model) {
+ for (const warning of model.importWarningsThatShouldBeShownToUser) {
+ importWarningsEl.addMessage(
+ `Import Warning: ${warning.type}: ${warning.message}`, [{
+ buttonText: 'Dismiss',
+ onClick(event, infobar) {
+ infobar.visible = false;
+ }
+ }]);
+ }
+ }
+
+ // Do things that are selection specific
+ if (modelInstanceChanged) {
+ this.updateProcessList_();
+ this.updateMetadataButtonVisibility_();
+ this.brushingStateController_.modelDidChange();
+ this.onViewportChanged_();
+ }
+
+ this.doneBuilding_();
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ get trackView() {
+ return this.trackView_;
+ },
+
+ get settings() {
+ if (!this.settings_) {
+ this.settings_ = new tr.b.Settings();
+ }
+ return this.settings_;
+ },
+
+ /**
+ * Deprecated. Kept around because third_party code occasionally calls
+ * this to set up embedding.
+ */
+ set focusElement(value) {
+ throw new Error('This is deprecated. Please set globalMode to true.');
+ },
+
+ bindKeyListeners_() {
+ const hkc = this.hotkeyController;
+
+ // Shortcuts that *can* steal focus from the console and the filter text
+ // box.
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: '`'.charCodeAt(0),
+ useCapture: true,
+ thisArg: this,
+ callback(e) {
+ this.scriptingCtl_.toggleVisibility();
+ if (!this.scriptingCtl_.hasFocus) {
+ this.focus();
+ }
+ e.stopPropagation();
+ }
+ }));
+
+ // Shortcuts that *can* steal focus from the filter text box.
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: '/'.charCodeAt(0),
+ useCapture: true,
+ thisArg: this,
+ callback(e) {
+ if (this.scriptingCtl_.hasFocus) return;
+
+ if (this.findCtl_.hasFocus) {
+ this.focus();
+ } else {
+ this.findCtl_.focus();
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }));
+
+ // Shortcuts that *can't* steal focus.
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: '?'.charCodeAt(0),
+ useCapture: false,
+ thisArg: this,
+ callback(e) {
+ this.$.view_help_button.click();
+ e.stopPropagation();
+ }
+ }));
+
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: 'v'.charCodeAt(0),
+ useCapture: false,
+ thisArg: this,
+ callback(e) {
+ this.toggleHighlightVSync_();
+ e.stopPropagation();
+ }
+ }));
+ },
+
+ onViewportChanged_(e) {
+ const spc = this.sidePanelContainer_;
+ if (!this.trackView_) {
+ spc.rangeOfInterest.reset();
+ return;
+ }
+
+ const vr = this.trackView_.viewport.interestRange.asRangeObject();
+ if (!spc.rangeOfInterest.equals(vr)) {
+ spc.rangeOfInterest = vr;
+ }
+
+ if (this.railScoreSpan_ && this.model) {
+ this.railScoreSpan_.model = this.model;
+ }
+ },
+
+ toggleHighlightVSync_() {
+ this.highlightVSyncCheckbox_.checked =
+ !this.highlightVSyncCheckbox_.checked;
+ },
+
+ setFindCtlText(string) {
+ this.findCtl_.setText(string);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html
new file mode 100644
index 00000000000..48c2e72df76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html
@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+
+<dom-module id='tr-ui-timeline-view-help-overlay'>
+ <template>
+ <style>
+ :host {
+ flex: 1 1 auto;
+ flex-direction: row;
+ display: flex;
+ width: 700px;
+ }
+ .column {
+ width: 50%;
+ }
+ h2 {
+ font-size: 1.2em;
+ margin: 0;
+ margin-top: 5px;
+ text-align: center;
+ }
+ h3 {
+ margin: 0;
+ margin-left: 126px;
+ margin-top: 10px;
+ }
+ .pair {
+ flex: 1 1 auto;
+ flex-direction: row;
+ display: flex;
+ }
+ .command {
+ font-family: monospace;
+ margin-right: 5px;
+ text-align: right;
+ width: 150px;
+ }
+ .action {
+ font-size: 0.9em;
+ text-align: left;
+ width: 200px;
+ }
+ tr-ui-b-mouse-mode-icon {
+ border: 1px solid #888;
+ border-radius: 3px;
+ box-shadow: inset 0 0 2px rgba(0,0,0,0.3);
+ display: inline-block;
+ margin-right: 1px;
+ position: relative;
+ top: 4px;
+ }
+ .mouse-mode-icon.pan-mode {
+ background-position: -1px -11px;
+ }
+ .mouse-mode-icon.select-mode {
+ background-position: -1px -41px;
+ }
+ .mouse-mode-icon.zoom-mode {
+ background-position: -1px -71px;
+ }
+ .mouse-mode-icon.timing-mode {
+ background-position: -1px -101px;
+ }
+ </style>
+ <div class="column left">
+ <h2>Navigation</h2>
+ <div class='pair'>
+ <div class='command'>w/s</div>
+ <div class='action'>Zoom in/out (+shift: faster)</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>a/d</div>
+ <div class='action'>Pan left/right (+shift: faster)</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>&rarr;/shift-TAB</div>
+ <div class='action'>Select previous event</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>&larr;/TAB</div>
+ <div class='action'>Select next event</div>
+ </div>
+
+ <h2>Mouse Controls</h2>
+ <div class='pair'>
+ <div class='command'>click</div>
+ <div class='action'>Select event</div>
+ </div>
+ <div class='pair'>
+ <div class='command'>alt-mousewheel</div>
+ <div class='action'>Zoom in/out</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="SELECTION"></tr-ui-b-mouse-mode-icon>
+ Select mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Box select</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'><span class='mod'></span>-click/drag</div>
+ <div class='action'>Add events to the current selection</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>double click</div>
+ <div class='action'>Select all events with same title</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="PANSCAN"></tr-ui-b-mouse-mode-icon>
+ Pan mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Pan the view</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="ZOOM"></tr-ui-b-mouse-mode-icon>
+ Zoom mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Zoom in/out by dragging up/down</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="TIMING"></tr-ui-b-mouse-mode-icon>
+ Timing mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Create or move markers</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>double click</div>
+ <div class='action'>Set marker range to slice</div>
+ </div>
+ </div>
+
+ <div class="column right">
+ <h2>General</h2>
+ <div class='pair'>
+ <div class='command'>1-4</div>
+ <div class='action'>Switch mouse mode</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>shift</div>
+ <div class='action'>Hold for temporary select</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>space</div>
+ <div class='action'>Hold for temporary pan</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>/</div>
+ <div class='action'>Search</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>enter</div>
+ <div class='action'>Step through search results</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>f</div>
+ <div class='action'>Zoom into selection</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>z/0</div>
+ <div class='action'>Reset zoom and pan</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>g/G</div>
+ <div class='action'>Toggle 60hz grid</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>v</div>
+ <div class='action'>Highlight VSync</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>h</div>
+ <div class='action'>Toggle low/high details</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>m</div>
+ <div class='action'>Mark current selection</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>p</div>
+ <div class='action'>Select power samples over current selection interval</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>`</div>
+ <div class='action'>Show or hide the scripting console</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>?</div>
+ <div class='action'>Show help</div>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-view-help-overlay',
+
+ ready() {
+ const mod = tr.isMac ? 'cmd ' : 'ctrl';
+ const spans = Polymer.dom(this.root).querySelectorAll(
+ 'span.mod');
+ for (let i = 0; i < spans.length; i++) {
+ Polymer.dom(spans[i]).textContent = mod;
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html
new file mode 100644
index 00000000000..1b705eea28c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/timeline_view_help_overlay.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('inactive', function() {
+ const el = document.createElement('tr-ui-timeline-view-help-overlay');
+ this.addHTMLOutput(el);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html
new file mode 100644
index 00000000000..45f0efa1a2a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-timeline-view-metadata-overlay'>
+ <template>
+ <style>
+ :host {
+ width: 700px;
+
+ overflow: auto;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-view-metadata-overlay',
+
+ created() {
+ this.metadata_ = undefined;
+ },
+
+ ready() {
+ this.$.table.tableColumns = [
+ {
+ title: 'name',
+ value: d => d.name,
+ },
+ {
+ title: 'value',
+ value: d => {
+ const gov = document.createElement('tr-ui-a-generic-object-view');
+ gov.object = d.value;
+ return gov;
+ },
+ }
+ ];
+ },
+
+ get metadata() {
+ return this.metadata_;
+ },
+
+ set metadata(metadata) {
+ this.metadata_ = metadata;
+ this.$.table.tableRows = this.metadata_;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html
new file mode 100644
index 00000000000..82f974ac7e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+<link rel="import" href="/tracing/ui/timeline_view_metadata_overlay.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('inactive', function() {
+ const el = document.createElement('tr-ui-timeline-view-metadata-overlay');
+ el.metadata = [
+ {
+ name: 'clientInfo',
+ value: {
+ command_line: './out/Release/Chromium.app/Contents/MacOS/Chromium --enable-threaded-compositing --force-compositing-mode --enable-impl-side-painting --enable-skia-benchmarking --allow-webui-compositing --flag-switches-begin --force-compositing-mode --disable-threaded-compositing --flag-switches-end', // @suppress longLineCheck
+ version: 'Chrome/29.0.1521.0'
+ }
+ },
+ {
+ name: 'somethingElse',
+ value: 'fascinating!'
+ }
+ ];
+
+ this.addHTMLOutput(el);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html
new file mode 100644
index 00000000000..7591f82df77
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Task = tr.b.Task;
+
+ function setupTimeline() {
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ const view = document.createElement('tr-ui-timeline-view');
+ Polymer.dom(view).appendChild(container);
+ view.trackViewContainer_ = container;
+ return view;
+ }
+
+ const createFullyPopulatedModel = function(opt_withError, opt_withMetadata) {
+ const withError = opt_withError !== undefined ? opt_withError : true;
+ const withMetadata = opt_withMetadata !== undefined ?
+ opt_withMetadata : true;
+
+ const numTests = 50;
+ let testIndex = 0;
+ const startTime = 0;
+
+ const model = new tr.Model();
+ const io = new tr.importer.ImportOptions();
+ model.importOptions = io;
+
+ const cpu = model.kernel.getOrCreateCpu(0);
+ cpu.getOrCreateCounter('Category Name', 'Counter Name Here');
+ cpu.createSubSlices();
+
+ for (testIndex = 0; testIndex < numTests; ++testIndex) {
+ const process = model.getOrCreateProcess(10000 + testIndex);
+ if (testIndex % 2 === 0) {
+ const thread = process.getOrCreateThread('Thread Name Here');
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'foo', 'a', 0, startTime, {}, 1));
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'bar', 'b', 0, startTime + 23, {}, 10));
+ } else {
+ const thread = process.getOrCreateThread('Name');
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'foo', 'a', 0, startTime + 4, {}, 11));
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'bar', 'b', 0, startTime + 22, {}, 14));
+ }
+ }
+ const p1000 = model.getOrCreateProcess(1000);
+ const objects = p1000.objects;
+ objects.idWasCreated('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10);
+ objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10,
+ 'snapshot-1');
+ objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 25,
+ 'snapshot-2');
+ objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 40,
+ 'snapshot-3');
+ objects.idWasDeleted('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 45);
+ model.updateCategories_();
+
+ // Add a known problematic piece of data to test the import errors UI.
+ model.importWarning({
+ type: 'test_error',
+ message: 'Synthetic Import Error',
+ showToUser: true,
+ });
+ model.updateBounds();
+
+ // Add data with metadata information stored
+ model.metadata.push({name: 'a', value: 'testA'});
+ model.metadata.push({name: 'b', value: 'testB'});
+ model.metadata.push({name: 'c', value: 'testC'});
+
+ return model;
+ };
+
+ const visibleTracks = function(trackButtons) {
+ return trackButtons.reduce(function(numVisible, button) {
+ const style = button.parentElement.style;
+ const visible = (style.display.indexOf('none') === -1);
+ return visible ? numVisible + 1 : numVisible;
+ }, 0);
+ };
+
+ const modelsEquivalent = function(lhs, rhs) {
+ if (lhs.length !== rhs.length) return false;
+
+ return lhs.every(function(lhsItem, index) {
+ const rhsItem = rhs[index];
+ return rhsItem.regexpText === lhsItem.regexpText &&
+ rhsItem.isOn === lhsItem.isOn;
+ });
+ };
+
+ test('instantiate', async function() {
+ const model11 = createFullyPopulatedModel(true, true);
+
+ const view = setupTimeline();
+ view.style.height = '400px';
+ view.style.border = '1px solid black';
+ view.model = model11;
+
+ const simpleButton1 = document.createElement('tr-ui-b-toolbar-button');
+ Polymer.dom(simpleButton1).textContent = 'M';
+ Polymer.dom(view.leftControls).appendChild(simpleButton1);
+
+ const simpleButton2 = document.createElement('tr-ui-b-toolbar-button');
+ Polymer.dom(simpleButton2).textContent = 'am button';
+ Polymer.dom(view.leftControls).appendChild(simpleButton2);
+
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+ });
+
+ test('changeModelToSomethingDifferent', async function() {
+ const model00 = createFullyPopulatedModel(false, false);
+ const model11 = createFullyPopulatedModel(true, true);
+
+ const view = setupTimeline();
+ view.style.height = '400px';
+ view.model = model00;
+ view.model = undefined;
+ view.model = model11;
+ view.model = model00;
+
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+ });
+
+ test('setModelToSameThingAgain', async function() {
+ const model = createFullyPopulatedModel(false, false);
+
+ // Create a view with a model.
+ const view = setupTimeline();
+ this.addHTMLOutput(view);
+ view.style.height = '400px';
+ view.model = model;
+ const sc = view.brushingStateController;
+
+ // Mutate the model and update the view.
+ const t123 = model.getOrCreateProcess(123).getOrCreateThread(123);
+ t123.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'somethingUnusual', start: 0, duration: 5}));
+ view.model = model;
+
+ await view.builtPromise;
+
+ // Verify that the new bits of the model show up in the view.
+ const selection = new tr.model.EventSet();
+ const filter = new tr.c.TitleOrCategoryFilter('somethingUnusual');
+ const filterTask = sc.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ Task.RunSynchronously(filterTask);
+ assert.strictEqual(selection.length, 1);
+ });
+
+ test('setModelBeforeAttached', async function() {
+ const view = document.createElement('tr-ui-timeline-view');
+ view.style.height = '400px';
+ view.model = createFullyPopulatedModel(false, false);
+
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+ Polymer.dom(view).appendChild(container);
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+ });
+
+ test('filterProcessesUI', async function() {
+ const view = document.createElement('tr-ui-timeline-view');
+ view.style.height = '400px';
+ view.model = createFullyPopulatedModel(false, false);
+
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+ Polymer.dom(view).appendChild(container);
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+
+ const procFilter = Polymer.dom(view.processFilter_);
+ const checkboxes = procFilter.querySelectorAll('input[type=checkbox]');
+ assert.lengthOf(checkboxes, 52);
+
+ const trackView =
+ view.trackViewContainer_.querySelector('tr-ui-timeline-track-view');
+ const countVisibleTracks = () => trackView.processViews.filter(
+ view => view.visible).length;
+ assert.strictEqual(checkboxes.length, trackView.processViews.length);
+ assert.strictEqual(countVisibleTracks(), 52);
+ assert.isTrue(trackView.processViews[0].visible);
+
+ checkboxes[0].click();
+ assert.strictEqual(countVisibleTracks(), 51);
+ assert.isFalse(trackView.processViews[0].visible);
+ assert.isTrue(trackView.processViews[1].visible);
+
+ // Hide a track. Validate that the checkbox updated state correctly.
+ trackView.processViews[1].visible = false;
+ assert.isFalse(trackView.processViews[1].visible);
+ assert.isFalse(checkboxes[1].checked);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html
new file mode 100644
index 00000000000..58dafb55aff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html
@@ -0,0 +1,442 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/animation.html">
+<link rel="import" href="/tracing/ui/base/animation_controller.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/timeline_interest_range.html">
+<link rel="import" href="/tracing/ui/tracks/container_to_track_map.html">
+<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Code for the viewport.
+ */
+tr.exportTo('tr.ui', function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const TimelineInterestRange = tr.ui.TimelineInterestRange;
+
+ const IDEAL_MAJOR_MARK_DISTANCE_PX = 150;
+ // Keep 5 digits of precision when rounding the major mark distances.
+ const MAJOR_MARK_ROUNDING_FACTOR = 100000;
+
+ class AnimationControllerProxy {
+ constructor(target) {
+ this.target_ = target;
+ }
+
+ get panX() {
+ return this.target_.currentDisplayTransform_.panX;
+ }
+
+ set panX(panX) {
+ this.target_.currentDisplayTransform_.panX = panX;
+ }
+
+ get panY() {
+ return this.target_.currentDisplayTransform_.panY;
+ }
+
+ set panY(panY) {
+ this.target_.currentDisplayTransform_.panY = panY;
+ }
+
+ get scaleX() {
+ return this.target_.currentDisplayTransform_.scaleX;
+ }
+
+ set scaleX(scaleX) {
+ this.target_.currentDisplayTransform_.scaleX = scaleX;
+ }
+
+ cloneAnimationState() {
+ return this.target_.currentDisplayTransform_.clone();
+ }
+
+ xPanWorldPosToViewPos(xWorld, xView) {
+ this.target_.currentDisplayTransform_.xPanWorldPosToViewPos(
+ xWorld, xView, this.target_.modelTrackContainer_.canvas.clientWidth);
+ }
+ }
+
+ /**
+ * The TimelineViewport manages the transform used for navigating
+ * within the timeline. It is a simple transform:
+ * x' = (x+pan) * scale
+ *
+ * The timeline code tries to avoid directly accessing this transform,
+ * instead using this class to do conversion between world and viewspace,
+ * as well as the math for centering the viewport in various interesting
+ * ways.
+ *
+ * @constructor
+ * @extends {tr.b.EventTarget}
+ */
+ function TimelineViewport(parentEl) {
+ this.parentEl_ = parentEl;
+ this.modelTrackContainer_ = undefined;
+ this.currentDisplayTransform_ = new TimelineDisplayTransform();
+ this.initAnimationController_();
+
+ // Flow events
+ this.showFlowEvents_ = false;
+
+ // Highlights.
+ this.highlightVSync_ = false;
+
+ // High details.
+ this.highDetails_ = false;
+
+ // Grid system.
+ this.gridTimebase_ = 0;
+ this.gridStep_ = 1000 / 60;
+ this.gridEnabled_ = false;
+
+ // Init logic.
+ this.hasCalledSetupFunction_ = false;
+
+ this.onResize_ = this.onResize_.bind(this);
+ this.onModelTrackControllerScroll_ =
+ this.onModelTrackControllerScroll_.bind(this);
+
+ this.timeMode_ = TimelineViewport.TimeMode.TIME_IN_MS;
+ // Major mark positions are where the gridlines/ruler marks are placed along
+ // the x-axis.
+ this.majorMarkWorldPositions_ = [];
+ this.majorMarkUnit_ = undefined;
+ this.interestRange_ = new TimelineInterestRange(this);
+
+ this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
+ this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+
+ this.dispatchChangeEvent = this.dispatchChangeEvent.bind(this);
+ }
+
+ TimelineViewport.TimeMode = {
+ TIME_IN_MS: 0,
+ REVISIONS: 1
+ };
+
+ TimelineViewport.prototype = {
+ __proto__: tr.b.EventTarget.prototype,
+
+ /**
+ * @return {boolean} Whether the current timeline is attached to the
+ * document.
+ */
+ get isAttachedToDocumentOrInTestMode() {
+ // Allow not providing a parent element, used by tests.
+ if (this.parentEl_ === undefined) return;
+ return tr.ui.b.isElementAttachedToDocument(this.parentEl_);
+ },
+
+ onResize_() {
+ this.dispatchChangeEvent();
+ },
+
+ /**
+ * Fires the change event on this viewport. Used to notify listeners
+ * to redraw when the underlying model has been mutated.
+ */
+ dispatchChangeEvent() {
+ tr.b.dispatchSimpleEvent(this, 'change');
+ },
+
+ detach() {
+ window.removeEventListener('resize', this.dispatchChangeEvent);
+ },
+
+ initAnimationController_() {
+ this.dtAnimationController_ = new tr.ui.b.AnimationController();
+ this.dtAnimationController_.addEventListener(
+ 'didtick', function(e) {
+ this.onCurentDisplayTransformChange_(e.oldTargetState);
+ }.bind(this));
+
+ this.dtAnimationController_.target = new AnimationControllerProxy(this);
+ },
+
+ get currentDisplayTransform() {
+ return this.currentDisplayTransform_;
+ },
+
+ setDisplayTransformImmediately(displayTransform) {
+ this.dtAnimationController_.cancelActiveAnimation();
+
+ const oldDisplayTransform =
+ this.dtAnimationController_.target.cloneAnimationState();
+ this.currentDisplayTransform_.set(displayTransform);
+ this.onCurentDisplayTransformChange_(oldDisplayTransform);
+ },
+
+ queueDisplayTransformAnimation(animation) {
+ if (!(animation instanceof tr.ui.b.Animation)) {
+ throw new Error('animation must be instanceof tr.ui.b.Animation');
+ }
+ this.dtAnimationController_.queueAnimation(animation);
+ },
+
+ onCurentDisplayTransformChange_(oldDisplayTransform) {
+ // Ensure panY stays clamped in the track container's scroll range.
+ if (this.modelTrackContainer_) {
+ this.currentDisplayTransform.panY = tr.b.math.clamp(
+ this.currentDisplayTransform.panY,
+ 0,
+ this.modelTrackContainer_.scrollHeight -
+ this.modelTrackContainer_.clientHeight);
+ }
+
+ const changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
+ const yChanged = this.currentDisplayTransform.panY !==
+ oldDisplayTransform.panY;
+ if (yChanged) {
+ this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
+ }
+ if (changed) {
+ this.dispatchChangeEvent();
+ }
+ },
+
+ onModelTrackControllerScroll_(e) {
+ if (this.dtAnimationController_.activeAnimation &&
+ this.dtAnimationController_.activeAnimation.affectsPanY) {
+ this.dtAnimationController_.cancelActiveAnimation();
+ }
+ const panY = this.modelTrackContainer_.scrollTop;
+ this.currentDisplayTransform_.panY = panY;
+ },
+
+ get modelTrackContainer() {
+ return this.modelTrackContainer_;
+ },
+
+ set modelTrackContainer(m) {
+ if (this.modelTrackContainer_) {
+ this.modelTrackContainer_.removeEventListener('scroll',
+ this.onModelTrackControllerScroll_);
+ }
+
+ this.modelTrackContainer_ = m;
+ this.modelTrackContainer_.addEventListener('scroll',
+ this.onModelTrackControllerScroll_);
+ },
+
+ get showFlowEvents() {
+ return this.showFlowEvents_;
+ },
+
+ set showFlowEvents(showFlowEvents) {
+ this.showFlowEvents_ = showFlowEvents;
+ this.dispatchChangeEvent();
+ },
+
+ get highlightVSync() {
+ return this.highlightVSync_;
+ },
+
+ set highlightVSync(highlightVSync) {
+ this.highlightVSync_ = highlightVSync;
+ this.dispatchChangeEvent();
+ },
+
+ get highDetails() {
+ return this.highDetails_;
+ },
+
+ set highDetails(highDetails) {
+ this.highDetails_ = highDetails;
+ this.dispatchChangeEvent();
+ },
+
+ get gridEnabled() {
+ return this.gridEnabled_;
+ },
+
+ set gridEnabled(enabled) {
+ if (this.gridEnabled_ === enabled) return;
+
+ this.gridEnabled_ = enabled && true;
+ this.dispatchChangeEvent();
+ },
+
+ get gridTimebase() {
+ return this.gridTimebase_;
+ },
+
+ set gridTimebase(timebase) {
+ if (this.gridTimebase_ === timebase) return;
+
+ this.gridTimebase_ = timebase;
+ this.dispatchChangeEvent();
+ },
+
+ get gridStep() {
+ return this.gridStep_;
+ },
+
+ get interestRange() {
+ return this.interestRange_;
+ },
+
+ get majorMarkWorldPositions() {
+ return this.majorMarkWorldPositions_;
+ },
+
+ get majorMarkUnit() {
+ switch (this.timeMode_) {
+ case TimelineViewport.TimeMode.TIME_IN_MS:
+ return tr.b.Unit.byName.timeInMsAutoFormat;
+ case TimelineViewport.TimeMode.REVISIONS:
+ return tr.b.Unit.byName.count;
+ default:
+ throw new Error(
+ 'Cannot get Unit for unsupported time mode ' + this.timeMode_);
+ }
+ },
+
+ get timeMode() {
+ return this.timeMode_;
+ },
+
+ set timeMode(mode) {
+ this.timeMode_ = mode;
+ this.dispatchChangeEvent();
+ },
+
+ updateMajorMarkData(viewLWorld, viewRWorld) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const dt = this.currentDisplayTransform;
+
+ const idealMajorMarkDistancePix =
+ IDEAL_MAJOR_MARK_DISTANCE_PX * pixelRatio;
+ const idealMajorMarkDistanceWorld =
+ dt.xViewVectorToWorld(idealMajorMarkDistancePix);
+
+ const majorMarkDistanceWorld = tr.b.math.preferredNumberLargerThanMin(
+ idealMajorMarkDistanceWorld);
+
+ const firstMajorMark = Math.floor(
+ viewLWorld / majorMarkDistanceWorld) * majorMarkDistanceWorld;
+
+ this.majorMarkWorldPositions_ = [];
+ for (let curX = firstMajorMark;
+ curX < viewRWorld;
+ curX += majorMarkDistanceWorld) {
+ this.majorMarkWorldPositions_.push(
+ Math.floor(MAJOR_MARK_ROUNDING_FACTOR * curX) /
+ MAJOR_MARK_ROUNDING_FACTOR);
+ }
+ },
+
+ drawMajorMarkLines(ctx, viewHeight) {
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ ctx.beginPath();
+ for (const majorMark of this.majorMarkWorldPositions_) {
+ const x = this.currentDisplayTransform.xWorldToView(majorMark);
+ tr.ui.b.drawLine(ctx, x, 0, x, viewHeight);
+ }
+ ctx.strokeStyle = '#ddd';
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ drawGridLines(ctx, viewLWorld, viewRWorld, viewHeight) {
+ if (!this.gridEnabled) return;
+
+ const dt = this.currentDisplayTransform;
+ let x = this.gridTimebase;
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ ctx.beginPath();
+ while (x < viewRWorld) {
+ if (x >= viewLWorld) {
+ // Do conversion to viewspace here rather than on
+ // x to avoid precision issues.
+ const vx = Math.floor(dt.xWorldToView(x));
+ tr.ui.b.drawLine(ctx, vx, 0, vx, viewHeight);
+ }
+
+ x += this.gridStep;
+ }
+ ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ /**
+ * Helper for selection previous or next.
+ * @param {boolean} offset If positive, select one forward (next).
+ * Else, select previous.
+ *
+ * @return {boolean} true if current selection changed.
+ */
+ getShiftedSelection(selection, offset) {
+ const newSelection = new tr.model.EventSet();
+ for (const event of selection) {
+ // If this is a flow event, then move to its slice based on the
+ // offset direction.
+ if (event instanceof tr.model.FlowEvent) {
+ if (offset > 0) {
+ newSelection.push(event.endSlice);
+ } else if (offset < 0) {
+ newSelection.push(event.startSlice);
+ } else {
+ /* Do nothing. Zero offsets don't do anything. */
+ }
+ continue;
+ }
+
+ const track = this.trackForEvent(event);
+ track.addEventNearToProvidedEventToSelection(
+ event, offset, newSelection);
+ }
+
+ if (newSelection.length === 0) return undefined;
+
+ return newSelection;
+ },
+
+ rebuildEventToTrackMap() {
+ // TODO(charliea): Make the event to track map have a similar interface
+ // to the container to track map so that we can just clear() here.
+ this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
+ this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_);
+ },
+
+ rebuildContainerToTrackMap() {
+ this.containerToTrackMap.clear();
+ this.modelTrackContainer_.addContainersToTrackMap(
+ this.containerToTrackMap);
+ },
+
+ trackForEvent(event) {
+ return this.eventToTrackMap_[event.guid];
+ }
+ };
+
+ return {
+ TimelineViewport,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html
new file mode 100644
index 00000000000..5715182e720
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Location = tr.model.Location;
+ const Model = tr.Model;
+
+ test('memoization', function() {
+ const vp = new tr.ui.TimelineViewport(document.createElement('div'));
+
+ const slice = { guid: 1 };
+
+ vp.modelTrackContainer = {
+ addEventsToTrackMap(eventToTrackMap) {
+ eventToTrackMap.addEvent(slice, 'track');
+ },
+ addEventListener() {}
+ };
+
+ assert.isUndefined(vp.trackForEvent(slice));
+ vp.rebuildEventToTrackMap();
+
+ assert.strictEqual(vp.trackForEvent(slice), 'track');
+ });
+
+ test('shiftedSelection', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 5, {}, 1));
+
+ const viewport = new tr.ui.TimelineViewport();
+ const track = new tr.ui.tracks.SliceTrack(viewport);
+ viewport.modelTrackContainer = track;
+ track.slices = t1.sliceGroup.slices;
+
+ viewport.rebuildEventToTrackMap();
+
+ const sel = new tr.model.EventSet();
+ sel.push(t1.sliceGroup.slices[0]);
+
+ const shifted = track.viewport.getShiftedSelection(sel, 1);
+ assert.isTrue(shifted.equals(
+ new tr.model.EventSet(t1.sliceGroup.slices[1])));
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html
new file mode 100644
index 00000000000..571b0543bb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of alert objects.
+ * @constructor
+ * @extends {LetterDotTrack}
+ */
+ const AlertTrack = tr.ui.b.define(
+ 'alert-track', tr.ui.tracks.LetterDotTrack);
+
+ AlertTrack.prototype = {
+ __proto__: tr.ui.tracks.LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Alerts';
+ this.alerts_ = undefined;
+ },
+
+ get alerts() {
+ return this.alerts_;
+ },
+
+ set alerts(alerts) {
+ this.alerts_ = alerts;
+ if (alerts === undefined) {
+ this.items = undefined;
+ return;
+ }
+ this.items = this.alerts_.map(function(alert) {
+ return new tr.ui.tracks.LetterDot(
+ alert, String.fromCharCode(9888), alert.colorId, alert.start);
+ });
+ }
+ };
+
+ return {
+ AlertTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html
new file mode 100644
index 00000000000..4e60180b00e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AlertTrack = tr.ui.tracks.AlertTrack;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'One alert');
+ const ALERT_INFO_2 = new tr.model.EventInfo(
+ 'Alert 2', 'Another alert');
+
+ const createAlerts = function() {
+ const alerts = [
+ new tr.model.Alert(ALERT_INFO_1, 5),
+ new tr.model.Alert(ALERT_INFO_1, 20),
+ new tr.model.Alert(ALERT_INFO_2, 35),
+ new tr.model.Alert(ALERT_INFO_2, 50)
+ ];
+ return alerts;
+ };
+
+ test('instantiate', function() {
+ const alerts = createAlerts();
+ alerts[1].selectionState = SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = AlertTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.alerts = alerts;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+
+ assert.strictEqual(5, track.items[0].start);
+ });
+
+ test('modelMapping', function() {
+ const alerts = createAlerts();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = AlertTrack(viewport);
+ track.alerts = alerts;
+
+ const a0 = track.items[0].modelItem;
+ assert.strictEqual(a0, alerts[0]);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html
new file mode 100644
index 00000000000..d922030ce70
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a AsyncSliceGroup.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const AsyncSliceGroupTrack = tr.ui.b.define(
+ 'async-slice-group-track',
+ tr.ui.tracks.MultiRowTrack);
+
+ AsyncSliceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('async-slice-group-track');
+ this.group_ = undefined;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ track.asyncStyle = true;
+ return track;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(group) {
+ this.group_ = group;
+ this.buildAndSetSubRows_();
+ },
+
+ get eventContainer() {
+ return this.group;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.group, this);
+ },
+
+ buildAndSetSubRows_() {
+ if (this.group_.viewSubGroups.length <= 1) {
+ // No nested groups or just only one, the most common case.
+ const rows = groupAsyncSlicesIntoSubRows(this.group_.slices);
+ const rowsWithHeadings = rows.map(row => {
+ return {row, heading: undefined};
+ });
+ this.setPrebuiltSubRows(this.group_, rowsWithHeadings);
+ return;
+ }
+
+ // We have nested grouping level (no further levels supported),
+ // so process sub-groups separately and preserve their titles.
+ const rowsWithHeadings = [];
+ for (const subGroup of this.group_.viewSubGroups) {
+ const subGroupRows = groupAsyncSlicesIntoSubRows(subGroup.slices);
+ if (subGroupRows.length === 0) {
+ continue;
+ }
+ for (let i = 0; i < subGroupRows.length; i++) {
+ rowsWithHeadings.push({
+ row: subGroupRows[i],
+ heading: (i === 0 ? subGroup.title : '')
+ });
+ }
+ }
+ this.setPrebuiltSubRows(this.group_, rowsWithHeadings);
+ }
+ };
+
+ /**
+ * Strip away wrapper slice which are used to group slices into
+ * a single track but provide no information themselves.
+ */
+ function stripSlice_(slice) {
+ if (slice.subSlices !== undefined && slice.subSlices.length === 1) {
+ const subSlice = slice.subSlices[0];
+ if (tr.b.math.approximately(subSlice.start, slice.start, 1) &&
+ tr.b.math.approximately(subSlice.duration, slice.duration, 1)) {
+ return subSlice;
+ }
+ }
+ return slice;
+ }
+
+ /**
+ * Unwrap the list of non-overlapping slices into a number of rows where
+ * the top row holds original slices and additional rows hold nested slices
+ * of ones from the row above them.
+ */
+ function makeLevelSubRows_(slices) {
+ const rows = [];
+ const putSlice = (slice, level) => {
+ while (rows.length <= level) {
+ rows.push([]);
+ }
+ rows[level].push(slice);
+ };
+ const putSliceRecursively = (slice, level) => {
+ putSlice(slice, level);
+ if (slice.subSlices !== undefined) {
+ for (const subSlice of slice.subSlices) {
+ putSliceRecursively(subSlice, level + 1);
+ }
+ }
+ };
+
+ for (const slice of slices) {
+ putSliceRecursively(stripSlice_(slice), 0);
+ }
+ return rows;
+ }
+
+ /**
+ * Breaks up the list of slices into a number of rows:
+ * - Which contain non-overlapping slices.
+ * - If slice has nested slices, they're placed onto the row below.
+ * Sorting may be skipped if slices are already sorted by start timestamp.
+ */
+ function groupAsyncSlicesIntoSubRows(slices, opt_skipSort) {
+ if (!opt_skipSort) {
+ slices.sort((x, y) => x.start - y.start);
+ }
+
+ // The algorithm is fairly simple:
+ // - Level is a group of rows, where the top row holds original slices and
+ // additional rows hold nested slices of ones from the row above them.
+ // - Make a level by putting sorted slices, skipping if one's overlapping.
+ // - Repeat and make more levels while we're having residual slices left.
+ const rows = [];
+ let slicesLeft = slices;
+ while (slicesLeft.length !== 0) {
+ // Make a level.
+ const fit = [];
+ const unfit = [];
+ let levelEndTime = -1;
+
+ for (const slice of slicesLeft) {
+ if (slice.start >= levelEndTime) {
+ // Assuming nested slices lie within parent's boundaries.
+ levelEndTime = slice.end;
+ fit.push(slice);
+ } else {
+ unfit.push(slice);
+ }
+ }
+ rows.push(...makeLevelSubRows_(fit));
+ slicesLeft = unfit;
+ }
+ return rows;
+ }
+
+ return {
+ AsyncSliceGroupTrack,
+ groupAsyncSlicesIntoSubRows,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html
new file mode 100644
index 00000000000..96003e1b5f2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html
@@ -0,0 +1,328 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSliceGroup = tr.model.AsyncSliceGroup;
+ const AsyncSliceGroupTrack = tr.ui.tracks.AsyncSliceGroupTrack;
+ const Process = tr.model.Process;
+ const ProcessTrack = tr.ui.tracks.ProcessTrack;
+ const Thread = tr.model.Thread;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const groupAsyncSlicesIntoSubRows = tr.ui.tracks.groupAsyncSlicesIntoSubRows;
+
+ test('filterSubRows', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ assert.strictEqual(track.children.length, 1);
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_empty', function() {
+ const rows = groupAsyncSlicesIntoSubRows([]);
+ assert.strictEqual(rows.length, 0);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_trivial', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+
+ const s1 = newAsyncSlice(10, 200, t1, t1);
+ const s2 = newAsyncSlice(300, 30, t1, t1);
+
+ const slices = [s2, s1];
+ const rows = groupAsyncSlicesIntoSubRows(slices);
+
+ assert.strictEqual(rows.length, 1);
+ assert.sameMembers(rows[0], [s1, s2]);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_nonTrivial', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+
+ const s1 = newAsyncSlice(10, 200, t1, t1); // Should be stripped.
+ const s1s1 = newAsyncSlice(10, 200, t1, t1);
+ s1.subSlices = [s1s1];
+
+ const s2 = newAsyncSlice(300, 30, t1, t1);
+ const s2s1 = newAsyncSlice(300, 10, t1, t1);
+ const s2s2 = newAsyncSlice(310, 20, t1, t1); // Should not be stripped.
+ const s2s2s1 = newAsyncSlice(310, 20, t1, t1);
+ s2s2.subSlices = [s2s2s1];
+ s2.subSlices = [s2s2, s2s1];
+
+ const s3 = newAsyncSlice(200, 50, t1, t1); // Overlaps with s1.
+ const s3s1 = newAsyncSlice(220, 5, t1, t1);
+ s3.subSlices = [s3s1];
+
+ const slices = [s2, s3, s1];
+ const rows = groupAsyncSlicesIntoSubRows(slices);
+
+ assert.strictEqual(rows.length, 5);
+ assert.sameMembers(rows[0], [s1s1, s2]);
+ assert.sameMembers(rows[1], [s2s1, s2s2]);
+ assert.sameMembers(rows[2], [s2s2s1]);
+ assert.sameMembers(rows[3], [s3]);
+ assert.sameMembers(rows[4], [s3s1]);
+ });
+
+ test('rebuildSubRows_twoNonOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ const s1 = newAsyncSlice(0, 1, t1, t1);
+ const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s1.subSlices = [subs1];
+ g.push(s1);
+ g.push(newAsyncSlice(1, 1, t1, t1));
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ const subRows = track.subRows;
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.sameMembers(g.slices[1].subSlices, []);
+ });
+
+ test('rebuildSubRows_twoOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const s1 = newAsyncSlice(0, 1, t1, t1);
+ const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s1.subSlices = [subs1];
+ const s2 = newAsyncSlice(0, 1.5, t1, t1);
+ const subs2 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s2.subSlices = [subs2];
+ g.push(s1);
+ g.push(s2);
+
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.strictEqual(subRows[1][0], g.slices[1].subSlices[0]);
+ });
+
+ test('rebuildSubRows_threePartlyOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(0, 1.5, t1, t1));
+ g.push(newAsyncSlice(1, 1.5, t1, t1));
+ g.updateBounds();
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ const subRows = track.subRows;
+
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.strictEqual(subRows[0][0], g.slices[0]);
+ assert.strictEqual(subRows[0][1], g.slices[2]);
+ assert.strictEqual(subRows[1][0], g.slices[1]);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.sameMembers(g.slices[0].subSlices, []);
+ assert.sameMembers(g.slices[1].subSlices, []);
+ assert.sameMembers(g.slices[2].subSlices, []);
+ });
+
+ test('rebuildSubRows_threeOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(0, 1.5, t1, t1));
+ g.push(newAsyncSlice(2, 1, t1, t1));
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.strictEqual(subRows[0][0], g.slices[0]);
+ assert.strictEqual(subRows[1][0], g.slices[1]);
+ assert.strictEqual(subRows[0][1], g.slices[2]);
+ });
+
+ test('rebuildSubRows_twoViewSubGroups', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSliceNamed('foo', 0, 1, t1, t1));
+ g.push(newAsyncSliceNamed('foo', 2, 1, t1, t1));
+ g.push(newAsyncSliceNamed('bar', 1, 2, t1, t1));
+ g.push(newAsyncSliceNamed('bar', 3, 2, t1, t1));
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ track.heading = 'sup';
+
+ assert.strictEqual(track.subRows.length, 2);
+ const subTracks = Polymer.dom(track).children;
+ assert.strictEqual(subTracks.length, 3);
+ assert.strictEqual(subTracks[0].slices.length, 0);
+ assert.strictEqual(subTracks[1].slices.length, 2);
+ assert.strictEqual(subTracks[2].slices.length, 2);
+ const headings =
+ [subTracks[0].heading, subTracks[1].heading, subTracks[2].heading];
+ assert.sameMembers(headings, ['foo', 'bar', 'sup']);
+ });
+
+ // Tests that no slices and their sub slices overlap.
+ test('rebuildSubRows_NonOverlappingSubSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const slice1 = newAsyncSlice(0, 5, t1, t1);
+ const slice1Child = newAsyncSlice(1, 2, t1, t1);
+ slice1.subSlices = [slice1Child];
+ const slice2 = newAsyncSlice(3, 5, t1, t1);
+ const slice3 = newAsyncSlice(5, 4, t1, t1);
+ const slice3Child = newAsyncSlice(6, 2, t1, t1);
+ slice3.subSlices = [slice3Child];
+ g.push(slice1);
+ g.push(slice2);
+ g.push(slice3);
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ // Checks each sub row to see that we don't have any overlapping slices.
+ for (let i = 0; i < subRows.length; i++) {
+ const row = subRows[i];
+ for (let j = 0; j < row.length; j++) {
+ for (let k = j + 1; k < row.length; k++) {
+ assert.isTrue(row[j].end <= row[k].start);
+ }
+ }
+ }
+ });
+
+ test('rebuildSubRows_NonOverlappingSubSlicesThreeNestedLevels', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const slice1 = newAsyncSlice(0, 4, t1, t1);
+ const slice1Child = newAsyncSlice(1, 2, t1, t1);
+ slice1.subSlices = [slice1Child];
+ const slice2 = newAsyncSlice(2, 7, t1, t1);
+ const slice3 = newAsyncSlice(5, 5, t1, t1);
+ const slice3Child = newAsyncSlice(6, 3, t1, t1);
+ const slice3Child2 = newAsyncSlice(7, 1, t1, t1);
+ slice3.subSlices = [slice3Child];
+ slice3Child.subSlices = [slice3Child2];
+ g.push(slice1);
+ g.push(slice2);
+ g.push(slice3);
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ // Checks each sub row to see that we don't have any overlapping slices.
+ for (let i = 0; i < subRows.length; i++) {
+ const row = subRows[i];
+ for (let j = 0; j < row.length; j++) {
+ for (let k = j + 1; k < row.length; k++) {
+ assert.isTrue(row[j].end <= row[k].start);
+ }
+ }
+ }
+ });
+
+ test('asyncSliceGroupContainerMap', function() {
+ const vp = new tr.ui.TimelineViewport();
+ const containerToTrack = vp.containerToTrackMap;
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new AsyncSliceGroup(thread);
+
+ const processTrack = new ProcessTrack(vp);
+ const threadTrack = new ThreadTrack(vp);
+ const groupTrack = new AsyncSliceGroupTrack(vp);
+ processTrack.process = process;
+ threadTrack.thread = thread;
+ groupTrack.group = group;
+ Polymer.dom(processTrack).appendChild(threadTrack);
+ Polymer.dom(threadTrack).appendChild(groupTrack);
+
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+
+ assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+ assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+ assert.isUndefined(
+ containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'));
+
+ vp.modelTrackContainer = {
+ addContainersToTrackMap(containerToTrackMap) {
+ processTrack.addContainersToTrackMap(containerToTrackMap);
+ },
+ addEventListener() {}
+ };
+ vp.rebuildContainerToTrackMap();
+
+ // Check that all tracks call childs' addContainersToTrackMap()
+ // by checking the resulting map.
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123'), processTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456'), threadTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'),
+ groupTrack);
+
+ // Check the track's eventContainer getter.
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html
new file mode 100644
index 00000000000..1b73f367636
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A point in a chart series with x (timestamp) and y (value) coordinates
+ * and an associated model item. The point can optionally also have a base
+ * y coordinate (which for example corresponds to the bottom edge of the
+ * associated bar in a bar chart).
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function ChartPoint(modelItem, x, y, opt_yBase) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.x = x;
+ this.y = y;
+ this.dotLetter = undefined;
+
+ // If the base y-coordinate is undefined, the bottom edge of the associated
+ // bar in a bar chart will start at the outer bottom edge (which is most
+ // likely slightly below zero).
+ this.yBase = opt_yBase;
+ }
+
+ ChartPoint.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype,
+ };
+
+ return {
+ ChartPoint,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html
new file mode 100644
index 00000000000..e2d8bc3e11c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+
+ test('checkFields_withoutYBase', function() {
+ const event = {};
+ const point = new ChartPoint(event, 42, -7);
+
+ assert.strictEqual(point.modelItem, event);
+ assert.strictEqual(point.x, 42);
+ assert.strictEqual(point.y, -7);
+ assert.isUndefined(point.yBase);
+ });
+
+ test('checkFields_withYBase', function() {
+ const event = {};
+ const point = new ChartPoint(event, 111, 222, 333);
+
+ assert.strictEqual(point.modelItem, event);
+ assert.strictEqual(point.x, 111);
+ assert.strictEqual(point.y, 222);
+ assert.strictEqual(point.yBase, 333);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html
new file mode 100644
index 00000000000..45025d13e0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html
@@ -0,0 +1,566 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * The type of a chart series.
+ * @enum
+ */
+ const ChartSeriesType = {
+ LINE: 0,
+ AREA: 1
+ };
+
+ // The default rendering configuration for ChartSeries.
+ const DEFAULT_RENDERING_CONFIG = {
+ // The type of the chart series.
+ chartType: ChartSeriesType.LINE,
+
+ // The size of a selected point dot in device-independent pixels (circle
+ // diameter).
+ selectedPointSize: 4,
+
+ // The size of an unselected point dot in device-independent pixels (square
+ // width/height).
+ unselectedPointSize: 3,
+
+ // Whether the selected dots should be solid circles of the line color, or
+ // filled with the background's selection color.
+ solidSelectedDots: false,
+
+ // The color of the chart.
+ colorId: 0,
+
+ // The width of the top line in device-independent pixels.
+ lineWidth: 1,
+
+ // Minimum distance between points in physical pixels. Points which are
+ // closer than this distance will be skipped.
+ skipDistance: 1,
+
+ // Density in points per physical pixel at which unselected point dots
+ // become transparent.
+ unselectedPointDensityTransparent: 0.10,
+
+ // Density in points per physical pixel at which unselected point dots
+ // become fully opaque.
+ unselectedPointDensityOpaque: 0.05,
+
+ // Opacity of area chart background.
+ backgroundOpacity: 0.5,
+
+ // Whether to graph steps between points. Set to false for lines instead.
+ stepGraph: true
+ };
+
+ // The virtual width of the last point in a series (whose rectangle has zero
+ // width) in world timestamps difference for the purposes of selection.
+ const LAST_POINT_WIDTH = 16;
+
+ // Constants for sizing and font of points with dot letters.
+ const DOT_LETTER_RADIUS_PX = 7;
+ const DOT_LETTER_RADIUS_PADDING_PX = 0.5;
+ const DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX = 3;
+ const DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX = 1.5;
+ const DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX = 1;
+ const DOT_LETTER_FONT_WEIGHT = 400;
+ const DOT_LETTER_FONT_SIZE_PX = 9;
+ const DOT_LETTER_FONT = 'Arial';
+
+ /**
+ * Visual components of a ChartSeries.
+ * @enum
+ */
+ const ChartSeriesComponent = {
+ BACKGROUND: 0,
+ LINE: 1,
+ DOTS: 2
+ };
+
+ /**
+ * A series of points corresponding to a single chart on a chart track.
+ * This class is responsible for drawing the actual chart onto canvas.
+ *
+ * @constructor
+ */
+ function ChartSeries(points, seriesYAxis, opt_renderingConfig) {
+ this.points = points;
+ this.seriesYAxis = seriesYAxis;
+
+ this.useRenderingConfig_(opt_renderingConfig);
+ }
+
+ ChartSeries.prototype = {
+ useRenderingConfig_(opt_renderingConfig) {
+ const config = opt_renderingConfig || {};
+
+ // Store all configuration flags as private properties.
+ for (const [key, defaultValue] of
+ Object.entries(DEFAULT_RENDERING_CONFIG)) {
+ let value = config[key];
+ if (value === undefined) {
+ value = defaultValue;
+ }
+ this[key + '_'] = value;
+ }
+
+ // Avoid unnecessary recomputation in getters.
+ this.topPadding = this.bottomPadding = Math.max(
+ this.selectedPointSize_, this.unselectedPointSize_) / 2;
+ },
+
+ get range() {
+ const range = new tr.b.math.Range();
+ this.points.forEach(function(point) {
+ range.addValue(point.y);
+ }, this);
+ return range;
+ },
+
+ draw(ctx, transform, highDetails) {
+ if (this.points === undefined || this.points.length === 0) {
+ return;
+ }
+
+ // Draw the background.
+ if (this.chartType_ === ChartSeriesType.AREA) {
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.BACKGROUND,
+ highDetails);
+ }
+
+ // Draw the line at the top.
+ if (this.chartType_ === ChartSeriesType.LINE || highDetails) {
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.LINE,
+ highDetails);
+ }
+
+ // Draw the points.
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.DOTS,
+ highDetails);
+ },
+
+ drawComponent_(ctx, transform, component, highDetails) {
+ // We need to consider extra pixels outside the visible area to avoid
+ // visual glitches due to non-zero width of dots.
+ let extraPixels = 0;
+ if (component === ChartSeriesComponent.DOTS) {
+ extraPixels = Math.max(
+ this.selectedPointSize_, this.unselectedPointSize_);
+ }
+ const pixelRatio = transform.pixelRatio;
+ const leftViewX = transform.leftViewX - extraPixels * pixelRatio;
+ const rightViewX = transform.rightViewX + extraPixels * pixelRatio;
+ const leftTimestamp = transform.leftTimestamp - extraPixels;
+ const rightTimestamp = transform.rightTimestamp + extraPixels;
+
+ // Find the index of the first and last (partially) visible points.
+ const firstVisibleIndex = tr.b.findLowIndexInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ leftTimestamp);
+ let lastVisibleIndex = tr.b.findLowIndexInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ rightTimestamp);
+ if (lastVisibleIndex >= this.points.length ||
+ this.points[lastVisibleIndex].x > rightTimestamp) {
+ lastVisibleIndex--;
+ }
+
+ // Pre-calculate component style which does not depend on individual
+ // points:
+ // * Skip distance between points,
+ // * Selected (circle) and unselected (square) dot size,
+ // * Unselected dot opacity,
+ // * Selected dot edge color and width, and
+ // * Line component color and width.
+ const viewSkipDistance = this.skipDistance_ * pixelRatio;
+ let selectedCircleRadius;
+ let letterDotRadius;
+ let squareSize;
+ let squareHalfSize;
+ let squareOpacity;
+ let unselectedSeriesColor;
+ let currentStateSeriesColor;
+
+ ctx.save();
+ ctx.font =
+ DOT_LETTER_FONT_WEIGHT + ' ' +
+ Math.floor(DOT_LETTER_FONT_SIZE_PX * pixelRatio) + 'px ' +
+ DOT_LETTER_FONT;
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+
+ switch (component) {
+ case ChartSeriesComponent.DOTS: {
+ // Selected (circle) and unselected (square) dot size.
+ selectedCircleRadius =
+ (this.selectedPointSize_ / 2) * pixelRatio;
+ letterDotRadius =
+ Math.max(selectedCircleRadius, DOT_LETTER_RADIUS_PX * pixelRatio);
+ squareSize = this.unselectedPointSize_ * pixelRatio;
+ squareHalfSize = squareSize / 2;
+ unselectedSeriesColor = EventPresenter.getCounterSeriesColor(
+ this.colorId_, SelectionState.NONE);
+
+ // Unselected dot opacity.
+ if (!highDetails) {
+ // Unselected dots are not displayed in 'low details' mode.
+ squareOpacity = 0;
+ break;
+ }
+ const visibleIndexRange = lastVisibleIndex - firstVisibleIndex;
+ if (visibleIndexRange <= 0) {
+ // There is at most one visible point.
+ squareOpacity = 1;
+ break;
+ }
+ const visibleViewXRange =
+ transform.worldXToViewX(this.points[lastVisibleIndex].x) -
+ transform.worldXToViewX(this.points[firstVisibleIndex].x);
+ if (visibleViewXRange === 0) {
+ // Multiple visible points which all have the same timestamp.
+ squareOpacity = 1;
+ break;
+ }
+ const density = visibleIndexRange / visibleViewXRange;
+ const clampedDensity = tr.b.math.clamp(density,
+ this.unselectedPointDensityOpaque_,
+ this.unselectedPointDensityTransparent_);
+ const densityRange = this.unselectedPointDensityTransparent_ -
+ this.unselectedPointDensityOpaque_;
+ squareOpacity =
+ (this.unselectedPointDensityTransparent_ - clampedDensity) /
+ densityRange;
+ break;
+ }
+
+ case ChartSeriesComponent.LINE:
+ // Line component color and width.
+ ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
+ this.colorId_, SelectionState.NONE);
+ ctx.lineWidth = this.lineWidth_ * pixelRatio;
+ break;
+
+ case ChartSeriesComponent.BACKGROUND:
+ // Style depends on the selection state of individual points.
+ break;
+
+ default:
+ throw new Error('Invalid component: ' + component);
+ }
+
+ // The main loop which draws the given component of visible points from
+ // left to right. Given the potentially large number of points to draw,
+ // it should be considered performance-critical and function calls should
+ // be avoided when possible.
+ //
+ // Note that the background and line components are drawn in a delayed
+ // fashion: the rectangle/line that we draw in an iteration corresponds
+ // to the *previous* point. This does not apply to the dots, whose
+ // position is independent of the surrounding dots.
+ let previousViewX = undefined;
+ let previousViewY = undefined;
+ let previousViewYBase = undefined;
+ let lastSelectionState = undefined;
+ let baseSteps = undefined;
+ const startIndex = Math.max(firstVisibleIndex - 1, 0);
+ let currentViewX;
+
+ for (let i = startIndex; i < this.points.length; i++) {
+ const currentPoint = this.points[i];
+ currentViewX = transform.worldXToViewX(currentPoint.x);
+
+ // Stop drawing the points once we are to the right of the visible area.
+ if (currentViewX > rightViewX) {
+ if (previousViewX !== undefined) {
+ previousViewX = currentViewX = rightViewX;
+ if (component === ChartSeriesComponent.BACKGROUND ||
+ component === ChartSeriesComponent.LINE) {
+ ctx.lineTo(currentViewX, previousViewY);
+ }
+ }
+ break;
+ }
+
+ if (i + 1 < this.points.length) {
+ const nextPoint = this.points[i + 1];
+ const nextViewX = transform.worldXToViewX(nextPoint.x);
+
+ // Skip points that are too close to each other.
+ if (previousViewX !== undefined &&
+ nextViewX - previousViewX <= viewSkipDistance &&
+ nextViewX < rightViewX) {
+ continue;
+ }
+
+ // Start drawing right at the left side of the visible are (instead
+ // of potentially very far to the left).
+ if (currentViewX < leftViewX) {
+ currentViewX = leftViewX;
+ }
+ }
+
+ if (previousViewX !== undefined &&
+ currentViewX - previousViewX < viewSkipDistance) {
+ // We know that nextViewX > previousViewX + viewSkipDistance, so we
+ // can safely move this points's x over that much without passing
+ // nextViewX. This ensures that the previous point is visible when
+ // zoomed out very far.
+ currentViewX = previousViewX + viewSkipDistance;
+ }
+
+ const currentViewY = Math.round(transform.worldYToViewY(
+ currentPoint.y));
+ let currentViewYBase;
+ if (currentPoint.yBase === undefined) {
+ currentViewYBase = transform.outerBottomViewY;
+ } else {
+ currentViewYBase = Math.round(
+ transform.worldYToViewY(currentPoint.yBase));
+ }
+ const currentSelectionState = currentPoint.selectionState;
+ if (currentSelectionState !== lastSelectionState) {
+ const opacity = currentSelectionState === SelectionState.SELECTED ?
+ 1 : squareOpacity;
+ currentStateSeriesColor = EventPresenter.getCounterSeriesColor(
+ this.colorId_, currentSelectionState, opacity);
+ }
+
+ // Actually draw the given component of the point.
+ switch (component) {
+ case ChartSeriesComponent.DOTS:
+ // Draw the dot for the current point.
+ if (currentPoint.dotLetter) {
+ ctx.fillStyle = unselectedSeriesColor;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('black');
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY,
+ letterDotRadius + DOT_LETTER_RADIUS_PADDING_PX, 0,
+ 2 * Math.PI);
+ ctx.fill();
+ if (currentSelectionState === SelectionState.SELECTED) {
+ ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('olive');
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY, letterDotRadius, 0,
+ 2 * Math.PI);
+ ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('yellow');
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('black');
+ ctx.stroke();
+ }
+ ctx.fillStyle =
+ ColorScheme.getColorForReservedNameAsString('white');
+ ctx.fillText(currentPoint.dotLetter, currentViewX, currentViewY);
+ } else {
+ ctx.strokeStyle = unselectedSeriesColor;
+ ctx.lineWidth = pixelRatio;
+ if (currentSelectionState === SelectionState.SELECTED) {
+ if (this.solidSelectedDots_) {
+ ctx.fillStyle = ctx.strokeStyle;
+ } else {
+ ctx.fillStyle = currentStateSeriesColor;
+ }
+
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY, selectedCircleRadius, 0,
+ 2 * Math.PI);
+ ctx.fill();
+ ctx.stroke();
+ } else if (squareOpacity > 0) {
+ ctx.fillStyle = currentStateSeriesColor;
+ ctx.fillRect(currentViewX - squareHalfSize,
+ currentViewY - squareHalfSize, squareSize, squareSize);
+ }
+ }
+ break;
+
+ case ChartSeriesComponent.LINE:
+ // Draw the top line for the previous point (if applicable), or
+ // prepare for drawing the top line of the current point in the next
+ // iteration.
+ if (previousViewX === undefined) {
+ ctx.beginPath();
+ ctx.moveTo(currentViewX, currentViewY);
+ } else if (this.stepGraph_) {
+ ctx.lineTo(currentViewX, previousViewY);
+ }
+
+ // Move to the current point coordinate.
+ ctx.lineTo(currentViewX, currentViewY);
+ break;
+
+ case ChartSeriesComponent.BACKGROUND:
+ // Draw the background for the previous point (if applicable).
+ if (previousViewX !== undefined && this.stepGraph_) {
+ ctx.lineTo(currentViewX, previousViewY);
+ } else {
+ ctx.lineTo(currentViewX, currentViewY);
+ }
+
+ // Finish the bottom part of the backgound polygon, change
+ // background color and start a new polygon when the selection state
+ // changes (and at the beginning).
+ if (currentSelectionState !== lastSelectionState) {
+ if (previousViewX !== undefined) {
+ let previousBaseStepViewX = currentViewX;
+ for (let j = baseSteps.length - 1; j >= 0; j--) {
+ const baseStep = baseSteps[j];
+ const baseStepViewX = baseStep.viewX;
+ const baseStepViewY = baseStep.viewY;
+ ctx.lineTo(previousBaseStepViewX, baseStepViewY);
+ ctx.lineTo(baseStepViewX, baseStepViewY);
+ previousBaseStepViewX = baseStepViewX;
+ }
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.beginPath();
+ ctx.fillStyle = EventPresenter.getCounterSeriesColor(
+ this.colorId_, currentSelectionState,
+ this.backgroundOpacity_);
+ ctx.moveTo(currentViewX, currentViewYBase);
+ baseSteps = [];
+ }
+
+ if (currentViewYBase !== previousViewYBase ||
+ currentSelectionState !== lastSelectionState) {
+ baseSteps.push({viewX: currentViewX, viewY: currentViewYBase});
+ }
+
+ // Move to the current point coordinate.
+ ctx.lineTo(currentViewX, currentViewY);
+ break;
+
+ default:
+ throw new Error('Not reachable');
+ }
+
+ previousViewX = currentViewX;
+ previousViewY = currentViewY;
+ previousViewYBase = currentViewYBase;
+ lastSelectionState = currentSelectionState;
+ }
+
+ // If we still have an open background or top line polygon (which is
+ // always the case once we have started drawing due to the delayed fashion
+ // of drawing), we must close it.
+ if (previousViewX !== undefined) {
+ switch (component) {
+ case ChartSeriesComponent.DOTS:
+ // All dots were drawn in the main loop.
+ break;
+
+ case ChartSeriesComponent.LINE:
+ ctx.stroke();
+ break;
+
+ case ChartSeriesComponent.BACKGROUND: {
+ let previousBaseStepViewX = currentViewX;
+ for (let j = baseSteps.length - 1; j >= 0; j--) {
+ const baseStep = baseSteps[j];
+ const baseStepViewX = baseStep.viewX;
+ const baseStepViewY = baseStep.viewY;
+ ctx.lineTo(previousBaseStepViewX, baseStepViewY);
+ ctx.lineTo(baseStepViewX, baseStepViewY);
+ previousBaseStepViewX = baseStepViewX;
+ }
+ ctx.closePath();
+ ctx.fill();
+ break;
+ }
+
+ default:
+ throw new Error('Not reachable');
+ }
+ }
+ ctx.restore();
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ const points = this.points;
+
+ function getPointWidth(point, i) {
+ if (i === points.length - 1) {
+ return LAST_POINT_WIDTH * viewPixWidthWorld;
+ }
+ const nextPoint = points[i + 1];
+ return nextPoint.x - point.x;
+ }
+
+ function selectPoint(point) {
+ point.addToSelection(selection);
+ }
+
+ tr.b.iterateOverIntersectingIntervals(
+ this.points,
+ function(point) { return point.x; },
+ getPointWidth,
+ loWX,
+ hiWX,
+ selectPoint);
+ },
+
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (this.points === undefined) return false;
+
+ const index = this.points.findIndex(point => point.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex < 0 || newIndex >= this.points.length) return false;
+
+ this.points[newIndex].addToSelection(selection);
+ return true;
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ if (this.points === undefined) return;
+
+ const item = tr.b.findClosestElementInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ worldX,
+ worldMaxDist);
+
+ if (!item) return;
+
+ item.addToSelection(selection);
+ }
+ };
+
+ return {
+ ChartSeries,
+ ChartSeriesType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html
new file mode 100644
index 00000000000..b07e4276e26
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html
@@ -0,0 +1,331 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const Event = tr.model.Event;
+ const SelectionState = tr.model.SelectionState;
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const ChartTransform = tr.ui.tracks.ChartTransform;
+ const ChartSeriesType = tr.ui.tracks.ChartSeriesType;
+
+ const CANVAS_WIDTH = 800;
+ const CANVAS_HEIGHT = 80;
+
+ function getSelectionStateForTesting(index) {
+ index = index % 7;
+ if (index < 5) {
+ return SelectionState.getFromBrighteningLevel(index % 4);
+ }
+ return SelectionState.getFromDimmingLevel(index % 3);
+ }
+
+ function buildSeries(renderingConfig) {
+ const points = [];
+ for (let i = 0; i < 60; i++) {
+ const event = new Event();
+ event.index = i;
+ const phase = i * Math.PI / 15;
+ const value = Math.sin(phase);
+ const peakIndex = Math.floor((phase + Math.PI / 2) / (2 * Math.PI));
+ const base = peakIndex % 2 === 0 ? undefined : -1 + value / 1.5;
+ const point = new ChartPoint(event, i - 30, value, base);
+ points.push(point);
+ }
+ const seriesYAxis = new ChartSeriesYAxis(-1, 1);
+ return new ChartSeries(points, seriesYAxis, renderingConfig);
+ }
+
+ function drawSeriesWithDetails(test, series, highDetails) {
+ const div = document.createElement('div');
+ const canvas = document.createElement('canvas');
+ Polymer.dom(div).appendChild(canvas);
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ canvas.width = CANVAS_WIDTH * pixelRatio;
+ canvas.style.width = CANVAS_WIDTH + 'px';
+ canvas.height = CANVAS_HEIGHT * pixelRatio;
+ canvas.style.height = CANVAS_HEIGHT + 'px';
+
+ const displayTransform = new TimelineDisplayTransform();
+ displayTransform.scaleX = CANVAS_WIDTH * pixelRatio / 60;
+ displayTransform.panX = 30;
+
+ const transform = new ChartTransform(
+ displayTransform,
+ series.seriesYAxis,
+ CANVAS_WIDTH * pixelRatio,
+ CANVAS_HEIGHT * pixelRatio,
+ 10 * pixelRatio,
+ 10 * pixelRatio,
+ pixelRatio);
+
+ series.draw(canvas.getContext('2d'), transform, highDetails);
+
+ test.addHTMLOutput(div);
+ }
+
+ function drawSeries(test, series) {
+ drawSeriesWithDetails(test, series, false);
+ drawSeriesWithDetails(test, series, true);
+ series.stepGraph_ = !series.stepGraph_;
+ drawSeriesWithDetails(test, series, false);
+ drawSeriesWithDetails(test, series, true);
+ }
+
+ test('instantiate_defaultConfig', function() {
+ const series = buildSeries(undefined);
+ drawSeries(this, series);
+ });
+
+ test('instantiate_lineChart', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.LINE,
+ colorId: 4,
+ unselectedPointSize: 6,
+ lineWidth: 2,
+ unselectedPointDensityOpaque: 0.08
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_areaChart', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 2,
+ backgroundOpacity: 0.2
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_largeSkipDistance', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 1,
+ skipDistance: 40,
+ unselectedPointDensityTransparent: 0.07
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selection', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithSolidDots', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ solidSelectedDots: true,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithAllConfigFlags', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ colorId: 15,
+ lineWidth: 2,
+ skipDistance: 25,
+ unselectedPointDensityOpaque: 0.07,
+ unselectedPointDensityTransparent: 0.09,
+ backgroundOpacity: 0.8
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithDotLetters', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ solidSelectedDots: true,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ if (index % 10 === 3) {
+ point.dotLetter = 'P';
+ } else if (index % 10 === 7) {
+ point.dotLetter = '\u26A0';
+ }
+ });
+ drawSeries(this, series);
+ });
+
+ test('checkRange', function() {
+ const series = buildSeries();
+ const range = series.range;
+ assert.isFalse(range.isEmpty);
+ assert.closeTo(range.min, -1, 0.05);
+ assert.closeTo(range.max, 1, 0.05);
+ });
+
+ test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() {
+ const series = buildSeries();
+
+ // Too far left.
+ let sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -1000, -30.5, 40, sel);
+ assert.lengthOf(sel, 0);
+
+ // Select first point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -30.5, -29.5, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 0);
+
+ // Select second point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -28.8, -28.2, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 1);
+
+ // Select points in the middle.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -0.99, 1.01, 40, sel);
+ assert.lengthOf(sel, 3);
+ const iterator = sel[Symbol.iterator]();
+ assert.strictEqual(iterator.next().value.index, 29);
+ assert.strictEqual(iterator.next().value.index, 30);
+ assert.strictEqual(iterator.next().value.index, 31);
+
+ // Select the last point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 668.99, 668.99, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 59);
+
+ // Too far right.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 669.01, 2000, 40, sel);
+ assert.lengthOf(sel, 0);
+
+ // Select everything.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -29.01, 669.01, 40, sel);
+ assert.lengthOf(sel, 60);
+ });
+
+ test('checkaddEventNearToProvidedEventToSelection', function() {
+ const series = buildSeries();
+
+ // Invalid event.
+ let sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ new Event(), 1, sel));
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ new Event(), -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // First point.
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[0].modelItem, 1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 1);
+
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ series.points[0].modelItem, -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // Middle point.
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[30].modelItem, 1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 31);
+
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[30].modelItem, -1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 29);
+
+ // Last point.
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ series.points[59].modelItem, 1, sel));
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[59].modelItem, -1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 58);
+ });
+
+ test('checkAddClosestEventToSelection', function() {
+ const series = buildSeries();
+
+ // Left of first point.
+ let sel = new EventSet();
+ series.addClosestEventToSelection(-40, 9, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(-40, 11, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 0);
+
+ // Between two points.
+ sel = new EventSet();
+ series.addClosestEventToSelection(0.4, 0.3, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(0.4, 0.4, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 30);
+
+ // Right of last point.
+ sel = new EventSet();
+ series.addClosestEventToSelection(40, 10, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(40, 12, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 59);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html
new file mode 100644
index 00000000000..f34b4c68579
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const IDEAL_MAJOR_MARK_HEIGHT_PX = 30;
+ const AXIS_LABLE_MARGIN_PX = 10;
+ const AXIS_LABLE_FONT_SIZE_PX = 9;
+ const AXIS_LABLE_FONT = 'Arial';
+
+ /**
+ * A vertical axis for a (set of) chart series which maps an arbitrary range
+ * of values [min, max] to the unit range [0, 1].
+ *
+ * @constructor
+ */
+ function ChartSeriesYAxis(opt_min, opt_max) {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.bounds = new tr.b.math.Range();
+ if (opt_min !== undefined) this.bounds.addValue(opt_min);
+ if (opt_max !== undefined) this.bounds.addValue(opt_max);
+ }
+
+ ChartSeriesYAxis.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ valueToUnitRange(value) {
+ if (this.bounds.isEmpty) {
+ throw new Error('Chart series y-axis bounds are empty');
+ }
+ const bounds = this.bounds;
+ if (bounds.range === 0) return 0;
+ return (value - bounds.min) / bounds.range;
+ },
+
+ unitRangeToValue(unitRange) {
+ if (this.bounds.isEmpty) {
+ throw new Error('Chart series y-axis bounds are empty');
+ }
+ return unitRange * this.bounds.range + this.bounds.min;
+ },
+
+ /**
+ * Automatically set the y-axis bounds from the range of values of all
+ * series in a list.
+ *
+ * See the description of autoSetFromRange for the optional configuration
+ * argument flags.
+ */
+ autoSetFromSeries(series, opt_config) {
+ const range = new tr.b.math.Range();
+ series.forEach(function(s) {
+ range.addRange(s.range);
+ }, this);
+ this.autoSetFromRange(range, opt_config);
+ },
+
+ /**
+ * Automatically set the y-axis bound from a range of values.
+ *
+ * The following four flags, which affect the behavior of this method with
+ * respect to already defined bounds, can be present in the optional
+ * configuration (a flag is assumed to be false if it is not provided or if
+ * the configuration is not provided):
+ *
+ * - expandMin: allow decreasing the min bound (if range.min < this.min)
+ * - shrinkMin: allow increasing the min bound (if range.min > this.min)
+ * - expandMax: allow increasing the max bound (if range.max > this.max)
+ * - shrinkMax: allow decreasing the max bound (if range.max < this.max)
+ *
+ * This method will ensure that the resulting bounds are defined and valid
+ * (i.e. min <= max) provided that they were valid or empty before and the
+ * value range is non-empty and valid.
+ *
+ * Note that unless expanding/shrinking a bound is explicitly enabled in
+ * the configuration, non-empty bounds will not be changed under any
+ * circumstances.
+ *
+ * Observe that if no configuration is provided (or all flags are set to
+ * false), this method will only modify the y-axis bounds if they are empty.
+ */
+ autoSetFromRange(range, opt_config) {
+ if (range.isEmpty) return;
+
+ const bounds = this.bounds;
+ if (bounds.isEmpty) {
+ bounds.addRange(range);
+ return;
+ }
+
+ if (!opt_config) return;
+
+ const useRangeMin = (opt_config.expandMin && range.min < bounds.min ||
+ opt_config.shrinkMin && range.min > bounds.min);
+ const useRangeMax = (opt_config.expandMax && range.max > bounds.max ||
+ opt_config.shrinkMax && range.max < bounds.max);
+
+ // Neither bound is modified.
+ if (!useRangeMin && !useRangeMax) return;
+
+ // Both bounds are modified. Assuming the range argument is a valid
+ // range, no extra checks are necessary.
+ if (useRangeMin && useRangeMax) {
+ bounds.min = range.min;
+ bounds.max = range.max;
+ return;
+ }
+
+ // Only one bound is modified. We must ensure that it doesn't go
+ // over/under the other (unmodified) bound.
+ if (useRangeMin) {
+ bounds.min = Math.min(range.min, bounds.max);
+ } else {
+ bounds.max = Math.max(range.max, bounds.min);
+ }
+ },
+
+
+ majorMarkHeightWorld_(transform, pixelRatio) {
+ const idealMajorMarkHeightPx = IDEAL_MAJOR_MARK_HEIGHT_PX * pixelRatio;
+ const idealMajorMarkHeightWorld =
+ transform.vectorToWorldDistance(idealMajorMarkHeightPx);
+
+ return tr.b.math.preferredNumberLargerThanMin(idealMajorMarkHeightWorld);
+ },
+
+ draw(ctx, transform, showYAxisLabels, showYGridLines) {
+ if (!showYAxisLabels && !showYGridLines) return;
+
+ const pixelRatio = transform.pixelRatio;
+ const viewTop = transform.outerTopViewY;
+ const worldTop = transform.viewYToWorldY(viewTop);
+ const viewBottom = transform.outerBottomViewY;
+ const viewHeight = viewBottom - viewTop;
+ const viewLeft = transform.leftViewX;
+ const viewRight = transform.rightViewX;
+ const labelLeft = transform.leftYLabel;
+
+ ctx.save();
+ ctx.lineWidth = pixelRatio;
+ ctx.fillStyle = ColorScheme.getColorForReservedNameAsString('black');
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'center';
+
+ ctx.font =
+ (AXIS_LABLE_FONT_SIZE_PX * pixelRatio) + 'px ' + AXIS_LABLE_FONT;
+
+ // Draw left edge of chart series.
+ ctx.beginPath();
+ ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('black');
+ tr.ui.b.drawLine(
+ ctx, viewLeft, viewTop, viewLeft, viewBottom, viewLeft);
+ ctx.stroke();
+ ctx.closePath();
+
+ // Draw y-axis ticks and gridlines.
+ ctx.beginPath();
+ ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('grey');
+
+ const majorMarkHeight = this.majorMarkHeightWorld_(transform, pixelRatio);
+ const maxMajorMark = Math.max(transform.viewYToWorldY(viewTop),
+ Math.abs(transform.viewYToWorldY(viewBottom)));
+ for (let curWorldY = 0;
+ curWorldY <= maxMajorMark;
+ curWorldY += majorMarkHeight) {
+ const roundedUnitValue = Math.floor(curWorldY * 1000000) / 1000000;
+ const curViewYPositive = transform.worldYToViewY(curWorldY);
+ if (curViewYPositive >= viewTop) {
+ if (showYAxisLabels) {
+ ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
+ curViewYPositive - AXIS_LABLE_MARGIN_PX);
+ }
+ if (showYGridLines) {
+ tr.ui.b.drawLine(
+ ctx, viewLeft, curViewYPositive, viewRight, curViewYPositive);
+ }
+ }
+
+ const curViewYNegative = transform.worldYToViewY(-1 * curWorldY);
+ if (curViewYNegative <= viewBottom) {
+ if (showYAxisLabels) {
+ ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
+ curViewYNegative - AXIS_LABLE_MARGIN_PX);
+ }
+ if (showYGridLines) {
+ tr.ui.b.drawLine(
+ ctx, viewLeft, curViewYNegative, viewRight, curViewYNegative);
+ }
+ }
+ }
+ ctx.stroke();
+ ctx.restore();
+ }
+ };
+
+ return {
+ ChartSeriesYAxis,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html
new file mode 100644
index 00000000000..4a759e040d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html
@@ -0,0 +1,313 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const Range = tr.b.math.Range;
+
+ function buildRange() {
+ const range = new Range();
+ for (let i = 0; i < arguments.length; i++) {
+ range.addValue(arguments[i]);
+ }
+ return range;
+ }
+
+ function buildSeries() {
+ const points = [];
+ for (let i = 0; i < arguments.length; i++) {
+ points.push(new ChartPoint(undefined, i, arguments[i]));
+ }
+ return new ChartSeries(points, new ChartSeriesYAxis());
+ }
+
+ test('instantiate_emptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis();
+ assert.isTrue(seriesYAxis.bounds.isEmpty);
+ });
+
+ test('instantiate_nonEmptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-2, 12);
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -2);
+ assert.strictEqual(seriesYAxis.bounds.max, 12);
+ });
+
+ test('instantiate_equalBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(2.72);
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 2.72);
+ assert.strictEqual(seriesYAxis.bounds.max, 2.72);
+ });
+
+ test('checkValueToUnitRange_emptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis();
+ assert.throws(function() { seriesYAxis.valueToUnitRange(42); });
+ });
+
+ test('checkValueToUnitRange_nonEmptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(10, 20);
+
+ assert.strictEqual(seriesYAxis.valueToUnitRange(0), -1);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(10), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(15), 0.5);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(20), 1);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(30), 2);
+ });
+
+ test('checkValueToUnitRange_equalBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(3.14);
+
+ assert.strictEqual(seriesYAxis.valueToUnitRange(0), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(3.14), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(6.28), 0);
+ });
+
+ test('checkAutoSetFromRange_emptyBounds', function() {
+ // Empty range.
+ let seriesYAxis = new ChartSeriesYAxis();
+ seriesYAxis.autoSetFromRange(buildRange());
+ assert.isTrue(seriesYAxis.bounds.isEmpty);
+
+ // Non-empty range.
+ seriesYAxis = new ChartSeriesYAxis();
+ seriesYAxis.autoSetFromRange(buildRange(-1, 3));
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -1);
+ assert.strictEqual(seriesYAxis.bounds.max, 3);
+ });
+
+ test('checkAutoSetFromRange_nonEmptyBounds', function() {
+ // Empty range.
+ let seriesYAxis = new ChartSeriesYAxis(0, 1);
+ seriesYAxis.autoSetFromRange(buildRange());
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 1);
+
+ // No configuration.
+ seriesYAxis = new ChartSeriesYAxis(2, 3);
+ seriesYAxis.autoSetFromRange(buildRange(1, 4));
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 2);
+ assert.strictEqual(seriesYAxis.bounds.max, 3);
+
+ // Allow expanding min.
+ seriesYAxis = new ChartSeriesYAxis(-2, -1);
+ seriesYAxis.autoSetFromRange(buildRange(-3, 0), {expandMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -3);
+ assert.strictEqual(seriesYAxis.bounds.max, -1);
+
+ // Allow shrinking min.
+ seriesYAxis = new ChartSeriesYAxis(-2, -1);
+ seriesYAxis.autoSetFromRange(buildRange(-1.5, 0.5), {shrinkMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -1.5);
+ assert.strictEqual(seriesYAxis.bounds.max, -1);
+
+ seriesYAxis = new ChartSeriesYAxis(7, 8);
+ seriesYAxis.autoSetFromRange(buildRange(9, 10), {shrinkMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 8);
+ assert.strictEqual(seriesYAxis.bounds.max, 8);
+
+ // Allow expanding max.
+ seriesYAxis = new ChartSeriesYAxis(19, 20);
+ seriesYAxis.autoSetFromRange(buildRange(18, 21), {expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 19);
+ assert.strictEqual(seriesYAxis.bounds.max, 21);
+
+ // Allow shrinking max.
+ seriesYAxis = new ChartSeriesYAxis(30, 32);
+ seriesYAxis.autoSetFromRange(buildRange(29, 31), {shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 30);
+ assert.strictEqual(seriesYAxis.bounds.max, 31);
+
+ seriesYAxis = new ChartSeriesYAxis(41, 42);
+ seriesYAxis.autoSetFromRange(buildRange(39, 40), {shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 41);
+ assert.strictEqual(seriesYAxis.bounds.max, 41);
+
+ // Allow shrinking both bounds.
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(51, 52),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 51);
+ assert.strictEqual(seriesYAxis.bounds.max, 52);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(49, 52),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 50);
+ assert.strictEqual(seriesYAxis.bounds.max, 52);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(51, 54),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 51);
+ assert.strictEqual(seriesYAxis.bounds.max, 53);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(49, 54),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 50);
+ assert.strictEqual(seriesYAxis.bounds.max, 53);
+
+ // Allow expanding both bounds.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(0, 100),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.5, 100),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(0, 60.5),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ // Allow shrinking min and expanding max.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(62, 63),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 62);
+ assert.strictEqual(seriesYAxis.bounds.max, 63);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 63),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 63);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60.2);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 60.5),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ // Allow expanding min and shrinking max.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(62, 63),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 63),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 59);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 60.8);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 60.5),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 59);
+ assert.strictEqual(seriesYAxis.bounds.max, 60.5);
+
+ // Allow everything.
+ seriesYAxis = new ChartSeriesYAxis(200, 250);
+ seriesYAxis.autoSetFromRange(buildRange(150, 175),
+ {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 150);
+ assert.strictEqual(seriesYAxis.bounds.max, 175);
+
+ seriesYAxis = new ChartSeriesYAxis(0, 0.1);
+ seriesYAxis.autoSetFromRange(buildRange(0.2, 0.3),
+ {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0.2);
+ assert.strictEqual(seriesYAxis.bounds.max, 0.3);
+ });
+
+ test('checkAutoSetFromSeries_noSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [];
+
+ seriesYAxis.autoSetFromSeries(series);
+ assert.strictEqual(seriesYAxis.bounds.min, -100);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+ });
+
+ test('checkAutoSetFromSeries_oneSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [buildSeries(-80, 100, -40, 200)];
+
+ seriesYAxis.autoSetFromSeries(series, {shrinkMin: true, expandMax: true});
+ assert.strictEqual(seriesYAxis.bounds.min, -80);
+ assert.strictEqual(seriesYAxis.bounds.max, 200);
+ });
+
+ test('checkAutoSetFromSeries_multipleSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [
+ buildSeries(0, 20, 10, 30),
+ buildSeries(),
+ buildSeries(-500)
+ ];
+
+ seriesYAxis.autoSetFromSeries(series, {expandMin: true, shrinkMax: true});
+ assert.strictEqual(seriesYAxis.bounds.min, -500);
+ assert.strictEqual(seriesYAxis.bounds.max, 30);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html
new file mode 100644
index 00000000000..58ef08d651c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html
@@ -0,0 +1,281 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.chart-track {
+ height: 30px;
+ position: relative;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a chart.
+ *
+ * @constructor
+ * @extends {Track}
+ */
+ const ChartTrack =
+ tr.ui.b.define('chart-track', tr.ui.tracks.Track);
+
+ ChartTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('chart-track');
+ this.series_ = undefined;
+ this.axes_ = undefined;
+
+ // GUID -> {axis: ChartSeriesYAxis, series: [ChartSeries]}.
+ this.axisGuidToAxisData_ = undefined;
+
+ // The maximum top and bottom padding of all series.
+ this.topPadding_ = undefined;
+ this.bottomPadding_ = undefined;
+
+ this.showYAxisLabels_ = undefined;
+ this.showGridLines_ = undefined;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get series() {
+ return this.series_;
+ },
+
+ /**
+ * Set the list of chart series to be displayed on this track. The list
+ * is assumed to be sorted in increasing z-order (i.e. the last series in
+ * the list will be drawn at the top).
+ */
+ set series(series) {
+ this.series_ = series;
+ this.calculateAxisDataAndPadding_();
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ this.invalidateDrawingContainer();
+ },
+
+ get showYAxisLabels() {
+ return this.showYAxisLabels_;
+ },
+
+ set showYAxisLabels(showYAxisLabels) {
+ this.showYAxisLabels_ = showYAxisLabels;
+ this.invalidateDrawingContainer();
+ },
+
+ get showGridLines() {
+ return this.showGridLines_;
+ },
+
+ set showGridLines(showGridLines) {
+ this.showGridLines_ = showGridLines;
+ this.invalidateDrawingContainer();
+ },
+
+ get hasVisibleContent() {
+ return !!this.series && this.series.length > 0;
+ },
+
+ calculateAxisDataAndPadding_() {
+ if (!this.series_) {
+ this.axes_ = undefined;
+ this.axisGuidToAxisData_ = undefined;
+ this.topPadding_ = undefined;
+ this.bottomPadding_ = undefined;
+ return;
+ }
+
+ const axisGuidToAxisData = {};
+ let topPadding = 0;
+ let bottomPadding = 0;
+
+ this.series_.forEach(function(series) {
+ const seriesYAxis = series.seriesYAxis;
+ const axisGuid = seriesYAxis.guid;
+ if (!(axisGuid in axisGuidToAxisData)) {
+ axisGuidToAxisData[axisGuid] = {
+ axis: seriesYAxis,
+ series: []
+ };
+ if (!this.axes_) this.axes_ = [];
+ this.axes_.push(seriesYAxis);
+ }
+ axisGuidToAxisData[axisGuid].series.push(series);
+ topPadding = Math.max(topPadding, series.topPadding);
+ bottomPadding = Math.max(bottomPadding, series.bottomPadding);
+ }, this);
+
+ this.axisGuidToAxisData_ = axisGuidToAxisData;
+ this.topPadding_ = topPadding;
+ this.bottomPadding_ = bottomPadding;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawChart_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawChart_(viewLWorld, viewRWorld) {
+ if (!this.series_) return;
+
+ const ctx = this.context();
+
+ // Get track drawing parameters.
+ const displayTransform = this.viewport.currentDisplayTransform;
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const highDetails = this.viewport.highDetails;
+
+ // Pre-multiply all device-independent pixel parameters with the pixel
+ // ratio to avoid unnecessary recomputation in the performance-critical
+ // drawing code.
+ const width = bounds.width * pixelRatio;
+ const height = bounds.height * pixelRatio;
+ const topPadding = this.topPadding_ * pixelRatio;
+ const bottomPadding = this.bottomPadding_ * pixelRatio;
+
+ // Set up clipping.
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(0, 0, width, height);
+ ctx.clip();
+
+ // TODO(aiolos): Add support for secondary y-axis on right side of chart.
+ // https://github.com/catapult-project/catapult/issues/3008
+ // Draw y-axis grid lines.
+ if (this.axes_) {
+ if ((this.showGridLines_ || this.showYAxisLabels_) &&
+ this.axes_.length > 1) {
+ throw new Error('Only one axis allowed when showing grid lines.');
+ }
+ for (const yAxis of this.axes_) {
+ const chartTransform = new tr.ui.tracks.ChartTransform(
+ displayTransform, yAxis, width, height,
+ topPadding, bottomPadding, pixelRatio);
+ yAxis.draw(
+ ctx, chartTransform, this.showYAxisLabels_, this.showGridLines_);
+ }
+ }
+
+ // Draw all series in the increasing z-order.
+ for (const series of this.series) {
+ const chartTransform = new tr.ui.tracks.ChartTransform(
+ displayTransform, series.seriesYAxis, width, height, topPadding,
+ bottomPadding, pixelRatio);
+ series.draw(ctx, chartTransform, highDetails);
+ }
+
+ // Stop clipping.
+ ctx.restore();
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ // TODO(petrcermak): Consider adding the series to the track map instead
+ // of the track (a potential performance optimization).
+ this.series_.forEach(function(series) {
+ series.points.forEach(function(point) {
+ point.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ this.series_.forEach(function(series) {
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection);
+ }, this);
+ },
+
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ let foundItem = false;
+ this.series_.forEach(function(series) {
+ foundItem = foundItem || series.addEventNearToProvidedEventToSelection(
+ event, offset, selection);
+ }, this);
+ return foundItem;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ // Do nothing.
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.series_.forEach(function(series) {
+ series.addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }, this);
+ },
+
+ /**
+ * Automatically set the bounds of all axes on this track from the range of
+ * values of all series (in this track) associated with each of them.
+ *
+ * See the description of ChartSeriesYAxis.autoSetFromRange for the optional
+ * configuration argument flags.
+ */
+ autoSetAllAxes(opt_config) {
+ for (const axisData of Object.values(this.axisGuidToAxisData_)) {
+ const seriesYAxis = axisData.axis;
+ const series = axisData.series;
+ seriesYAxis.autoSetFromSeries(series, opt_config);
+ }
+ },
+
+ /**
+ * Automatically set the bounds of the provided axis from the range of
+ * values of all series (in this track) associated with it.
+ *
+ * See the description of ChartSeriesYAxis.autoSetFromRange for the optional
+ * configuration argument flags.
+ */
+ autoSetAxis(seriesYAxis, opt_config) {
+ const series = this.axisGuidToAxisData_[seriesYAxis.guid].series;
+ seriesYAxis.autoSetFromSeries(series, opt_config);
+ }
+ };
+
+ return {
+ ChartTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html
new file mode 100644
index 00000000000..405640a9b2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html
@@ -0,0 +1,454 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const ChartSeriesType = tr.ui.tracks.ChartSeriesType;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+ const Event = tr.model.Event;
+ const EventSet = tr.model.EventSet;
+ const EventToTrackMap = tr.ui.tracks.EventToTrackMap;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ function buildPoint(x, y) {
+ const event = new Event();
+ return new ChartPoint(event, x, y);
+ }
+
+ function buildTrack(opt_args) {
+ const viewport = (opt_args && opt_args.viewport) ?
+ opt_args.viewport : new Viewport(document.createElement('div'));
+
+ const seriesYAxis1 = new ChartSeriesYAxis(0, 2.5);
+
+ const points1 = [
+ buildPoint(-2.5, 2),
+ buildPoint(-1.5, 1),
+ buildPoint(-0.5, 0),
+ buildPoint(0.5, 1),
+ buildPoint(1.5, 2),
+ buildPoint(2.5, 0)
+ ];
+ const renderingConfig1 = {
+ chartType: ChartSeriesType.AREA,
+ colorId: 6,
+ selectedPointSize: 7
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig1.stepGraph = opt_args.stepGraph;
+ }
+ const series1 = new ChartSeries(points1, seriesYAxis1, renderingConfig1);
+
+ const points2 = [
+ buildPoint(-2.3, 0.2),
+ buildPoint(-1.3, 1.2),
+ buildPoint(-0.3, 2.2),
+ buildPoint(0.3, 1.2),
+ buildPoint(1.3, 0.2),
+ buildPoint(2.3, 0)
+ ];
+ const renderingConfig2 = {
+ chartType: ChartSeriesType.AREA,
+ colorId: 4,
+ selectedPointSize: 10
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig2.stepGraph = opt_args.stepGraph;
+ }
+ const series2 = new ChartSeries(points2, seriesYAxis1, renderingConfig2);
+
+ const seriesList = [series1, series2];
+
+ if (!opt_args || !opt_args.singleAxis) {
+ const seriesYAxis2 = new ChartSeriesYAxis(-100, 100);
+ const points3 = [
+ buildPoint(-3, -50),
+ buildPoint(-2.4, -40),
+ buildPoint(-1.8, -30),
+ buildPoint(-1.2, -20),
+ buildPoint(-0.6, -10),
+ buildPoint(0, 0),
+ buildPoint(0.6, 10),
+ buildPoint(1.2, 20),
+ buildPoint(1.8, 30),
+ buildPoint(2.4, 40),
+ buildPoint(3, 50)
+ ];
+ const renderingConfig3 = {
+ chartType: ChartSeriesType.LINE,
+ lineWidth: 2
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig3.stepGraph = opt_args.stepGraph;
+ }
+ const series3 = new ChartSeries(points3, seriesYAxis2, renderingConfig3);
+ seriesList.push(series3);
+ }
+
+ const track = new ChartTrack(viewport);
+ track.series = seriesList;
+
+ return track;
+ }
+
+ function buildDashboardTrack(opt_viewport) {
+ const viewport = opt_viewport || new Viewport(
+ document.createElement('div'));
+
+ const seriesYAxis = new ChartSeriesYAxis(0, 1.1);
+ const fileUrl = '/test_data/dashboard_test_points.json';
+ const pointsArray = JSON.parse(tr.b.getSync(fileUrl));
+ const points = [];
+ for (let i = 0; i < pointsArray.length; i++) {
+ points.push(buildPoint(pointsArray[i][0], pointsArray[i][1]));
+ }
+ const renderingConfig = {
+ chartType: ChartSeriesType.LINE,
+ lineWidth: 1,
+ stepGraph: false,
+ selectedPointSize: 10,
+ solidSelectedDots: true,
+ highDetail: false,
+ skipDistance: 0.4
+ };
+ const series = new ChartSeries(points, seriesYAxis, renderingConfig);
+
+ const track = new ChartTrack(viewport);
+ track.series = [series];
+
+ return track;
+ }
+
+ test('instantiate_lowDetailsWithoutSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[2].points[3].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_lowDetailsNoStepGraphWithoutSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport, stepGraph: false});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport, stepGraph: false});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[2].points[3].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionAndYAxisLabels',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showYAxisLabels = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionAndGridLines',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionYAxisLabelsAndGridLines',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showYAxisLabels = true;
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_dashboardChartStyleWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildDashboardTrack(viewport);
+ track.showYAxisLabels = true;
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[40].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(
+ 26610390797802200, 28950000891700000, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('checkPadding', function() {
+ const track = buildTrack();
+
+ // Padding should be equal to half maximum point size.
+ assert.strictEqual(track.topPadding_, 5);
+ assert.strictEqual(track.bottomPadding_, 5);
+ });
+
+ test('checkAddEventsToTrackMap', function() {
+ const track = buildTrack();
+ const eventToTrackMap = new EventToTrackMap();
+ track.addEventsToTrackMap(eventToTrackMap);
+ assert.lengthOf(Object.keys(eventToTrackMap), 23);
+ });
+
+ test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() {
+ const track = buildTrack();
+
+ const sel = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -1.1, -0.7, 0.01, sel);
+ assert.lengthOf(sel, 3);
+ const iter = sel[Symbol.iterator]();
+ assert.strictEqual(iter.next().value, track.series[0].points[1].modelItem);
+ assert.strictEqual(iter.next().value, track.series[1].points[1].modelItem);
+ assert.strictEqual(iter.next().value, track.series[2].points[3].modelItem);
+ });
+
+ test('checkaddEventNearToProvidedEventToSelection', function() {
+ const track = buildTrack();
+
+ // Fail to find a near item to the left in any series.
+ let sel = new EventSet();
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ track.series[0].points[0].modelItem, -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // Succeed at finding a near item to the right of one series.
+ sel = new EventSet();
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ track.series[1].points[1].modelItem, 1, sel));
+ assert.strictEqual(
+ tr.b.getOnlyElement(sel), track.series[1].points[2].modelItem);
+ });
+
+ test('checkAddClosestEventToSelection', function() {
+ const track = buildTrack();
+
+ const sel = new EventSet();
+ track.addClosestEventToSelection(-0.8, 0.4, 0.5, 1.5, sel);
+ assert.lengthOf(sel, 2);
+ const iter = sel[Symbol.iterator]();
+ assert.strictEqual(iter.next().value, track.series[0].points[2].modelItem);
+ assert.strictEqual(iter.next().value, track.series[2].points[4].modelItem);
+ });
+
+ test('checkAutoSetAllAxes', function() {
+ const track = buildTrack();
+ const seriesYAxis1 = track.series[0].seriesYAxis;
+ const seriesYAxis2 = track.series[2].seriesYAxis;
+
+ track.autoSetAllAxes({expandMax: true, shrinkMax: true});
+
+ // Min bounds of both axes should not have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.min, 0);
+ assert.strictEqual(seriesYAxis2.bounds.min, -100);
+
+ // Max bounds of both axes should have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.max, 2.2);
+ assert.strictEqual(seriesYAxis2.bounds.max, 50);
+ });
+
+ test('checkAutoSetAxis', function() {
+ const track = buildTrack();
+ const seriesYAxis1 = track.series[0].seriesYAxis;
+ const seriesYAxis2 = track.series[2].seriesYAxis;
+
+ track.autoSetAxis(seriesYAxis2,
+ {expandMin: true, shrinkMin: true, expandMax: true, shrinkMax: true});
+
+ // First axis should not have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.min, 0);
+ assert.strictEqual(seriesYAxis1.bounds.max, 2.5);
+
+ // Second axis should have been modified.
+ assert.strictEqual(seriesYAxis2.bounds.min, -50);
+ assert.strictEqual(seriesYAxis2.bounds.max, 50);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html
new file mode 100644
index 00000000000..f6bf6310116
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A helper object encapsulating all parameters necessary to draw a chart
+ * series and provides conversion between world coordinates and physical
+ * pixels.
+ *
+ * All parameters (except for pixelRatio) are assumed to be in physical pixels
+ * (i.e. already pre-multiplied with pixelRatio).
+ *
+ * The diagram below explains the meaning of the resulting fields with
+ * respect to a chart track:
+ *
+ * outerTopViewY -> +--------------------/-\------+ <- Top padding
+ * innerTopViewY -> + - - - - - - - - - -| |- - - + <- Axis max
+ * | .. ==\-/== |
+ * | === Series === |
+ * | ==/-\== .. |
+ * innerBottomViewY -> + - - -Point- - - - - - - - - + <- Axis min
+ * outerBottomViewY -> +-------\-/-------------------+ <- Bottom padding
+ * ^ ^
+ * leftViewX rightViewX
+ * leftTimeStamp rightTimestamp
+ *
+ * Labels starting with a lower case letter are the resulting fields of the
+ * transform object. Labels starting with an upper case letter correspond
+ * to the relevant chart track concepts.
+ *
+ * @constructor
+ */
+ function ChartTransform(displayTransform, axis, trackWidth,
+ trackHeight, topPadding, bottomPadding, pixelRatio) {
+ this.pixelRatio = pixelRatio;
+
+ // X axis.
+ this.leftViewX = 0;
+ this.rightViewX = trackWidth;
+ this.leftTimestamp = displayTransform.xViewToWorld(this.leftViewX);
+ this.rightTimestamp = displayTransform.xViewToWorld(this.rightViewX);
+
+ this.displayTransform_ = displayTransform;
+
+ // Y axis.
+ this.outerTopViewY = 0;
+ this.innerTopViewY = topPadding;
+ this.innerBottomViewY = trackHeight - bottomPadding;
+ this.outerBottomViewY = trackHeight;
+
+ this.axis_ = axis;
+ this.innerHeight_ = this.innerBottomViewY - this.innerTopViewY;
+ }
+
+ ChartTransform.prototype = {
+ worldXToViewX(worldX) {
+ return this.displayTransform_.xWorldToView(worldX);
+ },
+
+ viewXToWorldX(viewX) {
+ return this.displayTransform_.xViewToWorld(viewX);
+ },
+
+ vectorToWorldDistance(viewY) {
+ return this.axis_.bounds.range * Math.abs(viewY / this.innerHeight_);
+ },
+
+ viewYToWorldY(viewY) {
+ return this.axis_.unitRangeToValue(
+ 1 - (viewY - this.innerTopViewY) / this.innerHeight_);
+ },
+
+ worldYToViewY(worldY) {
+ const innerHeightCoefficient = 1 - this.axis_.valueToUnitRange(worldY);
+ return innerHeightCoefficient * this.innerHeight_ + this.innerTopViewY;
+ }
+ };
+
+ return {
+ ChartTransform,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html
new file mode 100644
index 00000000000..8d46e08aace
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const ChartTransform = tr.ui.tracks.ChartTransform;
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+
+ function buildChartTransform() {
+ const displayTransform = new TimelineDisplayTransform();
+ displayTransform.panX = -20;
+ displayTransform.scaleX = 0.5;
+
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+
+ const chartTransform = new ChartTransform(
+ displayTransform,
+ seriesYAxis,
+ 500, /* trackWidth */
+ 80, /* trackHeight */
+ 15, /* topPadding */
+ 5, /* bottomPadding */
+ 3 /* pixelRatio */);
+
+ return chartTransform;
+ }
+
+ test('checkFields', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.pixelRatio, 3);
+
+ assert.strictEqual(t.leftViewX, 0);
+ assert.strictEqual(t.rightViewX, 500);
+ assert.strictEqual(t.leftTimestamp, 20);
+ assert.strictEqual(t.rightTimestamp, 1020);
+
+ assert.strictEqual(t.outerTopViewY, 0);
+ assert.strictEqual(t.innerTopViewY, 15);
+ assert.strictEqual(t.innerBottomViewY, 75);
+ assert.strictEqual(t.outerBottomViewY, 80);
+ });
+
+ test('checkWorldXToViewX', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.worldXToViewX(-100), -60);
+ assert.strictEqual(t.worldXToViewX(0), -10);
+ assert.strictEqual(t.worldXToViewX(520), 250);
+ assert.strictEqual(t.worldXToViewX(1020), 500);
+ assert.strictEqual(t.worldXToViewX(1200), 590);
+ });
+
+ test('checkViewXToWorldX', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.viewXToWorldX(-60), -100);
+ assert.strictEqual(t.viewXToWorldX(-10), 0);
+ assert.strictEqual(t.viewXToWorldX(250), 520);
+ assert.strictEqual(t.viewXToWorldX(500), 1020);
+ assert.strictEqual(t.viewXToWorldX(590), 1200);
+ });
+
+ test('checkWorldYToViewY', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.worldYToViewY(-200), 105);
+ assert.strictEqual(t.worldYToViewY(-100), 75);
+ assert.strictEqual(t.worldYToViewY(0), 45);
+ assert.strictEqual(t.worldYToViewY(100), 15);
+ assert.strictEqual(t.worldYToViewY(200), -15);
+ });
+
+ test('checkViewYToWorldY', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.viewYToWorldY(105), -200);
+ assert.strictEqual(t.viewYToWorldY(75), -100);
+ assert.strictEqual(t.viewYToWorldY(45), 0);
+ assert.strictEqual(t.viewYToWorldY(15), 100);
+ assert.strictEqual(t.viewYToWorldY(-15), 200);
+ });
+
+ test('checkVectorToWorldDistance', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.vectorToWorldDistance(105), 350);
+ assert.strictEqual(t.vectorToWorldDistance(75), 250);
+ assert.strictEqual(t.vectorToWorldDistance(45), 150);
+ assert.strictEqual(t.vectorToWorldDistance(15), 50);
+ assert.strictEqual(t.vectorToWorldDistance(-15), 50);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html
new file mode 100644
index 00000000000..ecaac0dd3b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * ContainerToTrackMap is a class to handle building and accessing a map
+ * between an EventContainer's stableId and its handling track.
+ *
+ * @constructor
+ */
+ function ContainerToTrackMap() {
+ this.stableIdToTrackMap_ = {};
+ }
+
+ ContainerToTrackMap.prototype = {
+ addContainer(container, track) {
+ if (!track) {
+ throw new Error('Must provide a track.');
+ }
+ this.stableIdToTrackMap_[container.stableId] = track;
+ },
+
+ clear() {
+ this.stableIdToTrackMap_ = {};
+ },
+
+ getTrackByStableId(stableId) {
+ return this.stableIdToTrackMap_[stableId];
+ }
+ };
+
+ return {
+ ContainerToTrackMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html
new file mode 100644
index 00000000000..454c1df585c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const Task = tr.b.Task;
+
+ /**
+ * A generic track that contains other tracks as its children.
+ * @constructor
+ */
+ const ContainerTrack = tr.ui.b.define('container-track', tr.ui.tracks.Track);
+ ContainerTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ },
+
+ detach() {
+ Polymer.dom(this).textContent = '';
+ },
+
+ get tracks_() {
+ const tracks = [];
+ for (let i = 0; i < this.children.length; i++) {
+ if (this.children[i] instanceof tr.ui.tracks.Track) {
+ tracks.push(this.children[i]);
+ }
+ }
+ return tracks;
+ },
+
+ drawTrack(type) {
+ this.tracks_.forEach(function(track) {
+ track.drawTrack(type);
+ });
+ },
+
+ /**
+ * Adds items intersecting the given range to a selection.
+ * @param {number} loVX Lower X bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVX Upper X bound of the interval to search, in
+ * viewspace.
+ * @param {number} loY Lower Y bound of the interval to search, in
+ * viewspace space.
+ * @param {number} hiY Upper Y bound of the interval to search, in
+ * viewspace space.
+ * @param {Selection} selection Selection to which to add results.
+ */
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ const trackClientRect = this.tracks_[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.tracks_[i].addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addIntersectingEventsInRangeToSelection.
+ apply(this, arguments);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ for (const track of this.tracks_) {
+ track.addEventsToTrackMap(eventToTrackMap);
+ }
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ this.tracks_[i].addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ }
+ },
+
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const task = new Task();
+ for (let i = 0; i < this.tracks_.length; i++) {
+ task.subTask(function(i) {
+ return function() {
+ this.tracks_[i].addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ };
+ }(i), this);
+ }
+ return task;
+ },
+
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ const trackClientRect = this.tracks_[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.tracks_[i].addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ this.tracks_.forEach(function(track) {
+ track.addContainersToTrackMap(containerToTrackMap);
+ });
+ },
+
+ clearTracks_() {
+ this.tracks_.forEach(function(track) {
+ Polymer.dom(this).removeChild(track);
+ }, this);
+ }
+ };
+
+ return {
+ ContainerTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html
new file mode 100644
index 00000000000..7f25e41bf6a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a Counter object.
+ * @constructor
+ * @extends {ChartTrack}
+ */
+ const CounterTrack = tr.ui.b.define('counter-track', tr.ui.tracks.ChartTrack);
+
+ CounterTrack.prototype = {
+ __proto__: tr.ui.tracks.ChartTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ChartTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('counter-track');
+ },
+
+ get counter() {
+ return this.chart;
+ },
+
+ set counter(counter) {
+ this.heading = counter.name + ': ';
+ this.series = CounterTrack.buildChartSeriesFromCounter(counter);
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ getModelEventFromItem(chartValue) {
+ return chartValue;
+ }
+ };
+
+ CounterTrack.buildChartSeriesFromCounter = function(counter) {
+ const numSeries = counter.series.length;
+ const totals = counter.totals;
+
+ // Create one common axis for all series.
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+
+ // Build one chart series for each counter series.
+ const chartSeries = counter.series.map(function(series, seriesIndex) {
+ const chartPoints = series.samples.map(function(sample, sampleIndex) {
+ const total = totals[sampleIndex * numSeries + seriesIndex];
+ return new tr.ui.tracks.ChartPoint(sample, sample.timestamp, total);
+ });
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: series.color
+ };
+ return new tr.ui.tracks.ChartSeries(
+ chartPoints, seriesYAxis, renderingConfig);
+ });
+
+ // Show the first series (with the smallest cumulative value) at the top.
+ chartSeries.reverse();
+
+ return chartSeries;
+ };
+
+ return {
+ CounterTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html
new file mode 100644
index 00000000000..3a4f84a14b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function getSynchronous(url) {
+ const req = new XMLHttpRequest();
+ req.open('GET', url, false);
+ // Without the mime type specified like this, the file's bytes are not
+ // retrieved correctly.
+ req.overrideMimeType('text/plain; charset=x-user-defined');
+ req.send(null);
+ return req.responseText;
+ }
+
+ const ZOOM_STEPS = 10;
+ const ZOOM_COEFFICIENT = 1.2;
+
+ let model = undefined;
+
+ let drawingContainer;
+ let viewportDiv;
+
+ let viewportWidth;
+ let worldMid;
+
+ let startScale = undefined;
+
+ function timedCounterTrackPerfTest(name, testFn, iterations) {
+ function setUpOnce() {
+ if (model !== undefined) return;
+
+ const fileUrl = '/test_data/counter_tracks.html';
+ const events = getSynchronous(fileUrl);
+ model = tr.c.TestUtils.newModelWithEvents([events]);
+ }
+
+ function setUp() {
+ setUpOnce();
+ viewportDiv = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(viewportDiv);
+
+ drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ viewport.modelTrackContainer = drawingContainer;
+
+ const modelTrack = new tr.ui.tracks.ModelTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(modelTrack);
+
+ modelTrack.model = model;
+
+ Polymer.dom(viewportDiv).appendChild(drawingContainer);
+
+ this.addHTMLOutput(viewportDiv);
+
+ // Size the canvas.
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ // Size the viewport.
+ viewportWidth = drawingContainer.canvas.width;
+ const min = model.bounds.min;
+ const range = model.bounds.range;
+ worldMid = min + range / 2;
+
+ const boost = range * 0.15;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(min - boost, min + range + boost, viewportWidth);
+ modelTrack.viewport.setDisplayTransformImmediately(dt);
+ startScale = dt.scaleX;
+
+ // Select half of the counter samples.
+ for (const pid in model.processes) {
+ const counters = model.processes[pid].counters;
+ for (const cid in counters) {
+ const series = counters[cid].series;
+ for (let i = 0; i < series.length; i++) {
+ const samples = series[i].samples;
+ for (let j = Math.floor(samples.length / 2); j < samples.length;
+ j++) {
+ samples[j].selectionState =
+ tr.model.SelectionState.SELECTED;
+ }
+ }
+ }
+ }
+ }
+
+ function tearDown() {
+ viewportDiv.innerText = '';
+ drawingContainer = undefined;
+ }
+
+ timedPerfTest(name, testFn, {
+ setUp,
+ tearDown,
+ iterations
+ });
+ }
+
+ const n110100 = [1, 10, 100];
+ n110100.forEach(function(val) {
+ timedCounterTrackPerfTest(
+ 'draw_softwareCanvas_' + val,
+ function() {
+ let scale = startScale;
+ for (let i = 0; i < ZOOM_STEPS; i++) {
+ const dt =
+ drawingContainer.viewport.currentDisplayTransform.clone();
+ scale *= ZOOM_COEFFICIENT;
+ dt.scaleX = scale;
+ dt.xPanWorldPosToViewPos(worldMid, 'center', viewportWidth);
+ drawingContainer.viewport.setDisplayTransformImmediately(dt);
+ drawingContainer.draw_();
+ }
+ }, val);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html
new file mode 100644
index 00000000000..dd0286b6b67
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Counter = tr.model.Counter;
+ const Viewport = tr.ui.TimelineViewport;
+ const CounterTrack = tr.ui.tracks.CounterTrack;
+
+ const runTest = function(timestamps, samples, testFn) {
+ const testEl = document.createElement('div');
+
+ const ctr = new Counter(undefined, 'foo', '', 'foo');
+ const n = samples.length;
+
+ for (let i = 0; i < n; ++i) {
+ ctr.addSeries(new tr.model.CounterSeries('value' + i,
+ ColorScheme.getColorIdForGeneralPurposeString('value' + i)));
+ }
+
+ for (let i = 0; i < samples.length; ++i) {
+ for (let k = 0; k < timestamps.length; ++k) {
+ ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+ }
+ }
+
+ ctr.updateBounds();
+
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new CounterTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ // Force the container to update sizes so the test can use coordinates that
+ // make sense. This has to be after the adding of the track as we need to
+ // use the track header to figure out our positioning.
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ track.heading = ctr.name;
+ track.counter = ctr;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 10, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ testFn(ctr, drawingContainer, track);
+ };
+
+ test('instantiate', function() {
+ const ctr = new Counter(undefined, 'testBasicCounter', '',
+ 'testBasicCounter');
+ ctr.addSeries(new tr.model.CounterSeries('value1',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ 'testBasicCounter.value1')));
+ ctr.addSeries(new tr.model.CounterSeries('value2',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ 'testBasicCounter.value2')));
+
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+ for (let i = 0; i < samples.length; ++i) {
+ for (let k = 0; k < timestamps.length; ++k) {
+ ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+ }
+ }
+
+ ctr.updateBounds();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new CounterTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = ctr.name;
+ track.counter = ctr;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 7.7, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('basicCounterXPointPicking', function() {
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+ runTest.call(this, timestamps, samples, function(ctr, container, track) {
+ const clientRect = track.getBoundingClientRect();
+ const y75 = clientRect.top + (0.75 * clientRect.height);
+
+ // In bounds.
+ let sel = new tr.model.EventSet();
+ let x = 0.15 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+
+ let nextSeriesIndex = 1;
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.series.counter, ctr);
+ assert.strictEqual(event.getSampleIndex(), 1);
+ assert.strictEqual(event.series.seriesIndex, nextSeriesIndex--);
+ }
+
+ // Outside bounds.
+ sel = new tr.model.EventSet();
+ x = -0.5 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+ assert.strictEqual(sel.length, 0);
+
+ sel = new tr.model.EventSet();
+ x = 0.8 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+ assert.strictEqual(sel.length, 0);
+ });
+ });
+
+ test('counterTrackAddClosestEventToSelection', function() {
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 4, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+ runTest.call(this, timestamps, samples, function(ctr, container, track) {
+ // Before with not range.
+ let sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, 0, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Before with negative range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, -10, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Before first sample.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 0);
+ }
+
+ // Between and closer to sample before.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 1);
+ }
+
+ // Between samples with bad range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(1.45, 0.25, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Between and closer to next sample.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(4.7, 6, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 5);
+ }
+
+ // After last sample with good range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(8.5, 2, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 7);
+ }
+
+ // After last sample with bad range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(10, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html
new file mode 100644
index 00000000000..3a6c627fb38
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * Visualizes a Cpu using a series of SliceTracks.
+ * @constructor
+ */
+ const CpuTrack =
+ tr.ui.b.define('cpu-track', tr.ui.tracks.ContainerTrack);
+ CpuTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('cpu-track');
+ this.detailedMode_ = true;
+ },
+
+ get cpu() {
+ return this.cpu_;
+ },
+
+ set cpu(cpu) {
+ this.cpu_ = cpu;
+ this.updateContents_();
+ },
+
+ get detailedMode() {
+ return this.detailedMode_;
+ },
+
+ set detailedMode(detailedMode) {
+ this.detailedMode_ = detailedMode;
+ this.updateContents_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(value) {
+ this.tooltip_ = value;
+ this.updateContents_();
+ },
+
+ get hasVisibleContent() {
+ if (this.cpu_ === undefined) return false;
+
+ const cpu = this.cpu_;
+ if (cpu.slices.length) return true;
+
+ if (cpu.samples && cpu.samples.length) return true;
+
+ if (Object.keys(cpu.counters).length > 0) return true;
+
+ return false;
+ },
+
+ updateContents_() {
+ this.detach();
+ if (!this.cpu_) return;
+
+ const slices = this.cpu_.slices;
+ if (slices.length) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ track.heading = this.cpu_.userFriendlyName + ':';
+ Polymer.dom(this).appendChild(track);
+ }
+
+ if (this.detailedMode_) {
+ this.appendSamplesTracks_();
+
+ for (const counterName in this.cpu_.counters) {
+ const counter = this.cpu_.counters[counterName];
+ const track = new tr.ui.tracks.CounterTrack(this.viewport);
+ track.heading = this.cpu_.userFriendlyName + ' ' +
+ counter.name + ':';
+ track.counter = counter;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+ },
+
+ appendSamplesTracks_() {
+ const samples = this.cpu_.samples;
+ if (samples === undefined || samples.length === 0) {
+ return;
+ }
+ const samplesByTitle = {};
+ samples.forEach(function(sample) {
+ if (samplesByTitle[sample.title] === undefined) {
+ samplesByTitle[sample.title] = [];
+ }
+ samplesByTitle[sample.title].push(sample);
+ });
+
+ const sampleTitles = Object.keys(samplesByTitle);
+ sampleTitles.sort();
+
+ sampleTitles.forEach(function(sampleTitle) {
+ const samples = samplesByTitle[sampleTitle];
+ const samplesTrack = new tr.ui.tracks.SliceTrack(this.viewport);
+ samplesTrack.group = this.cpu_;
+ samplesTrack.slices = samples;
+ samplesTrack.heading = this.cpu_.userFriendlyName + ': ' +
+ sampleTitle;
+ samplesTrack.tooltip = this.cpu_.userFriendlyDetails;
+ samplesTrack.selectionGenerator = function() {
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < samplesTrack.slices.length; i++) {
+ selection.push(samplesTrack.slices[i]);
+ }
+ return selection;
+ };
+ Polymer.dom(this).appendChild(samplesTrack);
+ }, this);
+ }
+ };
+
+ return {
+ CpuTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html
new file mode 100644
index 00000000000..442992522f5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Cpu = tr.model.Cpu;
+ const CpuTrack = tr.ui.tracks.CpuTrack;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const StackFrame = tr.model.StackFrame;
+ const Sample = tr.model.Sample;
+ const Thread = tr.model.Thread;
+ const Viewport = tr.ui.TimelineViewport;
+
+ test('basicCpu', function() {
+ const cpu = new Cpu({}, 7);
+ cpu.slices = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8)
+ ];
+ cpu.updateBounds();
+
+ const testEl = document.createElement('div');
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+
+ const track = new CpuTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+
+ test('withSamples', function() {
+ let thread;
+ let cpu;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ cpu = model.kernel.getOrCreateCpu(1);
+ thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+ const nodeA = tr.c.TestUtils.newProfileNode(model, 'a');
+ const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA);
+ const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB);
+ const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA);
+
+ model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread,
+ undefined, 10));
+ model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread,
+ undefined, 10));
+
+ model.samples.push(new Sample(25, 'page_fault', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(35, 'page_fault', nodeD, thread,
+ undefined, 10));
+ }
+ });
+
+ const testEl = document.createElement('div');
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+
+ const track = new CpuTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html
new file mode 100644
index 00000000000..912220b8236
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<style>
+.cpu-usage-track {
+ height: 90px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+
+ /**
+ * A track that displays the cpu usage of a process.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.ChartTrack}
+ */
+ const CpuUsageTrack = tr.ui.b.define('cpu-usage-track', ChartTrack);
+
+ CpuUsageTrack.prototype = {
+ __proto__: ChartTrack.prototype,
+
+ decorate(viewport) {
+ ChartTrack.prototype.decorate.call(this, viewport);
+ this.classList.add('cpu-usage-track');
+ this.heading = 'CPU usage';
+ this.cpuUsageSeries_ = undefined;
+ },
+
+ // Given a tr.Model, it creates a cpu usage series and a graph.
+ initialize(model) {
+ if (model !== undefined) {
+ this.cpuUsageSeries_ = model.device.cpuUsageSeries;
+ } else {
+ this.cpuUsageSeries_ = undefined;
+ }
+ this.series = this.buildChartSeries_();
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ get hasVisibleContent() {
+ return !!this.cpuUsageSeries_ &&
+ this.cpuUsageSeries_.samples.length > 0;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ containerToTrackMap.addContainer(this.series_, this);
+ },
+
+ buildChartSeries_(yAxis, color) {
+ if (!this.hasVisibleContent) return [];
+
+ yAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+ const usageSamples = this.cpuUsageSeries_.samples;
+ const pts = new Array(usageSamples.length + 1);
+ for (let i = 0; i < usageSamples.length; i++) {
+ pts[i] = new tr.ui.tracks.ChartPoint(undefined,
+ usageSamples[i].start, usageSamples[i].usage);
+ }
+ pts[usageSamples.length] = new tr.ui.tracks.ChartPoint(undefined,
+ usageSamples[usageSamples.length - 1].start, 0);
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: color
+ };
+
+ return [new tr.ui.tracks.ChartSeries(pts, yAxis, renderingConfig)];
+ },
+ };
+
+ return {
+ CpuUsageTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html
new file mode 100644
index 00000000000..2970e81eaf8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html
@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/cpu/cpu_usage_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel='import' href='/tracing/ui/base/constants.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html">
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const DIFF_EPSILON = 0.0001;
+
+ // Input : slices is an array-of-array-of slices. Each top level array
+ // represents a process. So, each slice in one of the top level array
+ // will be placed in the same process.
+ function buildModel(slices) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ slices[i].forEach(s => thread.sliceGroup.pushSlice(s));
+ }
+ });
+ const auditor = new tr.e.audits.CpuUsageAuditor(model);
+ auditor.runAnnotate();
+ return model;
+ }
+
+ // Compare float arrays based on an epsilon since floating point arithmetic
+ // is not always 100% accurate.
+ function assertArrayValuesCloseTo(actualValue, expectedValue) {
+ assert.lengthOf(actualValue, expectedValue.length);
+ for (let i = 0; i < expectedValue.length; i++) {
+ assert.closeTo(actualValue[i], expectedValue[i], DIFF_EPSILON);
+ }
+ }
+
+ function createCpuUsageTrack(model, interval) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ div.appendChild(drawingContainer);
+ const track = new tr.ui.tracks.CpuUsageTrack(drawingContainer.viewport);
+ if (model !== undefined) {
+ setDisplayTransformFromBounds(viewport, model.bounds);
+ }
+ track.initialize(model, interval);
+ drawingContainer.appendChild(track);
+ this.addHTMLOutput(drawingContainer);
+ return track;
+ }
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ function setDisplayTransformFromBounds(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ }
+
+ test('computeCpuUsage_simple', function() {
+ // Set the boundaries, from 0-15 ms. This slice will not
+ // contain any CPU usage data, it's just to make the boundaries
+ // of the bins go as 0-1, 1-2, 2-3, etc. This also tests whether
+ // this function works properly in the presence of slices that
+ // don't include CPU usage data.
+ const bigSlice = new tr.model.ThreadSlice('', title, 0, 0, {}, 15);
+ // First thread.
+ // 0 5 10 15
+ // [ sliceA ]
+ // [ sliceB ] [C ]
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 0.5, {}, 5);
+ sliceA.cpuDuration = 5;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 2.5, {}, 8);
+ sliceB.cpuDuration = 6;
+ // The slice completely fits into an interval and is the last.
+ const sliceC = new tr.model.ThreadSlice('', title, 0, 12.5, {}, 2);
+ sliceC.cpuDuration = 1;
+
+ // Second thread.
+ // 0 5 10 15
+ // [ sliceD ][ sliceE ]
+ const sliceD = new tr.model.ThreadSlice('', title, 0, 3.5, {}, 3);
+ sliceD.cpuDuration = 3;
+ const sliceE = new tr.model.ThreadSlice('', title, 0, 6.5, {}, 6);
+ sliceE.cpuDuration = 3;
+
+ const model = buildModel([
+ [bigSlice, sliceA, sliceB, sliceC],
+ [sliceD, sliceE]
+ ]);
+
+ // Compute average CPU usage over A (but not over B and C).
+ const avgCpuUsageA = sliceA.cpuSelfTime / sliceA.selfTime;
+ // Compute average CPU usage over B, C, D, E. They don't have subslices.
+ const avgCpuUsageB = sliceB.cpuDuration / sliceB.duration;
+ const avgCpuUsageC = sliceC.cpuDuration / sliceC.duration;
+ const avgCpuUsageD = sliceD.cpuDuration / sliceD.duration;
+ const avgCpuUsageE = sliceE.cpuDuration / sliceE.duration;
+
+ const expectedValue = [
+ 0,
+ avgCpuUsageA,
+ avgCpuUsageA,
+ avgCpuUsageA + avgCpuUsageB,
+ avgCpuUsageA + avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageA + avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageE,
+ avgCpuUsageE,
+ avgCpuUsageC,
+ avgCpuUsageC,
+ 0
+ ];
+ const track = createCpuUsageTrack.call(this, model);
+ const actualValue = track.series[0].points.map(point => point.y);
+ assertArrayValuesCloseTo(actualValue, expectedValue);
+ });
+
+ test('computeCpuUsage_longDurationThreadSlice', function() {
+ // Create a slice covering 24 hours.
+ const sliceA = new tr.model.ThreadSlice(
+ '', title, 0, 0, {}, 24 * 60 * 60 * 1000);
+ sliceA.cpuDuration = sliceA.duration * 0.25;
+
+ const model = buildModel([[sliceA]]);
+
+ const track = createCpuUsageTrack.call(this, model);
+ const cpuSamples = track.series[0].points.map(point => point.y);
+
+ // All except the last sample is 0.25, since sliceA.cpuDuration was set to
+ // 0.25 of the total.
+ for (const cpuSample of cpuSamples.slice(0, cpuSamples.length - 1)) {
+ assert.closeTo(cpuSample, 0.25, DIFF_EPSILON);
+ }
+ // The last sample is 0.
+ assert.closeTo(cpuSamples[cpuSamples.length - 1], 0, DIFF_EPSILON);
+ });
+
+ test('instantiate', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023);
+ sliceA.cpuDuration = 25;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769);
+ sliceB.cpuDuration = 1.5;
+ const sliceC = new tr.model.ThreadSlice('', title, 0, 11.239, {}, 5.8769);
+ sliceC.cpuDuration = 5;
+ const sliceD = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01);
+ sliceD.cpuDuration = 4;
+
+ const model = buildModel([[sliceA, sliceB, sliceC, sliceD]]);
+ createCpuUsageTrack.call(this, model);
+ });
+
+ test('hasVisibleContent_trueWithThreadSlicePresent', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01);
+ sliceA.cpuDuration = 4;
+ const model = buildModel([[sliceA]]);
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedProcessModel', function() {
+ const track = createCpuUsageTrack.call(this, undefined);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithNoThreadSlice', function() {
+ // model with a CPU and a thread but no ThreadSlice.
+ const model = buildModel([]);
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_trueWithSubSlices', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023);
+ sliceA.cpuDuration = 25;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769);
+ sliceB.cpuDuration = 1.5;
+
+ const model = buildModel([[sliceA, sliceB]]);
+ const process = model.getProcess(1);
+ // B will become lowest level slices of A.
+ process.getThread(0).sliceGroup.createSubSlices();
+ assert.strictEqual(
+ sliceA.cpuSelfTime, (sliceA.cpuDuration - sliceB.cpuDuration));
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html
new file mode 100644
index 00000000000..a068a7ebebb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/power_series_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ContainerTrack = tr.ui.tracks.ContainerTrack;
+
+ // TODO(charliea): Make this track collapsible.
+ /**
+ * Track to visualize the device model.
+ *
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const DeviceTrack = tr.ui.b.define('device-track', ContainerTrack);
+
+ DeviceTrack.prototype = {
+
+ __proto__: ContainerTrack.prototype,
+
+ decorate(viewport) {
+ ContainerTrack.prototype.decorate.call(this, viewport);
+
+ Polymer.dom(this).classList.add('device-track');
+ this.device_ = undefined;
+ this.powerSeriesTrack_ = undefined;
+ },
+
+ get device() {
+ return this.device_;
+ },
+
+ set device(device) {
+ this.device_ = device;
+ this.updateContents_();
+ },
+
+ get powerSeriesTrack() {
+ return this.powerSeriesTrack_;
+ },
+
+ get hasVisibleContent() {
+ return (this.powerSeriesTrack_ &&
+ this.powerSeriesTrack_.hasVisibleContent);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.call(
+ this, containerToTrackMap);
+ containerToTrackMap.addContainer(this.device, this);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ this.tracks_.forEach(function(track) {
+ track.addEventsToTrackMap(eventToTrackMap);
+ });
+ },
+
+ appendPowerSeriesTrack_() {
+ this.powerSeriesTrack_ = new tr.ui.tracks.PowerSeriesTrack(this.viewport);
+ this.powerSeriesTrack_.powerSeries = this.device.powerSeries;
+
+ if (this.powerSeriesTrack_.hasVisibleContent) {
+ Polymer.dom(this).appendChild(this.powerSeriesTrack_);
+ Polymer.dom(this).appendChild(
+ new tr.ui.tracks.SpacingTrack(this.viewport));
+ }
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+ this.appendPowerSeriesTrack_();
+ }
+ };
+
+ return {
+ DeviceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html
new file mode 100644
index 00000000000..fdd3b392993
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel='import' href='/tracing/model/device.html'>
+<link rel='import' href='/tracing/model/model.html'>
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel='import' href='/tracing/ui/timeline_display_transform.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel='import' href='/tracing/ui/tracks/device_track.html'>
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+<link rel='import' href='/tracing/ui/tracks/event_to_track_map.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const DeviceTrack = tr.ui.tracks.DeviceTrack;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ const createDrawingContainer = function(series) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ if (series) {
+ series.updateBounds();
+ setDisplayTransformFromBounds(viewport, series.bounds);
+ }
+
+ return drawingContainer;
+ };
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ const setDisplayTransformFromBounds = function(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ };
+
+ test('instantiate', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+ device.powerSeries.addPowerSample(1, 3);
+ device.powerSeries.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+ });
+
+ test('instantiate_noPowerSeries', function() {
+ const device = new Device(new Model());
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ // Adding output should throw due to no visible content.
+ assert.throw(function() { this.addHTMLOutput(drawingContainer); });
+ });
+
+ test('setDevice_clearsTrackBeforeUpdating', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+ device.powerSeries.addPowerSample(1, 3);
+ device.powerSeries.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+
+ // Set the device twice and make sure that this doesn't result in
+ // the track appearing twice.
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+
+ // The device track should still have two subtracks: one counter track and
+ // one spacing track.
+ assert.strictEqual(track.tracks_.length, 2);
+ });
+
+ test('addContainersToTrackMap', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+
+ const drawingContainer = createDrawingContainer(device.series);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+
+ const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+ track.addContainersToTrackMap(containerToTrackMap);
+
+ assert.strictEqual(containerToTrackMap.getTrackByStableId('Device'), track);
+ assert.strictEqual(
+ containerToTrackMap.getTrackByStableId('Device.PowerSeries'),
+ track.powerSeriesTrack);
+ });
+
+ test('addEventsToTrackMap', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new DeviceTrack(viewport);
+ track.device = device;
+
+ const eventToTrackMap = new tr.ui.tracks.EventToTrackMap();
+ track.addEventsToTrackMap(eventToTrackMap);
+
+ const expected = new tr.ui.tracks.EventToTrackMap();
+ expected[device.powerSeries.samples[0].guid] = track.powerSeriesTrack;
+ expected[device.powerSeries.samples[1].guid] = track.powerSeriesTrack;
+
+ assert.deepEqual(eventToTrackMap, expected);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css
new file mode 100644
index 00000000000..a8f4d17c91c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css
@@ -0,0 +1,18 @@
+/* Copyright (c) 2012 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.
+ */
+
+.drawing-container {
+ display: inline;
+ overflow: auto;
+ overflow-x: hidden;
+ position: relative;
+}
+
+.drawing-container-canvas {
+ display: block;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html
new file mode 100644
index 00000000000..d13a3a5383c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/drawing_container.css">
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const DrawType = {
+ GENERAL_EVENT: 1,
+ INSTANT_EVENT: 2,
+ BACKGROUND: 3,
+ GRID: 4,
+ FLOW_ARROWS: 5,
+ MARKERS: 6,
+ HIGHLIGHTS: 7,
+ ANNOTATIONS: 8
+ };
+
+ // Must be > 1.0. This is the maximum multiple by which the size
+ // of the canvas can exceed the window dimensions. For example
+ // if window.innerHeight is 1000 and this is 1.4, then the
+ // largest the canvas height can be set to is 1400px assuming a
+ // window.devicePixelRatio of 1.
+ // Currently this value is set rather large to mostly match
+ // previous behavior & performance. This should be reduced to
+ // be as small as possible once raw drawing performance is improved
+ // such that a repaint doesn't incur a large jank
+ const MAX_OVERSIZE_MULTIPLE = 3.0;
+ const REDRAW_SLOP = (MAX_OVERSIZE_MULTIPLE - 1) / 2;
+
+ const DrawingContainer = tr.ui.b.define('drawing-container',
+ tr.ui.tracks.Track);
+
+ DrawingContainer.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('drawing-container');
+
+ this.canvas_ = document.createElement('canvas');
+ this.canvas_.className = 'drawing-container-canvas';
+ this.canvas_.style.left = tr.ui.b.constants.HEADING_WIDTH + 'px';
+ Polymer.dom(this).appendChild(this.canvas_);
+
+ this.ctx_ = this.canvas_.getContext('2d');
+ this.offsetY_ = 0;
+
+ this.viewportChange_ = this.viewportChange_.bind(this);
+ this.viewport.addEventListener('change', this.viewportChange_);
+
+ window.addEventListener('resize', this.windowResized_.bind(this));
+ this.addEventListener('scroll', this.scrollChanged_.bind(this));
+ },
+
+ // Needed to support the calls in TimelineTrackView.
+ get canvas() {
+ return this.canvas_;
+ },
+
+ context() {
+ return this.ctx_;
+ },
+
+ viewportChange_() {
+ this.invalidate();
+ },
+
+ windowResized_() {
+ this.invalidate();
+ },
+
+ scrollChanged_() {
+ if (this.updateOffsetY_()) {
+ this.invalidate();
+ }
+ },
+
+ invalidate() {
+ if (this.rafPending_) return;
+
+ this.rafPending_ = true;
+
+ tr.b.requestPreAnimationFrame(this.preDraw_, this);
+ },
+
+ preDraw_() {
+ this.rafPending_ = false;
+ this.updateCanvasSizeIfNeeded_();
+
+ tr.b.requestAnimationFrameInThisFrameIfPossible(this.draw_, this);
+ },
+
+ draw_() {
+ this.ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+
+ const typesToDraw = [
+ DrawType.BACKGROUND,
+ DrawType.HIGHLIGHTS,
+ DrawType.GRID,
+ DrawType.INSTANT_EVENT,
+ DrawType.GENERAL_EVENT,
+ DrawType.MARKERS,
+ DrawType.ANNOTATIONS,
+ DrawType.FLOW_ARROWS
+ ];
+
+ for (const idx in typesToDraw) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ this.children[i].drawTrack(typesToDraw[idx]);
+ }
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.canvas_.getBoundingClientRect();
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(
+ bounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ this.viewport.drawGridLines(
+ this.ctx_, viewLWorld, viewRWorld, viewHeight);
+ },
+
+ // Update's this.offsetY_, returning true if the value has changed
+ // and thus a redraw is needed, or false if it did not change.
+ updateOffsetY_() {
+ const maxYDelta = window.innerHeight * REDRAW_SLOP;
+ let newOffset = this.scrollTop - maxYDelta;
+ if (Math.abs(newOffset - this.offsetY_) <= maxYDelta) return false;
+ // Now clamp to the valid range.
+ const maxOffset = this.scrollHeight -
+ this.canvas_.getBoundingClientRect().height;
+ newOffset = Math.max(0, Math.min(newOffset, maxOffset));
+ if (newOffset !== this.offsetY_) {
+ this.offsetY_ = newOffset;
+ return true;
+ }
+ return false;
+ },
+
+ updateCanvasSizeIfNeeded_() {
+ const visibleChildTracks =
+ Array.from(this.children).filter(this.visibleFilter_);
+
+ if (visibleChildTracks.length === 0) {
+ return;
+ }
+
+ const thisBounds = this.getBoundingClientRect();
+
+ const firstChildTrackBounds =
+ visibleChildTracks[0].getBoundingClientRect();
+ const lastChildTrackBounds =
+ visibleChildTracks[visibleChildTracks.length - 1].
+ getBoundingClientRect();
+
+ const innerWidth = firstChildTrackBounds.width -
+ tr.ui.b.constants.HEADING_WIDTH;
+ const innerHeight = Math.min(
+ lastChildTrackBounds.bottom - firstChildTrackBounds.top,
+ Math.floor(window.innerHeight * MAX_OVERSIZE_MULTIPLE));
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ if (this.canvas_.width !== innerWidth * pixelRatio) {
+ this.canvas_.width = innerWidth * pixelRatio;
+ this.canvas_.style.width = innerWidth + 'px';
+ }
+
+ if (this.canvas_.height !== innerHeight * pixelRatio) {
+ this.canvas_.height = innerHeight * pixelRatio;
+ this.canvas_.style.height = innerHeight + 'px';
+ }
+
+ if (this.canvas_.top !== this.offsetY_) {
+ this.canvas_.top = this.offsetY_;
+ this.canvas_.style.top = this.offsetY_ + 'px';
+ }
+ },
+
+ visibleFilter_(element) {
+ if (!(element instanceof tr.ui.tracks.Track)) return false;
+
+ return window.getComputedStyle(element).display !== 'none';
+ },
+
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ const trackClientRect = this.children[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.children[i].addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ this.children[i].addEventsToTrackMap(eventToTrackMap);
+ }
+ }
+ };
+
+ return {
+ DrawingContainer,
+ DrawType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html
new file mode 100644
index 00000000000..7b778b1c332
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ let generalModel;
+ function getOrCreateGeneralModel() {
+ if (generalModel !== undefined) {
+ generalModel;
+ }
+ const fileUrl = '/test_data/thread_time_visualisation.json.gz';
+ const events = tr.b.getSync(fileUrl);
+ generalModel = tr.c.TestUtils.newModelWithEvents([events]);
+ return generalModel;
+ }
+
+ function DCPerfTestCase(testName, opt_options) {
+ tr.b.unittest.PerfTestCase.call(this, testName, undefined, opt_options);
+ this.viewportDiv = undefined;
+ this.drawingContainer = undefined;
+ this.viewport = undefined;
+ }
+ DCPerfTestCase.prototype = {
+ __proto__: tr.b.unittest.PerfTestCase.prototype,
+
+ setUp(model) {
+ this.viewportDiv = document.createElement('div');
+
+ this.viewport = new tr.ui.TimelineViewport(this.viewportDiv);
+
+ this.drawingContainer = new tr.ui.tracks.DrawingContainer(this.viewport);
+ this.viewport.modelTrackContainer = this.drawingContainer;
+
+ const modelTrack = new tr.ui.tracks.ModelTrack(this.viewport);
+ Polymer.dom(this.drawingContainer).appendChild(modelTrack);
+
+ modelTrack.model = model;
+
+ Polymer.dom(this.viewportDiv).appendChild(this.drawingContainer);
+
+ this.addHTMLOutput(this.viewportDiv);
+
+ // Size the canvas.
+ this.drawingContainer.updateCanvasSizeIfNeeded_();
+
+ // Size the viewport.
+ const w = this.drawingContainer.canvas.width;
+ const min = model.bounds.min;
+ const range = model.bounds.range;
+
+ const boost = range * 0.15;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(min - boost, min + range + boost, w);
+ this.viewport.setDisplayTransformImmediately(dt);
+ },
+
+ runOneIteration() {
+ this.drawingContainer.draw_();
+ }
+ };
+
+
+ function GeneralDCPerfTestCase(testName, opt_options) {
+ DCPerfTestCase.call(this, testName, opt_options);
+ }
+
+ GeneralDCPerfTestCase.prototype = {
+ __proto__: DCPerfTestCase.prototype,
+
+ setUp() {
+ const model = getOrCreateGeneralModel();
+ DCPerfTestCase.prototype.setUp.call(this, model);
+ }
+ };
+
+ // Failing on Chrome canary, see
+ // https://github.com/catapult-project/catapult/issues/1826
+ flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_One',
+ {iterations: 1}));
+ // Failing on Chrome stable on Windows, see
+ // https://github.com/catapult-project/catapult/issues/1908
+ flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_Ten',
+ {iterations: 10}));
+ test(new GeneralDCPerfTestCase('draw_softwareCanvas_AHundred',
+ {iterations: 100}));
+
+ function AsyncDCPerfTestCase(testName, opt_options) {
+ DCPerfTestCase.call(this, testName, opt_options);
+ }
+
+ AsyncDCPerfTestCase.prototype = {
+ __proto__: DCPerfTestCase.prototype,
+
+ setUp() {
+ const model = tr.c.TestUtils.newModel(function(m) {
+ const proc = m.getOrCreateProcess(1);
+ for (let tid = 1; tid <= 5; tid++) {
+ const thread = proc.getOrCreateThread(tid);
+ for (let i = 0; i < 5000; i++) {
+ const mod = Math.floor(i / 100) % 4;
+ const slice = tr.c.TestUtils.newAsyncSliceEx({
+ name: 'Test' + i,
+ colorId: tid + mod,
+ id: tr.b.GUID.allocateSimple(),
+ start: i * 10,
+ duration: 9,
+ isTopLevel: true
+ });
+ thread.asyncSliceGroup.push(slice);
+ }
+ }
+ });
+ DCPerfTestCase.prototype.setUp.call(this, model);
+
+ const w = this.drawingContainer.canvas.width;
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(-2000, 54000, w);
+ this.viewport.setDisplayTransformImmediately(dt);
+ }
+ };
+ test(new AsyncDCPerfTestCase('draw_asyncSliceHeavy_Twenty',
+ {iterations: 20}));
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html
new file mode 100644
index 00000000000..f8ba209d01b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * EventToTrackMap provides a mapping mechanism between events and the
+ * tracks those events belong on.
+ * @constructor
+ */
+ function EventToTrackMap() {}
+
+ EventToTrackMap.prototype = {
+ addEvent(event, track) {
+ if (!track) {
+ throw new Error('Must provide a track.');
+ }
+ this[event.guid] = track;
+ }
+ };
+
+ return {
+ EventToTrackMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html
new file mode 100644
index 00000000000..3e8de1d9831
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const startCompare = function(x, y) { return x.start - y.start; };
+
+ /**
+ * Track enabling quick selection of frame slices/events.
+ * @constructor
+ */
+ const FrameTrack = tr.ui.b.define(
+ 'frame-track', tr.ui.tracks.LetterDotTrack);
+
+ FrameTrack.prototype = {
+ __proto__: tr.ui.tracks.LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Frames';
+
+ this.frames_ = undefined;
+ this.items = undefined;
+ },
+
+ get frames() {
+ return this.frames_;
+ },
+
+ set frames(frames) {
+ this.frames_ = frames;
+ if (frames === undefined) return;
+
+ this.frames_ = this.frames_.slice();
+ this.frames_.sort(startCompare);
+
+ // letter dots
+ this.items = this.frames_.map(function(frame) {
+ return new FrameDot(frame);
+ });
+ }
+ };
+
+ /**
+ * @constructor
+ * @extends {LetterDot}
+ */
+ function FrameDot(frame) {
+ tr.ui.tracks.LetterDot.call(this, frame, 'F', frame.colorId, frame.start);
+ }
+
+ FrameDot.prototype = {
+ __proto__: tr.ui.tracks.LetterDot.prototype
+ };
+
+ return {
+ FrameTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html
new file mode 100644
index 00000000000..94189d0fecb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/frame.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/frame_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Frame = tr.model.Frame;
+ const FrameTrack = tr.ui.tracks.FrameTrack;
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createFrames = function() {
+ let frames = undefined;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(1);
+ for (let i = 1; i < 5; i++) {
+ const slice = tr.c.TestUtils.newSliceEx(
+ {title: 'work for frame', start: i * 20, duration: 10});
+ thread.sliceGroup.pushSlice(slice);
+ const events = [slice];
+ const threadTimeRanges =
+ [{thread, start: slice.start, end: slice.end}];
+ process.frames.push(new Frame(events, threadTimeRanges));
+ }
+ frames = process.frames;
+ });
+ return frames;
+ };
+
+ test('instantiate', function() {
+ const frames = createFrames();
+ frames[1].selectionState = SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = FrameTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.frames = frames;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.strictEqual(track.items[0].start, 20);
+ });
+
+ test('modelMapping', function() {
+ const frames = createFrames();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = FrameTrack(viewport);
+ track.frames = frames;
+
+ const a0 = track.items[0].modelItem;
+ assert.strictEqual(a0, frames[0]);
+ });
+
+ test('selectionMapping', function() {
+ const frames = createFrames();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = FrameTrack(viewport);
+ track.frames = frames;
+
+ const selection = new EventSet();
+ track.items[0].addToSelection(selection);
+
+ // select both frame, but not its component slice
+ assert.strictEqual(selection.length, 1);
+
+ let frameCount = 0;
+ let eventCount = 0;
+ selection.forEach(function(event) {
+ if (event instanceof Frame) {
+ assert.strictEqual(event, frames[0]);
+ frameCount++;
+ } else {
+ eventCount++;
+ }
+ });
+ assert.strictEqual(frameCount, 1);
+ assert.strictEqual(eventCount, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html
new file mode 100644
index 00000000000..aaf9bc0a80d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const USED_MEMORY_TRACK_HEIGHT = 50;
+ const ALLOCATED_MEMORY_TRACK_HEIGHT = 50;
+
+ /**
+ * A track that displays an array of GlobalMemoryDump objects.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const GlobalMemoryDumpTrack = tr.ui.b.define(
+ 'global-memory-dump-track', tr.ui.tracks.ContainerTrack);
+
+ GlobalMemoryDumpTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.memoryDumps_ = undefined;
+ },
+
+ get memoryDumps() {
+ return this.memoryDumps_;
+ },
+
+ set memoryDumps(memoryDumps) {
+ this.memoryDumps_ = memoryDumps;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ // Show no tracks if there are no dumps.
+ if (!this.memoryDumps_ || !this.memoryDumps_.length) return;
+
+ this.appendDumpDotsTrack_();
+ this.appendUsedMemoryTrack_();
+ this.appendAllocatedMemoryTrack_();
+ },
+
+ appendDumpDotsTrack_() {
+ const items = tr.ui.tracks.buildMemoryLetterDots(this.memoryDumps_);
+ if (!items) return;
+
+ const track = new tr.ui.tracks.LetterDotTrack(this.viewport);
+ track.heading = 'Memory Dumps';
+ track.items = items;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendUsedMemoryTrack_() {
+ const tracks = [];
+ const perProcessSeries =
+ tr.ui.tracks.buildGlobalUsedMemoryChartSeries(this.memoryDumps_);
+ if (perProcessSeries !== undefined) {
+ tracks.push({name: 'Memory per process', series: perProcessSeries});
+ } else {
+ tracks.push.apply(tracks, tr.ui.tracks.buildSystemMemoryChartSeries(
+ this.memoryDumps_[0].model));
+ }
+
+ for (const {name, series} of tracks) {
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = name;
+ track.height = USED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ },
+
+ appendAllocatedMemoryTrack_() {
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ this.memoryDumps_);
+ if (!series) return;
+
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = 'Memory per component';
+ track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ };
+
+ return {
+ GlobalMemoryDumpTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html
new file mode 100644
index 00000000000..20fa869bbf0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Viewport = tr.ui.TimelineViewport;
+ const GlobalMemoryDumpTrack = tr.ui.tracks.GlobalMemoryDumpTrack;
+ const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps;
+
+ function instantiateTrack(withVMRegions, withAllocatorDumps,
+ expectedTrackCount) {
+ const dumps = createTestGlobalMemoryDumps(
+ withVMRegions, withAllocatorDumps);
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new GlobalMemoryDumpTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.invalidate();
+
+ track.memoryDumps = dumps;
+ this.addHTMLOutput(div);
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.lengthOf(track.tracks_, expectedTrackCount);
+ }
+
+ test('instantiate_dotsOnly', function() {
+ instantiateTrack.call(this, false, false, 1);
+ });
+
+ test('instantiate_withVMRegions', function() {
+ instantiateTrack.call(this, true, false, 2);
+ });
+
+ test('instantiate_withMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, true, 2);
+ });
+
+ test('instantiate_withBoth', function() {
+ instantiateTrack.call(this, true, true, 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html
new file mode 100644
index 00000000000..7ae139672d2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/kernel_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of interaction records.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const InteractionTrack = tr.ui.b.define(
+ 'interaction-track', tr.ui.tracks.MultiRowTrack);
+
+ InteractionTrack.prototype = {
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Interactions';
+ this.subRows_ = [];
+ },
+
+ set model(model) {
+ this.setItemsToGroup(model.userModel.expectations, {
+ guid: tr.b.GUID.allocateSimple(),
+ model,
+ getSettingsKey() {
+ return undefined;
+ }
+ });
+ },
+
+ buildSubRows_(slices) {
+ if (this.subRows_.length) {
+ return this.subRows_;
+ }
+ this.subRows_.push(
+ ...tr.ui.tracks.groupAsyncSlicesIntoSubRows(slices, true));
+ return this.subRows_;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ }
+ };
+
+ return {
+ InteractionTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html
new file mode 100644
index 00000000000..ac0d5692c4d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/interaction_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // UserExpectations should be sorted by start time, not title, so that
+ // AsyncSliceGroupTrack.buildSubRows_ can lay them out in as few tracks as
+ // possible, so that they mesh instead of stacking unnecessarily.
+ test('instantiate', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+ const track = new tr.ui.tracks.InteractionTrack(viewport);
+ track.model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(1);
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 0, duration: 200}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 100, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 150, duration: 50}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 50, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 50}));
+ // Model.createImportTracesTask() automatically sorts UEs by start time.
+ });
+ assert.strictEqual(2, track.subRows_.length);
+ assert.strictEqual(2, track.subRows_[0].length);
+ assert.strictEqual(3, track.subRows_[1].length);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html
new file mode 100644
index 00000000000..b10547bc2e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/cpu_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track_base.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const Cpu = tr.model.Cpu;
+ const CpuTrack = tr.ui.tracks.cpu_track;
+ const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase;
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ /**
+ * @constructor
+ */
+ const KernelTrack = tr.ui.b.define('kernel-track', ProcessTrackBase);
+
+ KernelTrack.prototype = {
+ __proto__: ProcessTrackBase.prototype,
+
+ decorate(viewport) {
+ ProcessTrackBase.prototype.decorate.call(this, viewport);
+ },
+
+
+ // Kernel maps to processBase because we derive from ProcessTrackBase.
+ set kernel(kernel) {
+ this.processBase = kernel;
+ },
+
+ get kernel() {
+ return this.processBase;
+ },
+
+ get eventContainer() {
+ return this.kernel;
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 1;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.call(
+ this, containerToTrackMap);
+ containerToTrackMap.addContainer(this.kernel, this);
+ },
+
+ willAppendTracks_() {
+ const cpus = Object.values(this.kernel.cpus);
+ cpus.sort(tr.model.Cpu.compare);
+
+ let didAppendAtLeastOneTrack = false;
+ for (let i = 0; i < cpus.length; ++i) {
+ const cpu = cpus[i];
+ const track = new tr.ui.tracks.CpuTrack(this.viewport);
+ track.detailedMode = this.expanded;
+ track.cpu = cpu;
+ if (!track.hasVisibleContent) continue;
+ Polymer.dom(this).appendChild(track);
+ didAppendAtLeastOneTrack = true;
+ }
+ if (didAppendAtLeastOneTrack) {
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ }
+ };
+
+
+ return {
+ KernelTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html
new file mode 100644
index 00000000000..6a642e52ff9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.letter-dot-track {
+ height: 18px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * A track that displays an array of dots with filled letters inside them.
+ * @constructor
+ * @extends {Track}
+ */
+ const LetterDotTrack = tr.ui.b.define(
+ 'letter-dot-track', tr.ui.tracks.Track);
+
+ LetterDotTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('letter-dot-track');
+ this.items_ = undefined;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get items() {
+ return this.items_;
+ },
+
+ set items(items) {
+ this.items_ = items;
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ get dumpRadiusView() {
+ return 7 * (window.devicePixelRatio || 1);
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ if (this.items_ === undefined) return;
+
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawLetterDots_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawLetterDots_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const height = bounds.height * pixelRatio;
+ const halfHeight = height * 0.5;
+ const twoPi = Math.PI * 2;
+
+ // Culling parameters.
+ const dt = this.viewport.currentDisplayTransform;
+ const dumpRadiusView = this.dumpRadiusView;
+ const itemRadiusWorld = dt.xViewVectorToWorld(height);
+
+ // Draw the memory dumps.
+ const items = this.items_;
+ const loI = tr.b.findLowIndexInSortedArray(
+ items,
+ function(item) { return item.start; },
+ viewLWorld);
+
+ const oldFont = ctx.font;
+ ctx.font = '400 ' + Math.floor(9 * pixelRatio) + 'px Arial';
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+
+ const drawItems = function(selected) {
+ for (let i = loI; i < items.length; ++i) {
+ const item = items[i];
+ const x = item.start;
+ if (x - itemRadiusWorld > viewRWorld) break;
+
+ if (item.selected !== selected) continue;
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.fillStyle = EventPresenter.getSelectableItemColorAsString(item);
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, dumpRadiusView + 0.5, 0, twoPi);
+ ctx.fill();
+ if (item.selected) {
+ ctx.lineWidth = 3;
+ ctx.strokeStyle = 'rgb(100,100,0)';
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, dumpRadiusView, 0, twoPi);
+ ctx.lineWidth = 1.5;
+ ctx.strokeStyle = 'rgb(255,255,0)';
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.stroke();
+ }
+
+ ctx.fillStyle = 'rgb(255, 255, 255)';
+ ctx.fillText(item.dotLetter, xView, halfHeight);
+ }
+ };
+
+ // Draw unselected items first to make sure they don't occlude selected
+ // items.
+ drawItems(false);
+ drawItems(true);
+
+ ctx.lineWidth = 1;
+ ctx.font = oldFont;
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.items_ === undefined) return;
+
+ this.items_.forEach(function(item) {
+ item.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ if (this.items_ === undefined) return;
+
+ const itemRadiusWorld = viewPixWidthWorld * this.dumpRadiusView;
+ tr.b.iterateOverIntersectingIntervals(
+ this.items_,
+ function(x) { return x.start - itemRadiusWorld; },
+ function(x) { return 2 * itemRadiusWorld; },
+ loWX, hiWX,
+ function(item) {
+ item.addToSelection(selection);
+ }.bind(this));
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {event} The current event item.
+ * @param {Number} offset Number of slices away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (this.items_ === undefined) return;
+
+ const index = this.items_.findIndex(item => item.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < this.items_.length) {
+ this.items_[newIndex].addToSelection(selection);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ if (this.items_ === undefined) return;
+
+ const item = tr.b.findClosestElementInSortedArray(
+ this.items_,
+ function(x) { return x.start; },
+ worldX,
+ worldMaxDist);
+
+ if (!item) return;
+
+ item.addToSelection(selection);
+ }
+ };
+
+ /**
+ * A filled dot with a letter inside it.
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function LetterDot(modelItem, dotLetter, colorId, start) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.dotLetter = dotLetter;
+ this.colorId = colorId;
+ this.start = start;
+ }
+
+ LetterDot.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype
+ };
+
+ return {
+ LetterDotTrack,
+ LetterDot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html
new file mode 100644
index 00000000000..b37034afab2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const LetterDotTrack = tr.ui.tracks.LetterDotTrack;
+ const LetterDot = tr.ui.tracks.LetterDot;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createItems = function() {
+ const items = [
+ new LetterDot({selectionState: SelectionState.SELECTED}, 'a', 7, 5),
+ new LetterDot({selectionState: SelectionState.SELECTED}, 'b', 2, 20),
+ new LetterDot({selectionState: SelectionState.NONE}, 'c', 4, 35),
+ new LetterDot({selectionState: SelectionState.NONE}, 'd', 4, 50)
+ ];
+ return items;
+ };
+
+ test('instantiate', function() {
+ const items = createItems();
+
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = LetterDotTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.items = items;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 60, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('selectionHitTesting', function() {
+ const items = createItems();
+
+ const track = new LetterDotTrack(new Viewport());
+ track.items = items;
+
+ // Fake a view pixel size.
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const viewPixWidthWorld = 0.1 / devicePixelRatio;
+
+ // Hit outside range
+ let selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 3, 4, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 0);
+
+ // Hit the first item, via pixel-nearness.
+ selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 19.98, 19.99, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.strictEqual(selection[0], items[1].modelItem);
+
+ // Hit the instance, between the 1st and 2nd snapshots
+ selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 30, 50, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 2);
+ assert.strictEqual(selection[0], items[2].modelItem);
+ assert.strictEqual(selection[1], items[3].modelItem);
+ });
+
+ test('addEventNearToProvidedEventToSelection', function() {
+ const items = createItems();
+
+ const track = new LetterDotTrack(new Viewport());
+ track.items = items;
+
+ // Right from the middle of items.
+ const selection1 = [];
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ items[2].modelItem, 1, selection1));
+ assert.strictEqual(selection1.length, 1);
+ assert.strictEqual(selection1[0], items[3].modelItem);
+
+ // Left from the middle of items.
+ const selection2 = [];
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ items[2].modelItem, -1, selection2));
+ assert.strictEqual(selection2.length, 1);
+ assert.strictEqual(selection2[0], items[1].modelItem);
+
+ // Right from the right edge of items.
+ const selection3 = [];
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ items[3].modelItem, 1, selection3));
+ assert.strictEqual(selection3.length, 0);
+
+ // Left from the left edge of items.
+ const selection4 = [];
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ items[0].modelItem, -1, selection4));
+ assert.strictEqual(selection4.length, 0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html
new file mode 100644
index 00000000000..611bd8664f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for memory dump track tests.
+ */
+tr.exportTo('tr.ui.tracks', function() {
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const SelectionState = tr.model.SelectionState;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ function createVMRegions(pssValues) {
+ return VMRegionClassificationNode.fromRegions(
+ pssValues.map(function(pssValue, i) {
+ return VMRegion.fromDict({
+ startAddress: 1000 * i,
+ sizeInBytes: 1000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack' + i + ']',
+ byteStats: {
+ privateDirtyResident: pssValue / 3,
+ swapped: pssValue * 3,
+ proportionalResident: pssValue
+ }
+ });
+ }));
+ }
+
+ function createAllocatorDumps(memoryDump, dumpData) {
+ // Create the individual allocator dumps.
+ const allocatorDumps = {};
+ for (const [allocatorName, data] of Object.entries(dumpData)) {
+ const size = data.size;
+ assert.typeOf(size, 'number'); // Sanity check.
+ allocatorDumps[allocatorName] = newAllocatorDump(
+ memoryDump, allocatorName, {numerics: {size}});
+ }
+
+ // Add ownership links between them.
+ for (const [allocatorName, data] of Object.entries(dumpData)) {
+ const owns = data.owns;
+ if (owns === undefined) continue;
+
+ const ownerDump = allocatorDumps[allocatorName];
+ assert.isDefined(ownerDump); // Sanity check.
+ const ownedDump = allocatorDumps[owns];
+ assert.isDefined(ownedDump); // Sanity check.
+
+ addOwnershipLink(ownerDump, ownedDump);
+ }
+
+ return Object.values(allocatorDumps);
+ }
+
+ function addProcessMemoryDumpWithFields(globalMemoryDump, process, start,
+ opt_pssValues, opt_dumpData) {
+ const pmd = addProcessMemoryDump(globalMemoryDump, process, {ts: start});
+ if (opt_pssValues !== undefined) {
+ pmd.vmRegions = createVMRegions(opt_pssValues);
+ }
+ if (opt_dumpData !== undefined) {
+ pmd.memoryAllocatorDumps = createAllocatorDumps(pmd, opt_dumpData);
+ }
+ }
+
+ function createModelWithDumps(withVMRegions, withAllocatorDumps) {
+ const maybePssValues = function(pssValues) {
+ return withVMRegions ? pssValues : undefined;
+ };
+ const maybeDumpData = function(dumpData) {
+ return withAllocatorDumps ? dumpData : undefined;
+ };
+ return tr.c.TestUtils.newModel(function(model) {
+ // Construct a model with three processes.
+ const pa = model.getOrCreateProcess(3);
+ const pb = model.getOrCreateProcess(6);
+ const pc = model.getOrCreateProcess(9);
+
+ const gmd1 = addGlobalMemoryDump(model, {ts: 0, levelOfDetail: LIGHT});
+ addProcessMemoryDumpWithFields(gmd1, pa, 0, maybePssValues([111]));
+ addProcessMemoryDumpWithFields(gmd1, pb, 0.2, undefined,
+ maybeDumpData({oilpan: {size: 1024}}));
+
+ const gmd2 = addGlobalMemoryDump(model, {ts: 5, levelOfDetail: DETAILED});
+ addProcessMemoryDumpWithFields(gmd2, pa, 0);
+ addProcessMemoryDumpWithFields(gmd2, pb, 4.99, maybePssValues([100, 50]),
+ maybeDumpData({v8: {size: 512}}));
+ addProcessMemoryDumpWithFields(gmd2, pc, 5.12, undefined,
+ maybeDumpData({oilpan: {size: 128, owns: 'v8'},
+ v8: {size: 384, owns: 'tracing'}, tracing: {size: 65920}}));
+
+ const gmd3 = addGlobalMemoryDump(
+ model, {ts: 15, levelOfDetail: DETAILED});
+ addProcessMemoryDumpWithFields(gmd3, pa, 15.5, maybePssValues([]),
+ maybeDumpData({v8: {size: 768}}));
+ addProcessMemoryDumpWithFields(gmd3, pc, 14.5,
+ maybePssValues([70, 70, 70]), maybeDumpData({oilpan: {size: 512}}));
+
+ const gmd4 = addGlobalMemoryDump(model, {ts: 18, levelOfDetail: LIGHT});
+
+ const gmd5 = addGlobalMemoryDump(model,
+ {ts: 20, levelOfDetail: BACKGROUND});
+ addProcessMemoryDumpWithFields(gmd5, pa, 0, maybePssValues([105]));
+ addProcessMemoryDumpWithFields(gmd5, pb, 0.2, undefined,
+ maybeDumpData({oilpan: {size: 100}}));
+ });
+ }
+
+ function createTestGlobalMemoryDumps(withVMRegions, withAllocatorDumps) {
+ const model = createModelWithDumps(withVMRegions, withAllocatorDumps);
+ const dumps = model.globalMemoryDumps;
+ dumps[1].selectionState = SelectionState.HIGHLIGHTED;
+ dumps[2].selectionState = SelectionState.SELECTED;
+ return dumps;
+ }
+
+ function createTestProcessMemoryDumps(withVMRegions, withAllocatorDumps) {
+ const model = createModelWithDumps(withVMRegions, withAllocatorDumps);
+ const dumps = model.getProcess(9).memoryDumps;
+ dumps[0].selectionState = SelectionState.SELECTED;
+ dumps[1].selectionState = SelectionState.HIGHLIGHTED;
+ return dumps;
+ }
+
+ return {
+ createTestGlobalMemoryDumps,
+ createTestProcessMemoryDumps,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html
new file mode 100644
index 00000000000..a483d545880
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const DISPLAYED_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ const SYSTEM_MEMORY_CHART_RENDERING_CONFIG = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: ColorScheme.getColorIdForGeneralPurposeString('systemMemory'),
+ backgroundOpacity: 0.8
+ };
+ const SYSTEM_MEMORY_SERIES_NAMES = ['Used (KB)', 'Swapped (KB)'];
+
+ /** Extract PSS values of processes in a global memory dump. */
+ function extractGlobalMemoryDumpUsedSizes(globalMemoryDump, addSize) {
+ for (const [pid, pmd] of
+ Object.entries(globalMemoryDump.processMemoryDumps)) {
+ const mostRecentVmRegions = pmd.mostRecentVmRegions;
+ if (mostRecentVmRegions === undefined) continue;
+ addSize(pid, mostRecentVmRegions.byteStats.proportionalResident || 0,
+ pmd.process.userFriendlyName);
+ }
+ }
+
+ /** Extract sizes of root allocators in a process memory dump. */
+ function extractProcessMemoryDumpAllocatorSizes(processMemoryDump, addSize) {
+ const allocatorDumps = processMemoryDump.memoryAllocatorDumps;
+ if (allocatorDumps === undefined) return;
+
+ allocatorDumps.forEach(function(allocatorDump) {
+ // Don't show tracing overhead in the charts.
+ // TODO(petrcermak): Find a less hacky way to do this.
+ if (allocatorDump.fullName === 'tracing') return;
+
+ const allocatorSize = allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME];
+ if (allocatorSize === undefined) return;
+
+ const allocatorSizeValue = allocatorSize.value;
+ if (allocatorSizeValue === undefined) return;
+
+ addSize(allocatorDump.fullName, allocatorSizeValue);
+ });
+ }
+
+ /** Extract sizes of root allocators in a global memory dump. */
+ function extractGlobalMemoryDumpAllocatorSizes(globalMemoryDump, addSize) {
+ for (const pmd of Object.values(globalMemoryDump.processMemoryDumps)) {
+ extractProcessMemoryDumpAllocatorSizes(pmd, addSize);
+ }
+ }
+
+ /**
+ * A generic function which converts a list of memory dumps to a list of
+ * chart series.
+ *
+ * @param {!Array<!tr.model.ContainerMemoryDump>} memoryDumps List of
+ * container memory dumps.
+ * @param {!function(
+ * !tr.model.ContainerMemoryDump,
+ * !function(string, number, string=))} dumpSizeExtractor Callback for
+ * extracting sizes from a container memory dump.
+ * @return {(!Array<!tr.ui.tracks.ChartSeries>|undefined)} List of chart
+ * series (or undefined if no size is extracted from any container memory
+ * dump).
+ */
+ function buildMemoryChartSeries(memoryDumps, dumpSizeExtractor) {
+ const dumpCount = memoryDumps.length;
+ const idToTimestampToPoint = {};
+ const idToName = {};
+
+ // Extract the sizes of all components from each memory dump.
+ memoryDumps.forEach(function(dump, index) {
+ dumpSizeExtractor(dump, function addSize(id, size, opt_name) {
+ let timestampToPoint = idToTimestampToPoint[id];
+ if (timestampToPoint === undefined) {
+ idToTimestampToPoint[id] = timestampToPoint = new Array(dumpCount);
+ for (let i = 0; i < dumpCount; i++) {
+ const modelItem = memoryDumps[i];
+ timestampToPoint[i] = new tr.ui.tracks.ChartPoint(
+ modelItem, modelItem.start, 0);
+ }
+ }
+ timestampToPoint[index].y += size;
+ if (opt_name !== undefined) idToName[id] = opt_name;
+ });
+ });
+
+ // Do not generate any chart series if no sizes were extracted.
+ const ids = Object.keys(idToTimestampToPoint);
+ if (ids.length === 0) return undefined;
+
+ ids.sort();
+ for (let i = 0; i < dumpCount; i++) {
+ let baseSize = 0;
+ // Traverse |ids| in reverse (alphabetical) order so that the first id is
+ // at the top of the chart.
+ for (let j = ids.length - 1; j >= 0; j--) {
+ const point = idToTimestampToPoint[ids[j]][i];
+ point.yBase = baseSize;
+ point.y += baseSize;
+ baseSize = point.y;
+ }
+ }
+
+ // Create one common axis for all memory chart series.
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0);
+
+ // Build a chart series for each id.
+ const series = ids.map(function(id) {
+ const colorId = ColorScheme.getColorIdForGeneralPurposeString(
+ idToName[id] || id);
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId,
+ backgroundOpacity: 0.8
+ };
+ return new tr.ui.tracks.ChartSeries(idToTimestampToPoint[id],
+ seriesYAxis, renderingConfig);
+ });
+
+ // Ensure that the series at the top of the chart are drawn last.
+ series.reverse();
+
+ return series;
+ }
+
+ /**
+ * Transform a list of memory dumps to a list of letter dots (with letter 'M'
+ * inside).
+ */
+ function buildMemoryLetterDots(memoryDumps) {
+ const backgroundMemoryColorId =
+ ColorScheme.getColorIdForReservedName('background_memory_dump');
+ const lightMemoryColorId =
+ ColorScheme.getColorIdForReservedName('light_memory_dump');
+ const detailedMemoryColorId =
+ ColorScheme.getColorIdForReservedName('detailed_memory_dump');
+ return memoryDumps.map(function(memoryDump) {
+ let memoryColorId;
+ switch (memoryDump.levelOfDetail) {
+ case BACKGROUND:
+ memoryColorId = backgroundMemoryColorId;
+ break;
+ case DETAILED:
+ memoryColorId = detailedMemoryColorId;
+ break;
+ case LIGHT:
+ default:
+ memoryColorId = lightMemoryColorId;
+ }
+ return new tr.ui.tracks.LetterDot(
+ memoryDump, 'M', memoryColorId, memoryDump.start);
+ });
+ }
+
+ /**
+ * Convert a list of global memory dumps to a list of chart series (one per
+ * process). Each series represents the evolution of the memory used by the
+ * process over time.
+ */
+ function buildGlobalUsedMemoryChartSeries(globalMemoryDumps) {
+ return buildMemoryChartSeries(globalMemoryDumps,
+ extractGlobalMemoryDumpUsedSizes);
+ }
+
+ /**
+ * Convert a list of process memory dumps to a list of chart series (one per
+ * root allocator). Each series represents the evolution of the size of a the
+ * corresponding root allocator (e.g. 'v8') over time.
+ */
+ function buildProcessAllocatedMemoryChartSeries(processMemoryDumps) {
+ return buildMemoryChartSeries(processMemoryDumps,
+ extractProcessMemoryDumpAllocatorSizes);
+ }
+
+ /**
+ * Convert a list of global memory dumps to a list of chart series (one per
+ * root allocator). Each series represents the evolution of the size of a the
+ * corresponding root allocator (e.g. 'v8') over time.
+ */
+ function buildGlobalAllocatedMemoryChartSeries(globalMemoryDumps) {
+ return buildMemoryChartSeries(globalMemoryDumps,
+ extractGlobalMemoryDumpAllocatorSizes);
+ }
+
+ /**
+ * Converts system memory counters in the model to a list of
+ * {'name': trackName, 'series': ChartSeries}.
+ */
+ function buildSystemMemoryChartSeries(model) {
+ if (model.kernel.counters === undefined) return;
+ const memoryCounter = model.kernel.counters['global.SystemMemory'];
+ if (memoryCounter === undefined) return;
+
+ const tracks = [];
+ for (const name of SYSTEM_MEMORY_SERIES_NAMES) {
+ const series = memoryCounter.series.find(series => series.name === name);
+ if (series === undefined || series.samples.length === 0) return;
+
+ const chartPoints = [];
+ const valueRange = new tr.b.math.Range();
+ for (const sample of series.samples) {
+ chartPoints.push(new tr.ui.tracks.ChartPoint(
+ sample, sample.timestamp, sample.value, 0));
+ valueRange.addValue(sample.value);
+ }
+ // Stretch min to max range over the top half of a chart for readability.
+ const baseLine = Math.max(0, valueRange.min - valueRange.range);
+ const axisY = new tr.ui.tracks.ChartSeriesYAxis(baseLine, valueRange.max);
+ const chartSeries =
+ [new tr.ui.tracks.ChartSeries(chartPoints, axisY,
+ SYSTEM_MEMORY_CHART_RENDERING_CONFIG)];
+ tracks.push({
+ name: 'System Memory ' + name,
+ series: chartSeries
+ });
+ }
+ return tracks;
+ }
+
+ return {
+ buildMemoryLetterDots,
+ buildGlobalUsedMemoryChartSeries,
+ buildProcessAllocatedMemoryChartSeries,
+ buildGlobalAllocatedMemoryChartSeries,
+ buildSystemMemoryChartSeries,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html
new file mode 100644
index 00000000000..f4f9451b4b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html
@@ -0,0 +1,270 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SelectionState = tr.model.SelectionState;
+ const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps;
+ const createTestProcessMemoryDumps =
+ tr.ui.tracks.createTestProcessMemoryDumps;
+
+ test('buildMemoryLetterDots_withoutVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const items = tr.ui.tracks.buildMemoryLetterDots(dumps);
+
+ assert.lengthOf(items, 5);
+ assert.strictEqual(items[0].start, 0);
+ assert.strictEqual(items[1].start, 5);
+ assert.strictEqual(items[2].start, 15);
+ assert.strictEqual(items[3].start, 18);
+ assert.strictEqual(items[4].start, 20);
+
+ // Check model mapping.
+ assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(items[2].selected);
+ assert.strictEqual(items[3].modelItem, dumps[3]);
+ });
+
+ test('buildMemoryLetterDots_withVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const items = tr.ui.tracks.buildMemoryLetterDots(dumps);
+
+ assert.lengthOf(items, 5);
+ assert.strictEqual(items[0].start, 0);
+ assert.strictEqual(items[1].start, 5);
+ assert.strictEqual(items[2].start, 15);
+ assert.strictEqual(items[3].start, 18);
+ assert.strictEqual(items[4].start, 20);
+
+ // Check model mapping.
+ assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(items[2].selected);
+ assert.strictEqual(items[3].modelItem, dumps[3]);
+ });
+
+ test('buildGlobalUsedMemoryChartSeries_withoutVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildGlobalUsedMemoryChartSeries_withVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(true, false);
+ const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps);
+
+ assert.lengthOf(series, 3);
+
+ const sa = series[2];
+ const sb = series[1];
+ const sc = series[0];
+
+ assert.lengthOf(sa.points, 5);
+ assert.lengthOf(sb.points, 5);
+ assert.lengthOf(sc.points, 5);
+
+ // Process A: VM regions defined -> sum their PSS values (111).
+ // Process B: VM regions undefined and no previous value -> assume zero.
+ // Process C: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[0].x, 0);
+ assert.strictEqual(sb.points[0].x, 0);
+ assert.strictEqual(sc.points[0].x, 0);
+ assert.strictEqual(sa.points[0].y, 111);
+ assert.strictEqual(sb.points[0].y, 0);
+ assert.strictEqual(sc.points[0].y, 0);
+ assert.strictEqual(sa.points[0].yBase, 0);
+ assert.strictEqual(sb.points[0].yBase, 0);
+ assert.strictEqual(sc.points[0].yBase, 0);
+
+ // Process A: VM regions undefined -> assume previous value (111).
+ // Process B: VM regions defined -> sum their PSS values (100 + 50).
+ // Process C: VM regions undefined -> assume previous value (0).
+ assert.strictEqual(sa.points[1].x, 5);
+ assert.strictEqual(sb.points[1].x, 5);
+ assert.strictEqual(sc.points[1].x, 5);
+ assert.strictEqual(sa.points[1].y, 150 + 111);
+ assert.strictEqual(sb.points[1].y, 150);
+ assert.strictEqual(sc.points[1].y, 0);
+ assert.strictEqual(sa.points[1].yBase, 150);
+ assert.strictEqual(sb.points[1].yBase, 0);
+ assert.strictEqual(sc.points[1].yBase, 0);
+
+ // Process A: VM regions defined -> sum their PSS values (0).
+ // Process B: Memory dump not present -> assume process not alive (0).
+ // Process C: VM regions defined -> sum their PSS values (70 + 70 + 70).
+ assert.strictEqual(sa.points[2].x, 15);
+ assert.strictEqual(sb.points[2].x, 15);
+ assert.strictEqual(sc.points[2].x, 15);
+ assert.strictEqual(sa.points[2].y, 210);
+ assert.strictEqual(sb.points[2].y, 210);
+ assert.strictEqual(sc.points[2].y, 210);
+ assert.strictEqual(sa.points[2].yBase, 210);
+ assert.strictEqual(sb.points[2].yBase, 210);
+ assert.strictEqual(sc.points[2].yBase, 0);
+
+ // All processes: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[3].x, 18);
+ assert.strictEqual(sb.points[3].x, 18);
+ assert.strictEqual(sc.points[3].x, 18);
+ assert.strictEqual(sa.points[3].y, 0);
+ assert.strictEqual(sb.points[3].y, 0);
+ assert.strictEqual(sc.points[3].y, 0);
+ assert.strictEqual(sa.points[3].yBase, 0);
+ assert.strictEqual(sb.points[3].yBase, 0);
+ assert.strictEqual(sc.points[3].yBase, 0);
+
+ // Process A: VM regions defined -> sum their PSS values (105).
+ // Process B: VM regions undefined and no previous value -> assume zero.
+ // Process C: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[4].x, 20);
+ assert.strictEqual(sb.points[4].x, 20);
+ assert.strictEqual(sc.points[4].x, 20);
+ assert.strictEqual(sa.points[4].y, 105);
+ assert.strictEqual(sb.points[4].y, 0);
+ assert.strictEqual(sc.points[4].y, 0);
+ assert.strictEqual(sa.points[4].yBase, 0);
+ assert.strictEqual(sb.points[4].yBase, 0);
+ assert.strictEqual(sc.points[4].yBase, 0);
+
+ // Check model mapping.
+ assert.strictEqual(sa.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sb.points[2].selected);
+ assert.strictEqual(sc.points[3].modelItem, dumps[3]);
+ });
+
+ test('buildGlobalAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildGlobalAllocatedMemoryChartSeries_withMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestGlobalMemoryDumps(false, true);
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.lengthOf(series, 2);
+
+ const so = series[1];
+ const sv = series[0];
+
+ assert.lengthOf(so.points, 5);
+ assert.lengthOf(sv.points, 5);
+
+ // Oilpan: Only process B dumps allocated objects size (1024).
+ // V8: No process dumps allocated objects size (0).
+ assert.strictEqual(so.points[0].x, 0);
+ assert.strictEqual(sv.points[0].x, 0);
+ assert.strictEqual(so.points[0].y, 1024);
+ assert.strictEqual(sv.points[0].y, 0);
+ assert.strictEqual(so.points[0].yBase, 0);
+ assert.strictEqual(sv.points[0].yBase, 0);
+
+ // Oilpan: Process B did not provide a value and process C dumps (128).
+ // V8: Processes B and C dump (512 + 256).
+ assert.strictEqual(so.points[1].x, 5);
+ assert.strictEqual(sv.points[1].x, 5);
+ assert.strictEqual(so.points[1].y, 768 + 128);
+ assert.strictEqual(sv.points[1].y, 768);
+ assert.strictEqual(so.points[1].yBase, 768);
+ assert.strictEqual(sv.points[1].yBase, 0);
+
+ // Oilpan: Process B assumed not alive and process C dumps (512)
+ // V8: Process A dumps now, process B assumed not alive, process C did
+ // not provide a value (768).
+ assert.strictEqual(so.points[2].x, 15);
+ assert.strictEqual(sv.points[2].x, 15);
+ assert.strictEqual(so.points[2].y, 768 + 512);
+ assert.strictEqual(sv.points[2].y, 768);
+ assert.strictEqual(so.points[2].yBase, 768);
+ assert.strictEqual(sv.points[2].yBase, 0);
+
+ // All processes: Memory dump not present -> assume process not alive
+ // (0).
+ assert.strictEqual(so.points[3].x, 18);
+ assert.strictEqual(sv.points[3].x, 18);
+ assert.strictEqual(so.points[3].y, 0);
+ assert.strictEqual(sv.points[3].y, 0);
+ assert.strictEqual(so.points[3].yBase, 0);
+ assert.strictEqual(sv.points[3].yBase, 0);
+
+ // Oilpan: Only process B dumps allocated objects size (100).
+ // V8: No process dumps allocated objects size (0).
+ assert.strictEqual(so.points[4].x, 20);
+ assert.strictEqual(sv.points[4].x, 20);
+ assert.strictEqual(so.points[4].y, 100);
+ assert.strictEqual(sv.points[4].y, 0);
+
+ // Check model mapping.
+ assert.strictEqual(
+ so.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sv.points[2].selected);
+ assert.strictEqual(so.points[3].modelItem, dumps[3]);
+ });
+
+ test('buildProcessAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestProcessMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildProcessAllocatedMemoryChartSeries_withMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestProcessMemoryDumps(false, true);
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ dumps);
+
+ // There should be only 2 series (because 'tracing' is not shown in the
+ // charts).
+ assert.lengthOf(series, 2);
+
+ const so = series[1];
+ const sv = series[0];
+
+ assert.lengthOf(so.points, 2);
+ assert.lengthOf(sv.points, 2);
+
+ // Oilpan: Process dumps (128).
+ // V8: Process dumps (256).
+ assert.strictEqual(so.points[0].x, 5.12);
+ assert.strictEqual(sv.points[0].x, 5.12);
+ assert.strictEqual(so.points[0].y, 256 + 128);
+ assert.strictEqual(sv.points[0].y, 256);
+ assert.strictEqual(so.points[0].yBase, 256);
+ assert.strictEqual(sv.points[0].yBase, 0);
+
+ // Oilpan: Process dumps (512).
+ // V8: Process did not provide a value (0).
+ assert.strictEqual(so.points[1].x, 14.5);
+ assert.strictEqual(sv.points[1].x, 14.5);
+ assert.strictEqual(so.points[1].y, 512);
+ assert.strictEqual(sv.points[1].y, 0);
+ assert.strictEqual(so.points[1].yBase, 0);
+ assert.strictEqual(sv.points[1].yBase, 0);
+
+ // Check model mapping.
+ assert.strictEqual(
+ so.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sv.points[0].selected);
+ assert.strictEqual(so.points[1].modelItem, dumps[1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html
new file mode 100644
index 00000000000..bf59f11349e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const LetterDotTrack = tr.ui.tracks.LetterDotTrack;
+
+ /**
+ * A track that displays global memory events.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.LetterDotTrack}
+ */
+ const MemoryTrack = tr.ui.b.define('memory-track', LetterDotTrack);
+
+ MemoryTrack.prototype = {
+ __proto__: LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.classList.add('memory-track');
+ this.heading = 'Memory Events';
+ this.lowMemoryEvents_ = undefined;
+ },
+
+ initialize(model) {
+ if (model !== undefined) {
+ this.lowMemoryEvents_ = model.device.lowMemoryEvents;
+ } else {
+ this.lowMemoryEvents_ = undefined;
+ }
+
+ if (this.hasVisibleContent) {
+ this.items = this.buildMemoryLetterDots_(this.lowMemoryEvents_);
+ }
+ },
+
+ get hasVisibleContent() {
+ return !!this.lowMemoryEvents_ && this.lowMemoryEvents_.length !== 0;
+ },
+
+ buildMemoryLetterDots_(memoryEvents) {
+ return memoryEvents.map(
+ memoryEvent => new tr.ui.tracks.LetterDot(
+ memoryEvent,
+ 'K',
+ ColorScheme.getColorIdForReservedName('background_memory_dump'),
+ memoryEvent.start
+ )
+ );
+ },
+ };
+
+ return {
+ MemoryTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html
new file mode 100644
index 00000000000..60f1c8ccd2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/memory/lowmemory_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/memory_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ // Input : slices is an array-of-array-of slices. Each top level array
+ // represents a process. So, each slice in one of the top level array
+ // will be placed in the same process.
+ function buildModel(slices) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ slices[i].forEach(s => thread.sliceGroup.pushSlice(s));
+ }
+ });
+
+ const auditor = new tr.e.audits.LowMemoryAuditor(model);
+ auditor.runAnnotate();
+ return model;
+ }
+
+ function createMemoryTrack(model, interval) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ div.appendChild(drawingContainer);
+ const track = new tr.ui.tracks.MemoryTrack(drawingContainer.viewport);
+ if (model !== undefined) {
+ setDisplayTransformFromBounds(viewport, model.bounds);
+ }
+ track.initialize(model, interval);
+ drawingContainer.appendChild(track);
+ this.addHTMLOutput(drawingContainer);
+ return track;
+ }
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ function setDisplayTransformFromBounds(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ }
+
+ test('instantiate', function() {
+ const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {});
+ const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {});
+
+ const model = buildModel([[sliceA, sliceB]]);
+ createMemoryTrack.call(this, model);
+ });
+
+ test('hasVisibleContent_trueWithThreadSlicePresent', function() {
+ const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {});
+ const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {});
+ const model = buildModel([[sliceA, sliceB]]);
+ const track = createMemoryTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedProcessModel', function() {
+ const track = createMemoryTrack.call(this, undefined);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithNoThreadSlice', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 7.6211, {});
+ const model = buildModel([[sliceA]]);
+ const track = createMemoryTrack.call(this, model);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html
new file mode 100644
index 00000000000..45404899b7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html
@@ -0,0 +1,534 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html">
+<link rel="import" href="/tracing/ui/tracks/device_track.html">
+<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/interaction_track.html">
+<link rel="import" href="/tracing/ui/tracks/kernel_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track.html">
+
+<style>
+.model-track {
+ flex-grow: 1;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const SelectionState = tr.model.SelectionState;
+ const ColorScheme = tr.b.ColorScheme;
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ /**
+ * Visualizes a Model by building ProcessTracks and CpuTracks.
+ * @constructor
+ */
+ const ModelTrack = tr.ui.b.define('model-track', tr.ui.tracks.ContainerTrack);
+
+ ModelTrack.VSYNC_HIGHLIGHT_ALPHA = 0.1;
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT = 0.20;
+ ModelTrack.VSYNC_DENSITY_OPAQUE = 0.10;
+ ModelTrack.VSYNC_DENSITY_RANGE =
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT - ModelTrack.VSYNC_DENSITY_OPAQUE;
+
+ /**
+ * Generate a zebra striping from a list of times.
+ *
+ * @param {!Array.<number>} times A sorted array of timestamps.
+ * @param {number} minTime the lower bound of time to start striping at.
+ * @param {number} maxTime the upper bound of time to stop striping at.
+ * of |times|.
+ *
+ * @returns {!Array.<tr.b.math.Range>} An array of ranges where each element
+ * represents the time range that a stripe covers. Each range is a subset
+ * of the interval [minTime, maxTime].
+ */
+ ModelTrack.generateStripes_ = function(times, minTime, maxTime) {
+ if (times.length === 0) return [];
+
+ // Find the lowest and highest index within the viewport.
+ const lowIndex = tr.b.findLowIndexInSortedArray(
+ times, (x => x), minTime);
+ let highIndex = lowIndex - 1;
+ while (times[highIndex + 1] <= maxTime) {
+ highIndex++;
+ }
+
+ const stripes = [];
+ // Must start at an even index and end at an odd index.
+ for (let i = lowIndex - (lowIndex % 2); i <= highIndex; i += 2) {
+ const left = i < lowIndex ? minTime : times[i];
+ const right = i + 1 > highIndex ? maxTime : times[i + 1];
+ stripes.push(tr.b.math.Range.fromExplicitRange(left, right));
+ }
+
+ return stripes;
+ };
+
+
+ ModelTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('model-track');
+
+ this.upperMode_ = false;
+ this.annotationViews_ = [];
+ this.vSyncTimes_ = [];
+ },
+
+ get processViews() {
+ return Polymer.dom(this).querySelectorAll('.process-track-base');
+ },
+
+ // upperMode is true if the track is being used on the ruler.
+ get upperMode() {
+ return this.upperMode_;
+ },
+
+ set upperMode(upperMode) {
+ this.upperMode_ = upperMode;
+ this.updateContents_();
+ },
+
+ detach() {
+ tr.ui.tracks.ContainerTrack.prototype.detach.call(this);
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+
+ this.model_.addEventListener('annotationChange',
+ this.updateAnnotations_.bind(this));
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 0;
+ },
+
+ updateContents_() {
+ Polymer.dom(this).textContent = '';
+ if (!this.model_) return;
+
+ if (this.upperMode_) {
+ this.updateContentsForUpperMode_();
+ } else {
+ this.updateContentsForLowerMode_();
+ }
+ },
+
+ updateContentsForUpperMode_() {
+ },
+
+ updateContentsForLowerMode_() {
+ if (this.model_.userModel.expectations.length > 1) {
+ const mrt = new tr.ui.tracks.InteractionTrack(this.viewport_);
+ mrt.model = this.model_;
+ Polymer.dom(this).appendChild(mrt);
+ }
+
+ if (this.model_.alerts.length) {
+ const at = new tr.ui.tracks.AlertTrack(this.viewport_);
+ at.alerts = this.model_.alerts;
+ Polymer.dom(this).appendChild(at);
+ }
+
+ if (this.model_.globalMemoryDumps.length) {
+ const gmdt = new tr.ui.tracks.GlobalMemoryDumpTrack(this.viewport_);
+ gmdt.memoryDumps = this.model_.globalMemoryDumps;
+ Polymer.dom(this).appendChild(gmdt);
+ }
+
+ this.appendDeviceTrack_();
+ this.appendCpuUsageTrack_();
+ this.appendMemoryTrack_();
+ this.appendKernelTrack_();
+
+ // Get a sorted list of processes.
+ const processes = this.model_.getAllProcesses();
+ processes.sort(tr.model.Process.compare);
+
+ for (let i = 0; i < processes.length; ++i) {
+ const process = processes[i];
+
+ const track = new tr.ui.tracks.ProcessTrack(this.viewport);
+ track.process = process;
+ if (!track.hasVisibleContent) continue;
+
+ Polymer.dom(this).appendChild(track);
+ }
+ this.viewport_.rebuildEventToTrackMap();
+ this.viewport_.rebuildContainerToTrackMap();
+ this.vSyncTimes_ = this.model_.device.vSyncTimestamps;
+
+ this.updateAnnotations_();
+ },
+
+ getContentBounds() { return this.model.bounds; },
+
+ addAnnotation(annotation) {
+ this.model.addAnnotation(annotation);
+ },
+
+ removeAnnotation(annotation) {
+ this.model.removeAnnotation(annotation);
+ },
+
+ updateAnnotations_() {
+ this.annotationViews_ = [];
+ const annotations = this.model_.getAllAnnotations();
+ for (let i = 0; i < annotations.length; i++) {
+ this.annotationViews_.push(
+ annotations[i].getOrCreateView(this.viewport_));
+ }
+ this.invalidateDrawingContainer();
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (!this.model_) return;
+
+ const tracks = this.children;
+ for (let i = 0; i < tracks.length; ++i) {
+ tracks[i].addEventsToTrackMap(eventToTrackMap);
+ }
+
+ if (this.instantEvents === undefined) return;
+
+ const vp = this.viewport_;
+ this.instantEvents.forEach(function(ev) {
+ eventToTrackMap.addEvent(ev, this);
+ }.bind(this));
+ },
+
+ appendDeviceTrack_() {
+ const device = this.model.device;
+ const track = new tr.ui.tracks.DeviceTrack(this.viewport);
+ track.device = this.model.device;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendKernelTrack_() {
+ const kernel = this.model.kernel;
+ const track = new tr.ui.tracks.KernelTrack(this.viewport);
+ track.kernel = this.model.kernel;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendCpuUsageTrack_() {
+ const track = new tr.ui.tracks.CpuUsageTrack(this.viewport);
+ track.initialize(this.model);
+ if (!track.hasVisibleContent) return;
+
+ this.appendChild(track);
+ },
+
+ appendMemoryTrack_() {
+ const track = new tr.ui.tracks.MemoryTrack(this.viewport);
+ track.initialize(this.model);
+ if (!track.hasVisibleContent) return;
+
+ Polymer.dom(this).appendChild(track);
+ },
+
+ drawTrack(type) {
+ const ctx = this.context();
+ if (!this.model_) return;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ switch (type) {
+ case tr.ui.tracks.DrawType.GRID:
+ this.viewport.drawMajorMarkLines(ctx, viewHeight);
+ // The model is the only thing that draws grid lines.
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.FLOW_ARROWS:
+ if (this.model_.flowIntervalTree.size === 0) {
+ ctx.restore();
+ return;
+ }
+
+ this.drawFlowArrows_(viewLWorld, viewRWorld);
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.INSTANT_EVENT:
+ if (!this.model_.instantEvents ||
+ this.model_.instantEvents.length === 0) {
+ break;
+ }
+
+ tr.ui.b.drawInstantSlicesAsLines(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.model_.instantEvents,
+ 4);
+
+ break;
+
+ case tr.ui.tracks.DrawType.MARKERS:
+ if (!this.viewport.interestRange.isEmpty) {
+ this.viewport.interestRange.draw(
+ ctx, viewLWorld, viewRWorld, viewHeight);
+ this.viewport.interestRange.drawIndicators(
+ ctx, viewLWorld, viewRWorld);
+ }
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.HIGHLIGHTS:
+ this.drawVSyncHighlight(
+ ctx, dt, viewLWorld, viewRWorld, viewHeight);
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.ANNOTATIONS:
+ for (let i = 0; i < this.annotationViews_.length; i++) {
+ this.annotationViews_[i].draw(ctx);
+ }
+ ctx.restore();
+ return;
+ }
+ ctx.restore();
+
+ tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+ },
+
+ drawFlowArrows_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+ ctx.lineWidth = 1;
+
+ const events =
+ this.model_.flowIntervalTree.findIntersection(viewLWorld, viewRWorld);
+
+ // When not showing flow events, show only highlighted/selected ones.
+ const onlyHighlighted = !this.viewport.showFlowEvents;
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ for (let i = 0; i < events.length; ++i) {
+ if (onlyHighlighted &&
+ events[i].selectionState !== SelectionState.SELECTED &&
+ events[i].selectionState !== SelectionState.HIGHLIGHTED) {
+ continue;
+ }
+ 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);
+
+ // TODO(nduca): Figure out how to draw flow arrows even when
+ // processes are collapsed, bug #931.
+ 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;
+ // Nothing to do if both ends of the track are collapsed.
+ 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();
+ },
+
+ /**
+ * Highlights VSync events on the model track (using "zebra" striping).
+ */
+ 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);
+ }
+
+ return bounds.top - canvasBounds.top + (bounds.height / 2);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onPickHit(instantEvent) {
+ selection.push(instantEvent);
+ }
+ const instantEventWidth = 3 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.model_.instantEvents,
+ function(x) { return x.start; },
+ function(x) { return x.duration + instantEventWidth; },
+ loWX, hiWX,
+ onPickHit.bind(this));
+
+ tr.ui.tracks.ContainerTrack.prototype.
+ addIntersectingEventsInRangeToSelectionInWorldSpace.
+ apply(this, arguments);
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.addClosestInstantEventToSelection(this.model_.instantEvents,
+ worldX, worldMaxDist, selection);
+ tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ }
+ };
+
+ return {
+ ModelTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html
new file mode 100644
index 00000000000..53e19146ae2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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.
+-->
+
+<link rel="import" href="/tracing/model/thread.html">
+<link rel="import" href="/tracing/ui/tracks/model_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Range = tr.b.math.Range;
+ const VIEW_L_WORLD = 100;
+ const VIEW_R_WORLD = 1000;
+
+ function testGenerateStripes(times, expectedRanges) {
+ const ranges = tr.ui.tracks.ModelTrack.generateStripes_(
+ times, VIEW_L_WORLD, VIEW_R_WORLD);
+
+ assert.sameDeepMembers(ranges, expectedRanges);
+ }
+
+ test('generateStripesInside', function() {
+ const range200To500 = Range.fromExplicitRange(200, 500);
+ const range800To900 = Range.fromExplicitRange(800, 900);
+ const range998To999 = Range.fromExplicitRange(998, 999);
+ testGenerateStripes([], []);
+ testGenerateStripes([200, 500], [range200To500]);
+ testGenerateStripes([200, 500, 800, 900], [range200To500, range800To900]);
+ testGenerateStripes(
+ [200, 500, 800, 900, 998, 999],
+ [range200To500, range800To900, range998To999]);
+ });
+
+ test('generateStripesOutside', function() {
+ const range101To999 = Range.fromExplicitRange(101, 999);
+ // Far left.
+ testGenerateStripes([0, 99], []);
+ testGenerateStripes([0, 10, 50, 99], []);
+ testGenerateStripes([0, 99, 101, 999], [range101To999]);
+ testGenerateStripes([0, 10, 50, 99, 101, 999], [range101To999]);
+
+ // Far right.
+ testGenerateStripes([1001, 2000], []);
+ testGenerateStripes([1001, 2000, 3000, 4000], []);
+ testGenerateStripes([101, 999, 1001, 2000], [range101To999]);
+ testGenerateStripes([101, 999, 1001, 2000, 3000, 4000], [range101To999]);
+
+ // Far both.
+ testGenerateStripes([0, 99, 1001, 2000], []);
+ testGenerateStripes([0, 10, 50, 99, 1001, 2000], []);
+ testGenerateStripes([0, 10, 50, 99, 1001, 2000, 3000, 4000], []);
+ testGenerateStripes([0, 99, 101, 999, 1001, 2000], [range101To999]);
+ });
+
+ test('generateStripesOverlap', function() {
+ const rangeLeftWorldTo101 = Range.fromExplicitRange(VIEW_L_WORLD, 101);
+ const range102To103 = Range.fromExplicitRange(102, 103);
+ const range200To900 = Range.fromExplicitRange(200, 900);
+ const range997To998 = Range.fromExplicitRange(997, 998);
+ const range999ToRightWorld = Range.fromExplicitRange(999, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+
+
+ // Left overlap.
+ testGenerateStripes([0, 101], [rangeLeftWorldTo101]);
+ testGenerateStripes([0, 1, 2, 101], [rangeLeftWorldTo101]);
+ testGenerateStripes(
+ [2, 101, 102, 103],
+ [rangeLeftWorldTo101, range102To103]);
+ testGenerateStripes(
+ [0, 1, 2, 101, 102, 103],
+ [rangeLeftWorldTo101, range102To103]);
+ testGenerateStripes(
+ [0, 1, 2, 101, 102, 103, 1001, 3000],
+ [rangeLeftWorldTo101, range102To103]);
+
+ // Right overlap.
+ testGenerateStripes([999, 2000], [range999ToRightWorld]);
+ testGenerateStripes([999, 2000, 3000, 4000], [range999ToRightWorld]);
+ testGenerateStripes(
+ [997, 998, 999, 2000],
+ [range997To998, range999ToRightWorld]);
+ testGenerateStripes(
+ [997, 998, 999, 2000, 3000, 4000],
+ [range997To998, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 997, 998, 999, 2000, 3000, 4000],
+ [range997To998, range999ToRightWorld]);
+
+ // Both overlap.
+ testGenerateStripes([0, 2000], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes(
+ [0, 101, 999, 2000],
+ [rangeLeftWorldTo101, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 101, 200, 900, 999, 2000],
+ [rangeLeftWorldTo101, range200To900, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 90, 101, 999, 2000, 3000, 4000],
+ [rangeLeftWorldTo101, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 90, 101, 200, 900, 999, 2000, 3000, 4000],
+ [rangeLeftWorldTo101, range200To900, range999ToRightWorld]);
+ });
+
+ test('generateStripesOdd', function() {
+ const range500To900 = Range.fromExplicitRange(500, 900);
+ const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200);
+ const rangeLeftWorldTo500 = Range.fromExplicitRange(VIEW_L_WORLD, 500);
+ const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+
+ // One VSync.
+ testGenerateStripes([0], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes([500], [range500ToRightWorld]);
+ testGenerateStripes([1500], []);
+
+ // Multiple VSyncs.
+ testGenerateStripes([0, 10, 20], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes([0, 500, 2000], [rangeLeftWorldTo500]);
+ testGenerateStripes([0, 10, 500], [range500ToRightWorld]);
+ testGenerateStripes([0, 10, 2000], []);
+ testGenerateStripes(
+ [0, 200, 500],
+ [rangeLeftWorldTo200, range500ToRightWorld]);
+ testGenerateStripes(
+ [0, 200, 500, 900],
+ [rangeLeftWorldTo200, range500To900]);
+ });
+
+ test('generateStripesBorder', function() {
+ const rangeLeftWorldToLeftWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_L_WORLD);
+ const rangeRightWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_R_WORLD, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+ const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200);
+ const range200To500 = Range.fromExplicitRange(200, 500);
+ const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD);
+ testGenerateStripes([0, VIEW_L_WORLD], [rangeLeftWorldToLeftWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, VIEW_L_WORLD],
+ [rangeLeftWorldToLeftWorld]);
+ testGenerateStripes(
+ [VIEW_R_WORLD, 2000],
+ [rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_R_WORLD, VIEW_R_WORLD],
+ [rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, VIEW_R_WORLD],
+ [rangeLeftWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, 200, 500, VIEW_R_WORLD],
+ [rangeLeftWorldTo200, range500ToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, 200, 500, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, range200To500,
+ rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, 10, VIEW_L_WORLD, VIEW_R_WORLD, 2000, 3000],
+ [rangeLeftWorldToRightWorld]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html
new file mode 100644
index 00000000000..8b5fd837f0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html
@@ -0,0 +1,240 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/model_settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a group of objects in multiple rows.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const MultiRowTrack = tr.ui.b.define(
+ 'multi-row-track', tr.ui.tracks.ContainerTrack);
+
+ MultiRowTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.tooltip_ = '';
+ this.heading_ = '';
+
+ this.groupingSource_ = undefined;
+ this.itemsToGroup_ = undefined;
+
+ this.defaultToCollapsedWhenSubRowCountMoreThan = 1;
+
+ this.currentSubRowsWithHeadings_ = undefined;
+ this.expanded_ = true;
+ },
+
+ get itemsToGroup() {
+ return this.itemsToGroup_;
+ },
+
+ setItemsToGroup(itemsToGroup, opt_groupingSource) {
+ this.itemsToGroup_ = itemsToGroup;
+ this.groupingSource_ = opt_groupingSource;
+ this.currentSubRowsWithHeadings_ = undefined;
+ this.updateContents_();
+ this.updateExpandedStateFromGroupingSource_();
+ },
+
+ /**
+ * Opt-out from using buildSubRows_() and provide prebuilt rows.
+ * Array of {row: [rowItems...], heading} dicts is expected as an argument.
+ */
+ setPrebuiltSubRows(groupingSource, subRowsWithHeadings) {
+ this.itemsToGroup_ = undefined;
+ this.groupingSource_ = groupingSource;
+ this.currentSubRowsWithHeadings_ = subRowsWithHeadings;
+ this.updateContents_();
+ this.updateExpandedStateFromGroupingSource_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(h) {
+ this.heading_ = h;
+ this.updateHeadingAndTooltip_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(t) {
+ this.tooltip_ = t;
+ this.updateHeadingAndTooltip_();
+ },
+
+ get subRows() {
+ return this.currentSubRowsWithHeadings_.map(elem => elem.row);
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 0;
+ },
+
+ get expanded() {
+ return this.expanded_;
+ },
+
+ set expanded(expanded) {
+ if (this.expanded_ === expanded) return;
+
+ this.expanded_ = expanded;
+ this.expandedStateChanged_();
+ },
+
+ onHeadingClicked_(e) {
+ if (this.subRows.length <= 1) return;
+
+ this.expanded = !this.expanded;
+
+ if (this.groupingSource_) {
+ const modelSettings = new tr.model.ModelSettings(
+ this.groupingSource_.model);
+ modelSettings.setSettingFor(this.groupingSource_, 'expanded',
+ this.expanded);
+ }
+
+ e.stopPropagation();
+ },
+
+ updateExpandedStateFromGroupingSource_() {
+ if (this.groupingSource_) {
+ const numSubRows = this.subRows.length;
+ const modelSettings = new tr.model.ModelSettings(
+ this.groupingSource_.model);
+ if (numSubRows > 1) {
+ let defaultExpanded;
+ if (numSubRows > this.defaultToCollapsedWhenSubRowCountMoreThan) {
+ defaultExpanded = false;
+ } else {
+ defaultExpanded = true;
+ }
+ this.expanded = modelSettings.getSettingFor(
+ this.groupingSource_, 'expanded', defaultExpanded);
+ } else {
+ this.expanded = undefined;
+ }
+ }
+ },
+
+ expandedStateChanged_() {
+ const minH = Math.max(2, Math.ceil(18 / this.children.length));
+ const h = (this.expanded_ ? 18 : minH) + 'px';
+
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].height = h;
+ if (i === 0) {
+ this.children[i].arrowVisible = true;
+ }
+ this.children[i].expanded = this.expanded;
+ }
+
+ if (this.children.length === 1) {
+ this.children[0].expanded = true;
+ this.children[0].arrowVisible = false;
+ }
+ },
+
+ updateContents_() {
+ tr.ui.tracks.ContainerTrack.prototype.updateContents_.call(this);
+ this.detach(); // Clear sub-tracks.
+
+ if (this.currentSubRowsWithHeadings_ === undefined) {
+ // No prebuilt rows, build it.
+ if (this.itemsToGroup_ === undefined) {
+ return;
+ }
+ const subRows = this.buildSubRows_(this.itemsToGroup_);
+ this.currentSubRowsWithHeadings_ = subRows.map(row => {
+ return {row, heading: undefined};
+ });
+ }
+ if (this.currentSubRowsWithHeadings_ === undefined ||
+ this.currentSubRowsWithHeadings_.length === 0) {
+ return;
+ }
+
+ const addSubTrackEx = (items, opt_heading) => {
+ const track = this.addSubTrack_(items);
+ if (opt_heading !== undefined) {
+ track.heading = opt_heading;
+ }
+ track.addEventListener(
+ 'heading-clicked', this.onHeadingClicked_.bind(this));
+ };
+
+ if (this.currentSubRowsWithHeadings_[0].heading !== undefined &&
+ this.currentSubRowsWithHeadings_[0].heading !== this.heading_) {
+ // Create an empty row to render the group's title there.
+ addSubTrackEx([]);
+ }
+
+ for (const subRowWithHeading of this.currentSubRowsWithHeadings_) {
+ const subRow = subRowWithHeading.row;
+ if (subRow.length === 0) {
+ continue;
+ }
+ addSubTrackEx(subRow, subRowWithHeading.heading);
+ }
+
+ this.updateHeadingAndTooltip_();
+ this.expandedStateChanged_();
+ },
+
+ updateHeadingAndTooltip_() {
+ if (!Polymer.dom(this).firstChild) return;
+
+ Polymer.dom(this).firstChild.heading = this.heading_;
+ Polymer.dom(this).firstChild.tooltip = this.tooltip_;
+ },
+
+ /**
+ * Breaks up the list of slices into N rows, each of which is a list of
+ * slices that are non overlapping.
+ */
+ buildSubRows_(itemsToGroup) {
+ throw new Error('Not implemented');
+ },
+
+ addSubTrack_(subRowItems) {
+ throw new Error('Not implemented');
+ },
+
+ areArrayContentsSame_(a, b) {
+ if (!a || !b) return false;
+
+ if (!a.length || !b.length) return false;
+
+ if (a.length !== b.length) return false;
+
+ for (let i = 0; i < a.length; ++i) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+ }
+ };
+
+ return {
+ MultiRowTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html
new file mode 100644
index 00000000000..b97b48c3ef0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a ObjectInstanceGroup.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const ObjectInstanceGroupTrack = tr.ui.b.define(
+ 'object-instance-group-track', tr.ui.tracks.MultiRowTrack);
+
+ ObjectInstanceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('object-instance-group-track');
+ this.objectInstances_ = undefined;
+ },
+
+ get objectInstances() {
+ return this.itemsToGroup;
+ },
+
+ set objectInstances(objectInstances) {
+ this.setItemsToGroup(objectInstances);
+ },
+
+ addSubTrack_(objectInstances) {
+ const hasMultipleRows = this.subRows.length > 1;
+ const track = new tr.ui.tracks.ObjectInstanceTrack(this.viewport);
+ track.objectInstances = objectInstances;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ },
+
+ buildSubRows_(objectInstances) {
+ objectInstances.sort(function(x, y) {
+ return x.creationTs - y.creationTs;
+ });
+
+ const subRows = [];
+ for (let i = 0; i < objectInstances.length; i++) {
+ const objectInstance = objectInstances[i];
+
+ let found = false;
+ for (let j = 0; j < subRows.length; j++) {
+ const subRow = subRows[j];
+ const lastItemInSubRow = subRow[subRow.length - 1];
+ if (objectInstance.creationTs >= lastItemInSubRow.deletionTs) {
+ found = true;
+ subRow.push(objectInstance);
+ break;
+ }
+ }
+ if (!found) {
+ subRows.push([objectInstance]);
+ }
+ }
+ return subRows;
+ },
+ updateHeadingAndTooltip_() {
+ }
+ };
+
+ return {
+ ObjectInstanceGroupTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css
new file mode 100644
index 00000000000..0919e85524e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2012 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.
+ */
+
+.object-instance-track {
+ height: 18px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html
new file mode 100644
index 00000000000..f21a87d04db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html
@@ -0,0 +1,294 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/object_instance_track.css">
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const SelectionState = tr.model.SelectionState;
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ /**
+ * A track that displays an array of Slice objects.
+ * @constructor
+ * @extends {Track}
+ */
+ const ObjectInstanceTrack = tr.ui.b.define(
+ 'object-instance-track', tr.ui.tracks.Track);
+
+ ObjectInstanceTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('object-instance-track');
+ this.objectInstances_ = [];
+ this.objectSnapshots_ = [];
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get objectInstances() {
+ return this.objectInstances_;
+ },
+
+ set objectInstances(objectInstances) {
+ if (!objectInstances || objectInstances.length === 0) {
+ this.heading = '';
+ this.objectInstances_ = [];
+ this.objectSnapshots_ = [];
+ return;
+ }
+ this.heading = objectInstances[0].baseTypeName;
+ this.objectInstances_ = objectInstances;
+ this.objectSnapshots_ = [];
+ this.objectInstances_.forEach(function(instance) {
+ this.objectSnapshots_.push.apply(
+ this.objectSnapshots_, instance.snapshots);
+ }, this);
+ this.objectSnapshots_.sort(function(a, b) {
+ return a.ts - b.ts;
+ });
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ get snapshotRadiusView() {
+ return 7 * (window.devicePixelRatio || 1);
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawObjectInstances_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawObjectInstances_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const height = bounds.height * pixelRatio;
+ const halfHeight = height * 0.5;
+ const twoPi = Math.PI * 2;
+
+ // Culling parameters.
+ const dt = this.viewport.currentDisplayTransform;
+ const snapshotRadiusView = this.snapshotRadiusView;
+ const snapshotRadiusWorld = dt.xViewVectorToWorld(height);
+
+ // Instances
+ const objectInstances = this.objectInstances_;
+ let loI = tr.b.findLowIndexInSortedArray(
+ objectInstances,
+ function(instance) {
+ return instance.deletionTs;
+ },
+ viewLWorld);
+ ctx.save();
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ for (let i = loI; i < objectInstances.length; ++i) {
+ const instance = objectInstances[i];
+ const x = instance.creationTs;
+ if (x > viewRWorld) break;
+
+ const right = instance.deletionTs === Number.MAX_VALUE ?
+ viewRWorld : instance.deletionTs;
+ const xView = dt.xWorldToView(x);
+ const widthView = dt.xWorldVectorToView(right - x);
+ ctx.fillStyle = EventPresenter.getObjectInstanceColor(instance);
+ ctx.fillRect(xView, pixelRatio, widthView, height - 2 * pixelRatio);
+ }
+ ctx.restore();
+
+ // Snapshots. Has to run in worldspace because ctx.arc gets transformed.
+ const objectSnapshots = this.objectSnapshots_;
+ loI = tr.b.findLowIndexInSortedArray(
+ objectSnapshots,
+ function(snapshot) {
+ return snapshot.ts + snapshotRadiusWorld;
+ },
+ viewLWorld);
+ for (let i = loI; i < objectSnapshots.length; ++i) {
+ const snapshot = objectSnapshots[i];
+ const x = snapshot.ts;
+ if (x - snapshotRadiusWorld > viewRWorld) break;
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.fillStyle = EventPresenter.getObjectSnapshotColor(snapshot);
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, snapshotRadiusView, 0, twoPi);
+ ctx.fill();
+ if (snapshot.selected) {
+ ctx.lineWidth = 5;
+ ctx.strokeStyle = 'rgb(100,100,0)';
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, snapshotRadiusView - 1, 0, twoPi);
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = 'rgb(255,255,0)';
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.stroke();
+ }
+ }
+ ctx.lineWidth = 1;
+
+ // For performance reasons we only check the SelectionState of the first
+ // instance. If it's DIMMED we assume that all are DIMMED.
+ // TODO(egraether): Allow partial highlight.
+ let selectionState = SelectionState.NONE;
+ if (objectInstances.length &&
+ objectInstances[0].selectionState === SelectionState.DIMMED) {
+ selectionState = SelectionState.DIMMED;
+ }
+
+ // Dim the track when there is an active highlight.
+ if (selectionState === SelectionState.DIMMED) {
+ const width = bounds.width * pixelRatio;
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+ }
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.objectInstance_ !== undefined) {
+ this.objectInstance_.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ }
+
+ if (this.objectSnapshots_ !== undefined) {
+ this.objectSnapshots_.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ }
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ // Pick snapshots first.
+ let foundSnapshot = false;
+ function onSnapshot(snapshot) {
+ selection.push(snapshot);
+ foundSnapshot = true;
+ }
+ const snapshotRadiusView = this.snapshotRadiusView;
+ const snapshotRadiusWorld = viewPixWidthWorld * snapshotRadiusView;
+ tr.b.iterateOverIntersectingIntervals(
+ this.objectSnapshots_,
+ function(x) { return x.ts - snapshotRadiusWorld; },
+ function(x) { return 2 * snapshotRadiusWorld; },
+ loWX, hiWX,
+ onSnapshot);
+ if (foundSnapshot) return;
+
+ // Try picking instances.
+ tr.b.iterateOverIntersectingIntervals(
+ this.objectInstances_,
+ function(x) { return x.creationTs; },
+ function(x) { return x.deletionTs - x.creationTs; },
+ loWX, hiWX,
+ (value) => { selection.push(value); });
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {event} The current event item.
+ * @param {Number} offset Number of slices away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ let events;
+ if (event instanceof tr.model.ObjectSnapshot) {
+ events = this.objectSnapshots_;
+ } else if (event instanceof tr.model.ObjectInstance) {
+ events = this.objectInstances_;
+ } else {
+ throw new Error('Unrecognized event');
+ }
+
+ const index = events.indexOf(event);
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < events.length) {
+ selection.push(events[newIndex]);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const snapshot = tr.b.findClosestElementInSortedArray(
+ this.objectSnapshots_,
+ function(x) { return x.ts; },
+ worldX,
+ worldMaxDist);
+
+ if (!snapshot) return;
+
+ selection.push(snapshot);
+
+ // TODO(egraether): Search for object instances as well, which was not
+ // implemented because it makes little sense with the current visual and
+ // needs to take care of overlapping intervals.
+ }
+ };
+
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(ObjectInstanceTrack, options);
+
+ return {
+ ObjectInstanceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html
new file mode 100644
index 00000000000..8312d0ba7e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/object_collection.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const ObjectInstanceTrack = tr.ui.tracks.ObjectInstanceTrack;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createObjects = function() {
+ const objects = new tr.model.ObjectCollection({});
+ const scopedId1 = new tr.model.ScopedId('ptr', '0x1000');
+ objects.idWasCreated(scopedId1, 'tr.e.cc', 'Frame', 10);
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 10, 'snapshot-1');
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 25, 'snapshot-2');
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 40, 'snapshot-3');
+ objects.idWasDeleted(scopedId1, 'tr.e.cc', 'Frame', 45);
+
+ const scopedId2 = new tr.model.ScopedId('ptr', '0x1001');
+ objects.idWasCreated(scopedId2, 'skia', 'Picture', 20);
+ objects.addSnapshot(scopedId2, 'skia', 'Picture', 20, 'snapshot-1');
+ objects.idWasDeleted(scopedId2, 'skia', 'Picture', 25);
+ return objects;
+ };
+
+ test('instantiate', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+ frames[0].snapshots[1].selectionState =
+ tr.model.SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = ObjectInstanceTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasic';
+ track.objectInstances = frames;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('selectionHitTestingWithThreadTrack', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+
+ const track = ObjectInstanceTrack(new Viewport());
+ track.objectInstances = frames;
+
+ // Hit outside range
+ let selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 8, 8.1, 0.1, selection);
+ assert.strictEqual(selection.length, 0);
+
+ // Hit the first snapshot, via pixel-nearness.
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 9.98, 9.99, 0.1, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectSnapshot);
+
+ // Hit the instance, between the 1st and 2nd snapshots
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 20, 20.1, 0.1, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectInstance);
+ });
+
+ test('addEventNearToProvidedEventToSelection', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+
+ const track = ObjectInstanceTrack(new Viewport());
+ track.objectInstances = frames;
+
+ const instance = new tr.model.ObjectInstance(
+ {}, new tr.model.ScopedId('ptr', '0x1000'), 'cat', 'n', 10);
+
+ assert.doesNotThrow(function() {
+ track.addEventNearToProvidedEventToSelection(instance, 0, undefined);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html
new file mode 100644
index 00000000000..e43bce0cec2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays threads with only scheduling information but no
+ * slices. By default it's collapsed to minimize initial visual difference
+ * while allowing the user to drill-down into whatever process is
+ * interesting to them.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const OtherThreadsTrack = tr.ui.b.define(
+ 'other-threads-track', tr.ui.tracks.OtherThreadsTrack);
+
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ OtherThreadsTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+ this.header_ = document.createElement('tr-ui-b-heading');
+ this.header_.addEventListener('click', this.onHeaderClick_.bind(this));
+ this.header_.heading = 'Other Threads';
+ this.header_.tooltip = 'Threads with only scheduling information';
+ this.header_.arrowVisible = true;
+
+ this.threads_ = [];
+ this.expanded = false;
+ this.collapsible_ = true;
+ },
+
+ set threads(threads) {
+ this.threads_ = threads;
+ this.updateContents_();
+ },
+
+ set collapsible(collapsible) {
+ this.collapsible_ = collapsible;
+ this.updateContents_();
+ },
+
+ onHeaderClick_(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.expanded = !this.expanded;
+ },
+
+ get expanded() {
+ return this.header_.expanded;
+ },
+
+ set expanded(expanded) {
+ expanded = !!expanded;
+
+ if (this.expanded === expanded) return;
+
+ this.header_.expanded = expanded;
+
+ // Expanding and collapsing tracks is, essentially, growing and shrinking
+ // the viewport. We dispatch a change event to trigger any processing
+ // to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.detach();
+ if (this.collapsible_) {
+ Polymer.dom(this).appendChild(this.header_);
+ }
+ if (this.expanded || !this.collapsible_) {
+ for (const thread of this.threads_) {
+ const track = new tr.ui.tracks.ThreadTrack(this.viewport);
+ track.thread = thread;
+ if (!track.hasVisibleContent) return;
+
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ }
+ }
+ };
+
+ return {
+ OtherThreadsTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html
new file mode 100644
index 00000000000..d32cd21e9a3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<style>
+.power-series-track {
+ height: 90px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+
+ /**
+ * A track that displays a PowerSeries.
+ *
+ * @constructor
+ * @extends {ChartTrack}
+ */
+ const PowerSeriesTrack = tr.ui.b.define('power-series-track', ChartTrack);
+
+ PowerSeriesTrack.prototype = {
+ __proto__: ChartTrack.prototype,
+
+ decorate(viewport) {
+ ChartTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('power-series-track');
+ this.heading = 'Power';
+ this.powerSeries_ = undefined;
+ },
+
+ set powerSeries(powerSeries) {
+ this.powerSeries_ = powerSeries;
+
+ this.series = this.buildChartSeries_();
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ get hasVisibleContent() {
+ return (this.powerSeries_ && this.powerSeries_.samples.length > 0);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ containerToTrackMap.addContainer(this.powerSeries_, this);
+ },
+
+ buildChartSeries_() {
+ if (!this.hasVisibleContent) return [];
+
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+ const pts = this.powerSeries_.samples.map(function(smpl) {
+ return new tr.ui.tracks.ChartPoint(smpl, smpl.start, smpl.powerInW);
+ });
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: ColorScheme.getColorIdForGeneralPurposeString(this.heading)
+ };
+
+ return [new tr.ui.tracks.ChartSeries(pts, seriesYAxis, renderingConfig)];
+ }
+ };
+
+ return {
+ PowerSeriesTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html
new file mode 100644
index 00000000000..9e8b03aa168
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel='import' href='/tracing/model/device.html'>
+<link rel='import' href='/tracing/model/model.html'>
+<link rel='import' href='/tracing/model/power_series.html'>
+<link rel='import' href='/tracing/ui/base/constants.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel='import' href='/tracing/ui/tracks/container_to_track_map.html'>
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+<link rel="import" href="/tracing/ui/tracks/power_series_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+ const PowerSeriesTrack = tr.ui.tracks.PowerSeriesTrack;
+
+ const createDrawingContainer = function(series) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ if (series) {
+ series.updateBounds();
+ setDisplayTransformFromBounds(viewport, series.bounds);
+ }
+
+ return drawingContainer;
+ };
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ const setDisplayTransformFromBounds = function(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ };
+
+ test('instantiate', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(0.5, 2);
+ series.addPowerSample(1, 3);
+ series.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(series);
+ const track = new PowerSeriesTrack(drawingContainer.viewport);
+ track.powerSeries = series;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+ });
+
+ test('hasVisibleContent_trueWithPowerSamplesPresent', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(0.5, 2);
+ series.addPowerSample(1, 3);
+ series.addPowerSample(1.5, 4);
+
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ track.powerSeries = series;
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedPowerSeries', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ track.powerSeries = undefined;
+
+ assert.notOk(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithEmptyPowerSeries', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ const series = new PowerSeries(new Model().device);
+ track.powerSeries = series;
+
+ assert.notOk(track.hasVisibleContent);
+ });
+
+ test('addContainersToTrackMap', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const powerSeriesTrack = new PowerSeriesTrack(viewport);
+ const series = new PowerSeries(new Model().device);
+ powerSeriesTrack.powerSeries = series;
+
+ const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+ powerSeriesTrack.addContainersToTrackMap(containerToTrackMap);
+
+ assert.strictEqual(
+ containerToTrackMap.getTrackByStableId('Device.PowerSeries'),
+ powerSeriesTrack);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html
new file mode 100644
index 00000000000..247d707f58f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ALLOCATED_MEMORY_TRACK_HEIGHT = 50;
+
+ /**
+ * A track that displays an array of ProcessMemoryDump objects.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const ProcessMemoryDumpTrack = tr.ui.b.define(
+ 'process-memory-dump-track', tr.ui.tracks.ContainerTrack);
+
+ ProcessMemoryDumpTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.memoryDumps_ = undefined;
+ },
+
+ get memoryDumps() {
+ return this.memoryDumps_;
+ },
+
+ set memoryDumps(memoryDumps) {
+ this.memoryDumps_ = memoryDumps;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ // Show no tracks if there are no dumps.
+ if (!this.memoryDumps_ || !this.memoryDumps_.length) return;
+
+ this.appendAllocatedMemoryTrack_();
+ },
+
+ appendAllocatedMemoryTrack_() {
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ this.memoryDumps_);
+ if (!series) return;
+
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = 'Memory per component';
+ track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ };
+
+ return {
+ ProcessMemoryDumpTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html
new file mode 100644
index 00000000000..897d2883c62
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Viewport = tr.ui.TimelineViewport;
+ const ProcessMemoryDumpTrack = tr.ui.tracks.ProcessMemoryDumpTrack;
+ const createTestProcessMemoryDumps =
+ tr.ui.tracks.createTestProcessMemoryDumps;
+
+ function instantiateTrack(withVMRegions, withAllocatorDumps,
+ expectedTrackCount) {
+ const dumps = createTestProcessMemoryDumps(
+ withVMRegions, withAllocatorDumps);
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new ProcessMemoryDumpTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.invalidate();
+
+ track.memoryDumps = dumps;
+
+ // TODO(petrcermak): Check that the div has indeed zero size.
+ if (expectedTrackCount > 0) {
+ this.addHTMLOutput(div);
+ }
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.lengthOf(track.tracks_, expectedTrackCount);
+ }
+
+ test('instantiate_withoutMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, false, 0);
+ });
+ test('instantiate_withMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, true, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html
new file mode 100644
index 00000000000..c6560f40118
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ /**
+ * Visualizes a Process's state using a series of rects to represent activity.
+ * @constructor
+ */
+ const ProcessSummaryTrack = tr.ui.b.define('process-summary-track',
+ tr.ui.tracks.RectTrack);
+
+ ProcessSummaryTrack.buildRectsFromProcess = function(process) {
+ if (!process) return [];
+
+ const ops = [];
+ // build list of start/end ops for each top level or important slice
+ const pushOp = function(isStart, time, slice) {
+ ops.push({
+ isStart,
+ time,
+ slice
+ });
+ };
+ for (const tid in process.threads) {
+ const sliceGroup = process.threads[tid].sliceGroup;
+
+ sliceGroup.topLevelSlices.forEach(function(slice) {
+ pushOp(true, slice.start, undefined);
+ pushOp(false, slice.end, undefined);
+ });
+ sliceGroup.slices.forEach(function(slice) {
+ if (slice.important) {
+ pushOp(true, slice.start, slice);
+ pushOp(false, slice.end, slice);
+ }
+ });
+ }
+ ops.sort(function(a, b) { return a.time - b.time; });
+
+ const rects = [];
+ /**
+ * Build a row of rects which display one way for unimportant activity,
+ * and during important slices, show up as those important slices.
+ *
+ * If an important slice starts in the middle of another,
+ * just drop it on the floor.
+ */
+ const genericColorId = ColorScheme.getColorIdForReservedName(
+ 'generic_work');
+ const pushRect = function(start, end, slice) {
+ rects.push(new tr.ui.tracks.Rect(
+ slice, /* modelItem: show selection state of slice if present */
+ slice ? slice.title : '', /* title */
+ slice ? slice.colorId : genericColorId, /* colorId */
+ start, /* start */
+ end - start /* duration */));
+ };
+ let depth = 0;
+ let currentSlice = undefined;
+ let lastStart = undefined;
+ ops.forEach(function(op) {
+ depth += op.isStart ? 1 : -1;
+
+ if (currentSlice) {
+ // simply find end of current important slice
+ if (!op.isStart && op.slice === currentSlice) {
+ // important slice has ended
+ pushRect(lastStart, op.time, currentSlice);
+ lastStart = depth >= 1 ? op.time : undefined;
+ currentSlice = undefined;
+ }
+ } else {
+ if (op.isStart) {
+ if (depth === 1) {
+ lastStart = op.time;
+ currentSlice = op.slice;
+ } else if (op.slice) {
+ // switch to slice
+ if (op.time !== lastStart) {
+ pushRect(lastStart, op.time, undefined);
+ lastStart = op.time;
+ }
+ currentSlice = op.slice;
+ }
+ } else {
+ if (depth === 0) {
+ pushRect(lastStart, op.time, undefined);
+ lastStart = undefined;
+ }
+ }
+ }
+ });
+ return rects;
+ };
+
+ ProcessSummaryTrack.prototype = {
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get process() {
+ return this.process_;
+ },
+
+ set process(process) {
+ this.process_ = process;
+ this.rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+ }
+ };
+
+ return {
+ ProcessSummaryTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html
new file mode 100644
index 00000000000..1d071f9d0ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/ui/tracks/process_summary_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProcessSummaryTrack = tr.ui.tracks.ProcessSummaryTrack;
+
+ test('buildRectSimple', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ process = model.getOrCreateProcess(1);
+ // XXXX
+ // XXXX
+ const thread1 = process.getOrCreateThread(1);
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 1, duration: 4}));
+ const thread2 = process.getOrCreateThread(2);
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 4, duration: 4}));
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(rects.length, 1);
+ const rect = rects[0];
+ assert.closeTo(rect.start, 1, 1e-5);
+ assert.closeTo(rect.end, 8, 1e-5);
+ });
+
+ test('buildRectComplex', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ process = model.getOrCreateProcess(1);
+ // XXXX X X XX
+ // XXXX XXX X
+ const thread1 = process.getOrCreateThread(1);
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 1, duration: 4}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 9, duration: 1}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 11, duration: 1}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 13, duration: 2}));
+ const thread2 = process.getOrCreateThread(2);
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 4, duration: 4}));
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 9, duration: 3}));
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 16, duration: 1}));
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(4, rects.length);
+ assert.closeTo(rects[0].start, 1, 1e-5);
+ assert.closeTo(rects[0].end, 8, 1e-5);
+ assert.closeTo(rects[1].start, 9, 1e-5);
+ assert.closeTo(rects[1].end, 12, 1e-5);
+ assert.closeTo(rects[2].start, 13, 1e-5);
+ assert.closeTo(rects[2].end, 15, 1e-5);
+ assert.closeTo(rects[3].start, 16, 1e-5);
+ assert.closeTo(rects[3].end, 17, 1e-5);
+ });
+
+ test('buildRectImportantSlice', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ // [ unimportant ]
+ // [important]
+ const a = tr.c.TestUtils.newSliceEx(
+ {title: 'unimportant', start: 4, duration: 21});
+ const b = tr.c.TestUtils.newSliceEx(
+ {title: 'important', start: 9, duration: 11});
+ b.important = true;
+ process = model.getOrCreateProcess(1);
+ process.getOrCreateThread(1).sliceGroup.pushSlices([a, b]);
+
+ model.importantSlice = b;
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(3, rects.length);
+ assert.closeTo(rects[0].start, 4, 1e-5);
+ assert.closeTo(rects[0].end, 9, 1e-5);
+ assert.closeTo(rects[1].start, 9, 1e-5);
+ assert.closeTo(rects[1].end, 20, 1e-5);
+ assert.closeTo(rects[2].start, 20, 1e-5);
+ assert.closeTo(rects[2].end, 25, 1e-5);
+
+ // middle rect represents important slice, so colorId & title are preserved
+ assert.strictEqual(rects[1].title, model.importantSlice.title);
+ assert.strictEqual(rects[1].colorId, model.importantSlice.colorId);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html
new file mode 100644
index 00000000000..1be51cbf4cb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track_base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase;
+
+ /**
+ * @constructor
+ */
+ const ProcessTrack = tr.ui.b.define('process-track', ProcessTrackBase);
+
+ ProcessTrack.prototype = {
+ __proto__: ProcessTrackBase.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ProcessTrackBase.prototype.decorate.call(this, viewport);
+ },
+
+ drawTrack(type) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.INSTANT_EVENT: {
+ if (!this.processBase.instantEvents ||
+ this.processBase.instantEvents.length === 0) {
+ break;
+ }
+
+ const ctx = this.context();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+
+ tr.ui.b.drawInstantSlicesAsLines(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.processBase.instantEvents,
+ 2);
+
+ ctx.restore();
+
+ break;
+ }
+
+ case tr.ui.tracks.DrawType.BACKGROUND:
+ this.drawBackground_();
+ // Don't bother recursing further, Process is the only level that
+ // draws backgrounds.
+ return;
+ }
+
+ tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+ },
+
+ drawBackground_() {
+ const ctx = this.context();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ let draw = false;
+ ctx.fillStyle = '#eee';
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track) ||
+ (this.children[i] instanceof tr.ui.tracks.SpacingTrack)) {
+ continue;
+ }
+
+ draw = !draw;
+ if (!draw) continue;
+
+ const bounds = this.children[i].getBoundingClientRect();
+ ctx.fillRect(0, pixelRatio * (bounds.top - canvasBounds.top),
+ ctx.canvas.width, pixelRatio * bounds.height);
+ }
+ },
+
+ // Process maps to processBase because we derive from ProcessTrackBase.
+ set process(process) {
+ this.processBase = process;
+ },
+
+ get process() {
+ return this.processBase;
+ },
+
+ get eventContainer() {
+ return this.process;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.process, this);
+ },
+
+ appendMemoryDumpTrack_() {
+ const processMemoryDumps = this.process.memoryDumps;
+ if (processMemoryDumps.length) {
+ const pmdt = new tr.ui.tracks.ProcessMemoryDumpTrack(this.viewport_);
+ pmdt.memoryDumps = processMemoryDumps;
+ Polymer.dom(this).appendChild(pmdt);
+ }
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onPickHit(instantEvent) {
+ selection.push(instantEvent);
+ }
+ const instantEventWidth = 2 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.processBase.instantEvents,
+ function(x) { return x.start; },
+ function(x) { return x.duration + instantEventWidth; },
+ loWX, hiWX,
+ onPickHit.bind(this));
+
+ tr.ui.tracks.ContainerTrack.prototype.
+ addIntersectingEventsInRangeToSelectionInWorldSpace.
+ apply(this, arguments);
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.addClosestInstantEventToSelection(this.processBase.instantEvents,
+ worldX, worldMaxDist, selection);
+ tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ }
+ };
+
+ return {
+ ProcessTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css
new file mode 100644
index 00000000000..25fa5f015b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css
@@ -0,0 +1,39 @@
+/* Copyright (c) 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.
+ */
+
+.process-track-header {
+ display: flex;
+ flex: 0 0 auto;
+ background-image: -webkit-gradient(linear,
+ 0 0, 100% 0,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ font-size: 75%;
+}
+
+.process-track-name {
+ flex-grow: 1;
+}
+
+.process-track-name:before {
+ content: '\25B8'; /* Right triangle */
+ padding: 0 5px;
+}
+
+.process-track-base.expanded .process-track-name:before {
+ content: '\25BE'; /* Down triangle */
+}
+
+.process-track-close {
+ color: black;
+ border: 1px solid transparent;
+ padding: 0px 2px;
+}
+
+.process-track-close:hover {
+ border: 1px solid grey;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html
new file mode 100644
index 00000000000..89358b8411e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html
@@ -0,0 +1,313 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/process_track_base.css">
+
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/model_settings.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/counter_track.html">
+<link rel="import" href="/tracing/ui/tracks/frame_track.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/other_threads_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_summary_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ObjectSnapshotView = tr.ui.analysis.ObjectSnapshotView;
+ const ObjectInstanceView = tr.ui.analysis.ObjectInstanceView;
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ /**
+ * Visualizes a Process by building ThreadTracks and CounterTracks.
+ * @constructor
+ */
+ const ProcessTrackBase =
+ tr.ui.b.define('process-track-base', tr.ui.tracks.ContainerTrack);
+
+ ProcessTrackBase.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+ this.processBase_ = undefined;
+
+ Polymer.dom(this).classList.add('process-track-base');
+ Polymer.dom(this).classList.add('expanded');
+
+ this.processNameEl_ = tr.ui.b.createSpan();
+ Polymer.dom(this.processNameEl_).classList.add('process-track-name');
+
+ this.closeEl_ = tr.ui.b.createSpan();
+ Polymer.dom(this.closeEl_).classList.add('process-track-close');
+ this.closeEl_.textContent = 'X';
+
+ this.headerEl_ = tr.ui.b.createDiv({className: 'process-track-header'});
+ Polymer.dom(this.headerEl_).appendChild(this.processNameEl_);
+ Polymer.dom(this.headerEl_).appendChild(this.closeEl_);
+ this.headerEl_.addEventListener('click', this.onHeaderClick_.bind(this));
+
+ Polymer.dom(this).appendChild(this.headerEl_);
+ },
+
+ get processBase() {
+ return this.processBase_;
+ },
+
+ set processBase(processBase) {
+ this.processBase_ = processBase;
+
+ if (this.processBase_) {
+ const modelSettings = new tr.model.ModelSettings(
+ this.processBase_.model);
+ const defaultValue = this.processBase_.important;
+ this.expanded = modelSettings.getSettingFor(
+ this.processBase_, 'expanded', defaultValue);
+ }
+
+ this.updateContents_();
+ },
+
+ get expanded() {
+ return Polymer.dom(this).classList.contains('expanded');
+ },
+
+ set expanded(expanded) {
+ expanded = !!expanded;
+
+ if (this.expanded === expanded) return;
+
+ Polymer.dom(this).classList.toggle('expanded');
+
+ // Expanding and collapsing tracks is, essentially, growing and shrinking
+ // the viewport. We dispatch a change event to trigger any processing
+ // to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ if (!this.processBase_) return;
+
+ const modelSettings = new tr.model.ModelSettings(this.processBase_.model);
+ modelSettings.setSettingFor(this.processBase_, 'expanded', expanded);
+ this.updateContents_();
+ this.viewport.rebuildEventToTrackMap();
+ this.viewport.rebuildContainerToTrackMap();
+ },
+
+ set visible(visible) {
+ if (visible === this.visible) return;
+ this.hidden = !visible;
+
+ tr.b.dispatchSimpleEvent(this, 'visibility');
+ // Changing the visibility of the tracks can grow and shrink the viewport.
+ // We dispatch a change event to trigger any processing to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ if (!this.processBase_) return;
+
+ this.updateContents_();
+ this.viewport.rebuildEventToTrackMap();
+ this.viewport.rebuildContainerToTrackMap();
+ },
+
+ get visible() {
+ return !this.hidden;
+ },
+
+ get hasVisibleContent() {
+ if (this.expanded) {
+ return this.children.length > 1;
+ }
+ return true;
+ },
+
+ onHeaderClick_(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (e.target === this.closeEl_) {
+ this.visible = false;
+ } else {
+ this.expanded = !this.expanded;
+ }
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ if (!this.processBase_) return;
+
+ Polymer.dom(this.processNameEl_).textContent =
+ this.processBase_.userFriendlyName;
+ this.headerEl_.title = this.processBase_.userFriendlyDetails;
+
+ // Create the object instance tracks for this process.
+ this.willAppendTracks_();
+ if (this.expanded) {
+ this.appendMemoryDumpTrack_();
+ this.appendObjectInstanceTracks_();
+ this.appendCounterTracks_();
+ this.appendFrameTrack_();
+ this.appendThreadTracks_();
+ } else {
+ this.appendSummaryTrack_();
+ }
+ this.didAppendTracks_();
+ },
+
+ willAppendTracks_() {
+ },
+
+ didAppendTracks_() {
+ },
+
+ appendMemoryDumpTrack_() {
+ },
+
+ appendSummaryTrack_() {
+ const track = new tr.ui.tracks.ProcessSummaryTrack(this.viewport);
+ track.process = this.process;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ // no spacing track, since this track only shown in collapsed state
+ },
+
+ appendFrameTrack_() {
+ const frames = this.process ? this.process.frames : undefined;
+ if (!frames || !frames.length) return;
+
+ const track = new tr.ui.tracks.FrameTrack(this.viewport);
+ track.frames = frames;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendObjectInstanceTracks_() {
+ const instancesByTypeName =
+ this.processBase_.objects.getAllInstancesByTypeName();
+ const instanceTypeNames = Object.keys(instancesByTypeName);
+ instanceTypeNames.sort();
+
+ let didAppendAtLeastOneTrack = false;
+ instanceTypeNames.forEach(function(typeName) {
+ const allInstances = instancesByTypeName[typeName];
+
+ // If a object snapshot has a view it will be shown,
+ // unless the view asked for it to not be shown.
+ let instanceViewInfo = ObjectInstanceView.getTypeInfo(
+ undefined, typeName);
+ let snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+ undefined, typeName);
+ if (instanceViewInfo && !instanceViewInfo.metadata.showInTrackView) {
+ instanceViewInfo = undefined;
+ }
+ if (snapshotViewInfo && !snapshotViewInfo.metadata.showInTrackView) {
+ snapshotViewInfo = undefined;
+ }
+ const hasViewInfo = instanceViewInfo || snapshotViewInfo;
+
+ // There are some instances that don't merit their own track in
+ // the UI. Filter them out.
+ const visibleInstances = [];
+ for (let i = 0; i < allInstances.length; i++) {
+ const instance = allInstances[i];
+
+ // Do not create tracks for instances that have no snapshots.
+ if (instance.snapshots.length === 0) continue;
+
+ // Do not create tracks for instances that have implicit snapshots
+ // and don't have a view.
+ if (instance.hasImplicitSnapshots && !hasViewInfo) continue;
+
+ visibleInstances.push(instance);
+ }
+ if (visibleInstances.length === 0) return;
+
+ // Look up the constructor for this track, or use the default
+ // constructor if none exists.
+ let trackConstructor =
+ tr.ui.tracks.ObjectInstanceTrack.getConstructor(
+ undefined, typeName);
+ if (!trackConstructor) {
+ snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+ undefined, typeName);
+ if (snapshotViewInfo && snapshotViewInfo.metadata.showInstances) {
+ trackConstructor = tr.ui.tracks.ObjectInstanceGroupTrack;
+ } else {
+ trackConstructor = tr.ui.tracks.ObjectInstanceTrack;
+ }
+ }
+ const track = new trackConstructor(this.viewport);
+ track.objectInstances = visibleInstances;
+ Polymer.dom(this).appendChild(track);
+ didAppendAtLeastOneTrack = true;
+ }, this);
+ if (didAppendAtLeastOneTrack) {
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ },
+
+ appendCounterTracks_() {
+ // Add counter tracks for this process.
+ const counters = Object.values(this.processBase.counters);
+ counters.sort(tr.model.Counter.compare);
+
+ // Create the counters for this process.
+ counters.forEach(function(counter) {
+ const track = new tr.ui.tracks.CounterTrack(this.viewport);
+ track.counter = counter;
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }.bind(this));
+ },
+
+ appendThreadTracks_() {
+ // Get a sorted list of threads.
+ const threads = Object.values(this.processBase.threads);
+ threads.sort(tr.model.Thread.compare);
+
+ // Create the threads.
+ const otherThreads = [];
+ let hasVisibleThreads = false;
+ threads.forEach(function(thread) {
+ const track = new tr.ui.tracks.ThreadTrack(this.viewport);
+ track.thread = thread;
+ if (!track.hasVisibleContent) return;
+
+ if (track.hasSlices) {
+ hasVisibleThreads = true;
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ } else if (track.hasTimeSlices) {
+ otherThreads.push(thread);
+ }
+ }.bind(this));
+
+ if (otherThreads.length > 0) {
+ // If there's only 1 thread with scheduling-only information don't
+ // bother making a group, just display it directly
+ // Similarly if we are a process with only scheduling-only threads
+ // don't bother making a group as the process itself serves
+ // as the collapsable group
+ const track = new tr.ui.tracks.OtherThreadsTrack(this.viewport);
+ track.threads = otherThreads;
+ track.collapsible = otherThreads.length > 1 && hasVisibleThreads;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+ };
+
+ return {
+ ProcessTrackBase,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css
new file mode 100644
index 00000000000..0467c91562c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2014 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.
+ */
+
+.rect-track {
+ height: 18px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html
new file mode 100644
index 00000000000..65e073d32ee
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/rect_track.css">
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/fast_rect_renderer.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Rect objects.
+ * @constructor
+ * @extends {Track}
+ */
+ const RectTrack = tr.ui.b.define(
+ 'rect-track', tr.ui.tracks.Track);
+
+ RectTrack.prototype = {
+
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('rect-track');
+ this.asyncStyle_ = false;
+ this.rects_ = null;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ set selectionGenerator(generator) {
+ this.heading_.selectionGenerator = generator;
+ },
+
+ set expanded(expanded) {
+ this.heading_.expanded = !!expanded;
+ },
+
+ set arrowVisible(arrowVisible) {
+ this.heading_.arrowVisible = !!arrowVisible;
+ },
+
+ get expanded() {
+ return this.heading_.expanded;
+ },
+
+ get asyncStyle() {
+ return this.asyncStyle_;
+ },
+
+ set asyncStyle(v) {
+ this.asyncStyle_ = !!v;
+ },
+
+ get rects() {
+ return this.rects_;
+ },
+
+ set rects(rects) {
+ this.rects_ = rects || [];
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ this.invalidateDrawingContainer();
+ },
+
+ get hasVisibleContent() {
+ return this.rects_.length > 0;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawRects_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawRects_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+
+ ctx.save();
+ const bounds = this.getBoundingClientRect();
+ tr.ui.b.drawSlices(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.rects_,
+ this.asyncStyle_);
+ ctx.restore();
+
+ if (bounds.height <= 6) return;
+
+ let fontSize;
+ let yOffset;
+ if (bounds.height < 15) {
+ fontSize = 6;
+ yOffset = 1.0;
+ } else {
+ fontSize = 10;
+ yOffset = 2.5;
+ }
+ tr.ui.b.drawLabels(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ this.rects_,
+ this.asyncStyle_,
+ fontSize,
+ yOffset);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.rects_ === undefined || this.rects_ === null) {
+ return;
+ }
+
+ this.rects_.forEach(function(rect) {
+ rect.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onRect(rect) {
+ rect.addToSelection(selection);
+ }
+ onRect = onRect.bind(this);
+ const instantEventWidth = 2 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.rects_,
+ function(x) { return x.start; },
+ function(x) {
+ return x.duration === 0 ?
+ x.duration + instantEventWidth :
+ x.duration;
+ },
+ loWX, hiWX,
+ onRect);
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {rect} The current rect.
+ * @param {Number} offset Number of rects away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ const index = this.rects_.findIndex(rect => rect.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex < 0 || newIndex >= this.rects_.length) return false;
+
+ this.rects_[newIndex].addToSelection(selection);
+ return true;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ for (let i = 0; i < this.rects_.length; ++i) {
+ // TODO(petrcermak): Rather than unpacking the proxy item here,
+ // we should probably add an addToSelectionIfMatching(selection, filter)
+ // method to SelectableItem (#900).
+ const modelItem = this.rects_[i].modelItem;
+ if (!modelItem) continue;
+
+ if (filter.matchSlice(modelItem)) {
+ selection.push(modelItem);
+ }
+ }
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const rect = tr.b.findClosestIntervalInSortedIntervals(
+ this.rects_,
+ function(x) { return x.start; },
+ function(x) { return x.end; },
+ worldX,
+ worldMaxDist);
+
+ if (!rect) return;
+
+ rect.addToSelection(selection);
+ }
+ };
+
+ /**
+ * A filled rectangle with a title.
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function Rect(modelItem, title, colorId, start, duration) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.title = title;
+ this.colorId = colorId;
+ this.start = start;
+ this.duration = duration;
+ this.end = start + duration;
+ }
+
+ Rect.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype
+ };
+
+ return {
+ RectTrack,
+ Rect,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html
new file mode 100644
index 00000000000..ec81a8835d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html
@@ -0,0 +1,412 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const RectTrack = tr.ui.tracks.RectTrack;
+ const Rect = tr.ui.tracks.Rect;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const Viewport = tr.ui.TimelineViewport;
+
+ test('instantiate_withRects', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasicRects';
+ track.rects = [
+ new Rect(undefined, 'a', 0, 1, 1),
+ new Rect(undefined, 'b', 1, 2.1, 4.8),
+ new Rect(undefined, 'b', 1, 7, 0.5),
+ new Rect(undefined, 'c', 2, 7.6, 0.4)
+ ];
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_withSlices', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasicSlices';
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_shrinkingRectSize', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testShrinkingRectSizes';
+ let x = 0;
+ const widths = [10, 5, 4, 3, 2, 1, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05];
+ const slices = [];
+ for (let i = 0; i < widths.length; i++) {
+ const s = new Rect(undefined, 'a', 1, x, widths[i]);
+ x += s.duration + 0.5;
+ slices.push(s);
+ }
+ track.rects = slices;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 1.1 * x, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_elide', function() {
+ const optDicts = [{ trackName: 'elideOff', elide: false },
+ { trackName: 'elideOn', elide: true }];
+
+ const tooLongTitle = 'Unless eliding this SHOULD NOT BE DISPLAYED. ';
+ const bigTitle = 'Very big title name that goes on longer ' +
+ 'than you may expect';
+
+ for (const dictIndex in optDicts) {
+ const dict = optDicts[dictIndex];
+
+ const div = document.createElement('div');
+ Polymer.dom(div).appendChild(document.createTextNode(dict.trackName));
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.SHOULD_ELIDE_TEXT = dict.elide;
+ track.heading = 'Visual: ' + dict.trackName;
+ track.rects = [
+ // title, colorId, start, args, opt_duration
+ new Rect(undefined, 'a ' + tooLongTitle + bigTitle, 0, 1, 1),
+ new Rect(undefined, bigTitle, 1, 2.1, 4.8),
+ new Rect(undefined, 'cccc cccc cccc', 1, 7, 0.5),
+ new Rect(undefined, 'd', 2, 7.6, 1.0)
+ ];
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 9.5, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ }
+ });
+
+ test('findAllObjectsMatchingInRectTrack', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+ const selection = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('b'), selection);
+
+ const predictedSelection = new EventSet(
+ [track.rects[1].modelItem, track.rects[2].modelItem]);
+ assert.isTrue(selection.equals(predictedSelection));
+ });
+
+ test('selectionHitTesting', function() {
+ const testEl = document.createElement('div');
+ Polymer.dom(testEl).appendChild(
+ tr.ui.b.createScopedStyle('heading { width: 100px; }'));
+ testEl.style.width = '600px';
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ track.heading = 'testSelectionHitTesting';
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 5, {}, 4.8)
+ ];
+ const y = track.getBoundingClientRect().top + 5;
+ const pixelRatio = window.devicePixelRatio || 1;
+ const wW = 10;
+ const vW = drawingContainer.canvas.getBoundingClientRect().width;
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, wW, vW * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let selection = new EventSet();
+ let x = (1.5 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(new EventSet(track.rects[0].modelItem)));
+
+ selection = new EventSet();
+ x = (2.1 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.strictEqual(0, selection.length);
+
+ selection = new EventSet();
+ x = (6.8 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(new EventSet(track.rects[1].modelItem)));
+
+ selection = new EventSet();
+ x = (9.9 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.strictEqual(0, selection.length);
+ });
+
+ test('elide', function() {
+ const testEl = document.createElement('div');
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ const bigtitle = 'Super duper long long title ' +
+ 'holy moly when did you get so verbose?';
+ const smalltitle = 'small';
+ track.heading = 'testElide';
+ track.rects = [
+ // title, colorId, start, args, opt_duration
+ new ThreadSlice('', bigtitle, 0, 1, {}, 1),
+ new ThreadSlice('', smalltitle, 1, 2, {}, 1)
+ ];
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 3.3, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let stringWidthPair = undefined;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ // Small titles on big slices are not elided.
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ smalltitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ smalltitle),
+ 1);
+ assert.strictEqual(smalltitle, stringWidthPair.string);
+
+ // Keep shrinking the slice until eliding starts.
+ let elidedWhenSmallEnough = false;
+ for (let sliceLength = 1; sliceLength >= 0.00001; sliceLength /= 2.0) {
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ smalltitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ smalltitle),
+ sliceLength);
+ if (stringWidthPair.string.length < smalltitle.length) {
+ elidedWhenSmallEnough = true;
+ break;
+ }
+ }
+ assert.isTrue(elidedWhenSmallEnough);
+
+ // Big titles are elided immediately.
+ let superBigTitle = '';
+ for (let x = 0; x < 10; x++) {
+ superBigTitle += bigtitle;
+ }
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ superBigTitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ superBigTitle),
+ 1);
+ assert.isTrue(stringWidthPair.string.length < superBigTitle.length);
+
+ // And elided text ends with ...
+ const len = stringWidthPair.string.length;
+ assert.strictEqual('...', stringWidthPair.string.substring(len - 3, len));
+ });
+
+ test('rectTrackAddItemNearToProvidedEvent', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+ let sel = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('b'), sel);
+
+ // Select to the right of B.
+ const selRight = new EventSet();
+ let ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), 1, selRight);
+ assert.isTrue(ret);
+ assert.strictEqual(
+ track.rects[2].modelItem, tr.b.getFirstElement(selRight));
+
+ // Select to the right of the 2nd b.
+ const selRight2 = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), 2, selRight2);
+ assert.isTrue(ret);
+ assert.strictEqual(
+ track.rects[3].modelItem, tr.b.getFirstElement(selRight2));
+
+ // Select to 2 to the right of the 2nd b.
+ const selRightOfRight = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(selRight), 1, selRightOfRight);
+ assert.isTrue(ret);
+ assert.strictEqual(track.rects[3].modelItem,
+ tr.b.getFirstElement(selRightOfRight));
+
+ // Select to the right of the rightmost slice.
+ let selNone = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(selRightOfRight), 1, selNone);
+ assert.isFalse(ret);
+ assert.strictEqual(0, selNone.length);
+
+ // Select A and then select left.
+ sel = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('a'), sel);
+
+ selNone = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), -1, selNone);
+ assert.isFalse(ret);
+ assert.strictEqual(0, selNone.length);
+ });
+
+ test('rectTrackAddClosestEventToSelection', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+
+ // Before with not range.
+ let sel = new EventSet();
+ track.addClosestEventToSelection(0, 0, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Before with negative range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.5, -10, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Before first slice.
+ sel = new EventSet();
+ track.addClosestEventToSelection(0.5, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem)));
+
+ // Within first slice closer to start.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem)));
+
+ // Between slices with good range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(2.08, 3, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem)));
+
+ // Between slices with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(2.05, 0.03, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Within slice closer to end.
+ sel = new EventSet();
+ track.addClosestEventToSelection(6, 100, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem)));
+
+ // Within slice with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.8, 0.1, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // After last slice with good range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(8.5, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[3].modelItem)));
+
+ // After last slice with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(10, 1, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html
new file mode 100644
index 00000000000..1f764019cfc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Sample objects.
+ * @constructor
+ * @extends {RectTrack}
+ */
+ const SampleTrack = tr.ui.b.define(
+ 'sample-track', tr.ui.tracks.RectTrack);
+
+ SampleTrack.prototype = {
+
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get samples() {
+ return this.rects;
+ },
+
+ set samples(samples) {
+ this.rects = samples;
+ }
+ };
+
+ return {
+ SampleTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html
new file mode 100644
index 00000000000..0fb17df65f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/profile_node.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/sample_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SampleTrack = tr.ui.tracks.SampleTrack;
+ const Sample = tr.model.Sample;
+ const ProfileNode = tr.model.ProfileNode;
+
+ test('modelMapping', function() {
+ const track = new SampleTrack(new tr.ui.TimelineViewport());
+ const node = new ProfileNode(1, {
+ functionName: 'a'
+ }, undefined);
+ const sample = new Sample(10, 'instructions_retired', node);
+ track.samples = [sample];
+ const me0 = track.rects[0].modelItem;
+ assert.strictEqual(me0, sample);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html
new file mode 100644
index 00000000000..36f09566b07
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a SliceGroup.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const SliceGroupTrack = tr.ui.b.define(
+ 'slice-group-track', tr.ui.tracks.MultiRowTrack);
+
+ SliceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('slice-group-track');
+ this.group_ = undefined;
+ // Set the collapse threshold so we don't collapse by default, but the
+ // user can explicitly collapse if they want it.
+ this.defaultToCollapsedWhenSubRowCountMoreThan = 100;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(group) {
+ this.group_ = group;
+ this.setItemsToGroup(this.group_.slices, this.group_);
+ },
+
+ get eventContainer() {
+ return this.group;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.group, this);
+ },
+
+ /**
+ * Breaks up the list of slices into N rows, each of which is a list of
+ * slices that are non overlapping.
+ */
+ buildSubRows_(slices) {
+ const precisionUnit = this.group.model.intrinsicTimeUnit;
+
+ // This function works by walking through slices by start time.
+ //
+ // The basic idea here is to insert each slice as deep into the subrow
+ // list as it can go such that every subSlice is fully contained by its
+ // parent slice.
+ //
+ // Visually, if we start with this:
+ // 0: [ a ]
+ // 1: [ b ]
+ // 2: [c][d]
+ //
+ // To place this slice:
+ // [e]
+ // We first check row 2's last item, [d]. [e] wont fit into [d] (they dont
+ // even intersect). So we go to row 1. That gives us [b], and [d] wont fit
+ // into that either. So, we go to row 0 and its last slice, [a]. That can
+ // completely contain [e], so that means we should add [e] as a subchild
+ // of [a]. That puts it on row 1, yielding:
+ // 0: [ a ]
+ // 1: [ b ][e]
+ // 2: [c][d]
+ //
+ // If we then get this slice:
+ // [f]
+ // We do the same deepest-to-shallowest walk of the subrows trying to fit
+ // it. This time, it doesn't fit in any open slice. So, we simply append
+ // it to row 0:
+ // 0: [ a ] [f]
+ // 1: [ b ][e]
+ // 2: [c][d]
+ if (!slices.length) return [];
+
+ const ops = [];
+ for (let i = 0; i < slices.length; i++) {
+ if (slices[i].subSlices) {
+ slices[i].subSlices.splice(0,
+ slices[i].subSlices.length);
+ }
+ ops.push(i);
+ }
+
+ ops.sort(function(ix, iy) {
+ const x = slices[ix];
+ const y = slices[iy];
+ if (x.start !== y.start) return x.start - y.start;
+
+ // Elements get inserted into the slices array in order of when the
+ // slices start. Because slices must be properly nested, we break
+ // start-time ties by assuming that the elements appearing earlier in
+ // the slices array (and thus ending earlier) start earlier.
+ return ix - iy;
+ });
+
+ const subRows = [[]];
+ this.badSlices_ = []; // TODO(simonjam): Connect this again.
+
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ const slice = slices[op];
+
+ // Try to fit the slice into the existing subrows.
+ let inserted = false;
+ for (let j = subRows.length - 1; j >= 0; j--) {
+ if (subRows[j].length === 0) continue;
+
+ const insertedSlice = subRows[j][subRows[j].length - 1];
+ if (slice.start < insertedSlice.start) {
+ this.badSlices_.push(slice);
+ inserted = true;
+ }
+ if (insertedSlice.bounds(slice, precisionUnit)) {
+ // Insert it into subRow j + 1.
+ while (subRows.length <= j + 1) {
+ subRows.push([]);
+ }
+ subRows[j + 1].push(slice);
+ if (insertedSlice.subSlices) {
+ insertedSlice.subSlices.push(slice);
+ }
+ inserted = true;
+ break;
+ }
+ }
+ if (inserted) continue;
+
+ // Append it to subRow[0] as a root.
+ subRows[0].push(slice);
+ }
+
+ return subRows;
+ }
+ };
+
+ return {
+ SliceGroupTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html
new file mode 100644
index 00000000000..a8b5842f945
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html
@@ -0,0 +1,299 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProcessTrack = tr.ui.tracks.ProcessTrack;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const SliceGroup = tr.model.SliceGroup;
+ const SliceGroupTrack = tr.ui.tracks.SliceGroupTrack;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('subRowBuilderBasic', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+ const sB = group.pushSlice(newSliceEx({title: 'a', start: 3, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderBasic2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderNestedExactly', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 4}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sB]);
+ assert.deepEqual(subRows[1], [sA]);
+ });
+
+ test('subRowBuilderInstantEvents', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderTwoInstantEvents', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOutOfOrderAddition', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ][ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderOutOfOrderAddition2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 5}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOnNestedZeroLength', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b1 ] []<- b2 where b2.duration = 0 and b2.end === a.end.
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB1 = group.pushSlice(newSliceEx(
+ {title: 'b1', start: 1, duration: 2}));
+ const sB2 = group.pushSlice(newSliceEx(
+ {title: 'b2', start: 4, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB1, sB2]);
+ });
+
+ test('subRowBuilderOnGroup1', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ] [ c ]
+ // [ b ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx({title: 'c', start: 5, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA, sC]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOnGroup2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ] [ d ]
+ // [ b ]
+ // [ c ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx(
+ {title: 'c', start: 1.75, duration: 0.5}));
+ const sD = group.pushSlice(newSliceEx(
+ {title: 'c', start: 5, duration: 0.25}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+
+ const subRows = track.subRows;
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 3);
+ assert.deepEqual(subRows[0], [sA, sD]);
+ assert.deepEqual(subRows[1], [sB]);
+ assert.deepEqual(subRows[2], [sC]);
+ });
+
+ test('trackFiltering', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+
+ assert.strictEqual(track.subRows.length, 2);
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('sliceGroupContainerMap', function() {
+ const vp = new tr.ui.TimelineViewport();
+ const containerToTrack = vp.containerToTrackMap;
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new SliceGroup(thread);
+
+ const processTrack = new ProcessTrack(vp);
+ const threadTrack = new ThreadTrack(vp);
+ const groupTrack = new SliceGroupTrack(vp);
+ processTrack.process = process;
+ threadTrack.thread = thread;
+ groupTrack.group = group;
+ Polymer.dom(processTrack).appendChild(threadTrack);
+ Polymer.dom(threadTrack).appendChild(groupTrack);
+
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+
+ assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+ assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+ assert.isUndefined(
+ containerToTrack.getTrackByStableId('123.456.SliceGroup'));
+
+ vp.modelTrackContainer = {
+ addContainersToTrackMap(containerToTrackMap) {
+ processTrack.addContainersToTrackMap(containerToTrackMap);
+ },
+ addEventListener() {}
+ };
+ vp.rebuildContainerToTrackMap();
+
+ // Check that all tracks call childs' addContainersToTrackMap()
+ // by checking the resulting map.
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123'), processTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456'), threadTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456.SliceGroup'), groupTrack);
+
+ // Check the track's eventContainer getter.
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html
new file mode 100644
index 00000000000..1e1386bff66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Slice objects.
+ * @constructor
+ * @extends {RectTrack}
+ */
+ const SliceTrack = tr.ui.b.define(
+ 'slice-track', tr.ui.tracks.RectTrack);
+
+ SliceTrack.prototype = {
+
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get slices() {
+ return this.rects;
+ },
+
+ set slices(slices) {
+ this.rects = slices;
+ }
+ };
+
+ return {
+ SliceTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html
new file mode 100644
index 00000000000..7ba42d3dc79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SliceTrack = tr.ui.tracks.SliceTrack;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ test('modelMapping', function() {
+ const track = new SliceTrack(new tr.ui.TimelineViewport());
+ const slice = new ThreadSlice('', 'a', 0, 1, {}, 1);
+ track.slices = [slice];
+ const me0 = track.rects[0].modelItem;
+ assert.strictEqual(slice, me0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css
new file mode 100644
index 00000000000..094eee0862d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css
@@ -0,0 +1,7 @@
+/* Copyright (c) 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.
+ */
+.spacing-track {
+ height: 4px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html
new file mode 100644
index 00000000000..a321066daa2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/spacing_track.css">
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track used to provide whitespace between the tracks above and below it.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.Track}
+ */
+ const SpacingTrack = tr.ui.b.define('spacing-track', tr.ui.tracks.Track);
+
+ SpacingTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('spacing-track');
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ }
+ };
+
+ return {
+ SpacingTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html
new file mode 100644
index 00000000000..7a292c04113
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays traces as stacked bars.
+ * @constructor
+ * @extends {Track}
+ */
+ const StackedBarsTrack = tr.ui.b.define(
+ 'stacked-bars-track', tr.ui.tracks.Track);
+
+ StackedBarsTrack.prototype = {
+
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('stacked-bars-track');
+ this.objectInstance_ = null;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ const objectSnapshots = this.objectInstance_.snapshots;
+ objectSnapshots.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ },
+
+ /**
+ * Used to hit-test clicks in the graph.
+ */
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onSnapshot(snapshot) {
+ selection.push(snapshot);
+ }
+
+ const snapshots = this.objectInstance_.snapshots;
+ const maxBounds = this.objectInstance_.parent.model.bounds.max;
+
+ tr.b.iterateOverIntersectingIntervals(
+ snapshots,
+ function(x) { return x.ts; },
+ function(x, i) {
+ if (i === snapshots.length - 1) {
+ if (snapshots.length === 1) {
+ return maxBounds;
+ }
+
+ return snapshots[i].ts - snapshots[i - 1].ts;
+ }
+
+ return snapshots[i + 1].ts - snapshots[i].ts;
+ },
+ loWX, hiWX,
+ onSnapshot);
+ },
+
+ /**
+ * Add the item to the left or right of the provided item, if any, to the
+ * selection.
+ * @param {slice} The current slice.
+ * @param {Number} offset Number of slices away from the object to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (!(event instanceof tr.model.ObjectSnapshot)) {
+ throw new Error('Unrecognized event');
+ }
+ const objectSnapshots = this.objectInstance_.snapshots;
+ const index = objectSnapshots.indexOf(event);
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < objectSnapshots.length) {
+ selection.push(objectSnapshots[newIndex]);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const snapshot = tr.b.findClosestElementInSortedArray(
+ this.objectInstance_.snapshots,
+ function(x) { return x.ts; },
+ worldX,
+ worldMaxDist);
+
+ if (!snapshot) return;
+
+ selection.push(snapshot);
+ }
+ };
+
+ return {
+ StackedBarsTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css
new file mode 100644
index 00000000000..4e063bbad48
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css
@@ -0,0 +1,10 @@
+/* Copyright (c) 2012 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.
+ */
+
+.thread-track {
+ flex-direction: column;
+ display: flex;
+ position: relative;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html
new file mode 100644
index 00000000000..c6ea8fa576c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/thread_track.css">
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/async_slice_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/sample_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * Visualizes a Thread using a series of SliceTracks.
+ * @constructor
+ */
+ const ThreadTrack = tr.ui.b.define('thread-track',
+ tr.ui.tracks.ContainerTrack);
+ ThreadTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('thread-track');
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ },
+
+ get thread() {
+ return this.thread_;
+ },
+
+ set thread(thread) {
+ this.thread_ = thread;
+ this.updateContents_();
+ },
+
+ get hasVisibleContent() {
+ return this.tracks_.length > 0;
+ },
+
+ get hasSlices() {
+ return this.thread_.asyncSliceGroup.length > 0 ||
+ this.thread_.sliceGroup.length > 0;
+ },
+
+ get hasTimeSlices() {
+ return this.thread_.timeSlices;
+ },
+
+ get eventContainer() {
+ return this.thread;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.thread, this);
+ },
+
+ updateContents_() {
+ this.detach();
+
+ if (!this.thread_) return;
+
+ this.heading_.heading = this.thread_.userFriendlyName;
+ this.heading_.tooltip = this.thread_.userFriendlyDetails;
+
+ if (this.thread_.asyncSliceGroup.length) {
+ this.appendAsyncSliceTracks_();
+ }
+
+ this.appendThreadSamplesTracks_();
+
+ let needsHeading = false;
+ if (this.thread_.timeSlices) {
+ const timeSlicesTrack = new tr.ui.tracks.SliceTrack(this.viewport);
+ timeSlicesTrack.heading = '';
+ timeSlicesTrack.height = tr.ui.b.THIN_SLICE_HEIGHT + 'px';
+ timeSlicesTrack.slices = this.thread_.timeSlices;
+ if (timeSlicesTrack.hasVisibleContent) {
+ needsHeading = true;
+ Polymer.dom(this).appendChild(timeSlicesTrack);
+ }
+ }
+
+ if (this.thread_.sliceGroup.length) {
+ const track = new tr.ui.tracks.SliceGroupTrack(this.viewport);
+ track.heading = this.thread_.userFriendlyName;
+ track.tooltip = this.thread_.userFriendlyDetails;
+ track.group = this.thread_.sliceGroup;
+ if (track.hasVisibleContent) {
+ needsHeading = false;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+
+ if (needsHeading) {
+ Polymer.dom(this).appendChild(this.heading_);
+ }
+ },
+
+ appendAsyncSliceTracks_() {
+ const subGroups = this.thread_.asyncSliceGroup.viewSubGroups;
+ // TODO(kraynov): Support nested sub-groups.
+ subGroups.forEach(function(subGroup) {
+ const asyncTrack = new tr.ui.tracks.AsyncSliceGroupTrack(this.viewport);
+ asyncTrack.group = subGroup;
+ asyncTrack.heading = subGroup.title;
+ if (asyncTrack.hasVisibleContent) {
+ Polymer.dom(this).appendChild(asyncTrack);
+ }
+ }, this);
+ },
+
+ appendThreadSamplesTracks_() {
+ const threadSamples = this.thread_.samples;
+ if (threadSamples === undefined || threadSamples.length === 0) {
+ return;
+ }
+ const samplesByTitle = {};
+ threadSamples.forEach(function(sample) {
+ if (samplesByTitle[sample.title] === undefined) {
+ samplesByTitle[sample.title] = [];
+ }
+ samplesByTitle[sample.title].push(sample);
+ });
+
+ const sampleTitles = Object.keys(samplesByTitle);
+ sampleTitles.sort();
+
+ sampleTitles.forEach(function(sampleTitle) {
+ const samples = samplesByTitle[sampleTitle];
+ const samplesTrack = new tr.ui.tracks.SampleTrack(this.viewport);
+ samplesTrack.group = this.thread_;
+ samplesTrack.samples = samples;
+ samplesTrack.heading = this.thread_.userFriendlyName + ': ' +
+ sampleTitle;
+ samplesTrack.tooltip = this.thread_.userFriendlyDetails;
+ samplesTrack.selectionGenerator = function() {
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < samplesTrack.samples.length; i++) {
+ selection.push(samplesTrack.samples[i]);
+ }
+ return selection;
+ };
+ Polymer.dom(this).appendChild(samplesTrack);
+ }, this);
+ },
+
+ collapsedDidChange(collapsed) {
+ if (collapsed) {
+ let h = parseInt(this.tracks[0].height);
+ for (let i = 0; i < this.tracks.length; ++i) {
+ if (h > 2) {
+ this.tracks[i].height = Math.floor(h) + 'px';
+ } else {
+ this.tracks[i].style.display = 'none';
+ }
+ h = h * 0.5;
+ }
+ } else {
+ for (let i = 0; i < this.tracks.length; ++i) {
+ this.tracks[i].height = this.tracks[0].height;
+ this.tracks[i].style.display = '';
+ }
+ }
+ }
+ };
+
+ return {
+ ThreadTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html
new file mode 100644
index 00000000000..1ece1aa3f93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/instant_event.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const HighlightInstantEvent = tr.model.ThreadHighlightInstantEvent;
+ const Process = tr.model.Process;
+ const EventSet = tr.model.EventSet;
+ const StackFrame = tr.model.StackFrame;
+ const Sample = tr.model.Sample;
+ const Thread = tr.model.Thread;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const Viewport = tr.ui.TimelineViewport;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('selectionHitTestingWithThreadTrack', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'b', 0, 5.1, {}, 4));
+
+ const testEl = document.createElement('div');
+ Polymer.dom(testEl).appendChild(
+ tr.ui.b.createScopedStyle('heading { width: 100px; }'));
+ testEl.style.width = '600px';
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new ThreadTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.updateCanvasSizeIfNeeded_();
+ track.thread = t1;
+
+ const y = track.getBoundingClientRect().top;
+ const h = track.getBoundingClientRect().height;
+ const wW = 10;
+ const vW = drawingContainer.canvas.getBoundingClientRect().width;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, wW, vW);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let selection = new EventSet();
+ const x = (1.5 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(
+ new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]])));
+
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelection(
+ (1.5 / wW) * vW, (1.8 / wW) * vW,
+ y, y + h, selection);
+ assert.isTrue(selection.equals(
+ new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]])));
+ });
+
+ test('filterThreadSlices', function() {
+ const model = new tr.Model();
+ const thread = new Thread(new Process(model, 7), 1);
+ thread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0, duration: 0}));
+ thread.asyncSliceGroup.push(newAsyncSliceNamed('a', 0, 5, thread, thread));
+ const t = new ThreadTrack(new tr.ui.TimelineViewport());
+ t.thread = thread;
+
+ assert.strictEqual(t.tracks_.length, 2);
+ assert.instanceOf(t.tracks_[0], tr.ui.tracks.AsyncSliceGroupTrack);
+ assert.instanceOf(t.tracks_[1], tr.ui.tracks.SliceGroupTrack);
+ });
+
+ test('sampleThreadSlices', function() {
+ let thread;
+ let cpu;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ cpu = model.kernel.getOrCreateCpu(1);
+ thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+ const nodeA = tr.c.TestUtils.newProfileNode(model, 'a');
+ const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA);
+ const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB);
+ const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA);
+
+ model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread,
+ undefined, 10));
+ model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread,
+ undefined, 10));
+
+ model.samples.push(new Sample(25, 'page_fault', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(35, 'page_fault', nodeD, thread,
+ undefined, 10));
+ }
+ });
+
+ const t = new ThreadTrack(new tr.ui.TimelineViewport());
+ t.thread = thread;
+ assert.strictEqual(t.tracks_.length, 2);
+
+ // Instructions retired
+ const t0 = t.tracks_[0];
+ assert.notEqual(t0.heading.indexOf('instructions_retired'), -1);
+ assert.instanceOf(t0, tr.ui.tracks.SampleTrack);
+ assert.strictEqual(t0.samples.length, 4);
+ t0.samples.forEach(function(s) {
+ assert.instanceOf(s, tr.model.Sample);
+ });
+
+ // page_fault
+ const t1 = t.tracks_[1];
+ assert.notEqual(t1.heading.indexOf('page_fault'), -1);
+ assert.instanceOf(t1, tr.ui.tracks.SampleTrack);
+ assert.strictEqual(t1.samples.length, 2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css
new file mode 100644
index 00000000000..3d56eef5b8d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css
@@ -0,0 +1,33 @@
+/* Copyright (c) 2012 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.
+ */
+
+.track-button {
+ background-color: rgba(255, 255, 255, 0.5);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ color: rgba(0,0,0,0.2);
+ font-size: 10px;
+ height: 12px;
+ text-align: center;
+ width: 12px;
+}
+
+.track-button:hover {
+ background-color: rgba(255, 255, 255, 1.0);
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 1);
+}
+
+.track-close-button {
+ left: 2px;
+ position: absolute;
+ top: 2px;
+}
+
+.track-collapse-button {
+ left: 3px;
+ position: absolute;
+ top: 2px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html
new file mode 100644
index 00000000000..fccad427740
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="stylesheet" href="/tracing/ui/tracks/track.css">
+
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * The base class for all tracks, which render data into a provided div.
+ * @constructor
+ */
+ const Track = tr.ui.b.define('track',
+ tr.ui.b.ContainerThatDecoratesItsChildren);
+ Track.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorate(viewport) {
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+ if (viewport === undefined) {
+ throw new Error('viewport is required when creating a Track.');
+ }
+
+ this.viewport_ = viewport;
+ Polymer.dom(this).classList.add('track');
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ get drawingContainer() {
+ if (this instanceof tr.ui.tracks.DrawingContainer) return this;
+ let cur = this.parentElement;
+ while (cur) {
+ if (cur instanceof tr.ui.tracks.DrawingContainer) return cur;
+ cur = cur.parentElement;
+ }
+ return undefined;
+ },
+
+ get eventContainer() {
+ },
+
+ invalidateDrawingContainer() {
+ const dc = this.drawingContainer;
+ if (dc) dc.invalidate();
+ },
+
+ context() {
+ // This is a little weird here, but we have to be able to walk up the
+ // parent tree to get the context.
+ if (!Polymer.dom(this).parentNode) return undefined;
+
+ if (!Polymer.dom(this).parentNode.context) {
+ throw new Error('Parent container does not support context() method.');
+ }
+ return Polymer.dom(this).parentNode.context();
+ },
+
+ decorateChild_(childTrack) {
+ },
+
+ undecorateChild_(childTrack) {
+ if (childTrack.detach) {
+ childTrack.detach();
+ }
+ },
+
+ updateContents_() {
+ },
+
+ /**
+ * Wrapper function around draw() that performs transformations on the
+ * context necessary for the track's contents to be drawn in the right place
+ * given the current pan and zoom.
+ */
+ drawTrack(type) {
+ const ctx = this.context();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ this.draw(type, viewLWorld, viewRWorld, viewHeight);
+ ctx.restore();
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ },
+
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loVY, hiVY, selection) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const dt = this.viewport.currentDisplayTransform;
+ const viewPixWidthWorld = dt.xViewVectorToWorld(1);
+ const loWX = dt.xViewToWorld(loVX * pixelRatio);
+ const hiWX = dt.xViewToWorld(hiVX * pixelRatio);
+
+ const clientRect = this.getBoundingClientRect();
+ const a = Math.max(loVY, clientRect.top);
+ const b = Math.min(hiVY, clientRect.bottom);
+ if (a > b) return;
+
+ this.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ },
+
+ /**
+ * Gets implemented by supporting track types. The method adds the event
+ * closest to worldX to the selection.
+ *
+ * @param {number} worldX The position that is looked for.
+ * @param {number} worldMaxDist The maximum distance allowed from worldX to
+ * the event.
+ * @param {number} loY Lower Y bound of the search interval in view space.
+ * @param {number} hiY Upper Y bound of the search interval in view space.
+ * @param {Selection} selection Selection to which to add hits.
+ */
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ },
+
+ addClosestInstantEventToSelection(instantEvents, worldX,
+ worldMaxDist, selection) {
+ const instantEvent = tr.b.findClosestElementInSortedArray(
+ instantEvents,
+ function(x) { return x.start; },
+ worldX,
+ worldMaxDist);
+
+ if (!instantEvent) return;
+
+ selection.push(instantEvent);
+ }
+ };
+
+ return {
+ Track,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html
new file mode 100644
index 00000000000..620a35c8040
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html
@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.x-axis-track {
+ height: 12px;
+}
+
+.x-axis-track.tall-mode {
+ height: 30px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays the x-axis.
+ * @constructor
+ * @extends {Track}
+ */
+ const XAxisTrack = tr.ui.b.define('x-axis-track', tr.ui.tracks.Track);
+
+ XAxisTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('x-axis-track');
+ this.strings_secs_ = [];
+ this.strings_msecs_ = [];
+ this.strings_usecs_ = [];
+ this.strings_nsecs_ = [];
+
+ this.viewportChange_ = this.viewportChange_.bind(this);
+ viewport.addEventListener('change', this.viewportChange_);
+
+ const heading = document.createElement('tr-ui-b-heading');
+ heading.arrowVisible = false;
+ Polymer.dom(this).appendChild(heading);
+ },
+
+ detach() {
+ tr.ui.tracks.Track.prototype.detach.call(this);
+ this.viewport.removeEventListener('change',
+ this.viewportChange_);
+ },
+
+ viewportChange_() {
+ if (this.viewport.interestRange.isEmpty) {
+ Polymer.dom(this).classList.remove('tall-mode');
+ } else {
+ Polymer.dom(this).classList.add('tall-mode');
+ }
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GRID:
+ this.drawGrid_(viewLWorld, viewRWorld);
+ break;
+ case tr.ui.tracks.DrawType.MARKERS:
+ this.drawMarkers_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawGrid_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ const trackBounds = this.getBoundingClientRect();
+ const width = canvasBounds.width * pixelRatio;
+ const height = trackBounds.height * pixelRatio;
+
+ const hasInterestRange = !this.viewport.interestRange.isEmpty;
+
+ const xAxisHeightPx = hasInterestRange ? (height * 2) / 5 : height;
+
+ const vp = this.viewport;
+ const dt = vp.currentDisplayTransform;
+
+ vp.updateMajorMarkData(viewLWorld, viewRWorld);
+ const majorMarkDistanceWorld = vp.majorMarkWorldPositions.length > 1 ?
+ vp.majorMarkWorldPositions[1] - vp.majorMarkWorldPositions[0] : 0;
+
+ const numTicksPerMajor = 5;
+ const minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
+ const minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld);
+
+ const minorTickHeight = Math.floor(xAxisHeightPx * 0.25);
+
+ ctx.save();
+
+ ctx.lineWidth = Math.round(pixelRatio);
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ const crispLineCorrection = (ctx.lineWidth % 2) / 2;
+ ctx.translate(crispLineCorrection, -crispLineCorrection);
+
+ ctx.fillStyle = 'rgb(0, 0, 0)';
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'top';
+
+ ctx.font = (9 * pixelRatio) + 'px sans-serif';
+
+ const tickLabels = [];
+ ctx.beginPath();
+ for (let i = 0; i < vp.majorMarkWorldPositions.length; i++) {
+ const curXWorld = vp.majorMarkWorldPositions[i];
+ const curXView = dt.xWorldToView(curXWorld);
+ const displayText = vp.majorMarkUnit.format(
+ curXWorld, {deltaValue: majorMarkDistanceWorld});
+ ctx.fillText(displayText, curXView + (2 * pixelRatio), 0);
+
+ // Draw major mark.
+ tr.ui.b.drawLine(ctx, curXView, 0, curXView, xAxisHeightPx);
+
+ // Draw minor marks.
+ if (minorMarkDistancePx) {
+ for (let j = 1; j < numTicksPerMajor; ++j) {
+ const xView = Math.floor(curXView + minorMarkDistancePx * j);
+ tr.ui.b.drawLine(ctx,
+ xView, xAxisHeightPx - minorTickHeight,
+ xView, xAxisHeightPx);
+ }
+ }
+ }
+
+ // Draw bottom bar.
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ tr.ui.b.drawLine(ctx, 0, height, width, height);
+ ctx.stroke();
+
+ // Give distance between directly adjacent markers.
+ if (!hasInterestRange) return;
+
+ // Draw middle bar.
+ tr.ui.b.drawLine(ctx, 0, xAxisHeightPx, width, xAxisHeightPx);
+ ctx.stroke();
+
+ // Distance Variables.
+ let displayDistance;
+ const displayTextColor = 'rgb(0,0,0)';
+
+ // Arrow Variables.
+ const arrowSpacing = 10 * pixelRatio;
+ const arrowColor = 'rgb(128,121,121)';
+ const arrowPosY = xAxisHeightPx * 1.75;
+ const arrowWidthView = 3 * pixelRatio;
+ const arrowLengthView = 10 * pixelRatio;
+ const spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
+
+ ctx.textBaseline = 'middle';
+ ctx.font = (14 * pixelRatio) + 'px sans-serif';
+ const textPosY = arrowPosY;
+
+ const interestRange = vp.interestRange;
+
+ // If the range is zero, draw it's min timestamp next to the line.
+ if (interestRange.range === 0) {
+ const markerWorld = interestRange.min;
+ const markerView = dt.xWorldToView(markerWorld);
+
+ const textToDraw = vp.majorMarkUnit.format(markerWorld);
+ let textLeftView = markerView + 4 * pixelRatio;
+ const textWidthView = ctx.measureText(textToDraw).width;
+
+ // Put text to the left in case it gets cut off.
+ if (textLeftView + textWidthView > width) {
+ textLeftView = markerView - 4 * pixelRatio - textWidthView;
+ }
+
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+ return;
+ }
+
+ const leftMarker = interestRange.min;
+ const rightMarker = interestRange.max;
+
+ const leftMarkerView = dt.xWorldToView(leftMarker);
+ const rightMarkerView = dt.xWorldToView(rightMarker);
+
+ const distanceBetweenMarkers = interestRange.range;
+ const distanceBetweenMarkersView =
+ dt.xWorldVectorToView(distanceBetweenMarkers);
+ const positionInMiddleOfMarkersView =
+ leftMarkerView + (distanceBetweenMarkersView / 2);
+
+ const textToDraw = vp.majorMarkUnit.format(distanceBetweenMarkers);
+ const textWidthView = ctx.measureText(textToDraw).width;
+ const spaceForArrowsAndTextView =
+ textWidthView + spaceForArrowsView + arrowSpacing;
+
+ // Set text positions.
+ let textLeftView = positionInMiddleOfMarkersView - textWidthView / 2;
+ const textRightView = textLeftView + textWidthView;
+
+ if (spaceForArrowsAndTextView > distanceBetweenMarkersView) {
+ // Print the display distance text right of the 2 markers.
+ textLeftView = rightMarkerView + 2 * arrowSpacing;
+
+ // Put text to the left in case it gets cut off.
+ if (textLeftView + textWidthView > width) {
+ textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView;
+ }
+
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+
+ // Draw the arrows pointing from outside in and a line in between.
+ ctx.strokeStyle = arrowColor;
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView,
+ arrowPosY);
+ ctx.stroke();
+
+ ctx.fillStyle = arrowColor;
+ tr.ui.b.drawArrow(ctx,
+ leftMarkerView - 1.5 * arrowSpacing, arrowPosY,
+ leftMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ tr.ui.b.drawArrow(ctx,
+ rightMarkerView + 1.5 * arrowSpacing, arrowPosY,
+ rightMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ } else if (spaceForArrowsView <= distanceBetweenMarkersView) {
+ let leftArrowStart;
+ let rightArrowStart;
+ if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
+ // Print the display distance text.
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+
+ leftArrowStart = textLeftView - arrowSpacing;
+ rightArrowStart = textRightView + arrowSpacing;
+ } else {
+ leftArrowStart = positionInMiddleOfMarkersView;
+ rightArrowStart = positionInMiddleOfMarkersView;
+ }
+
+ // Draw the arrows pointing inside out.
+ ctx.strokeStyle = arrowColor;
+ ctx.fillStyle = arrowColor;
+ tr.ui.b.drawArrow(ctx,
+ leftArrowStart, arrowPosY,
+ leftMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ tr.ui.b.drawArrow(ctx,
+ rightArrowStart, arrowPosY,
+ rightMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ }
+
+ ctx.restore();
+ },
+
+ drawMarkers_(viewLWorld, viewRWorld) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const trackBounds = this.getBoundingClientRect();
+ const viewHeight = trackBounds.height * pixelRatio;
+
+ if (!this.viewport.interestRange.isEmpty) {
+ this.viewport.interestRange.draw(this.context(),
+ viewLWorld, viewRWorld, viewHeight);
+ }
+ },
+
+ /**
+ * Adds items intersecting the given range to a selection.
+ * @param {number} loVX Lower X bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVX Upper X bound of the interval to search, in
+ * viewspace.
+ * @param {number} loVY Lower Y bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVY Upper Y bound of the interval to search, in
+ * viewspace.
+ * @param {Selection} selection Selection to which to add results.
+ */
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection) {
+ // Does nothing. There's nothing interesting to pick on the xAxis
+ // track.
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ }
+ };
+
+ return {
+ XAxisTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html
new file mode 100644
index 00000000000..459c05cd122
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/x_axis_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_interestRange', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.interestRange.min = 300;
+ viewport.interestRange.max = 300;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_singlePointInterestRange', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.interestRange.min = 300;
+ viewport.interestRange.max = 400;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ function testTimeMode(mode, testInstance, numDigits, opt_unit) {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.timeMode = mode;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const trackContext = drawingContainer.ctx_;
+ const oldFillText = trackContext.fillText;
+ const fillTextText = [];
+ const fillTextThis = [];
+ trackContext.fillText = function(text, xPos, yPos) {
+ fillTextText.push(text);
+ fillTextThis.push(this);
+ return oldFillText.call(this, text, xPos, yPos);
+ };
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ testInstance.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ const formatter =
+ new Intl.NumberFormat(undefined, { numDigits, numDigits });
+ const formatFunction = function(value) {
+ let valueString = value.toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: numDigits
+ });
+ if (opt_unit) valueString += opt_unit;
+ return valueString;
+ };
+ const expectedText = viewport.majorMarkWorldPositions.map(
+ formatFunction);
+ assert.strictEqual(fillTextText.length, fillTextThis.length);
+ for (let i = 0; i < fillTextText.length; i++) {
+ assert.deepEqual(fillTextText[i], expectedText[i]);
+ assert.strictEqual(fillTextThis[i], trackContext);
+ }
+ }
+
+ test('instantiate_timeModeMs', function() {
+ testTimeMode(tr.ui.TimelineViewport.TimeMode.TIME_IN_MS,
+ this, 3, ' ms');
+ });
+
+ test('instantiate_timeModeRevisions', function() {
+ testTimeMode(tr.ui.TimelineViewport.TimeMode.REVISIONS, this, 0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html
new file mode 100644
index 00000000000..75b616332c2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<!--
+This element handles storing and retrieving the brushing state of arbitrary
+views (e.g. analysis sub-views). An element can use it by instantiating it and
+appending it to itself:
+
+ <div id="some-view-with-specific-brushing-state">
+ <tr-ui-b-view-specific-brushing-state view-id="unique-view-identifier">
+ </tr-ui-b-view-specific-brushing-state>
+ ... other child elements ...
+ </div>
+
+The state can then be retrieved from and pushed to the state element as
+follows:
+
+ newStateElement.set(state);
+ state = newStateElement.get();
+
+Under the hood, the state element searches the DOM tree for an ancestor element
+with a brushingStateController field to persist the state (see the
+tr.c.BrushingStateController and tr.ui.b.BrushingState classes for more
+details).
+-->
+<dom-module id='tr-ui-b-view-specific-brushing-state'>
+ <template></template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-view-specific-brushing-state',
+
+ /** Compulsory unique identifier of the associated view. */
+ get viewId() {
+ return this.getAttribute('view-id');
+ },
+
+ set viewId(viewId) {
+ Polymer.dom(this).setAttribute('view-id', viewId);
+ },
+
+ /**
+ * Retrieve the persisted state of the associated view. The returned object
+ * (or any of its fields) must not be modified by the caller (unless the
+ * object/field is treated as a reference).
+ *
+ * If no state has been persisted yet or there is no ancestor element with
+ * a brushingStateController field, this method returns undefined.
+ */
+ get() {
+ const viewId = this.viewId;
+ if (!viewId) {
+ throw new Error('Element must have a view-id attribute!');
+ }
+
+ const brushingStateController =
+ tr.c.BrushingStateController.getControllerForElement(this);
+ if (!brushingStateController) return undefined;
+
+ return brushingStateController.getViewSpecificBrushingState(viewId);
+ },
+
+ /**
+ * Persist the provided state of the associated view. The provided object
+ * (or any of its fields) must not be modified afterwards (unless the
+ * object/field is treated as a reference).
+ *
+ * If there is no ancestor element with a brushingStateController field,
+ * this method does nothing.
+ */
+ set(state) {
+ const viewId = this.viewId;
+ if (!viewId) {
+ throw new Error('Element must have a view-id attribute!');
+ }
+
+ const brushingStateController =
+ tr.c.BrushingStateController.getControllerForElement(this);
+ if (!brushingStateController) return;
+
+ brushingStateController.changeViewSpecificBrushingState(viewId, state);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html
new file mode 100644
index 00000000000..ee920cb1100
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/view_specific_brushing_state.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const BrushingStateController = tr.c.BrushingStateController;
+
+ function setStateElement(containerEl, viewId) {
+ const stateElement = document.createElement(
+ 'tr-ui-b-view-specific-brushing-state');
+ stateElement.viewId = viewId;
+ Polymer.dom(containerEl).appendChild(stateElement);
+ return stateElement;
+ }
+
+ function addChildDiv(element) {
+ const child = element.ownerDocument.createElement('div');
+ Polymer.dom(element).appendChild(child);
+ return child;
+ }
+
+ function addShadowChildDiv(element) {
+ const shadowRoot = element.createShadowRoot();
+ return addChildDiv(shadowRoot);
+ }
+
+ test('instantiate_withoutBrushingStateController', function() {
+ const containerEl = document.createElement('div');
+
+ const stateElement1 = setStateElement(containerEl, 'test-view');
+ assert.isUndefined(stateElement1.get());
+ stateElement1.set({e: 2.71828});
+ assert.isUndefined(stateElement1.get());
+ });
+
+ test('instantiate_withBrushingStateController', function() {
+ const rootEl = document.createElement('div');
+ const containerEl = addChildDiv(addShadowChildDiv(addChildDiv(rootEl)));
+ rootEl.brushingStateController = new BrushingStateController(undefined);
+
+ const stateElement1 = setStateElement(containerEl, 'test-view');
+ assert.isUndefined(stateElement1.get());
+ stateElement1.set({e: 2.71828});
+ assert.deepEqual(stateElement1.get(), {e: 2.71828});
+
+ const stateElement2 = setStateElement(containerEl, 'test-view-2');
+ assert.isUndefined(stateElement2.get());
+ stateElement2.set({pi: 3.14159});
+ assert.deepEqual(stateElement2.get(), {pi: 3.14159});
+
+ const stateElement3 = setStateElement(containerEl, 'test-view');
+ assert.deepEqual(stateElement3.get(), {e: 2.71828});
+
+ const stateElement4 = setStateElement(containerEl, 'test-view-2');
+ assert.deepEqual(stateElement4.get(), {pi: 3.14159});
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/__init__.py b/chromium/third_party/catapult/tracing/tracing/value/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/__init__.py
diff --git a/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html
new file mode 100644
index 00000000000..fb30056f60b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/legacy_unit_info.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v', function() {
+ class ChartJsonConverter {
+ /**
+ * Parses Values from |charts|, converts them to Histograms, and adds those
+ * to |histograms|.
+ *
+ * @param {!Array.<!Object>} charts
+ * @param {!tr.v.HistogramSet} histograms
+ */
+ static convertChartJson(charts, histograms) {
+ const traceValues = charts.charts.trace;
+
+ // The chromeperf dashboard requires some Diagnostics to be shared, even
+ // if there is only a single Histogram in the HistogramSet.
+ const diagnosticCaches = new Map();
+ function addSharedDiagnostic(hist, name, diagnostic) {
+ if (!diagnosticCaches.has(name)) {
+ diagnosticCaches.set(name, new Map());
+ }
+ const cache = diagnosticCaches.get(name);
+ const cacheKey = diagnostic instanceof tr.v.d.GenericSet ?
+ [...diagnostic].sort().join(',') : diagnostic.minDate;
+ if (!cache.has(cacheKey)) {
+ cache.set(cacheKey, diagnostic);
+ histograms.addSharedDiagnostic(diagnostic);
+ }
+ hist.diagnostics.set(name, cache.get(cacheKey));
+ }
+
+ for (const [name, pageValues] of Object.entries(charts.charts)) {
+ if (name === 'trace') continue;
+
+ const pageValuesCount = Object.keys(pageValues).length;
+ for (const [storyName, value] of Object.entries(pageValues)) {
+ if (pageValuesCount > 1 && storyName === 'summary') continue;
+
+ const unitInfo = tr.v.LEGACY_UNIT_INFO.get(value.units) || {};
+ const unitName = unitInfo.name || 'unitlessNumber';
+ const conversionFactor = unitInfo.conversionFactor || 1;
+
+ let improvementDirection = tr.b.ImprovementDirection.DONT_CARE;
+ if (unitInfo.defaultImprovementDirection !== undefined) {
+ improvementDirection = unitInfo.defaultImprovementDirection;
+ }
+ // Metrics have the final say.
+ if (value.improvement_direction !== undefined) {
+ improvementDirection =
+ ChartJsonConverter.convertImprovementDirection(
+ value.improvement_direction);
+ }
+ const unitNameSuffix = tr.b.Unit.nameSuffixForImprovementDirection(
+ improvementDirection);
+
+ const hist = histograms.createHistogram(
+ value.name || name,
+ tr.b.Unit.byName[unitName + unitNameSuffix], [], {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ description: value.description || '',
+ });
+
+ if (traceValues) {
+ const traceValue = traceValues[storyName] || {};
+ let traceUrl = traceValue.cloud_url;
+ if (!traceUrl && traceValue.file_path) {
+ traceUrl = 'file://' + traceValue.file_path;
+ }
+ if (traceUrl) {
+ addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.TRACE_URLS,
+ new tr.v.d.GenericSet([traceUrl]));
+ }
+ }
+
+ if (pageValuesCount > 1) {
+ addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet([storyName]));
+ }
+
+ const storyTags = [];
+ if (value.tir_label) {
+ storyTags.push(`tir_label:${value.tir_label}`);
+ }
+ if (value.story_tags) {
+ storyTags.push(...value.story_tags);
+ }
+ if (storyTags.length) {
+ addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.STORY_TAGS,
+ new tr.v.d.GenericSet(storyTags));
+ }
+
+ if (charts.benchmark_name) {
+ addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.BENCHMARKS,
+ new tr.v.d.GenericSet([charts.benchmark_name]));
+ }
+
+ if (charts.label) {
+ addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet([charts.label]));
+ }
+
+ if (charts.benchmarkStartMs) {
+ addSharedDiagnostic(hist, tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(charts.benchmarkStartMs));
+ }
+
+ if (value.type === 'histogram' || value.buckets) {
+ for (const bucket of value.buckets) {
+ // Take the center of the bin. This coarse granularity can amplify
+ // noise when a measurement moves from one bin to the next.
+ const sample = conversionFactor * (bucket.high + bucket.low) / 2;
+ for (let i = 0; i < bucket.count; ++i) {
+ hist.addSample(sample);
+ }
+ }
+ } else if (value.type === 'list_of_scalar_values' || value.values) {
+ // |value.values| is undefined if the list_of_scalar_values is
+ // empty.
+ if (value.values) {
+ for (const sample of value.values) {
+ hist.addSample(sample * conversionFactor);
+ }
+ }
+ } else if (value.type === 'scalar' || value.value !== undefined) {
+ hist.addSample(value.value * conversionFactor);
+ }
+ }
+ }
+ }
+
+ static convertImprovementDirection(improvementDirection) {
+ switch (improvementDirection) {
+ case 'down': return tr.b.ImprovementDirection.SMALLER_IS_BETTER;
+ case 'up': return tr.b.ImprovementDirection.BIGGER_IS_BETTER;
+ default: return tr.b.ImprovementDirection.DONT_CARE;
+ }
+ }
+ }
+
+ return {
+ ChartJsonConverter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html
new file mode 100644
index 00000000000..6b0bb9e49ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/chart_json_converter_test.html
@@ -0,0 +1,401 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/chart_json_converter.html">
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('emptyListOfScalarValues', function() {
+ const charts = {
+ benchmark_name: 'the.benchmark',
+ label: 'the_label',
+ charts: {
+ mean_frame_time: {
+ 'http://games.yahoo.com': {
+ std: 0.0,
+ name: 'mean_frame_time',
+ type: 'list_of_scalar_values',
+ improvement_direction: 'down',
+ units: 'ms',
+ page_id: 16,
+ description: 'Arithmetic mean of frame times.'
+ },
+ 'summary': {
+ std: 0.0,
+ name: 'mean_frame_time',
+ improvement_direction: 'down',
+ units: 'ms',
+ type: 'list_of_scalar_values',
+ description: 'Arithmetic mean of frame times.'
+ },
+ }
+ }
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ assert.lengthOf(histograms, 1);
+ const hist = [...histograms][0];
+ assert.strictEqual('mean_frame_time', hist.name);
+ assert.strictEqual('http://games.yahoo.com',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual('the.benchmark',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARKS).callback(hist));
+ assert.strictEqual('the_label',
+ tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist));
+ assert.strictEqual(0, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+
+ const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES);
+ assert.isTrue(stories.hasGuid);
+ assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid));
+
+ const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS);
+ assert.isTrue(benchmarks.hasGuid);
+ assert.strictEqual(benchmarks, histograms.lookupDiagnostic(
+ benchmarks.guid));
+
+ const labels = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.LABELS);
+ assert.isTrue(labels.hasGuid);
+ assert.strictEqual(labels, histograms.lookupDiagnostic(labels.guid));
+ });
+
+ test('convertWithoutName', function() {
+ const charts = {
+ benchmark_name: 'the.benchmark',
+ label: 'the_label',
+ charts: {
+ mean_frame_time: {
+ 'http://games.yahoo.com': {
+ std: 0.0,
+ type: 'list_of_scalar_values',
+ improvement_direction: 'down',
+ units: 'ms',
+ page_id: 16,
+ description: 'Arithmetic mean of frame times.'
+ },
+ 'summary': {
+ std: 0.0,
+ improvement_direction: 'down',
+ units: 'ms',
+ type: 'list_of_scalar_values',
+ description: 'Arithmetic mean of frame times.'
+ },
+ }
+ }
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ assert.lengthOf(histograms, 1);
+ const hist = [...histograms][0];
+ assert.strictEqual('mean_frame_time', hist.name);
+ assert.strictEqual('http://games.yahoo.com',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual('the.benchmark',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARKS).callback(hist));
+ assert.strictEqual('the_label',
+ tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist));
+ assert.strictEqual(0, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+
+ const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES);
+ assert.isTrue(stories.hasGuid);
+ assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid));
+
+ const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS);
+ assert.isTrue(benchmarks.hasGuid);
+ assert.strictEqual(benchmarks, histograms.lookupDiagnostic(
+ benchmarks.guid));
+
+ const labels = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.LABELS);
+ assert.isTrue(labels.hasGuid);
+ assert.strictEqual(labels, histograms.lookupDiagnostic(labels.guid));
+ });
+ test('convertWithoutTIRLabel', function() {
+ const charts = {
+ charts: {
+ mean_frame_time: {
+ 'http://games.yahoo.com': {
+ std: 0.0,
+ name: 'mean_frame_time',
+ type: 'list_of_scalar_values',
+ improvement_direction: 'down',
+ values: [42],
+ units: 'ms',
+ page_id: 16,
+ description: 'Arithmetic mean of frame times.'
+ },
+ 'summary': {
+ std: 0.0,
+ name: 'mean_frame_time',
+ improvement_direction: 'down',
+ values: [
+ 16.693,
+ 16.646,
+ 16.918,
+ 16.681
+ ],
+ units: 'ms',
+ type: 'list_of_scalar_values',
+ description: 'Arithmetic mean of frame times.'
+ },
+ }
+ }
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ assert.lengthOf(histograms, 1);
+ const hist = [...histograms][0];
+ assert.strictEqual('mean_frame_time', hist.name);
+ assert.strictEqual('http://games.yahoo.com',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual(42, hist.average);
+ assert.strictEqual(1, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+
+ const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES);
+ assert.isTrue(stories.hasGuid);
+ assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid));
+ });
+
+ test('convertWithoutType', function() {
+ const charts = {
+ charts: {
+ mean_frame_time: {
+ 'http://games.yahoo.com': {
+ std: 0.0,
+ name: 'mean_frame_time',
+ improvement_direction: 'down',
+ value: 42,
+ units: 'ms',
+ page_id: 16,
+ description: 'Arithmetic mean of frame times.'
+ },
+ 'summary': {
+ std: 0.0,
+ name: 'mean_frame_time',
+ improvement_direction: 'down',
+ values: [
+ 16.693,
+ 16.646,
+ 16.918,
+ 16.681
+ ],
+ units: 'ms',
+ type: 'list_of_scalar_values',
+ description: 'Arithmetic mean of frame times.'
+ },
+ }
+ }
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ assert.lengthOf(histograms, 1);
+ const hist = [...histograms][0];
+ assert.strictEqual('mean_frame_time', hist.name);
+ assert.strictEqual('http://games.yahoo.com',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual(42, hist.average);
+ assert.strictEqual(1, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+
+ const stories = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES);
+ assert.isTrue(stories.hasGuid);
+ assert.strictEqual(stories, histograms.lookupDiagnostic(stories.guid));
+ });
+
+ test('convertWithTIRLabel', function() {
+ const charts = {
+ charts: {
+ 'TIR-A@@value-name': {
+ 'story-name': {
+ name: 'value-name',
+ page_id: 7,
+ improvement_direction: 'down',
+ values: [42],
+ units: 'ms',
+ tir_label: 'TIR-A',
+ type: 'list_of_scalar_values',
+ },
+ 'summary': {
+ name: 'value-name',
+ improvement_direction: 'down',
+ values: [42],
+ units: 'ms',
+ tir_label: 'TIR-A',
+ type: 'list_of_scalar_values',
+ },
+ },
+ },
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ const hist = tr.b.getOnlyElement(histograms);
+ assert.strictEqual('value-name', hist.name);
+ assert.strictEqual(tr.b.getOnlyElement(hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.STORY_TAGS)), 'tir_label:TIR-A');
+ assert.strictEqual('story-name',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual(42, hist.average);
+ assert.strictEqual(1, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+ assert.isTrue(hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES).hasGuid);
+ assert.isTrue(hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.STORY_TAGS).hasGuid);
+ });
+
+ test('convertWithStoryTags', function() {
+ const charts = {
+ charts: {
+ 'TIR-A@@value-name': {
+ 'story-name': {
+ name: 'value-name',
+ page_id: 7,
+ improvement_direction: 'down',
+ values: [42],
+ units: 'ms',
+ story_tags: ['foo', 'bar'],
+ type: 'list_of_scalar_values',
+ },
+ 'summary': {
+ name: 'value-name',
+ improvement_direction: 'down',
+ values: [42],
+ units: 'ms',
+ story_tags: ['foo', 'bar'],
+ type: 'list_of_scalar_values',
+ },
+ },
+ },
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ const hist = tr.b.getOnlyElement(histograms);
+ assert.strictEqual('value-name', hist.name);
+ const tags = [...hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.STORY_TAGS)];
+ assert.lengthOf(tags, 2);
+ assert.strictEqual(tags[0], 'foo');
+ assert.strictEqual(tags[1], 'bar');
+ assert.strictEqual('story-name',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual(42, hist.average);
+ assert.strictEqual(1, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+ assert.isTrue(hist.diagnostics.get(tr.v.d.RESERVED_NAMES.STORIES).hasGuid);
+ assert.isTrue(hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.STORY_TAGS).hasGuid);
+ });
+
+ test('convertHistogram', function() {
+ const charts = {
+ charts: {
+ MPArch_RWH_TabSwitchPaintDuration: {
+ summary: {
+ units: 'ms',
+ buckets: [
+ {
+ high: 20,
+ count: 2,
+ low: 16,
+ },
+ {
+ high: 24,
+ count: 2,
+ low: 20,
+ }
+ ],
+ important: false,
+ type: 'histogram',
+ name: 'MPArch_RWH_TabSwitchPaintDuration',
+ }
+ }
+ }
+ };
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ assert.lengthOf(histograms, 1);
+ const hist = [...histograms][0];
+ assert.strictEqual('MPArch_RWH_TabSwitchPaintDuration', hist.name);
+ assert.strictEqual('',
+ tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(hist));
+ assert.strictEqual(20, hist.average);
+ assert.strictEqual(4, hist.numValues);
+ assert.strictEqual(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ hist.unit);
+ });
+
+ test('traceUrls', function() {
+ const charts = {
+ charts: {
+ measurementA: {
+ storyA: {
+ units: 'ms',
+ type: 'list_of_scalar_values',
+ values: [100],
+ name: 'measurementA',
+ },
+ storyB: {
+ units: 'ms',
+ type: 'list_of_scalar_values',
+ values: [200],
+ name: 'measurementA',
+ },
+ },
+ trace: {
+ storyA: {
+ name: 'trace',
+ type: 'trace',
+ file_path: '/home/user/storyA_1900-01-01_00-00-00.html',
+ },
+ storyB: {
+ name: 'trace',
+ type: 'trace',
+ cloud_url: 'https://console.developers.google.com/m/cloudstorage/chromium-telemetry/o/storyB_1900-01-01_00-00-00.html',
+ },
+ },
+ },
+ };
+ let histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ histograms = [...histograms];
+ assert.lengthOf(histograms, 2);
+ assert.strictEqual(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(histograms[0]), 'storyA');
+ assert.strictEqual(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES).callback(histograms[1]), 'storyB');
+ assert.strictEqual(tr.b.getOnlyElement(histograms[0].diagnostics.get(
+ tr.v.d.RESERVED_NAMES.TRACE_URLS)),
+ 'file:///home/user/storyA_1900-01-01_00-00-00.html');
+ assert.strictEqual(tr.b.getOnlyElement(histograms[1].diagnostics.get(
+ tr.v.d.RESERVED_NAMES.TRACE_URLS)),
+ 'https://console.developers.google.com/m/cloudstorage/chromium-telemetry/o/storyB_1900-01-01_00-00-00.html');
+ assert.isTrue(histograms[0].diagnostics.get(
+ tr.v.d.RESERVED_NAMES.TRACE_URLS).hasGuid);
+ assert.isTrue(histograms[1].diagnostics.get(
+ tr.v.d.RESERVED_NAMES.TRACE_URLS).hasGuid);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py
new file mode 100755
index 00000000000..10dabee5ccf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json.py
@@ -0,0 +1,24 @@
+# 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 os
+import tracing_project
+import vinn
+
+
+def ConvertChartJson(chart_json):
+ """Convert chart_json to Histograms.
+
+ Args:
+ chart_json: path to a file containing chart-json
+
+ Returns:
+ a Vinn result object whose 'returncode' indicates whether there was an
+ exception, and whose 'stdout' contains HistogramSet json.
+ """
+ return vinn.RunFile(
+ os.path.join(os.path.dirname(__file__),
+ 'convert_chart_json_cmdline.html'),
+ source_paths=tracing_project.TracingProject().source_paths,
+ js_args=[os.path.abspath(chart_json)])
diff --git a/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html
new file mode 100644
index 00000000000..3503b1db685
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/convert_chart_json_cmdline.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/value/chart_json_converter.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+if (tr.isHeadless) {
+ const charts = JSON.parse(tr.b.getSync('file://' + sys.argv[1]));
+ const histograms = new tr.v.HistogramSet();
+ tr.v.ChartJsonConverter.convertChartJson(charts, histograms);
+ console.log(JSON.stringify(histograms.asDicts()));
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/csv_builder.html b/chromium/third_party/catapult/tracing/tracing/value/csv_builder.html
new file mode 100644
index 00000000000..1627c592669
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/csv_builder.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v', function() {
+ const IGNORE_GROUPING_KEYS = [
+ 'name',
+ 'storyTags',
+ 'testPath',
+ ];
+
+ class CSVBuilder {
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ */
+ constructor(histograms) {
+ this.histograms_ = histograms;
+ this.table_ = [];
+ this.statisticsNames_ = new Set();
+ this.groupings_ = [];
+ }
+
+ build() {
+ this.prepare_();
+ this.buildHeader_();
+ this.buildTable_();
+ }
+
+ prepare_() {
+ for (const [key, grouping] of tr.v.HistogramGrouping.BY_KEY) {
+ if (IGNORE_GROUPING_KEYS.includes(key)) continue;
+ this.groupings_.push(grouping);
+ }
+ this.groupings_.push(new tr.v.GenericSetGrouping(
+ tr.v.d.RESERVED_NAMES.TRACE_URLS));
+
+ this.groupings_.sort((a, b) => a.key.localeCompare(b.key));
+
+ for (const hist of this.histograms_) {
+ for (const name of hist.statisticsNames) {
+ this.statisticsNames_.add(name);
+ }
+ }
+ this.statisticsNames_ = Array.from(this.statisticsNames_);
+ this.statisticsNames_.sort();
+ }
+
+ buildHeader_() {
+ const header = ['name', 'unit'];
+ for (const name of this.statisticsNames_) {
+ header.push(name);
+ }
+ for (const grouping of this.groupings_) {
+ header.push(grouping.key);
+ }
+ this.table_.push(header);
+ }
+
+ buildTable_() {
+ for (const hist of this.histograms_) {
+ const row = [hist.name, hist.unit.unitString];
+ this.table_.push(row);
+
+ for (const name of this.statisticsNames_) {
+ const stat = hist.getStatisticScalar(name);
+ if (stat) {
+ row.push(stat.value);
+ } else {
+ row.push('');
+ }
+ }
+
+ for (const grouping of this.groupings_) {
+ row.push(grouping.callback(hist));
+ }
+ }
+ }
+
+ toString() {
+ let str = '';
+ for (const row of this.table_) {
+ for (let i = 0; i < row.length; ++i) {
+ if (i > 0) {
+ str += ',';
+ }
+ let cell = '' + row[i];
+ cell = cell.replace(/\n/g, ' ');
+ if (cell.indexOf(',') >= 0 || cell.indexOf('"') >= 0) {
+ cell = '"' + cell.replace(/"/g, '""') + '"';
+ }
+ str += cell;
+ }
+ str += '\n';
+ }
+ return str;
+ }
+ }
+
+ return {
+ CSVBuilder,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html b/chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html
new file mode 100644
index 00000000000..b34faf35c7d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/csv_builder_test.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/csv_builder.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('csvBuilder', function() {
+ const hist0 = new tr.v.Histogram('hist0', tr.b.Unit.byName.sizeInBytes,
+ tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 10));
+ hist0.customizeSummaryOptions({
+ nans: true,
+ percentile: [0.1, 0.9],
+ });
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARKS,
+ new tr.v.d.GenericSet(['benchmark A']));
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['label A']));
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['story A']));
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.TRACE_URLS,
+ new tr.v.d.GenericSet(['file://a/b.html']));
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(0));
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.STORYSET_REPEATS,
+ new tr.v.d.GenericSet([0]));
+ for (let i = 0; i <= 1e3; i += 10) {
+ hist0.addSample(i);
+ }
+ hist0.addSample(NaN);
+
+ const hist1 = new tr.v.Histogram('hist1', tr.b.Unit.byName.sigma);
+ hist0.customizeSummaryOptions({
+ std: false,
+ count: false,
+ sum: false,
+ min: false,
+ max: false,
+ });
+
+ const hist2 = new tr.v.Histogram('hist2', tr.b.Unit.byName.count);
+ hist2.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARKS,
+ new tr.v.d.GenericSet(['benchmark A']));
+ hist0.diagnostics.set(tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(1499726648646));
+
+ const histograms = new tr.v.HistogramSet([hist0, hist1, hist2]);
+ let csv = new tr.v.CSVBuilder(histograms);
+ csv.build();
+ csv = csv.toString().split('\n');
+ assert.lengthOf(csv, histograms.length + 2);
+ assert.strictEqual(csv[0],
+ 'name,unit,avg,count,max,min,nans,pct_010,pct_090,std,sum,' +
+ 'architectures,benchmarks,benchmarkStart,bots,builds,deviceIds,' +
+ 'displayLabel,masters,memoryAmounts,osNames,osVersions,' +
+ 'productVersions,stories,storysetRepeats,traceStart,traceUrls');
+ assert.strictEqual(csv[1],
+ 'hist0,B,500,101,1000,0,1,150,950,293.00170647967224,50500,,' +
+ 'benchmark A,2017-07-10 22:44:08,,,,label A,,,,,,story A,0,,' +
+ 'file://a/b.html');
+ assert.strictEqual(csv[2],
+ 'hist1,σ,,0,-Infinity,Infinity,0,,,,0,,,,,,,Value,,,,,,,,,');
+ assert.strictEqual(csv[3],
+ 'hist2,,,0,-Infinity,Infinity,0,,,,0,,benchmark A,,,,,' +
+ 'benchmark A,,,,,,,,,');
+ assert.strictEqual(csv[4], '');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py
new file mode 100644
index 00000000000..a22a6ee39a9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2017 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.
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py
new file mode 100644
index 00000000000..e947e4d45cd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics.py
@@ -0,0 +1,180 @@
+# 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.
+
+import contextlib
+import json
+import os
+import tempfile
+
+from tracing.value import histogram_set
+from tracing.value import merge_histograms
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+
+
+ALL_NAMES = list(reserved_infos.AllNames())
+
+
+def _LoadHistogramSet(dicts):
+ hs = histogram_set.HistogramSet()
+ hs.ImportDicts(dicts)
+ return hs
+
+
+@contextlib.contextmanager
+def TempFile():
+ try:
+ temp = tempfile.NamedTemporaryFile(delete=False)
+ yield temp
+ finally:
+ os.unlink(temp.name)
+
+
+def GetTIRLabelFromHistogram(hist):
+ tags = hist.diagnostics.get(reserved_infos.STORY_TAGS.name) or []
+
+ tags_to_use = [t.split(':') for t in tags if ':' in t]
+
+ return '_'.join(v for _, v in sorted(tags_to_use))
+
+
+def ComputeTestPath(hist):
+ path = hist.name
+
+ # If a Histogram represents a summary across multiple stories, then its
+ # 'stories' diagnostic will contain the names of all of the stories.
+ # If a Histogram is not a summary, then its 'stories' diagnostic will contain
+ # the singular name of its story.
+ is_summary = list(
+ hist.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))
+
+ tir_label = GetTIRLabelFromHistogram(hist)
+ if tir_label and (
+ not is_summary or reserved_infos.STORY_TAGS.name in is_summary):
+ path += '/' + tir_label
+
+ is_ref = hist.diagnostics.get(reserved_infos.IS_REFERENCE_BUILD.name)
+ if is_ref and len(is_ref) == 1:
+ is_ref = is_ref.GetOnlyElement()
+
+ story_name = hist.diagnostics.get(reserved_infos.STORIES.name)
+ if story_name and len(story_name) == 1 and not is_summary:
+ escaped_story_name = story_name.GetOnlyElement()
+ path += '/' + escaped_story_name
+ if is_ref:
+ path += '_ref'
+ elif is_ref:
+ path += '/ref'
+
+ return path
+
+
+def _MergeHistogramSetByPath(hs):
+ with TempFile() as temp:
+ temp.write(json.dumps(hs.AsDicts()).encode('utf-8'))
+ temp.close()
+
+ return merge_histograms.MergeHistograms(temp.name, (
+ reserved_infos.TEST_PATH.name,))
+
+
+def _GetAndDeleteHadFailures(hs):
+ had_failures = False
+ for h in hs:
+ had_failures_diag = h.diagnostics.get(reserved_infos.HAD_FAILURES.name)
+ if had_failures_diag:
+ del h.diagnostics[reserved_infos.HAD_FAILURES.name]
+ had_failures = True
+ return had_failures
+
+
+def _MergeAndReplaceSharedDiagnostics(diagnostic_name, hs):
+ merged = None
+ for h in hs:
+ d = h.diagnostics.get(diagnostic_name)
+ if not d:
+ continue
+
+ if not merged:
+ merged = d
+ else:
+ merged.AddDiagnostic(d)
+ h.diagnostics[diagnostic_name] = merged
+
+
+def AddReservedDiagnostics(histogram_dicts, names_to_values):
+ # We need to generate summary statistics for anything that had a story, so
+ # filter out every histogram with no stories, then merge. If you keep the
+ # histograms with no story, you end up with duplicates.
+ hs_with_stories = _LoadHistogramSet(histogram_dicts)
+ hs_with_stories.FilterHistograms(
+ lambda h: not h.diagnostics.get(reserved_infos.STORIES.name, []))
+
+ hs_with_no_stories = _LoadHistogramSet(histogram_dicts)
+ hs_with_no_stories.FilterHistograms(
+ lambda h: h.diagnostics.get(reserved_infos.STORIES.name, []))
+
+ # TODO(#3987): Refactor recipes to call merge_histograms separately.
+ # This call combines all repetitions of a metric for a given story into a
+ # single histogram.
+ hs = histogram_set.HistogramSet()
+ hs.ImportDicts(hs_with_stories.AsDicts())
+
+ for h in hs:
+ h.diagnostics[reserved_infos.TEST_PATH.name] = (
+ generic_set.GenericSet([ComputeTestPath(h)]))
+
+ _GetAndDeleteHadFailures(hs)
+ dicts_across_repeats = _MergeHistogramSetByPath(hs)
+
+ had_failures = _GetAndDeleteHadFailures(hs_with_stories)
+
+ if not had_failures:
+ # This call creates summary metrics across each tag set of stories.
+ hs = histogram_set.HistogramSet()
+ hs.ImportDicts(hs_with_stories.AsDicts())
+ hs.FilterHistograms(lambda h: not GetTIRLabelFromHistogram(h))
+
+ for h in hs:
+ h.diagnostics[reserved_infos.SUMMARY_KEYS.name] = (
+ generic_set.GenericSet(['name', 'storyTags']))
+ h.diagnostics[reserved_infos.TEST_PATH.name] = (
+ generic_set.GenericSet([ComputeTestPath(h)]))
+
+ dicts_across_stories = _MergeHistogramSetByPath(hs)
+
+ # This call creates summary metrics across the entire story set.
+ hs = histogram_set.HistogramSet()
+ hs.ImportDicts(hs_with_stories.AsDicts())
+
+ for h in hs:
+ h.diagnostics[reserved_infos.SUMMARY_KEYS.name] = (
+ generic_set.GenericSet(['name']))
+ h.diagnostics[reserved_infos.TEST_PATH.name] = (
+ generic_set.GenericSet([ComputeTestPath(h)]))
+
+ dicts_across_names = _MergeHistogramSetByPath(hs)
+ else:
+ dicts_across_stories = []
+ dicts_across_names = []
+
+ # Now load everything into one histogram set. First we load the summary
+ # histograms, since we need to mark them with SUMMARY_KEYS.
+ # After that we load the rest, and then apply all the diagnostics specified
+ # on the command line. Finally, since we end up with a lot of diagnostics
+ # that no histograms refer to, we make sure to prune those.
+ histograms = histogram_set.HistogramSet()
+ histograms.ImportDicts(dicts_across_names)
+ histograms.ImportDicts(dicts_across_stories)
+ histograms.ImportDicts(dicts_across_repeats)
+ histograms.ImportDicts(hs_with_no_stories.AsDicts())
+
+ histograms.DeduplicateDiagnostics()
+ for name, value in names_to_values.items():
+ assert name in ALL_NAMES
+ histograms.AddSharedDiagnosticToAllHistograms(
+ name, generic_set.GenericSet([value]))
+ histograms.RemoveOrphanedDiagnostics()
+
+ return json.dumps(histograms.AsDicts())
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py
new file mode 100644
index 00000000000..e94589c2cb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/add_reserved_diagnostics_unittest.py
@@ -0,0 +1,237 @@
+# 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.
+
+import json
+import unittest
+
+from tracing.value import histogram
+from tracing.value import histogram_set
+from tracing.value.diagnostics import add_reserved_diagnostics
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+
+class AddReservedDiagnosticsUnittest(unittest.TestCase):
+
+ def _CreateHistogram(self, name, stories=None, tags=None, had_failures=False):
+ h = histogram.Histogram(name, 'count')
+ if stories:
+ h.diagnostics[reserved_infos.STORIES.name] = generic_set.GenericSet(
+ stories)
+ if tags:
+ h.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ tags)
+ if had_failures:
+ h.diagnostics[reserved_infos.HAD_FAILURES.name] = generic_set.GenericSet(
+ [True])
+ return h
+
+ def testAddReservedDiagnostics_InvalidDiagnostic_Raises(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo')])
+
+ with self.assertRaises(AssertionError):
+ add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'SOME INVALID DIAGNOSTIC': 'bar'})
+
+ def testAddReservedDiagnostics_DiagnosticsAdded(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo1', stories=['foo1']),
+ self._CreateHistogram('foo1', stories=['foo1']),
+ self._CreateHistogram('bar', stories=['bar1']),
+ self._CreateHistogram('bar', stories=['bar2']),
+ self._CreateHistogram('blah')])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ for h in new_hs:
+ self.assertIn('benchmarks', h.diagnostics)
+ benchmarks = list(h.diagnostics['benchmarks'])
+ self.assertEqual(['bar'], benchmarks)
+
+ def testAddReservedDiagnostics_SummaryAddedToMerged(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo1', stories=['foo1']),
+ self._CreateHistogram('foo1', stories=['foo1']),
+ self._CreateHistogram('bar', stories=['bar1']),
+ self._CreateHistogram('bar', stories=['bar2']),
+ self._CreateHistogram('blah')])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ expected = [
+ [u'foo1', [], [u'foo1']],
+ [u'bar', [], [u'bar1']],
+ [u'blah', [], []],
+ [u'bar', [u'name'], [u'bar1', u'bar2']],
+ [u'foo1', [u'name'], [u'foo1']],
+ [u'bar', [], [u'bar2']],
+ ]
+
+ for h in new_hs:
+ is_summary = sorted(
+ list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, [])))
+ stories = sorted(list(h.diagnostics.get(reserved_infos.STORIES.name, [])))
+ self.assertIn([h.name, is_summary, stories], expected)
+ expected.remove([h.name, is_summary, stories])
+
+ self.assertEqual(0, len(expected))
+
+ def testAddReservedDiagnostics_Repeats_Merged(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo1', stories=['foo1']),
+ self._CreateHistogram('foo1', stories=['foo1']),
+ self._CreateHistogram('foo2', stories=['foo2'])])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ expected = [
+ [u'foo2', [u'name']],
+ [u'foo1', [u'name']],
+ [u'foo2', []],
+ [u'foo1', []],
+ ]
+
+ for h in new_hs:
+ is_summary = sorted(
+ list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, [])))
+ self.assertIn([h.name, is_summary], expected)
+ expected.remove([h.name, is_summary])
+
+ self.assertEqual(0, len(expected))
+
+ def testAddReservedDiagnostics_Stories_Merged(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo', stories=['foo1']),
+ self._CreateHistogram('foo', stories=['foo2']),
+ self._CreateHistogram('bar', stories=['bar'])])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ expected = [
+ [u'foo', [], [u'foo2']],
+ [u'foo', [u'name'], [u'foo1', u'foo2']],
+ [u'bar', [u'name'], [u'bar']],
+ [u'foo', [], [u'foo1']],
+ [u'bar', [], [u'bar']],
+ ]
+
+ for h in new_hs:
+ is_summary = sorted(
+ list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, [])))
+ stories = sorted(list(h.diagnostics[reserved_infos.STORIES.name]))
+ self.assertIn([h.name, is_summary, stories], expected)
+ expected.remove([h.name, is_summary, stories])
+
+ self.assertEqual(0, len(expected))
+
+ def testAddReservedDiagnostics_NoStories_Unmerged(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo'),
+ self._CreateHistogram('foo'),
+ self._CreateHistogram('bar')])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ for h in new_hs:
+ self.assertNotIn(reserved_infos.SUMMARY_KEYS.name, h.diagnostics)
+
+ self.assertEqual(2, len(new_hs.GetHistogramsNamed('foo')))
+ self.assertEqual(1, len(new_hs.GetHistogramsNamed('bar')))
+
+ def testAddReservedDiagnostics_WithTags(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo', ['bar'], ['t:1']),
+ self._CreateHistogram('foo', ['bar'], ['t:2'])
+ ])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ expected = [
+ [u'foo', [u'name'], [u'bar'], [u't:1', u't:2']],
+ [u'foo', [], [u'bar'], [u't:1']],
+ [u'foo', [], [u'bar'], [u't:2']],
+ [u'foo', [u'name', u'storyTags'], [u'bar'], [u't:1']],
+ [u'foo', [u'name', u'storyTags'], [u'bar'], [u't:2']],
+ ]
+
+ for h in new_hs:
+ is_summary = sorted(
+ list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, [])))
+ stories = sorted(list(h.diagnostics[reserved_infos.STORIES.name]))
+ tags = sorted(list(h.diagnostics[reserved_infos.STORY_TAGS.name]))
+ self.assertIn([h.name, is_summary, stories, tags], expected)
+ expected.remove([h.name, is_summary, stories, tags])
+
+ self.assertEqual(0, len(expected))
+
+ def testAddReservedDiagnostics_WithTags_SomeIgnored(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram(
+ 'foo', stories=['story1'], tags=['t:1', 'ignored']),
+ self._CreateHistogram(
+ 'foo', stories=['story1'], tags=['t:1']),
+ ])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ expected = [
+ [u'foo', [u'name', u'storyTags'], [u'story1'], [u'ignored', u't:1']],
+ [u'foo', [], [u'story1'], [u'ignored', u't:1']],
+ [u'foo', [u'name'], [u'story1'], [u'ignored', u't:1']],
+ ]
+
+ for h in new_hs:
+ is_summary = sorted(
+ list(h.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, [])))
+ stories = sorted(list(h.diagnostics[reserved_infos.STORIES.name]))
+ tags = sorted(list(h.diagnostics[reserved_infos.STORY_TAGS.name]))
+ self.assertIn([h.name, is_summary, stories, tags], expected)
+ expected.remove([h.name, is_summary, stories, tags])
+
+ self.assertEqual(0, len(expected))
+
+ def testAddReservedDiagnostics_OmitsSummariesIfHadFailures(self):
+ hs = histogram_set.HistogramSet([
+ self._CreateHistogram('foo', ['bar'], had_failures=True)])
+
+ new_hs_json = add_reserved_diagnostics.AddReservedDiagnostics(
+ hs.AsDicts(), {'benchmarks': 'bar'})
+
+ new_hs = histogram_set.HistogramSet()
+ new_hs.ImportDicts(json.loads(new_hs_json))
+
+ self.assertEqual(len(new_hs), 1)
+
+ h = new_hs.GetFirstHistogram()
+ self.assertEqual(h.name, 'foo')
+ self.assertNotIn(reserved_infos.SUMMARY_KEYS.name, h.diagnostics)
+ self.assertNotIn(reserved_infos.HAD_FAILURES.name, h.diagnostics)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html
new file mode 100644
index 00000000000..48ec199d6ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+<link rel="import" href="/tracing/value/diagnostics/collected_related_event_set.html">
+<link rel="import" href="/tracing/value/diagnostics/date_range.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_ref.html">
+<link rel="import" href="/tracing/value/diagnostics/generic_set.html">
+<link rel="import" href="/tracing/value/diagnostics/related_event_set.html">
+<link rel="import" href="/tracing/value/diagnostics/related_name_map.html">
+<link rel="import" href="/tracing/value/diagnostics/scalar.html">
+<link rel="import" href="/tracing/value/diagnostics/unmergeable_diagnostic_set.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py
new file mode 100644
index 00000000000..95b18c65e7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics.py
@@ -0,0 +1,42 @@
+# Copyright 2017 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 importlib
+import sys
+
+
+# TODO(#3613): Flatten this to a list once diagnostics are in their own modules.
+_MODULES_BY_DIAGNOSTIC_NAME = {
+ 'Breakdown': 'diagnostics.breakdown',
+ 'GenericSet': 'diagnostics.generic_set',
+ 'UnmergeableDiagnosticSet': 'diagnostics.unmergeable_diagnostic_set',
+ 'RelatedEventSet': 'diagnostics.related_event_set',
+ 'DateRange': 'diagnostics.date_range',
+ 'RelatedNameMap': 'diagnostics.related_name_map',
+}
+
+
+_CLASSES_BY_NAME = {}
+
+
+def IsDiagnosticTypename(name):
+ return name in _MODULES_BY_DIAGNOSTIC_NAME
+
+
+def GetDiagnosticTypenames():
+ return _MODULES_BY_DIAGNOSTIC_NAME.keys()
+
+
+def GetDiagnosticClassForName(name):
+ assert IsDiagnosticTypename(name)
+
+ if name in _CLASSES_BY_NAME:
+ return _CLASSES_BY_NAME[name]
+
+ module_name = 'tracing.value.%s' % _MODULES_BY_DIAGNOSTIC_NAME[name]
+ importlib.import_module(module_name)
+
+ cls = getattr(sys.modules[module_name], name)
+ _CLASSES_BY_NAME[name] = cls
+ return cls
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py
new file mode 100644
index 00000000000..9004114c3d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/all_diagnostics_unittest.py
@@ -0,0 +1,27 @@
+# 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.
+
+import unittest
+
+from tracing.value.diagnostics import all_diagnostics
+
+
+class AllDiagnosticsUnittest(unittest.TestCase):
+
+ def testGetDiagnosticClassForName(self):
+ cls0 = all_diagnostics.GetDiagnosticClassForName('GenericSet')
+ gs0 = cls0(['foo'])
+ gs0_dict = gs0.AsDict()
+
+ # Run twice to ensure that the memoization isn't broken.
+ cls1 = all_diagnostics.GetDiagnosticClassForName('GenericSet')
+ gs1 = cls1(['foo'])
+ gs1_dict = gs1.AsDict()
+
+ self.assertEqual(gs0_dict['type'], 'GenericSet')
+ self.assertEqual(gs1_dict['type'], 'GenericSet')
+
+ def testGetDiagnosticClassForName_Bogus(self):
+ self.assertRaises(
+ AssertionError, all_diagnostics.GetDiagnosticClassForName, 'BogusDiag')
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html
new file mode 100644
index 00000000000..bf19b381170
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ class Breakdown extends tr.v.d.Diagnostic {
+ constructor() {
+ super();
+ this.values_ = new Map();
+ this.colorScheme = undefined;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ /**
+ * Add a Value by an explicit name to this map.
+ *
+ * @param {string} name
+ * @param {number} value
+ */
+ set(name, value) {
+ if (typeof name !== 'string' ||
+ typeof value !== 'number') {
+ throw new Error('Breakdown maps from strings to numbers');
+ }
+ this.values_.set(name, value);
+ }
+
+ /**
+ * @param {string} name
+ * @return {number}
+ */
+ get(name) {
+ return this.values_.get(name) || 0;
+ }
+
+ * [Symbol.iterator]() {
+ for (const pair of this.values_) {
+ yield pair;
+ }
+ }
+
+ get size() {
+ return this.values_.size;
+ }
+
+ 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 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py
new file mode 100644
index 00000000000..5272734f469
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown.py
@@ -0,0 +1,73 @@
+# 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.
+
+import math
+import numbers
+
+from tracing.value.diagnostics import diagnostic
+
+
+try:
+ StringTypes = basestring
+except NameError:
+ StringTypes = str
+
+
+class Breakdown(diagnostic.Diagnostic):
+ __slots__ = '_values', '_color_scheme'
+
+ def __init__(self):
+ super(Breakdown, self).__init__()
+ self._values = {}
+ self._color_scheme = None
+
+ @property
+ def color_scheme(self):
+ return self._color_scheme
+
+ @staticmethod
+ def FromDict(d):
+ result = Breakdown()
+ result._color_scheme = d.get('colorScheme')
+ for name, value in d['values'].items():
+ if value in ['NaN', 'Infinity', '-Infinity']:
+ value = float(value)
+ result.Set(name, value)
+ return result
+
+ def _AsDictInto(self, d):
+ d['values'] = {}
+ for name, value in self:
+ # JSON serializes NaN and the infinities as 'null', preventing
+ # distinguishing between them. Override that behavior by serializing them
+ # as their Javascript string names, not their python string names since
+ # the reference implementation is in Javascript.
+ if math.isnan(value):
+ value = 'NaN'
+ elif math.isinf(value):
+ if value > 0:
+ value = 'Infinity'
+ else:
+ value = '-Infinity'
+ d['values'][name] = value
+ if self._color_scheme:
+ d['colorScheme'] = self._color_scheme
+
+ def Set(self, name, value):
+ assert isinstance(name, StringTypes), (
+ 'Expected basestring, found %s: "%r"' % (type(name).__name__, name))
+ assert isinstance(value, numbers.Number), (
+ 'Expected number, found %s: "%r"', (type(value).__name__, value))
+ self._values[name] = value
+
+ def Get(self, name):
+ return self._values.get(name, 0)
+
+ def __iter__(self):
+ for name, value in self._values.items():
+ yield name, value
+
+ def __len__(self):
+ return len(self._values)
+
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html
new file mode 100644
index 00000000000..23b4e54359d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('merge', function() {
+ const a = new tr.v.d.Breakdown();
+ a.set('x', 1);
+ a.set('y', 2);
+
+ const b = new tr.v.d.Breakdown();
+ b.set('y', 3);
+ b.set('z', 4);
+
+ assert.isTrue(a.canAddDiagnostic(b));
+ assert.isTrue(b.canAddDiagnostic(a));
+
+ a.addDiagnostic(b);
+ assert.strictEqual(a.get('x'), 1);
+ assert.strictEqual(a.get('y'), 5);
+ assert.strictEqual(a.get('z'), 4);
+
+ a.colorScheme = 'fake color scheme';
+ assert.isFalse(a.canAddDiagnostic(b));
+ assert.isFalse(b.canAddDiagnostic(a));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py
new file mode 100644
index 00000000000..ab9c6733771
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/breakdown_unittest.py
@@ -0,0 +1,31 @@
+# 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.
+
+import json
+import math
+import unittest
+
+from tracing.value.diagnostics import breakdown
+from tracing.value.diagnostics import diagnostic
+
+
+class BreakdownUnittest(unittest.TestCase):
+
+ def testRoundtrip(self):
+ bd = breakdown.Breakdown()
+ bd.Set('one', 1)
+ bd.Set('m1', -1)
+ bd.Set('inf', float('inf'))
+ bd.Set('nun', float('nan'))
+ bd.Set('ninf', float('-inf'))
+ bd.Set('long', 2**65)
+ d = bd.AsDict()
+ clone = diagnostic.Diagnostic.FromDict(d)
+ self.assertEqual(json.dumps(d), json.dumps(clone.AsDict()))
+ self.assertEqual(clone.Get('one'), 1)
+ self.assertEqual(clone.Get('m1'), -1)
+ self.assertEqual(clone.Get('inf'), float('inf'))
+ self.assertTrue(math.isnan(clone.Get('nun')))
+ self.assertEqual(clone.Get('ninf'), float('-inf'))
+ self.assertEqual(clone.Get('long'), 2**65)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html
new file mode 100644
index 00000000000..b88a8bf3349
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/collected_related_event_set.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'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 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;
+ }
+
+ get(canonicalUrl) {
+ return this.eventSetsByCanonicalUrl_.get(canonicalUrl);
+ }
+
+ * [Symbol.iterator]() {
+ for (const [canonicalUrl, eventSet] of this.eventSetsByCanonicalUrl_) {
+ yield [canonicalUrl, eventSet];
+ }
+ }
+
+ canAddDiagnostic(otherDiagnostic) {
+ return otherDiagnostic instanceof tr.v.d.RelatedEventSet ||
+ otherDiagnostic instanceof tr.v.d.CollectedRelatedEventSet;
+ }
+
+ addEventSetForCanonicalUrl(canonicalUrl, events) {
+ let myEventSet = this.eventSetsByCanonicalUrl_.get(canonicalUrl);
+ if (myEventSet === undefined) {
+ myEventSet = new Set();
+ this.eventSetsByCanonicalUrl_.set(canonicalUrl, myEventSet);
+ }
+ for (const event of events) {
+ myEventSet.add(event);
+ }
+ }
+
+ addDiagnostic(otherDiagnostic) {
+ if (otherDiagnostic instanceof tr.v.d.CollectedRelatedEventSet) {
+ // Merge Maps of Sets.
+ for (const [canonicalUrl, otherEventSet] of otherDiagnostic) {
+ this.addEventSetForCanonicalUrl(canonicalUrl, otherEventSet);
+ }
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html
new file mode 100644
index 00000000000..f519326b553
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ /**
+ * This class represents a mergeable range of Date objects.
+ * This range can contain 1 or 2 Dates.
+ */
+ class DateRange extends tr.v.d.Diagnostic {
+ /**
+ * @param {number} ms
+ */
+ constructor(ms) {
+ super();
+ this.range_ = new tr.b.math.Range();
+ this.range_.addValue(ms);
+ }
+
+ get minDate() {
+ return new Date(this.range_.min);
+ }
+
+ get maxDate() {
+ return new Date(this.range_.max);
+ }
+
+ get durationMs() {
+ return this.range_.duration;
+ }
+
+ clone() {
+ const clone = new tr.v.d.DateRange(this.range_.min);
+ clone.addDiagnostic(this);
+ return clone;
+ }
+
+ equals(other) {
+ if (!(other instanceof DateRange)) return false;
+ return this.range_.equals(other.range_);
+ }
+
+ 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}`;
+ }
+
+ asDictInto_(d) {
+ d.min = this.range_.min;
+ if (this.durationMs === 0) return;
+ d.max = this.range_.max;
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py
new file mode 100644
index 00000000000..3cdce45d49c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range.py
@@ -0,0 +1,71 @@
+# 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.
+import datetime
+
+from tracing.value.diagnostics import diagnostic
+
+
+class DateRange(diagnostic.Diagnostic):
+ __slots__ = '_range',
+
+ def __init__(self, ms):
+ from tracing.value import histogram
+ super(DateRange, self).__init__()
+ self._range = histogram.Range()
+ self._range.AddValue(ms)
+
+ def __eq__(self, other):
+ if not isinstance(other, DateRange):
+ return False
+ return self._range == other._range
+
+ def __hash__(self):
+ return id(self)
+
+ @property
+ def min_date(self):
+ return datetime.datetime.utcfromtimestamp(self._range.min / 1000)
+
+ @property
+ def max_date(self):
+ return datetime.datetime.utcfromtimestamp(self._range.max / 1000)
+
+ @property
+ def min_timestamp(self):
+ return self._range.min
+
+ @property
+ def max_timestamp(self):
+ return self._range.max
+
+ @property
+ def duration_ms(self):
+ return self._range.duration
+
+ def __str__(self):
+ min_date = self.min_date.isoformat().replace('T', ' ')[:19]
+ if self.duration_ms == 0:
+ return min_date
+ max_date = self.max_date.isoformat().replace('T', ' ')[:19]
+ return min_date + ' - ' + max_date
+
+ def _AsDictInto(self, dct):
+ dct['min'] = self._range.min
+ if self.duration_ms == 0:
+ return
+ dct['max'] = self._range.max
+
+ @staticmethod
+ def FromDict(dct):
+ dr = DateRange(dct['min'])
+ if 'max' in dct:
+ dr._range.AddValue(dct['max'])
+ return dr
+
+ def CanAddDiagnostic(self, other_diagnostic):
+ return isinstance(other_diagnostic, DateRange)
+
+ def AddDiagnostic(self, other_diagnostic):
+ self._range.AddRange(other_diagnostic._range)
+
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py
new file mode 100644
index 00000000000..4636045da1c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/date_range_unittest.py
@@ -0,0 +1,31 @@
+# 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.
+
+import calendar
+import unittest
+
+from tracing.value.diagnostics import date_range
+from tracing.value.diagnostics import diagnostic
+
+
+class DateRangeUnittest(unittest.TestCase):
+
+ def testRoundtrip(self):
+ dr = date_range.DateRange(1496693745000)
+ dr.AddDiagnostic(date_range.DateRange(1496693746000))
+ self.assertEqual(calendar.timegm(dr.min_date.timetuple()), 1496693745)
+ self.assertEqual(calendar.timegm(dr.max_date.timetuple()), 1496693746)
+ clone = diagnostic.Diagnostic.FromDict(dr.AsDict())
+ self.assertEqual(clone.min_date, dr.min_date)
+ self.assertEqual(clone.max_date, dr.max_date)
+
+ def testMinTimestamp(self):
+ dr = date_range.DateRange(1496693745123)
+ dr.AddDiagnostic(date_range.DateRange(1496693746123))
+ self.assertEqual(dr.min_timestamp, 1496693745123)
+
+ def testMaxTimestamp(self):
+ dr = date_range.DateRange(1496693745123)
+ dr.AddDiagnostic(date_range.DateRange(1496693746123))
+ self.assertEqual(dr.max_timestamp, 1496693746123)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html
new file mode 100644
index 00000000000..cb3b4907563
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ class Diagnostic {
+ constructor() {
+ this.guid_ = undefined;
+ }
+
+ /**
+ * Returns a new Diagnostic that is like this one but distinct.
+ * This is useful when merging Diagnostics.
+ * @return {!tr.v.d.Diagnostic}
+ */
+ clone() {
+ return new this.constructor();
+ }
+
+ /**
+ * Return true if |this| can be merged with |otherDiagnostic|.
+ *
+ * @param {!tr.v.d.Diagnostic} otherDiagnostic
+ * @return {!boolean}
+ */
+ canAddDiagnostic(otherDiagnostic) {
+ return false;
+ }
+
+ /**
+ * If subclasses override canAddDiagnostic() then they must also override
+ * this method to modify |this| to include information from
+ * |otherDiagnostic|.
+ *
+ * @param {!tr.v.d.Diagnostic} otherDiagnostic
+ */
+ addDiagnostic(otherDiagnostic) {
+ throw new Error('Abstract virtual method: subclasses must override ' +
+ 'this method if they override canAddDiagnostic');
+ }
+
+ 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;
+ }
+
+ get hasGuid() {
+ return this.guid_ !== undefined;
+ }
+
+ /**
+ * If this Diagnostic is shared between multiple Histograms, then return its
+ * |guid|. Otherwise, serialize this Diagnostic to a dictionary.
+ *
+ * @return {string|!Object}
+ */
+ asDictOrReference() {
+ if (this.guid_ !== undefined) {
+ return this.guid_;
+ }
+ return this.asDict();
+ }
+
+ /**
+ * Serialize all of the information in this Diagnostic to a dictionary,
+ * regardless of whether it is shared between multiple Histograms.
+ *
+ * @return {!Object}
+ */
+ asDict() {
+ const result = {type: this.constructor.name};
+ if (this.guid_ !== undefined) {
+ result.guid = this.guid_;
+ }
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py
new file mode 100644
index 00000000000..d2decda8f12
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic.py
@@ -0,0 +1,95 @@
+# Copyright 2017 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 uuid
+
+try:
+ from py_utils import slots_metaclass
+ SlotsMetaclass = slots_metaclass.SlotsMetaclass # pylint: disable=invalid-name
+except ImportError:
+ # TODO(benjhayden): Figure out why py_utils doesn't work in dev_appserver.py
+ SlotsMetaclass = None # pylint: disable=invalid-name
+
+from tracing.value.diagnostics import all_diagnostics
+
+
+class Diagnostic(object):
+ __slots__ = '_guid',
+
+ # Ensure that new subclasses remember to specify __slots__ in order to prevent
+ # regressing memory consumption:
+ if SlotsMetaclass:
+ __metaclass__ = SlotsMetaclass
+
+ def __init__(self):
+ self._guid = None
+
+ def __ne__(self, other):
+ return not self == other
+
+ @property
+ def guid(self):
+ if self._guid is None:
+ self._guid = str(uuid.uuid4())
+ return self._guid
+
+ @guid.setter
+ def guid(self, g):
+ assert self._guid is None
+ self._guid = g
+
+ @property
+ def has_guid(self):
+ return self._guid is not None
+
+ def AsDictOrReference(self):
+ if self._guid:
+ return self._guid
+ return self.AsDict()
+
+ def AsDict(self):
+ dct = {'type': self.__class__.__name__}
+ if self._guid:
+ dct['guid'] = self._guid
+ self._AsDictInto(dct)
+ return dct
+
+ def _AsDictInto(self, unused_dct):
+ raise NotImplementedError
+
+ @staticmethod
+ def FromDict(dct):
+ cls = all_diagnostics.GetDiagnosticClassForName(dct['type'])
+ if not cls:
+ raise ValueError('Unrecognized diagnostic type: ' + dct['type'])
+ diagnostic = cls.FromDict(dct)
+ if 'guid' in dct:
+ diagnostic.guid = dct['guid']
+ return diagnostic
+
+ def ResetGuid(self, guid=None):
+ if guid:
+ self._guid = guid
+ else:
+ self._guid = str(uuid.uuid4())
+
+ def Inline(self):
+ """Inlines a shared diagnostic.
+
+ Any diagnostic that has a guid will be serialized as a reference, because it
+ is assumed that diagnostics with guids are shared. This method removes the
+ guid so that the diagnostic will be serialized by value.
+
+ Inling is used for example in the dashboard, where certain types of shared
+ diagnostics that vary on a per-upload basis are inlined for efficiency
+ reasons.
+ """
+ self._guid = None
+
+ def CanAddDiagnostic(self, unused_other_diagnostic):
+ return False
+
+ def AddDiagnostic(self, unused_other_diagnostic):
+ raise Exception('Abstract virtual method: subclasses must override '
+ 'this method if they override canAddDiagnostic')
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html
new file mode 100644
index 00000000000..79af5824b7e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<!--
+ Include all Diagnostic subclasses here so that DiagnosticMap.addDicts() and
+ DiagnosticMap.fromDict() always have access to all subclasses in the
+ Diagnostic registry.
+-->
+<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html">
+<link rel="import" href="/tracing/value/diagnostics/reserved_names.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ class DiagnosticMap extends Map {
+ /**
+ * @param {boolean=} opt_allowReservedNames defaults to true
+ */
+ constructor(opt_allowReservedNames) {
+ super();
+ if (opt_allowReservedNames === undefined) {
+ opt_allowReservedNames = true;
+ }
+ this.allowReservedNames_ = opt_allowReservedNames;
+ }
+
+ /**
+ * Add a new Diagnostic to this map.
+ *
+ * @param {string} name
+ * @param {!tr.v.d.Diagnostic} diagnostic
+ */
+ set(name, diagnostic) {
+ if (typeof(name) !== 'string') {
+ throw new Error(`name must be string, not ${name}`);
+ }
+
+ if (!(diagnostic instanceof tr.v.d.Diagnostic) &&
+ !(diagnostic instanceof tr.v.d.DiagnosticRef)) {
+ throw new Error(`Must be instanceof Diagnostic: ${diagnostic}`);
+ }
+
+ // TODO(#3507): Reserved names should never be UnmergeableDiagnosticSet.
+ 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);
+ }
+
+ /**
+ * Add Diagnostics from a dictionary of dictionaries.
+ *
+ * @param {Object} dict
+ */
+ 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') {
+ // Ignore RelatedHistograms and TagMaps.
+ // TODO(benjhayden): Forget about them in 2019 Q2.
+ 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);
+ }
+ }
+ }
+
+ asDict() {
+ const dict = {};
+ for (const [name, diagnostic] of this) {
+ dict[name] = diagnostic.asDictOrReference();
+ }
+ return dict;
+ }
+
+ static fromDict(d) {
+ const diagnostics = new DiagnosticMap();
+ diagnostics.addDicts(d);
+ return diagnostics;
+ }
+
+ /**
+ * Convert dictionary or ES6 Map to DiagnosticMap.
+ * @param {!Object|!Map.<string, !tr.v.d.Diagnostic>} obj
+ * @return {tr.v.d.DiagnosticMap}
+ */
+ 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) {
+ const myDiagnostic = this.get(name);
+
+ if (myDiagnostic !== undefined &&
+ myDiagnostic.canAddDiagnostic(otherDiagnostic)) {
+ myDiagnostic.addDiagnostic(otherDiagnostic);
+ continue;
+ }
+
+ // We need to avoid storing references to |otherDiagnostic| in both
+ // |this| and |other| because future merge()s may add yet other
+ // Diagnostics to |this|, and they shouldn't accidentally modify
+ // anything in |other|.
+ // Now, either |this| doesn't already have a Diagnostic named |name|
+ // (myDiagnostic is undefined), or
+ // |this| already has a Diagnostic named |name| that can't be merged
+ // with |otherDiagnostic|.
+ // Either way, we need to clone |otherDiagnostic|.
+ // However, clones produced via fromDict/toDict cannot necessarily be
+ // merged with yet other Diagnostics, either because of semantics (as in
+ // the case of TelemtryInfo and the like) or because guids must not be
+ // shared by distinct Diagnostics. Therefore, Diagnostics support
+ // another way of cloning that is specifically targeted at supporting
+ // merging: clone().
+
+ const clone = otherDiagnostic.clone();
+
+ if (myDiagnostic === undefined) {
+ this.set(name, clone);
+ continue;
+ }
+
+ // Now, |myDiagnostic| exists and it is unmergeable with |clone|, which
+ // is safe to store in |this|.
+ this.set(name, new tr.v.d.UnmergeableDiagnosticSet(
+ [myDiagnostic, clone]));
+ }
+ }
+ }
+
+ return {DiagnosticMap};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html
new file mode 100644
index 00000000000..8d0781fb22d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_map_test.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('clone', function() {
+ const diagnostics = new tr.v.d.DiagnosticMap();
+ diagnostics.set('generic', new tr.v.d.GenericSet([{a: ['b', 3]}]));
+ diagnostics.set('breakdown', new tr.v.d.Breakdown());
+ diagnostics.set('events', new tr.v.d.RelatedEventSet());
+
+ const clone = tr.v.d.DiagnosticMap.fromDict(diagnostics.asDict());
+ assert.instanceOf(clone.get('generic'), tr.v.d.GenericSet);
+ assert.deepEqual(tr.b.getOnlyElement(clone.get('generic')),
+ tr.b.getOnlyElement(diagnostics.get('generic')));
+ assert.instanceOf(clone.get('breakdown'), tr.v.d.Breakdown);
+ assert.instanceOf(clone.get('events'), tr.v.d.RelatedEventSet);
+ });
+
+ test('fromObject', function() {
+ assert.strictEqual(tr.v.d.DiagnosticMap.fromObject(
+ {a: new tr.v.d.GenericSet([])}).size, 1);
+ assert.strictEqual(tr.v.d.DiagnosticMap.fromObject(
+ new Map([['a', new tr.v.d.GenericSet([])]])).size, 1);
+ });
+
+ test('cloneWithRef', function() {
+ const diagnostics = new tr.v.d.DiagnosticMap();
+ diagnostics.set('ref', new tr.v.d.DiagnosticRef('abc'));
+
+ const clone = tr.v.d.DiagnosticMap.fromDict(diagnostics.asDict());
+ assert.instanceOf(clone.get('ref'), tr.v.d.DiagnosticRef);
+ assert.strictEqual(clone.get('ref').guid, 'abc');
+ });
+
+ test('requireFromDict', function() {
+ class MissingFromDict extends tr.v.d.Diagnostic { }
+ assert.throws(() => tr.v.d.Diagnostic.register(MissingFromDict));
+
+ class InvalidFromDict extends tr.v.d.Diagnostic {
+ static fromDict() {
+ }
+ }
+ assert.throws(() => tr.v.d.Diagnostic.register(InvalidFromDict));
+ });
+
+ test('merge', function() {
+ const event = tr.c.TestUtils.newSliceEx({
+ title: 'event',
+ start: 0,
+ duration: 1,
+ });
+ event.parentContainer = {
+ sliceGroup: {
+ stableId: 'fake_thread',
+ slices: [event],
+ },
+ };
+ const generic = new tr.v.d.GenericSet(['generic diagnostic']);
+ const generic2 = new tr.v.d.GenericSet(['generic diagnostic 2']);
+ const events = new tr.v.d.RelatedEventSet([event]);
+
+ // When Histograms are merged, first an empty clone is created with an empty
+ // DiagnosticMap.
+ const hist = new tr.v.Histogram('', tr.b.Unit.byName.count);
+
+ const hist2 = new tr.v.Histogram('', tr.b.Unit.byName.count);
+ hist2.diagnostics.set('a', generic);
+ hist.diagnostics.addDiagnostics(hist2.diagnostics);
+ assert.strictEqual(tr.b.getOnlyElement(generic),
+ tr.b.getOnlyElement(hist.diagnostics.get('a')));
+
+ // Separate keys are not merged.
+ const hist3 = new tr.v.Histogram('', tr.b.Unit.byName.count);
+ hist3.diagnostics.set('b', generic2);
+ hist.diagnostics.addDiagnostics(hist3.diagnostics);
+ assert.strictEqual(
+ tr.b.getOnlyElement(generic),
+ tr.b.getOnlyElement(hist.diagnostics.get('a')));
+ assert.strictEqual(
+ tr.b.getOnlyElement(generic2),
+ tr.b.getOnlyElement(hist.diagnostics.get('b')));
+
+ // Merging unmergeable diagnostics should produce an
+ // UnmergeableDiagnosticSet.
+ const hist4 = new tr.v.Histogram('', tr.b.Unit.byName.count);
+ hist4.diagnostics.set('a', new tr.v.d.RelatedNameMap());
+ hist.diagnostics.addDiagnostics(hist4.diagnostics);
+ assert.instanceOf(hist.diagnostics.get('a'),
+ tr.v.d.UnmergeableDiagnosticSet);
+ let diagnostics = Array.from(hist.diagnostics.get('a'));
+ assert.strictEqual(
+ tr.b.getOnlyElement(generic), tr.b.getOnlyElement(diagnostics[0]));
+
+ // UnmergeableDiagnosticSets are mergeable.
+ const hist5 = new tr.v.Histogram('', tr.b.Unit.byName.count);
+ hist5.diagnostics.set('a', new tr.v.d.UnmergeableDiagnosticSet([
+ events, generic2]));
+ hist.diagnostics.addDiagnostics(hist5.diagnostics);
+ assert.instanceOf(hist.diagnostics.get('a'),
+ tr.v.d.UnmergeableDiagnosticSet);
+ diagnostics = Array.from(hist.diagnostics.get('a'));
+ assert.lengthOf(diagnostics, 3);
+ assert.instanceOf(diagnostics[0], tr.v.d.GenericSet);
+ assert.deepEqual(Array.from(diagnostics[0]), [...generic, ...generic2]);
+ assert.instanceOf(diagnostics[2], tr.v.d.CollectedRelatedEventSet);
+ });
+
+ test('validateDiagnosticTypes', function() {
+ const hist = new tr.v.Histogram('', tr.b.Unit.byName.count);
+ function addInvalidDiagnosticType() {
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.TRACE_START, new tr.v.d.GenericSet(['foo']));
+ }
+ assert.throw(addInvalidDiagnosticType, Error,
+ `Diagnostics named "${tr.v.d.RESERVED_NAMES.TRACE_START}" must be ` +
+ 'DateRange, not GenericSet');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html
new file mode 100644
index 00000000000..ac30d88cd65
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ /**
+ * This is a placeholder to allow many DiagnosticMaps to contain references to
+ * the same Diagnostic.
+ */
+ class DiagnosticRef {
+ /**
+ * @param {string} guid
+ */
+ constructor(guid) {
+ this.guid = guid;
+ }
+
+ asDict() {
+ return this.guid;
+ }
+
+ asDictOrReference() {
+ return this.asDict();
+ }
+ }
+
+ return {
+ DiagnosticRef,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py
new file mode 100644
index 00000000000..fe86099af13
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_ref.py
@@ -0,0 +1,22 @@
+# Copyright 2017 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.
+
+
+class DiagnosticRef(object):
+ def __init__(self, guid):
+ self._guid = guid
+
+ @property
+ def guid(self):
+ return self._guid
+
+ @property
+ def has_guid(self):
+ return True
+
+ def AsDict(self):
+ return self.guid
+
+ def AsDictOrReference(self):
+ return self.AsDict()
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html
new file mode 100644
index 00000000000..55a49aa65b6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html">
+<link rel="import" href="/tracing/value/diagnostics/generic_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('roundtripPreservesGuid', function() {
+ const diagnostic = new tr.v.d.GenericSet(['generic']);
+ diagnostic.guid = 'foo';
+ const clone = tr.v.d.Diagnostic.fromDict(diagnostic.asDict());
+ assert.strictEqual('foo', clone.guid);
+ });
+
+ test('equalitySmokeTest', function() {
+ const infos = tr.v.d.Diagnostic.getAllRegisteredTypeInfos();
+
+ for (const info of infos) {
+ assert.hasOwnProperty(info, 'equals');
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py
new file mode 100644
index 00000000000..f3c1a1f9b64
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/diagnostic_unittest.py
@@ -0,0 +1,15 @@
+# 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.
+
+import unittest
+
+from tracing.value.diagnostics import all_diagnostics
+
+
+class DiagnosticUnittest(unittest.TestCase):
+
+ def testEqualityForSmoke(self):
+ for name in all_diagnostics.GetDiagnosticTypenames():
+ ctor = all_diagnostics.GetDiagnosticClassForName(name)
+ self.assertTrue(hasattr(ctor, '__eq__'))
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html
new file mode 100644
index 00000000000..293a5086508
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/discover_cmdline.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+function isDiagnosticSubclass(cls) {
+ cls = cls.__proto__;
+ while (cls) {
+ if (cls === tr.v.d.Diagnostic) return true;
+ cls = cls.__proto__;
+ }
+ return false;
+}
+
+function discoverDiagnostics(args) {
+ const discoveryMode = args.shift();
+ for (const arg of args) HTMLImportsLoader.loadHTML(arg);
+
+ const results = [];
+ if (discoveryMode === 'registry') {
+ for (const typeInfo of tr.v.d.Diagnostic.getAllRegisteredTypeInfos()) {
+ results.push(typeInfo.constructor.name);
+ }
+ } else if (discoveryMode === 'namespace') {
+ for (const cls of Object.values(tr.v.d)) {
+ if (isDiagnosticSubclass(cls)) results.push(cls.name);
+ }
+ } else {
+ console.log('First argument must be either "registry" or "namespace".');
+ return 1;
+ }
+ console.log(JSON.stringify(results));
+ return 0;
+}
+
+if (tr.isHeadless) {
+ quit(discoverDiagnostics(sys.argv.slice(1)));
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html
new file mode 100644
index 00000000000..d582272f0e6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/event_ref.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ /**
+ * This is a placeholder in case the referenced Event isn't available in
+ * memory to point to directly.
+ */
+ class EventRef {
+ /**
+ * @param {!Object} event
+ * @param {string} event.stableId
+ * @param {string} event.title
+ * @param {number} event.start
+ * @param {number} event.duration
+ */
+ constructor(event) {
+ this.stableId = event.stableId;
+ this.title = event.title;
+ this.start = event.start;
+ this.duration = event.duration;
+ this.end = this.start + this.duration;
+
+ // tr.v.d.RelatedEventSet identifies events using stableId, but
+ // tr.model.EventSet uses guid.
+ this.guid = tr.b.GUID.allocateSimple();
+ }
+ }
+
+ return {
+ EventRef,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html
new file mode 100644
index 00000000000..e0c0664b958
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ /**
+ * Stringify Arrays or dictionaries. Sorts dictionaries keys. Non-recursive.
+ *
+ * @param {!Object} obj
+ * @return {string}
+ */
+ function stableStringify(obj) {
+ let replacer;
+ if (!(obj instanceof Array) && obj !== null) {
+ replacer = Object.keys(obj).sort();
+ }
+ return JSON.stringify(obj, replacer);
+ }
+
+ /**
+ * @typedef {(null|number|string|boolean|Array.<!PlainOldData>|!Object)}
+ * PlainOldData
+ */
+
+ class GenericSet extends tr.v.d.Diagnostic {
+ /**
+ * @param {!Iterable.<!PlainOldData>} values
+ */
+ constructor(values) {
+ super();
+
+ if (typeof values[Symbol.iterator] !== 'function') {
+ throw new Error('GenericSet must be constructed from an interable.');
+ }
+
+ this.values_ = new Set(values);
+ this.has_objects_ = false;
+
+ for (const value of values) {
+ if (typeof value === 'object') {
+ this.has_objects_ = true;
+ }
+ }
+ }
+
+ get size() {
+ return this.values_.size;
+ }
+
+ get length() {
+ return this.values_.size;
+ }
+
+ * [Symbol.iterator]() {
+ for (const value of this.values_) {
+ yield value;
+ }
+ }
+
+ has(value) {
+ if (typeof value !== 'object') return this.values_.has(value);
+ const json = JSON.stringify(value);
+ for (const x of this) {
+ if (typeof x !== 'object') continue;
+ if (json === JSON.stringify(x)) return true;
+ }
+ return false;
+ }
+
+ equals(other) {
+ if (!(other instanceof GenericSet)) return false;
+ if (this.size !== other.size) return false;
+ for (const value of this) {
+ if (!other.has(value)) return false;
+ }
+ return true;
+ }
+
+ 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;
+ }
+
+ asDictInto_(d) {
+ d.values = Array.from(this);
+ }
+
+ static fromDict(d) {
+ return new GenericSet(d.values);
+ }
+
+ clone() {
+ return new GenericSet(this.values_);
+ }
+
+ canAddDiagnostic(otherDiagnostic) {
+ return otherDiagnostic instanceof GenericSet;
+ }
+
+ addDiagnostic(otherDiagnostic) {
+ const jsons = new Set();
+ for (const value of this) {
+ if (typeof value !== 'object') continue;
+ jsons.add(stableStringify(value));
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py
new file mode 100644
index 00000000000..e4a2b19f4c7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set.py
@@ -0,0 +1,82 @@
+# 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.
+
+import json
+
+from tracing.value.diagnostics import diagnostic
+
+
+class GenericSet(diagnostic.Diagnostic):
+ """Contains any Plain-Ol'-Data objects.
+
+ Contents are serialized using json.dumps(): None, boolean, number, string,
+ list, dict. Dicts, lists, and booleans are deduplicated by their JSON
+ representation. Dicts and lists are not hashable. (1 == True) and (0 ==
+ False) in Python, but not in JSON.
+ """
+ __slots__ = '_values', '_comparable_set'
+
+ def __init__(self, values):
+ super(GenericSet, self).__init__()
+
+ self._values = list(values)
+ self._comparable_set = None
+
+ def __iter__(self):
+ for value in self._values:
+ yield value
+
+ def __len__(self):
+ return len(self._values)
+
+ def __eq__(self, other):
+ return self._GetComparableSet() == other._GetComparableSet()
+
+ def __hash__(self):
+ return id(self)
+
+ def SetValues(self, values):
+ # Use a list because Python sets cannot store dicts or lists because they
+ # are not hashable.
+ self._values = list(values)
+
+ # Cache a set to facilitate comparing and merging GenericSets.
+ # Dicts, lists, and booleans are serialized; other types are not.
+ self._comparable_set = None
+
+ def _GetComparableSet(self):
+ if self._comparable_set is None:
+ self._comparable_set = set()
+ for value in self:
+ if isinstance(value, (dict, list, bool)):
+ self._comparable_set.add(json.dumps(value, sort_keys=True))
+ else:
+ self._comparable_set.add(value)
+ return self._comparable_set
+
+ def CanAddDiagnostic(self, other_diagnostic):
+ return isinstance(other_diagnostic, GenericSet)
+
+ def AddDiagnostic(self, other_diagnostic):
+ comparable_set = self._GetComparableSet()
+ for value in other_diagnostic:
+ if isinstance(value, (dict, list, bool)):
+ json_value = json.dumps(value, sort_keys=True)
+ if json_value not in comparable_set:
+ self._values.append(value)
+ self._comparable_set.add(json_value)
+ elif value not in comparable_set:
+ self._values.append(value)
+ self._comparable_set.add(value)
+
+ def _AsDictInto(self, dct):
+ dct['values'] = list(self)
+
+ @staticmethod
+ def FromDict(dct):
+ return GenericSet(dct['values'])
+
+ def GetOnlyElement(self):
+ assert len(self) == 1
+ return self._values[0]
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html
new file mode 100644
index 00000000000..8ce850e220b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/generic_set.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('merge', function() {
+ const a = new tr.v.d.GenericSet(['a']);
+ const b = new tr.v.d.GenericSet(['b']);
+
+ assert.isTrue(a.canAddDiagnostic(b));
+ assert.isTrue(b.canAddDiagnostic(a));
+
+ const ab = a.clone();
+ ab.addDiagnostic(b);
+ assert.deepEqual(Array.from(ab), ['a', 'b']);
+
+ const bab = b.clone();
+ bab.addDiagnostic(ab);
+ assert.deepEqual(Array.from(bab), ['b', 'a']);
+ });
+
+ test('mergeDictionaries', function() {
+ const a = new tr.v.d.GenericSet([{a: 1, b: 2}]);
+ const b = new tr.v.d.GenericSet([{b: 2, a: 1}]);
+ const ab = a.clone();
+ assert.strictEqual(tr.b.getOnlyElement(a), tr.b.getOnlyElement(ab));
+ ab.addDiagnostic(b);
+ assert.lengthOf(ab, 1);
+ assert.strictEqual(tr.b.getOnlyElement(a), tr.b.getOnlyElement(ab));
+ });
+
+ test('addDiagnosticWithNull', function() {
+ const a = new tr.v.d.GenericSet([]);
+ const b = new tr.v.d.GenericSet([null]);
+ a.addDiagnostic(b);
+ assert.lengthOf(a, 1);
+ assert.isTrue(a.has(null));
+ });
+
+ test('hashKey', function() {
+ const a = new tr.v.d.GenericSet(['a', 'b']);
+ assert.strictEqual(a.hashKey, 'ab');
+ });
+
+ test('setsWithoutObjectsSupportFastPath', function() {
+ const a = new tr.v.d.GenericSet(['a', 'b']);
+ assert.isDefined(a.hashKey);
+ });
+
+ test('setsWithObjectsDoNotSupportFastPath', function() {
+ const a = new tr.v.d.GenericSet([{foo: 'bar'}]);
+ assert.isUndefined(a.hashKey);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py
new file mode 100644
index 00000000000..00ba42e10a0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/generic_set_unittest.py
@@ -0,0 +1,100 @@
+# 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.
+
+import unittest
+
+from tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import generic_set
+
+
+class GenericSetUnittest(unittest.TestCase):
+
+ def testRoundtrip(self):
+ a_set = generic_set.GenericSet([
+ None,
+ True,
+ False,
+ 0,
+ 1,
+ 42,
+ [],
+ {},
+ [0, False],
+ {'a': 1, 'b': True},
+ ])
+ self.assertEqual(a_set, diagnostic.Diagnostic.FromDict(a_set.AsDict()))
+
+ def testEq(self):
+ a_set = generic_set.GenericSet([
+ None,
+ True,
+ False,
+ 0,
+ 1,
+ 42,
+ [],
+ {},
+ [0, False],
+ {'a': 1, 'b': True},
+ ])
+ b_set = generic_set.GenericSet([
+ {'b': True, 'a': 1},
+ [0, False],
+ {},
+ [],
+ 42,
+ 1,
+ 0,
+ False,
+ True,
+ None,
+ ])
+ self.assertEqual(a_set, b_set)
+
+ def testMerge(self):
+ a_set = generic_set.GenericSet([
+ None,
+ True,
+ False,
+ 0,
+ 1,
+ 42,
+ [],
+ {},
+ [0, False],
+ {'a': 1, 'b': True},
+ ])
+ b_set = generic_set.GenericSet([
+ {'b': True, 'a': 1},
+ [0, False],
+ {},
+ [],
+ 42,
+ 1,
+ 0,
+ False,
+ True,
+ None,
+ ])
+ self.assertTrue(a_set.CanAddDiagnostic(b_set))
+ self.assertTrue(b_set.CanAddDiagnostic(a_set))
+ a_set.AddDiagnostic(b_set)
+ self.assertEqual(a_set, b_set)
+ b_set.AddDiagnostic(a_set)
+ self.assertEqual(a_set, b_set)
+
+ c_dict = {'a': 1, 'b': 1}
+ c_set = generic_set.GenericSet([c_dict])
+ a_set.AddDiagnostic(c_set)
+ self.assertEqual(len(a_set), 1 + len(b_set))
+ self.assertIn(c_dict, a_set)
+
+ def testGetOnlyElement(self):
+ gs = generic_set.GenericSet(['foo'])
+ self.assertEqual(gs.GetOnlyElement(), 'foo')
+
+ def testGetOnlyElementRaises(self):
+ gs = generic_set.GenericSet([])
+ with self.assertRaises(AssertionError):
+ gs.GetOnlyElement()
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html
new file mode 100644
index 00000000000..8021e661159
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+<link rel="import" href="/tracing/value/diagnostics/event_ref.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ /**
+ * @typedef {!(tr.v.d.EventRef|tr.model.Event)} EventLike
+ */
+
+ /**
+ * A RelatedEventSet diagnostic contains references to Events
+ */
+ class RelatedEventSet extends tr.v.d.Diagnostic {
+ /**
+ * @param {!(tr.model.EventSet|Array.<EventLike>|EventLike)=} opt_events
+ */
+ constructor(opt_events) {
+ super();
+ this.eventsByStableId_ = new Map();
+ // TODO(#2431) Plumb canonicalUrl from event.model.
+ 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;
+ }
+
+ /**
+ * @param {!(tr.v.d.EventRef|tr.model.Event)} event
+ */
+ add(event) {
+ this.eventsByStableId_.set(event.stableId, event);
+ }
+
+ /**
+ * @param {!(tr.v.d.EventRef|tr.model.Event)} event
+ * @return {boolean}
+ */
+ 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 all EventRefs into Events by finding their stableIds in |model|.
+ * If a stableId cannot be found and |opt_required| is true, then throw an
+ * Error.
+ * If a stableId cannot be found and |opt_required| is false, then the
+ * EventRef will remain an EventRef.
+ *
+ * @param {!tr.model.Model} model
+ * @param {boolean=} opt_required
+ */
+ 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: tr.b.Unit.byName.timeStampInMs.truncate(event.start),
+ duration: tr.b.Unit.byName.timeDurationInMs.truncate(event.duration),
+ });
+ }
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py
new file mode 100644
index 00000000000..f4bbad3c596
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set.py
@@ -0,0 +1,34 @@
+# 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 tracing.value.diagnostics import diagnostic
+
+
+class RelatedEventSet(diagnostic.Diagnostic):
+ __slots__ = '_events_by_stable_id',
+
+ def __init__(self):
+ super(RelatedEventSet, self).__init__()
+ self._events_by_stable_id = {}
+
+ def Add(self, event):
+ self._events_by_stable_id[event['stableId']] = event
+
+ def __len__(self):
+ return len(self._events_by_stable_id)
+
+ def __iter__(self):
+ for event in self._events_by_stable_id.values():
+ yield event
+
+ @staticmethod
+ def FromDict(d):
+ result = RelatedEventSet()
+ for event in d['events']:
+ result.Add(event)
+ return result
+
+ def _AsDictInto(self, d):
+ d['events'] = [event for event in self]
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html
new file mode 100644
index 00000000000..8019f67172f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_test.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('eventSet', function() {
+ let slice = undefined;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ slice = tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ title: 'foo',
+ start: 0,
+ duration: 10
+ });
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ thread.sliceGroup.pushSlice(slice);
+ });
+
+ let d = new tr.v.d.RelatedEventSet(slice);
+ assert.strictEqual(tr.b.getOnlyElement([...d]), slice);
+
+ d = new tr.v.d.RelatedEventSet([slice]);
+ assert.strictEqual(tr.b.getOnlyElement([...d]), slice);
+
+ d = new tr.v.d.RelatedEventSet(new tr.model.EventSet([slice]));
+ assert.strictEqual(tr.b.getOnlyElement([...d]), slice);
+
+ const d2 = tr.v.d.Diagnostic.fromDict(d.asDict());
+ assert.instanceOf(d2, tr.v.d.RelatedEventSet);
+
+ assert.instanceOf(tr.b.getOnlyElement([...d2]), tr.v.d.EventRef);
+
+ d2.resolve(model, true);
+
+ assert.strictEqual(tr.b.getOnlyElement([...d2]), slice);
+ });
+
+ test('merge', function() {
+ let aSlice;
+ let bSlice;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ aSlice = tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ title: 'a',
+ start: 0,
+ duration: 10
+ });
+ bSlice = tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ title: 'b',
+ start: 1,
+ duration: 10
+ });
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ thread.sliceGroup.pushSlice(aSlice);
+ thread.sliceGroup.pushSlice(bSlice);
+ });
+ assert.notEqual(aSlice.stableId, bSlice.stableId);
+
+ const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count);
+ const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count);
+
+ const aEvents = new tr.v.d.RelatedEventSet(aSlice);
+ const bEvents = new tr.v.d.RelatedEventSet(bSlice);
+ aEvents.canonicalUrl_ = 'http://a';
+ bEvents.canonicalUrl_ = 'http://b';
+
+ aHist.diagnostics.set('events', aEvents);
+ bHist.diagnostics.set('events', bEvents);
+
+ let mergedHist = aHist.clone();
+ mergedHist.addHistogram(bHist);
+ mergedHist = tr.v.Histogram.fromDict(mergedHist.asDict());
+
+ const mergedEvents = mergedHist.diagnostics.get('events');
+ const aSlice2 = tr.b.getOnlyElement(mergedEvents.get('http://a'));
+ assert.strictEqual(aSlice.stableId, aSlice2.stableId);
+ const bSlice2 = tr.b.getOnlyElement(mergedEvents.get('http://b'));
+ assert.strictEqual(bSlice.stableId, bSlice2.stableId);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py
new file mode 100644
index 00000000000..b0a55e090a0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_event_set_unittest.py
@@ -0,0 +1,29 @@
+# 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.
+import unittest
+
+from tracing.value import histogram_unittest
+from tracing.value.diagnostics import related_event_set
+from tracing.value.diagnostics import diagnostic
+
+
+class RelatedEventSetUnittest(unittest.TestCase):
+ def testRoundtrip(self):
+ events = related_event_set.RelatedEventSet()
+ events.Add({
+ 'stableId': '0.0',
+ 'title': 'foo',
+ 'start': 0,
+ 'duration': 1,
+ })
+ d = events.AsDict()
+ clone = diagnostic.Diagnostic.FromDict(d)
+ self.assertEqual(
+ histogram_unittest.ToJSON(d), histogram_unittest.ToJSON(clone.AsDict()))
+ self.assertEqual(len(events), 1)
+ event = list(events)[0]
+ self.assertEqual(event['stableId'], '0.0')
+ self.assertEqual(event['title'], 'foo')
+ self.assertEqual(event['start'], 0)
+ self.assertEqual(event['duration'], 1)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html
new file mode 100644
index 00000000000..3004d6bf3e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ class RelatedNameMap extends tr.v.d.Diagnostic {
+ constructor(opt_info) {
+ super();
+ this.map_ = new Map();
+ }
+
+ 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}"`);
+ }
+ }
+ }
+
+ 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 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py
new file mode 100644
index 00000000000..c2584fa1b60
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map.py
@@ -0,0 +1,64 @@
+# 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 tracing.value.diagnostics import diagnostic
+
+
+class RelatedNameMap(diagnostic.Diagnostic):
+ __slots__ = '_map',
+
+ def __init__(self):
+ super(RelatedNameMap, self).__init__()
+ self._map = {}
+
+ def __len__(self):
+ return len(self._map)
+
+ def __eq__(self, other):
+ if not isinstance(other, RelatedNameMap):
+ return False
+ if set(self._map) != set(other._map):
+ return False
+ for key, name in self._map.items():
+ if name != other.Get(key):
+ return False
+ return True
+
+ def __hash__(self):
+ return id(self)
+
+ def CanAddDiagnostic(self, other):
+ return isinstance(other, RelatedNameMap)
+
+ def AddDiagnostic(self, other):
+ for key, name in other._map.items():
+ existing = self.Get(key)
+ if existing is None:
+ self.Set(key, name)
+ elif existing != name:
+ raise ValueError('Histogram names differ: "%s" != "%s"' % (
+ existing, name))
+
+ def Get(self, key):
+ return self._map.get(key)
+
+ def Set(self, key, name):
+ self._map[key] = name
+
+ def __iter__(self):
+ for key, name in self._map.items():
+ yield key, name
+
+ def Values(self):
+ return self._map.values()
+
+ def _AsDictInto(self, dct):
+ dct['names'] = dict(self._map)
+
+ @staticmethod
+ def FromDict(dct):
+ names = RelatedNameMap()
+ for key, name in dct['names'].items():
+ names.Set(key, name)
+ return names
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py
new file mode 100644
index 00000000000..13fae26d20e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/related_name_map_unittest.py
@@ -0,0 +1,50 @@
+# 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.
+
+import unittest
+
+from tracing.value import histogram_unittest
+from tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import related_name_map
+
+
+class RelatedNameMapUnittest(unittest.TestCase):
+ def testRoundtrip(self):
+ names = related_name_map.RelatedNameMap()
+ names.Set('a', 'A')
+ d = names.AsDict()
+ clone = diagnostic.Diagnostic.FromDict(d)
+ self.assertEqual(
+ histogram_unittest.ToJSON(d), histogram_unittest.ToJSON(clone.AsDict()))
+ self.assertEqual(clone.Get('a'), 'A')
+
+ def testMerge(self):
+ a_names = related_name_map.RelatedNameMap()
+ a_names.Set('a', 'A')
+ b_names = related_name_map.RelatedNameMap()
+ b_names.Set('b', 'B')
+ self.assertTrue(a_names.CanAddDiagnostic(b_names))
+ self.assertTrue(b_names.CanAddDiagnostic(a_names))
+ self.assertFalse(a_names.CanAddDiagnostic(generic_set.GenericSet([])))
+
+ a_names.AddDiagnostic(b_names)
+ self.assertEqual(a_names.Get('b'), 'B')
+ a_names.AddDiagnostic(b_names)
+ self.assertEqual(a_names.Get('b'), 'B')
+
+ b_names.Set('a', 'C')
+ with self.assertRaises(ValueError):
+ a_names.AddDiagnostic(b_names)
+
+ def testEquals(self):
+ a_names = related_name_map.RelatedNameMap()
+ a_names.Set('a', 'A')
+ self.assertNotEqual(a_names, generic_set.GenericSet([]))
+ b_names = related_name_map.RelatedNameMap()
+ self.assertNotEqual(a_names, b_names)
+ b_names.Set('a', 'B')
+ self.assertNotEqual(a_names, b_names)
+ b_names.Set('a', 'A')
+ self.assertEqual(a_names, b_names)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py
new file mode 100644
index 00000000000..54615b7a6fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_infos.py
@@ -0,0 +1,92 @@
+# Copyright 2017 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.
+
+class _Info(object):
+
+ def __init__(self, name, _type=None, entry_type=None):
+ self._name = name
+ self._type = _type
+ if entry_type is not None and self._type != 'GenericSet':
+ raise ValueError(
+ 'entry_type should only be specified if _type is GenericSet')
+ self._entry_type = entry_type
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def type(self):
+ return self._type
+
+ @property
+ def entry_type(self):
+ return self._entry_type
+
+
+ANGLE_REVISIONS = _Info('angleRevisions', 'GenericSet', str)
+ARCHITECTURES = _Info('architectures', 'GenericSet', str)
+BENCHMARKS = _Info('benchmarks', 'GenericSet', str)
+BENCHMARK_START = _Info('benchmarkStart', 'DateRange')
+BENCHMARK_DESCRIPTIONS = _Info('benchmarkDescriptions', 'GenericSet', str)
+BOTS = _Info('bots', 'GenericSet', str)
+BUG_COMPONENTS = _Info('bugComponents', 'GenericSet', str)
+BUILD_URLS = _Info('buildUrls', 'GenericSet', str)
+BUILDS = _Info('builds', 'GenericSet', int)
+CATAPULT_REVISIONS = _Info('catapultRevisions', 'GenericSet', str)
+CHROMIUM_COMMIT_POSITIONS = _Info('chromiumCommitPositions', 'GenericSet', int)
+CHROMIUM_REVISIONS = _Info('chromiumRevisions', 'GenericSet', str)
+DEVICE_IDS = _Info('deviceIds', 'GenericSet', str)
+DOCUMENTATION_URLS = _Info('documentationLinks', 'GenericSet', str)
+FUCHSIA_GARNET_REVISIONS = _Info('fuchsiaGarnetRevisions', 'GenericSet', str)
+FUCHSIA_PERIDOT_REVISIONS = _Info('fuchsiaPeridotRevisions', 'GenericSet', str)
+FUCHSIA_TOPAZ_REVISIONS = _Info('fuchsiaTopazRevisions', 'GenericSet', str)
+FUCHSIA_ZIRCON_REVISIONS = _Info('fuchsiaZirconRevisions', 'GenericSet', str)
+GPUS = _Info('gpus', 'GenericSet', str)
+HAD_FAILURES = _Info('hadFailures', 'GenericSet', bool)
+IS_REFERENCE_BUILD = _Info('isReferenceBuild', 'GenericSet', bool)
+LABELS = _Info('labels', 'GenericSet', str)
+LOG_URLS = _Info('logUrls', 'GenericSet', str)
+MASTERS = _Info('masters', 'GenericSet', str)
+MEMORY_AMOUNTS = _Info('memoryAmounts', 'GenericSet', int)
+OS_NAMES = _Info('osNames', 'GenericSet', str)
+OS_VERSIONS = _Info('osVersions', 'GenericSet', str)
+OWNERS = _Info('owners', 'GenericSet', str)
+POINT_ID = _Info('pointId', 'GenericSet', int)
+PRODUCT_VERSIONS = _Info('productVersions', 'GenericSet', str)
+REVISION_TIMESTAMPS = _Info('revisionTimestamps', 'DateRange')
+SKIA_REVISIONS = _Info('skiaRevisions', 'GenericSet', str)
+STORIES = _Info('stories', 'GenericSet', str)
+STORYSET_REPEATS = _Info('storysetRepeats', 'GenericSet', int)
+STORY_TAGS = _Info('storyTags', 'GenericSet', str)
+SUMMARY_KEYS = _Info('summaryKeys', 'GenericSet', str)
+TEST_PATH = _Info('testPath', 'GenericSet', str)
+TRACE_START = _Info('traceStart', 'DateRange')
+TRACE_URLS = _Info('traceUrls', 'GenericSet', str)
+V8_COMMIT_POSITIONS = _Info('v8CommitPositions', 'DateRange')
+V8_REVISIONS = _Info('v8Revisions', 'GenericSet', str)
+WEBRTC_REVISIONS = _Info('webrtcRevisions', 'GenericSet', str)
+
+
+def _CreateCachedInfoTypes():
+ info_types = {}
+ for info in globals().values():
+ if isinstance(info, _Info):
+ info_types[info.name] = info
+ return info_types
+
+_CACHED_INFO_TYPES = _CreateCachedInfoTypes()
+
+def GetTypeForName(name):
+ info = _CACHED_INFO_TYPES.get(name)
+ if info:
+ return info.type
+
+def AllInfos():
+ for info in _CACHED_INFO_TYPES.values():
+ yield info
+
+def AllNames():
+ for info in AllInfos():
+ yield info.name
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html
new file mode 100644
index 00000000000..1321815f0c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/reserved_names.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<!--
+ Include all Diagnostic subclasses here so that RESERVED_INFOS always has
+ access to all subclasses.
+-->
+<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.d', function() {
+ // Diagnostics that are produced outside of metrics (e.g. by telemetry) use
+ // reserved names.
+ 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},
+ 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},
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html
new file mode 100644
index 00000000000..397ec7d31b5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/scalar.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ class Scalar extends tr.v.d.Diagnostic {
+ /**
+ * @param {!tr.b.Scalar} value
+ */
+ constructor(value) {
+ super();
+ if (!(value instanceof tr.b.Scalar)) {
+ throw new Error('expected Scalar');
+ }
+ this.value = value;
+ }
+
+ clone() {
+ return new Scalar(this.value);
+ }
+
+ asDictInto_(d) {
+ d.value = this.value.asDict();
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html
new file mode 100644
index 00000000000..3bffc75caf0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v.d', function() {
+ class UnmergeableDiagnosticSet extends tr.v.d.Diagnostic {
+ /**
+ * @param {!Array.<!tr.v.d.Diagnostic>} diagnostics
+ */
+ constructor(diagnostics) {
+ super();
+ this._diagnostics = diagnostics;
+ }
+
+ clone() {
+ const clone = new tr.v.d.UnmergeableDiagnosticSet();
+ clone.addDiagnostic(this);
+ return clone;
+ }
+
+ canAddDiagnostic(otherDiagnostic) {
+ return true;
+ }
+
+ /**
+ * If |otherDiagnostic| is an UnmergeableDiagnosticSet, then add clones of
+ * its diagnostics to |this|. Otherwise, try to add |otherDiagnostic| to one
+ * of the diagnostics already in this set. If that fails, add a clone of
+ * |otherDiagnostic| to this set.
+ *
+ * @param {!tr.v.d.Diagnostic} otherDiagnostic
+ * @return {!tr.v.d.UnmergeableDiagnostic} this
+ */
+ 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);
+ }
+
+ 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 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py
new file mode 100644
index 00000000000..5387c3b9614
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/diagnostics/unmergeable_diagnostic_set.py
@@ -0,0 +1,52 @@
+# 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 tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import diagnostic_ref
+
+try:
+ StringTypes = basestring
+except NameError:
+ StringTypes = str
+
+
+class UnmergeableDiagnosticSet(diagnostic.Diagnostic):
+ __slots__ = '_diagnostics',
+
+ def __init__(self, diagnostics):
+ super(UnmergeableDiagnosticSet, self).__init__()
+ self._diagnostics = diagnostics
+
+ def __len__(self):
+ return len(self._diagnostics)
+
+ def __iter__(self):
+ for diag in self._diagnostics:
+ yield diag
+
+ def CanAddDiagnostic(self, unused_other_diagnostic):
+ return True
+
+ def AddDiagnostic(self, other_diagnostic):
+ if isinstance(other_diagnostic, UnmergeableDiagnosticSet):
+ self._diagnostics.extend(other_diagnostic._diagnostics)
+ return
+ for diag in self:
+ if diag.CanAddDiagnostic(other_diagnostic):
+ diag.AddDiagnostic(other_diagnostic)
+ return
+ self._diagnostics.append(other_diagnostic)
+
+ def _AsDictInto(self, d):
+ d['diagnostics'] = [d.AsDictOrReference() for d in self]
+
+ @staticmethod
+ def FromDict(dct):
+ def RefOrDiagnostic(d):
+ if isinstance(d, StringTypes):
+ return diagnostic_ref.DiagnosticRef(d)
+ return diagnostic.Diagnostic.FromDict(d)
+
+ return UnmergeableDiagnosticSet(
+ [RefOrDiagnostic(d) for d in dct['diagnostics']])
diff --git a/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py
new file mode 100644
index 00000000000..82b0ce3ceb8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter.py
@@ -0,0 +1,112 @@
+# 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.
+
+import json
+
+from tracing.value import histogram
+from tracing.value import histogram_set
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+
+
+def ConvertGtestJson(gtest_json):
+ """Convert JSON from a gtest perf test to Histograms.
+
+ Incoming data is in the following format:
+ {
+ 'metric1': {
+ 'units': 'unit1',
+ 'traces': {
+ 'story1': ['mean', 'std_dev'],
+ 'story2': ['mean', 'std_dev'],
+ },
+ 'important': ['testcase1', 'testcase2'],
+ },
+ 'metric2': {
+ 'units': 'unit2',
+ 'traces': {
+ 'story1': ['mean', 'std_dev'],
+ 'story2': ['mean', 'std_dev'],
+ },
+ 'important': ['testcase1', 'testcase2'],
+ },
+ ...
+ }
+ We ignore the 'important' fields and just assume everything should be
+ considered important.
+
+ We also don't bother adding any reserved diagnostics like mastername in this
+ script since that should be handled by the upload script.
+
+ Args:
+ gtest_json: A JSON dict containing perf output from a gtest
+
+ Returns:
+ A HistogramSet containing equivalent histograms and diagnostics
+ """
+
+ hs = histogram_set.HistogramSet()
+
+ for metric, metric_data in gtest_json.iteritems():
+ # Maintain the same unit if we're able to find an exact match, converting
+ # time units if possible. Otherwise use 'unitless'.
+ unit, multiplier = _ConvertUnit(metric_data.get('units'))
+
+ for story, story_data in metric_data['traces'].iteritems():
+ # We should only ever have the mean and standard deviation here.
+ assert len(story_data) == 2
+ h = histogram.Histogram(metric, unit)
+ h.diagnostics[reserved_infos.STORIES.name] = generic_set.GenericSet(
+ [story])
+ mean = float(story_data[0]) * multiplier
+ std_dev = float(story_data[1]) * multiplier
+ h.AddSample(mean)
+
+ # Synthesize the running statistics since we only have the mean + standard
+ # deviation instead of the actual data points.
+ h._running = histogram.RunningStatistics.FromDict([
+ 2, # count, we need this to be >1 in order for variance to work
+ mean, # max
+ 0, # meanlogs
+ mean, # mean
+ mean, # min
+ 2 * mean, # sum, this must be count * mean otherwise the reported mean
+ # is incorrect after merging statistics when reserved
+ # diagnostics are added.
+ std_dev * std_dev, # variance
+ ])
+
+ hs.AddHistogram(h)
+
+ return hs
+
+def ConvertGtestJsonFile(filepath):
+ """Convert JSON in a file from a gtest perf test to Histograms.
+
+ Contents of the given file will be overwritten with the new Histograms data.
+
+ Args:
+ filepath: The filepath to the JSON file to read/write from/to.
+ """
+ with open(filepath, 'r') as f:
+ data = json.load(f)
+ histograms = ConvertGtestJson(data)
+ with open(filepath, 'w') as f:
+ json.dump(histograms.AsDicts(), f)
+
+
+def _ConvertUnit(unit):
+ # We assume that smaller is better since we don't have an actual way to
+ # determine what the improvement direction is and most or all metrics from
+ # gtest perf tests have a downward improvement direction.
+ if unit in histogram.UNIT_NAMES:
+ return unit + '_smallerIsBetter', 1
+ # A number of existing gtest perf tests report time in units like
+ # microseconds, but histograms only support milliseconds. So, convert here if
+ # we can.
+ if unit == 'us':
+ return 'msBestFitFormat_smallerIsBetter', 0.001
+ if unit == 'ns':
+ return 'msBestFitFormat_smallerIsBetter', 0.000001
+ return 'unitless_smallerIsBetter', 1
diff --git a/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py
new file mode 100644
index 00000000000..b784143ee97
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/gtest_json_converter_unittest.py
@@ -0,0 +1,114 @@
+# 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.
+
+import unittest
+
+from tracing.value import gtest_json_converter
+from tracing.value.diagnostics import reserved_infos
+
+
+NANO_TO_MILLISECONDS = 0.000001
+
+
+class GtestJsonConverterUnittest(unittest.TestCase):
+
+ def testConvertBasic(self):
+ data = {
+ 'metric1': {
+ 'units': 'ms',
+ 'traces': {
+ 'story1': ['10.12345', '0.54321'],
+ 'story2': ['30', '0'],
+ }
+ },
+ 'metric2': {
+ 'units': 'ns',
+ 'traces': {
+ 'story1': ['100000.0', '2543.543'],
+ 'story2': ['12345.6789', '301.2'],
+ },
+ }
+ }
+ histograms = gtest_json_converter.ConvertGtestJson(data)
+ self.assertEqual(len(histograms), 4)
+
+ metric_histograms = histograms.GetHistogramsNamed('metric1')
+ self.assertEqual(len(metric_histograms), 2)
+ story1 = None
+ story2 = None
+ if metric_histograms[0].diagnostics[
+ reserved_infos.STORIES.name].GetOnlyElement() == 'story1':
+ story1 = metric_histograms[0]
+ story2 = metric_histograms[1]
+ else:
+ story2 = metric_histograms[0]
+ story1 = metric_histograms[1]
+
+ # assertAlmostEqual necessary to avoid floating point precision issues.
+ self.assertAlmostEqual(story1.average, 10.12345)
+ self.assertAlmostEqual(story1.standard_deviation, 0.54321)
+ self.assertAlmostEqual(story1.sum, story1.num_values * story1.average)
+ self.assertEqual(story2.average, 30)
+ self.assertEqual(story2.standard_deviation, 0)
+ self.assertEqual(story2.sum, story2.num_values * story2.average)
+ self.assertEqual(story1.unit, story2.unit)
+ self.assertEqual(story1.unit, 'ms_smallerIsBetter')
+
+ metric_histograms = histograms.GetHistogramsNamed('metric2')
+ self.assertEqual(len(metric_histograms), 2)
+ if metric_histograms[0].diagnostics[
+ reserved_infos.STORIES.name].GetOnlyElement() == 'story1':
+ story1 = metric_histograms[0]
+ story2 = metric_histograms[1]
+ else:
+ story2 = metric_histograms[0]
+ story1 = metric_histograms[1]
+
+ # assertAlmostEqual necessary to avoid floating point precision issues.
+ # We expect the numbers to be different than what was initially provided
+ # since this should be converted to milliseconds.
+ self.assertAlmostEqual(story1.average, 100000 * NANO_TO_MILLISECONDS)
+ self.assertAlmostEqual(story1.standard_deviation,
+ 2543.543 * NANO_TO_MILLISECONDS)
+ self.assertAlmostEqual(story1.sum, story1.num_values * story1.average)
+ self.assertAlmostEqual(story2.average, 12345.6789 * NANO_TO_MILLISECONDS)
+ self.assertAlmostEqual(story2.standard_deviation,
+ 301.2 * NANO_TO_MILLISECONDS)
+ self.assertAlmostEqual(story2.sum, story2.num_values * story2.average)
+ self.assertEqual(story1.unit, story2.unit)
+ self.assertEqual(story1.unit, 'msBestFitFormat_smallerIsBetter')
+
+ def testConvertUnknownUnit(self):
+ data = {
+ 'metric1': {
+ 'units': 'SomeUnknownUnit',
+ 'traces': {
+ 'story1': ['10', '1'],
+ 'story2': ['123.4', '7.89'],
+ }
+ }
+ }
+ histograms = gtest_json_converter.ConvertGtestJson(data)
+ self.assertEqual(len(histograms), 2)
+
+ metric_histograms = histograms.GetHistogramsNamed('metric1')
+ self.assertEqual(len(metric_histograms), 2)
+ story1 = None
+ story2 = None
+ if metric_histograms[0].diagnostics[
+ reserved_infos.STORIES.name].GetOnlyElement() == 'story1':
+ story1 = metric_histograms[0]
+ story2 = metric_histograms[1]
+ else:
+ story2 = metric_histograms[0]
+ story1 = metric_histograms[1]
+
+ self.assertEqual(story1.average, 10)
+ self.assertEqual(story1.standard_deviation, 1)
+ self.assertEqual(story1.sum, story1.num_values * story1.average)
+ self.assertAlmostEqual(story2.average, 123.4)
+ self.assertAlmostEqual(story2.standard_deviation, 7.89)
+ self.assertAlmostEqual(story2.sum, story2.num_values * story2.average)
+ self.assertEqual(story1.unit, story2.unit)
+ self.assertEqual(story1.unit, 'unitless_smallerIsBetter')
diff --git a/chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py
new file mode 100644
index 00000000000..9ca47d2a709
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler.py
@@ -0,0 +1,201 @@
+# Copyright 2017 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 codecs
+import collections
+import sys
+import time
+
+from tracing.value import histogram
+from tracing.value import histogram_set
+from tracing.value.diagnostics import breakdown
+from tracing.value.diagnostics import date_range
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import related_name_map
+from tracing.value.diagnostics import reserved_infos
+from tracing_build import render_histograms_viewer
+
+
+def _IsUserDefinedInstance(obj):
+ return str(type(obj)).startswith('<class ')
+
+
+class _HeapProfiler(object):
+ __slots__ = '_diagnostics_callback', '_histograms', '_seen'
+
+ def __init__(self, diagnostics_callback=None):
+ self._diagnostics_callback = diagnostics_callback
+ self._histograms = None
+ self._seen = set()
+
+ def Profile(self, root):
+ self._histograms = histogram_set.HistogramSet()
+ total_hist = self._GetOrCreateHistogram('heap')
+ total_hist.diagnostics['types'] = related_name_map.RelatedNameMap()
+ total_breakdown = breakdown.Breakdown()
+ total_size = self._Recurse(
+ root, total_hist.diagnostics['types'], total_breakdown)
+ builtins_size = total_size - sum(subsize for _, subsize in total_breakdown)
+
+ if builtins_size:
+ total_breakdown.Set('(builtin types)', builtins_size)
+ total_hist.AddSample(total_size, dict(types=total_breakdown))
+
+ self._histograms.AddSharedDiagnosticToAllHistograms(
+ reserved_infos.TRACE_START.name,
+ date_range.DateRange(time.time() * 1000))
+
+ return self._histograms
+
+ def _GetOrCreateHistogram(self, name):
+ hs = self._histograms.GetHistogramsNamed(name)
+ if len(hs) > 1:
+ raise Exception('Too many Histograms named %s' % name)
+
+ if len(hs) == 1:
+ return hs[0]
+
+ hist = histogram.Histogram(name, 'sizeInBytes_smallerIsBetter')
+ hist.CustomizeSummaryOptions(dict(std=False, min=False, max=False))
+ self._histograms.AddHistogram(hist)
+ return hist
+
+ def _Recurse(self, obj, parent_related_names, parent_breakdown):
+ if id(obj) in self._seen:
+ return 0
+ self._seen.add(id(obj))
+
+ size = sys.getsizeof(obj)
+
+ related_names = parent_related_names
+ types_breakdown = parent_breakdown
+
+ hist = None
+ if _IsUserDefinedInstance(obj):
+ type_name = type(obj).__name__
+ hist = self._GetOrCreateHistogram('heap:' + type_name)
+
+ related_names = hist.diagnostics.get('types')
+ if related_names is None:
+ related_names = related_name_map.RelatedNameMap()
+ types_breakdown = breakdown.Breakdown()
+
+ if isinstance(obj, dict):
+ for objkey, objvalue in obj.iteritems():
+ size += self._Recurse(objkey, related_names, types_breakdown)
+ size += self._Recurse(objvalue, related_names, types_breakdown)
+ elif isinstance(obj, (tuple, list, set, frozenset, collections.deque)):
+ # Can't use collections.Iterable because strings are iterable, but
+ # sys.getsizeof() already handles strings, we don't need to iterate over
+ # them.
+ for elem in obj:
+ size += self._Recurse(elem, related_names, types_breakdown)
+
+ # It is possible to subclass builtin types like dict and add properties to
+ # them, so handle __dict__ and __slots__ even if obj is a dict/list/etc.
+
+ properties_breakdown = breakdown.Breakdown()
+ if hasattr(obj, '__dict__'):
+ size += sys.getsizeof(obj.__dict__)
+ for dkey, dvalue in obj.__dict__.iteritems():
+ size += self._Recurse(dkey, related_names, types_breakdown)
+ dsize = self._Recurse(dvalue, related_names, types_breakdown)
+ properties_breakdown.Set(dkey, dsize)
+ size += dsize
+ size += self._Recurse(obj.__dict__, related_names, types_breakdown)
+
+ # It is possible for a class to use both __slots__ and __dict__ by listing
+ # __dict__ as a slot.
+
+ if hasattr(obj.__class__, '__slots__'):
+ for slot in obj.__class__.__slots__:
+ if slot == '__dict__':
+ # obj.__dict__ was already handled
+ continue
+ if not hasattr(obj, slot):
+ continue
+ slot_size = self._Recurse(
+ getattr(obj, slot), related_names, types_breakdown)
+ properties_breakdown.Set(slot, slot_size)
+ size += slot_size
+
+ if hist:
+ if len(related_names):
+ hist.diagnostics['types'] = related_names
+
+ parent_related_names.Set(type_name, hist.name)
+ parent_breakdown.Set(type_name, parent_breakdown.Get(type_name) + size)
+
+ builtins_size = size - sum(subsize for _, subsize in types_breakdown)
+ if builtins_size:
+ types_breakdown.Set('(builtin types)', builtins_size)
+
+ sample_diagnostics = {'types': types_breakdown}
+ if len(properties_breakdown):
+ sample_diagnostics['properties'] = properties_breakdown
+ if self._diagnostics_callback:
+ sample_diagnostics.update(self._diagnostics_callback(obj))
+
+ hist.AddSample(size, sample_diagnostics)
+
+ return size
+
+
+def Profile(root, label=None, html_filename=None, html_stream=None,
+ vulcanized_viewer=None, reset_results=False,
+ diagnostics_callback=None):
+ """Profiles memory consumed by the root object.
+
+ Produces a HistogramSet containing 1 Histogram for each user-defined class
+ encountered when recursing through the root object's properties.
+ Each Histogram contains 1 sample for each instance of the class.
+ Each sample contains 2 Breakdowns:
+ - 'types' allows drilling down into memory profiles for other classes, and
+ - 'properties' breaks down the size of an instance by its properties.
+
+ Args:
+ label: string label to distinguish these results from those produced by
+ other Profile() calls.
+ html_filename: string filename to write HTML results.
+ html_stream: file-like string to write HTML results.
+ vulcanized_viewer: HTML string
+ reset_results: whether to delete pre-existing results in
+ html_filename/html_stream
+ diagnostics_callback: function that takes an instance of a class, and
+ returns a dictionary from strings to Diagnostic objects.
+
+ Returns:
+ HistogramSet
+ """
+ # TODO(4068): Package this and its dependencies and a vulcanized viewer in
+ # order to remove the vulcanized_viewer parameter and simplify rendering the
+ # viewer.
+
+ profiler = _HeapProfiler(diagnostics_callback)
+ histograms = profiler.Profile(root)
+
+ if label:
+ histograms.AddSharedDiagnosticToAllHistograms(
+ reserved_infos.LABELS.name, generic_set.GenericSet([label]))
+
+ if html_filename and not html_stream:
+ open(html_filename, 'a').close() # Create file if it doesn't exist.
+ html_stream = codecs.open(html_filename, mode='r+', encoding='utf-8')
+
+ if html_stream:
+ # Vulcanizing the viewer requires a full catapult checkout, which is not
+ # available in some contexts such as appengine.
+ # Merely rendering the viewer requires a pre-vulcanized viewer HTML string.
+ # render_histograms_viewer does not require a full checkout, so it can run
+ # in restricted contexts such as appengine as long as a pre-vulcanized
+ # viewer is provided.
+ if vulcanized_viewer:
+ render_histograms_viewer.RenderHistogramsViewer(
+ histograms.AsDicts(), html_stream, reset_results, vulcanized_viewer)
+ else:
+ from tracing_build import vulcanize_histograms_viewer
+ vulcanize_histograms_viewer.VulcanizeAndRenderHistogramsViewer(
+ histograms.AsDicts(), html_stream)
+
+ return histograms
diff --git a/chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py
new file mode 100644
index 00000000000..d2904c8a151
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/heap_profiler_unittest.py
@@ -0,0 +1,58 @@
+# Copyright 2017 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 tracing.value import heap_profiler
+from tracing.value import histogram
+from tracing.value import histogram_set
+
+
+class HeapProfilerUnitTest(unittest.TestCase):
+
+ def testHeapProfiler(self):
+ test_data = histogram_set.HistogramSet()
+ for i in xrange(10):
+ test_hist = histogram.Histogram('test', 'n%')
+ test_hist.AddSample(i / 10.0)
+ test_data.AddHistogram(test_hist)
+
+ histograms = heap_profiler.Profile(test_data)
+
+ set_size_hist = histograms.GetHistogramNamed('heap:HistogramSet')
+ self.assertEquals(set_size_hist.num_values, 1)
+ # The exact sizes of python objects can vary between platforms and versions.
+ self.assertGreater(set_size_hist.sum, 10000)
+
+ hist_size_hist = histograms.GetHistogramNamed('heap:Histogram')
+ self.assertEquals(hist_size_hist.num_values, 10)
+ self.assertGreater(hist_size_hist.sum, 10000)
+
+ related_names = hist_size_hist.diagnostics['types']
+ self.assertEquals(related_names.Get('HistogramBin'), 'heap:HistogramBin')
+ self.assertEquals(related_names.Get('DiagnosticMap'), 'heap:DiagnosticMap')
+
+ properties = hist_size_hist.bins[33].diagnostic_maps[0]['properties']
+ types = hist_size_hist.bins[33].diagnostic_maps[0]['types']
+ self.assertGreater(len(properties), 3)
+ self.assertGreater(properties.Get('_bins'), 1000)
+ self.assertEquals(len(types), 4)
+ self.assertGreater(types.Get('HistogramBin'), 1000)
+ self.assertGreater(types.Get('(builtin types)'), 1000)
+
+ bin_size_hist = histograms.GetHistogramNamed('heap:HistogramBin')
+ self.assertEquals(bin_size_hist.num_values, 32)
+ self.assertGreater(bin_size_hist.sum, 1000)
+
+ diag_map_size_hist = histograms.GetHistogramNamed('heap:DiagnosticMap')
+ self.assertEquals(diag_map_size_hist.num_values, 10)
+ self.assertGreater(diag_map_size_hist.sum, 1000)
+
+ range_size_hist = histograms.GetHistogramNamed('heap:Range')
+ self.assertEquals(range_size_hist.num_values, 22)
+ self.assertGreater(range_size_hist.sum, 1000)
+
+ stats_size_hist = histograms.GetHistogramNamed('heap:RunningStatistics')
+ self.assertEquals(stats_size_hist.num_values, 10)
+ self.assertGreater(stats_size_hist.sum, 1000)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram.html b/chromium/third_party/catapult/tracing/tracing/value/histogram.html
new file mode 100644
index 00000000000..effc879666d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram.html
@@ -0,0 +1,1478 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/running_statistics.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+
+<script>
+'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';
+
+ /**
+ * Converts the given percent to a string in the format specified above.
+ * @param {number} percent The percent must be between 0.0 and 1.0.
+ * @param {boolean=} opt_force3 Whether to force the result to be 3 chars long
+ * @return {string}
+ */
+ 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');
+ }
+ // Pad short strings with zeros.
+ 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);
+ }
+
+ /**
+ * Converts the given string to a percent between 0 and 1.
+ * @param {string}
+ * @return {number}
+ */
+ function percentFromString(s) {
+ return parseFloat(s[0] + '.' + s.substr(1).replace(/_/g, ''));
+ }
+
+ class HistogramBin {
+ /**
+ * @param {!tr.b.math.Range} range
+ */
+ constructor(range) {
+ this.range = range;
+ this.count = 0;
+ this.diagnosticMaps = [];
+ }
+
+ /**
+ * @param {*} value
+ */
+ addSample(value) {
+ this.count += 1;
+ }
+
+ /**
+ * @param {!tr.v.d.DiagnosticMap} diagnostics
+ */
+ 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;
+ }
+
+ 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));
+ }
+ }
+ }
+
+ asDict() {
+ if (!this.diagnosticMaps.length) {
+ return [this.count];
+ }
+ // It's more efficient to serialize these 2 fields in an array. If you
+ // add any other fields, you should re-evaluate whether it would be more
+ // efficient to serialize as a dict.
+ 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],
+ // Don't include 'percentile' or 'iprs' here. Their default values are [],
+ // which is mutable. Callers may push to it, so there must be a different
+ // Array instance for each Histogram instance.
+ ]);
+
+ /**
+ * This is basically a histogram, but so much more.
+ * Histogram is serializable using asDict/fromDict.
+ * Histogram computes several statistics of its contents.
+ * Histograms can be merged.
+ * getDifferenceSignificance() test whether one Histogram is statistically
+ * significantly different from another Histogram.
+ * Histogram stores a random sample of the exact number values added to it.
+ * Histogram stores a random sample of optional per-sample DiagnosticMaps.
+ * Histogram is visualized by <tr-v-ui-histogram-span>, which supports
+ * selecting bins, and visualizing the DiagnosticMaps of selected bins.
+ *
+ * @param {!tr.b.Unit} unit
+ * @param {!tr.v.HistogramBinBoundaries=} opt_binBoundaries
+ */
+ 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);
+ }
+
+ // Serialize binBoundaries here instead of holding a reference to it in
+ // case it is modified.
+ this.binBoundariesDict_ = binBoundaries.asDict();
+
+ // HistogramBinBoundaries create empty HistogramBins. Save memory by
+ // sharing those empty HistogramBin instances with other Histograms. Wait
+ // to copy HistogramBins until we need to modify it (copy-on-write).
+ 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.summaryOptions = new Map(DEFAULT_SUMMARY_OPTIONS);
+ this.summaryOptions.set('percentile', []);
+ this.summaryOptions.set('iprs', []);
+ this.unit = unit;
+ }
+
+ /**
+ * Create a Histogram, configure it, and add samples to it.
+ *
+ * |samples| can be either
+ * 0. a number, or
+ * 1. a dictionary {value: number, diagnostics: dictionary}, or
+ * 2. an array of
+ * 2a. number, or
+ * 2b. dictionaries {value, diagnostics}.
+ *
+ * @param {string} name
+ * @param {!tr.b.Unit} unit
+ * @param {number|!Object|!Array.<(number|!Object)>} samples
+ * @param {!Object=} opt_options
+ * @param {!tr.v.HistogramBinBoundaries} opt_options.binBoundaries
+ * @param {!Object|!Map} opt_options.summaryOptions
+ * @param {!Object|!Map} opt_options.diagnostics
+ * @param {string} opt_options.description
+ * @return {!tr.v.Histogram}
+ */
+ 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) {
+ 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_;
+ }
+
+ get running() {
+ return this.running_;
+ }
+
+ 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_;
+ }
+
+ 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) {
+ // Copy HistogramBin on write, share the rest with the other
+ // Histograms that use the same HistogramBinBoundaries.
+ 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.running) {
+ hist.running_ = tr.b.math.RunningStatistics.fromDict(dict.running);
+ }
+ if (dict.summaryOptions) {
+ if (dict.summaryOptions.iprs) {
+ // Range.fromDict() requires isEmpty, which is unnecessarily verbose
+ // for this use case.
+ dict.summaryOptions.iprs = dict.summaryOptions.iprs.map(
+ r => tr.b.math.Range.fromExplicitRange(r[0], r[1]));
+ }
+ hist.customizeSummaryOptions(dict.summaryOptions);
+ }
+ if (dict.maxNumSampleValues !== undefined) {
+ hist.maxNumSampleValues = dict.maxNumSampleValues;
+ }
+ if (dict.sampleValues) {
+ hist.sampleValues_ = dict.sampleValues;
+ }
+ if (dict.numNans) {
+ hist.numNans = dict.numNans;
+ }
+ if (dict.nanDiagnostics) {
+ for (const map of dict.nanDiagnostics) {
+ hist.nanDiagnosticMaps.push(tr.v.d.DiagnosticMap.fromDict(map));
+ }
+ }
+ return hist;
+ }
+
+ get numValues() {
+ return this.running_ ? this.running_.count : 0;
+ }
+
+ get average() {
+ return this.running_ ? this.running_.mean : undefined;
+ }
+
+ get standardDeviation() {
+ return this.running_ ? this.running_.stddev : undefined;
+ }
+
+ get geometricMean() {
+ return this.running_ ? this.running_.geometricMean : 0;
+ }
+
+ get sum() {
+ return this.running_ ? this.running_.sum : 0;
+ }
+
+ get min() {
+ return this.running_ ? this.running_.min : Infinity;
+ }
+
+ get max() {
+ return this.running_ ? this.running_.max : -Infinity;
+ }
+
+ /**
+ * Requires that units agree.
+ * Returns DONT_CARE if that is the units' improvementDirection.
+ * Returns SIGNIFICANT if the Mann-Whitney U test returns a
+ * p-value less than opt_alpha or DEFAULT_ALPHA. Returns INSIGNIFICANT if
+ * the p-value is greater than alpha.
+ *
+ * @param {!tr.v.Histogram} other
+ * @param {number=} opt_alpha
+ * @return {!tr.b.math.Statistics.Significance}
+ */
+ getDifferenceSignificance(other, opt_alpha) {
+ if (this.unit !== other.unit) {
+ throw new Error('Cannot compare Histograms with different units');
+ }
+
+ if (this.unit.improvementDirection ===
+ tr.b.ImprovementDirection.DONT_CARE) {
+ return tr.b.math.Statistics.Significance.DONT_CARE;
+ }
+
+ if (!(other instanceof Histogram)) {
+ throw new Error('Unable to compute a p-value');
+ }
+
+ const testResult = tr.b.math.Statistics.mwu(
+ this.sampleValues, other.sampleValues, opt_alpha);
+ return testResult.significance;
+ }
+
+ /*
+ * Compute an approximation of percentile based on the counts in the bins.
+ * If the real percentile lies within |this.range| then the result of
+ * the function will deviate from the real percentile by at most
+ * the maximum width of the bin(s) within which the point(s)
+ * from which the real percentile would be calculated lie.
+ * If the real percentile is outside |this.range| then the function
+ * returns the closest range limit: |this.range.min| or |this.range.max|.
+ *
+ * @param {number} percent The percent must be between 0.0 and 1.0.
+ */
+ getApproximatePercentile(percent) {
+ if (percent < 0 || percent > 1) {
+ throw new Error('percent must be in [0,1]');
+ }
+ if (this.numValues === 0) return undefined;
+ if (this.allBins.length === 1) {
+ // Copy sampleValues, don't sort them in place, in order to preserve
+ // insertion order.
+ const sortedSampleValues = this.sampleValues.slice().sort(
+ (x, y) => x - y);
+ return sortedSampleValues[Math.floor((sortedSampleValues.length - 1) *
+ percent)];
+ }
+ let valuesToSkip = Math.floor((this.numValues - 1) * percent);
+ for (const bin of this.allBins) {
+ valuesToSkip -= bin.count;
+ if (valuesToSkip >= 0) continue;
+ if (bin.range.min === -Number.MAX_VALUE) {
+ return bin.range.max;
+ }
+ if (bin.range.max === Number.MAX_VALUE) {
+ return bin.range.min;
+ }
+ return bin.range.center;
+ }
+ return this.allBins[this.allBins.length - 1].range.min;
+ }
+
+ getBinIndexForValue(value) {
+ // Don't use subtraction to avoid arithmetic overflow.
+ 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)];
+ }
+
+ /**
+ * @param {number|*} value
+ * @param {(!Object|!tr.v.d.DiagnosticMap)=} 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);
+
+ value = this.unit.truncate(value);
+
+ const binIndex = this.getBinIndexForValue(value);
+ let bin = this.allBins[binIndex];
+ if (bin.count === 0) {
+ // Copy HistogramBin on write, share the rest with the other
+ // Histograms that use the same HistogramBinBoundaries.
+ 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);
+ }
+
+ sampleValuesInto(samples) {
+ for (const sampleValue of this.sampleValues) {
+ samples.push(sampleValue);
+ }
+ }
+
+ /**
+ * Return true if this Histogram can be added to |other|.
+ *
+ * @param {!tr.v.Histogram} other
+ * @return {boolean}
+ */
+ canAddHistogram(other) {
+ if (this.unit !== other.unit) {
+ return false;
+ }
+ if (this.binBoundariesDict_ === other.binBoundariesDict_) {
+ return true;
+ }
+ if (!this.binBoundariesDict_ || !other.binBoundariesDict_) {
+ return true;
+ }
+ // |binBoundariesDict_| may be equal even if they are not the same object.
+ 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;
+ }
+
+ /**
+ * Add |other| to this Histogram in-place if they can be added.
+ *
+ * @param {!tr.v.Histogram} other
+ */
+ 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];
+ // TODO(benjhayden) This can't propagate sample diagnostics until
+ // sampleValues are merged into bins alongside their diagnostics.
+ 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) {
+ // Copy HistogramBin on write, share the rest with the other
+ // Histograms that use the same HistogramBinBoundaries.
+ 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_);
+ }
+
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Controls which statistics are exported to dashboard for this Histogram.
+ * The options not included in the |summaryOptions| will not change.
+ *
+ * @param {!Object} summaryOptions
+ * @param {boolean=} summaryOptions.avg
+ * @param {boolean=} summaryOptions.count
+ * @param {boolean=} summaryOptions.geometricMean
+ * @param {boolean=} summaryOptions.max
+ * @param {boolean=} summaryOptions.min
+ * @param {boolean=} summaryOptions.nans
+ * @param {boolean=} summaryOptions.std
+ * @param {boolean=} summaryOptions.sum
+ * @param {!Array.<number>=} summaryOptions.percentile Numbers in (0,1)
+ * @param {!Array.<!tr.b.Range>=} summaryOptions.iprs Ranges of numbers in
+ * (0,1).
+ */
+ customizeSummaryOptions(summaryOptions) {
+ for (const [key, value] of Object.entries(summaryOptions)) {
+ this.summaryOptions.set(key, value);
+ }
+ }
+
+ /**
+ * @param {string} statName
+ * @param {!tr.v.Histogram=} opt_referenceHistogram
+ * @param {!HypothesisTestResult=} opt_mwu
+ * @return {!tr.b.Scalar}
+ * @throws {Error} When statName is not recognized, such as delta statistics
+ * when !this.canCompare(opt_referenceHistograms).
+ */
+ 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);
+ }
+ if (statName === 'geometricMean') {
+ if (typeof(this.geometricMean) !== 'number') return undefined;
+ return new tr.b.Scalar(this.unit, this.geometricMean);
+ }
+ if (statName === 'min' || statName === 'max' || statName === 'sum') {
+ if (this.running_ === undefined) {
+ this.running_ = new tr.b.math.RunningStatistics();
+ }
+ 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) === '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');
+ }
+
+ const suffix = tr.b.Unit.nameSuffixForImprovementDirection(
+ this.unit.improvementDirection);
+
+ const deltaIndex = statName.indexOf(DELTA);
+ if (deltaIndex >= 0) {
+ const baseStatName = statName.substr(deltaIndex + 1);
+ const thisStat = this.getStatisticScalar(baseStatName);
+ const otherStat = opt_referenceHistogram.getStatisticScalar(
+ baseStatName);
+ const deltaValue = thisStat.value - otherStat.value;
+
+ if (statName[0] === '%') {
+ return new tr.b.Scalar(
+ tr.b.Unit.byName['normalizedPercentageDelta' + suffix],
+ deltaValue / otherStat.value);
+ }
+ return new tr.b.Scalar(
+ thisStat.unit.correspondingDeltaUnit, deltaValue);
+ }
+
+ if (statName === Z_SCORE_NAME) {
+ return new tr.b.Scalar(
+ tr.b.Unit.byName['sigmaDelta' + suffix],
+ (this.average - opt_referenceHistogram.average) /
+ opt_referenceHistogram.standardDeviation);
+ }
+
+ 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);
+ }
+
+ /**
+ * @return {!Array.<string>} names of enabled summary statistics
+ */
+ 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);
+ }
+ }
+ return statisticsNames;
+ }
+
+ /**
+ * Returns true if delta statistics can be computed between |this| and
+ * |other|.
+ *
+ * @param {!tr.v.Histogram=} other
+ * @return {boolean}
+ */
+ canCompare(other) {
+ return other instanceof Histogram &&
+ this.unit === other.unit &&
+ this.numValues > 0 &&
+ other.numValues > 0;
+ }
+
+ /**
+ * Returns |statName| if it can be computed, or the related non-delta
+ * statistic if |statName| is a delta statistic and
+ * !this.canCompare(opt_referenceHist).
+ *
+ * @param {string} statName
+ * @param {!tr.v.Histogram=} opt_referenceHist
+ * @return {string}
+ */
+ 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';
+ }
+ const deltaIndex = statName.indexOf(DELTA);
+ if (deltaIndex < 0) return statName;
+ return statName.substr(deltaIndex + 1);
+ }
+
+ /**
+ * Returns names of delta statistics versions of given non-delta statistics
+ * names.
+ *
+ * @param {!Array.<string>} statNames
+ * @return {!Array.<string>}
+ */
+ static getDeltaStatisticsNames(statNames) {
+ const deltaNames = [];
+ for (const statName of statNames) {
+ deltaNames.push(`${DELTA}${statName}`);
+ deltaNames.push(`%${DELTA}${statName}`);
+ }
+ return deltaNames.concat([Z_SCORE_NAME, P_VALUE_NAME, U_STATISTIC_NAME]);
+ }
+
+ /**
+ * Returns a Map {statisticName: Scalar}.
+ *
+ * Each enabled summary option produces the corresponding value:
+ * min, max, count, sum, avg, or std.
+ * Each percentile 0.x produces pct_0x0.
+ * Each percentile 0.xx produces pct_0xx.
+ * Each percentile 0.xxy produces pct_0xx_y.
+ * Percentile 1.0 produces pct_100.
+ *
+ * @return {!Map.<string, Scalar>}
+ */
+ 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_;
+ }
+
+ /**
+ * Create a new Histogram instance that is just like |this|. This is useful
+ * when merging Histograms.
+ * @return {!tr.v.Histogram}
+ */
+ clone() {
+ const binBoundaries = HistogramBinBoundaries.fromDict(
+ this.binBoundariesDict_);
+ const hist = new Histogram(this.name, this.unit, binBoundaries);
+ for (const [stat, option] of this.summaryOptions) {
+ // Copy arrays, but not ipr Ranges.
+ if (stat === 'percentile' || stat === 'iprs') {
+ hist.summaryOptions.set(stat, Array.from(option));
+ } else {
+ hist.summaryOptions.set(stat, option);
+ }
+ }
+ hist.addHistogram(this);
+ return hist;
+ }
+
+ /**
+ * Produce a Histogram with |this| Histogram's name, unit, description,
+ * statistics, summaryOptions, sampleValues, and diagnostics, but with
+ * |newBoundaries|.
+ * sample diagnostics are not copied. In-bound Relationship
+ * diagnostics are broken.
+ *
+ * @param {!tr.v.HistogramBinBoundaries} newBoundaries
+ * @return {!tr.v.Histogram}
+ */
+ 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) {
+ // Copy the array of percentiles.
+ if (stat === 'percentile') {
+ 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();
+ 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();
+ 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') {
+ // Use a more compact JSON format than Range supports.
+ 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;
+ }
+ summaryOptions[name] = option;
+ anyOverriddenSummaryOptions = true;
+ }
+ if (anyOverriddenSummaryOptions) {
+ dict.summaryOptions = summaryOptions;
+ }
+
+ return dict;
+ }
+
+ allBinsAsDict_() {
+ // dict.allBins may be either an array or a dict, whichever is more
+ // efficient.
+ // The overhead of the array form is significant when the histogram is
+ // sparse, and the overhead of the dict form is significant when the
+ // histogram is dense.
+ // The dict form is more efficient when more than half of allBins are
+ // empty. The array form is more efficient when fewer than half of
+ // allBins are empty.
+
+ const numBins = this.allBins.length;
+
+ // If allBins are empty, then don't serialize anything for them.
+ 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();
+ }
+ }
+ return allBinsDict;
+ }
+
+ const allBinsArray = [];
+ for (let i = 0; i < numBins; ++i) {
+ allBinsArray.push(this.allBins[i].asDict());
+ }
+ return allBinsArray;
+ }
+
+ get defaultMaxNumSampleValues_() {
+ // Single-bin histograms might be rebin()ned, so they should retain enough
+ // samples that the rebinned histogram looks close enough.
+ return DEFAULT_SAMPLE_VALUES_PER_BIN * Math.max(
+ this.allBins.length, DEFAULT_REBINNED_COUNT);
+ }
+ }
+
+ // Some metrics only want to report average. This dictionary is provided to
+ // facilitate disabling all other statistics.
+ Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS = {
+ count: false,
+ max: false,
+ min: false,
+ std: false,
+ sum: false,
+ };
+
+ const HISTOGRAM_BIN_BOUNDARIES_CACHE = new Map();
+
+ /*
+ * Reusable builder for tr.v.Histogram objects.
+ *
+ * The bins of the Histogram are specified by adding the desired boundaries
+ * between bins. Initially, the builder has only a single boundary:
+ *
+ * range.min=range.max
+ * |
+ * |
+ * -MAX_VALUE <-----|-----------> +MAX_VALUE
+ * : resulting : resulting :
+ * : underflow : overflow :
+ * : bin : bin :
+ *
+ * If the single boundary is set to either -Number.MAX_VALUE or
+ * +Number.MAX_VALUE, then the builder will construct only a single bin:
+ *
+ * range.min=range.max
+ * |
+ * |
+ * -MAX_VALUE <-> +MAX_VALUE
+ * : resulting :
+ * : bin :
+ *
+ * More boundaries can be added (in increasing order) using addBinBoundary,
+ * addLinearBins and addExponentialBins:
+ *
+ * range.min range.max
+ * | | | | |
+ * | | | | |
+ * -MAX_VALUE <------|---------|---------|-----|---------|------> +MAX_VALUE
+ * : resulting : result. : result. : : result. : resulting :
+ * : underflow : central : central : ... : central : overflow :
+ * : bin : bin 0 : bin 1 : : bin N-1 : bin :
+ *
+ * An important feature of the builder is that it's reusable, i.e. it can be
+ * used to build multiple Histograms with the same bin structure.
+ */
+ class HistogramBinBoundaries {
+ /**
+ * Create a linearly scaled tr.v.HistogramBinBoundaries with |numBins| bins
+ * ranging from |min| to |max|.
+ *
+ * @param {number} min
+ * @param {number} max
+ * @param {number} numBins
+ * @return {tr.v.HistogramBinBoundaries}
+ */
+ static createLinear(min, max, numBins) {
+ return new HistogramBinBoundaries(min).addLinearBins(max, numBins);
+ }
+
+ /**
+ * Create an exponentially scaled tr.v.HistogramBinBoundaries with |numBins|
+ * bins ranging from |min| to |max|.
+ *
+ * @param {number} min
+ * @param {number} max
+ * @param {number} numBins
+ * @return {tr.v.HistogramBinBoundaries}
+ */
+ static createExponential(min, max, numBins) {
+ return new HistogramBinBoundaries(min).addExponentialBins(max, numBins);
+ }
+
+ /**
+ * @param {Array.<number>} binBoundaries
+ */
+ static createWithBoundaries(binBoundaries) {
+ const builder = new HistogramBinBoundaries(binBoundaries[0]);
+ for (const boundary of binBoundaries.slice(1)) {
+ builder.addBinBoundary(boundary);
+ }
+ return builder;
+ }
+
+ /**
+ * |minBinBoundary| will be the boundary between the underflow bin and the
+ * first central bin if other bin boundaries are added.
+ * If no other bin boundaries are added, then |minBinBoundary| will be the
+ * boundary between the underflow bin and the overflow bin.
+ * If no other bin boundaries are added and |minBinBoundary| is either
+ * -Number.MAX_VALUE or +Number.MAX_VALUE, then only a single binRange will
+ * be built.
+ *
+ * @param {number} minBinBoundary The minimum boundary between bins.
+ */
+ constructor(minBinBoundary) {
+ this.builder_ = [minBinBoundary];
+ this.range_ = new tr.b.math.Range();
+ this.range_.addValue(minBinBoundary);
+ this.binRanges_ = undefined;
+ this.bins_ = undefined;
+ }
+
+ get range() {
+ return this.range_;
+ }
+
+ asDict() {
+ if (this.builder_.length === 1 && this.builder_[0] === Number.MAX_VALUE) {
+ return undefined;
+ }
+
+ // Don't copy builder_ here so that Histogram.canAddHistogram() can test
+ // for object identity.
+ return this.builder_;
+ }
+
+ pushBuilderSlice_(slice) {
+ this.builder_.push(slice);
+ // Copy builder_ when it's modified so that Histogram.canAddHistogram()
+ // can test for object identity.
+ this.builder_ = this.builder_.slice();
+ }
+
+ static fromDict(dict) {
+ if (dict === undefined) {
+ return HistogramBinBoundaries.SINGULAR;
+ }
+
+ // When loading a results.html with many Histograms with the same bin
+ // boundaries, caching the HistogramBinBoundaries not only speeds up
+ // loading, but also prevents a bug where buildBinRanges_ is occasionally
+ // non-deterministic, which causes similar Histograms to be unmergeable.
+ const cacheKey = JSON.stringify(dict);
+ if (HISTOGRAM_BIN_BOUNDARIES_CACHE.has(cacheKey)) {
+ return HISTOGRAM_BIN_BOUNDARIES_CACHE.get(cacheKey);
+ }
+
+ const binBoundaries = new HistogramBinBoundaries(dict[0]);
+ for (const slice of dict.slice(1)) {
+ if (!(slice instanceof Array)) {
+ binBoundaries.addBinBoundary(slice);
+ continue;
+ }
+ switch (slice[0]) {
+ case HistogramBinBoundaries.SLICE_TYPE.LINEAR:
+ binBoundaries.addLinearBins(slice[1], slice[2]);
+ break;
+
+ case HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL:
+ binBoundaries.addExponentialBins(slice[1], slice[2]);
+ break;
+
+ default:
+ throw new Error('Unrecognized HistogramBinBoundaries slice type');
+ }
+ }
+ HISTOGRAM_BIN_BOUNDARIES_CACHE.set(cacheKey, binBoundaries);
+ return binBoundaries;
+ }
+
+ get bins() {
+ if (this.bins_ === undefined) {
+ this.buildBins_();
+ }
+ return this.bins_;
+ }
+
+ buildBins_() {
+ this.bins_ = this.binRanges.map(r => new HistogramBin(r));
+ // It would be nice to Object.freeze() the bins in order to catch bugs
+ // when we forget to copy a bin before writing to it, but that would slow
+ // down buildBins_ by 55%: https://jsperf.com/new-vs-new-freeze/1
+ }
+
+ /**
+ * @return {!Array.<!tr.b.math.Range>}
+ */
+ get binRanges() {
+ if (this.binRanges_ === undefined) {
+ this.buildBinRanges_();
+ }
+ return this.binRanges_;
+ }
+
+ buildBinRanges_() {
+ if (typeof this.builder_[0] !== 'number') {
+ throw new Error('Invalid start of builder_');
+ }
+ this.binRanges_ = [];
+ let prevBoundary = this.builder_[0];
+
+ if (prevBoundary > -Number.MAX_VALUE) {
+ // underflow bin
+ this.binRanges_.push(tr.b.math.Range.fromExplicitRange(
+ -Number.MAX_VALUE, prevBoundary));
+ }
+
+ for (const slice of this.builder_.slice(1)) {
+ if (!(slice instanceof Array)) {
+ this.binRanges_.push(
+ tr.b.math.Range.fromExplicitRange(prevBoundary, slice));
+ prevBoundary = slice;
+ continue;
+ }
+ const nextMaxBinBoundary = slice[1];
+ const binCount = slice[2];
+ const sliceMinBinBoundary = prevBoundary;
+
+ switch (slice[0]) {
+ case HistogramBinBoundaries.SLICE_TYPE.LINEAR:
+ {
+ const binWidth = (nextMaxBinBoundary - prevBoundary) / binCount;
+ for (let i = 1; i < binCount; i++) {
+ const boundary = sliceMinBinBoundary + i * binWidth;
+ this.binRanges_.push(tr.b.math.Range.fromExplicitRange(
+ prevBoundary, boundary));
+ prevBoundary = boundary;
+ }
+ break;
+ }
+
+ case HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL:
+ {
+ const binExponentWidth =
+ Math.log(nextMaxBinBoundary / prevBoundary) / binCount;
+ for (let i = 1; i < binCount; i++) {
+ const boundary = sliceMinBinBoundary * Math.exp(
+ i * binExponentWidth);
+ this.binRanges_.push(tr.b.math.Range.fromExplicitRange(
+ prevBoundary, boundary));
+ prevBoundary = boundary;
+ }
+ break;
+ }
+
+ default:
+ throw new Error('Unrecognized HistogramBinBoundaries slice type');
+ }
+ this.binRanges_.push(tr.b.math.Range.fromExplicitRange(
+ prevBoundary, nextMaxBinBoundary));
+ prevBoundary = nextMaxBinBoundary;
+ }
+ if (prevBoundary < Number.MAX_VALUE) {
+ // overflow bin
+ this.binRanges_.push(tr.b.math.Range.fromExplicitRange(
+ prevBoundary, Number.MAX_VALUE));
+ }
+ }
+
+ /**
+ * Add a bin boundary |nextMaxBinBoundary| to the builder.
+ *
+ * This operation effectively corresponds to appending a new central bin
+ * with the range [this.range.max, nextMaxBinBoundary].
+ *
+ * @param {number} nextMaxBinBoundary The added bin boundary (must be
+ * greater than |this.maxMinBoundary|).
+ */
+ addBinBoundary(nextMaxBinBoundary) {
+ if (nextMaxBinBoundary <= this.range.max) {
+ throw new Error('The added max bin boundary must be larger than ' +
+ 'the current max boundary');
+ }
+
+ // If binRanges_ had been built, then clear them.
+ this.binRanges_ = undefined;
+ this.bins_ = undefined;
+
+ this.pushBuilderSlice_(nextMaxBinBoundary);
+ this.range.addValue(nextMaxBinBoundary);
+ return this;
+ }
+
+ /**
+ * Add |binCount| linearly scaled bin boundaries up to |nextMaxBinBoundary|
+ * to the builder.
+ *
+ * This operation corresponds to appending |binCount| central bins of
+ * constant range width
+ * W = ((|nextMaxBinBoundary| - |this.range.max|) / |binCount|)
+ * with the following ranges:
+ *
+ * [|this.maxMinBoundary|, |this.maxMinBoundary| + W]
+ * [|this.maxMinBoundary| + W, |this.maxMinBoundary| + 2W]
+ * [|this.maxMinBoundary| + 2W, |this.maxMinBoundary| + 3W]
+ * ...
+ * [|this.maxMinBoundary| + (|binCount| - 2) * W,
+ * |this.maxMinBoundary| + (|binCount| - 2) * W]
+ * [|this.maxMinBoundary| + (|binCount| - 1) * W,
+ * |nextMaxBinBoundary|]
+ *
+ * @param {number} nextBinBoundary The last added bin boundary (must be
+ * greater than |this.maxMinBoundary|).
+ * @param {number} binCount Number of bins to be added (must be positive).
+ */
+ addLinearBins(nextMaxBinBoundary, binCount) {
+ if (binCount <= 0) {
+ throw new Error('Bin count must be positive');
+ }
+
+ if (nextMaxBinBoundary <= this.range.max) {
+ throw new Error('The new max bin boundary must be greater than ' +
+ 'the previous max bin boundary');
+ }
+
+ // If binRanges_ had been built, then clear them.
+ this.binRanges_ = undefined;
+ this.bins_ = undefined;
+
+ this.pushBuilderSlice_([
+ HistogramBinBoundaries.SLICE_TYPE.LINEAR,
+ nextMaxBinBoundary, binCount]);
+ this.range.addValue(nextMaxBinBoundary);
+ return this;
+ }
+
+ /**
+ * Add |binCount| exponentially scaled bin boundaries up to
+ * |nextMaxBinBoundary| to the builder.
+ *
+ * This operation corresponds to appending |binCount| central bins with
+ * a constant difference between the logarithms of their range min and max
+ * D = ((ln(|nextMaxBinBoundary|) - ln(|this.range.max|)) / |binCount|)
+ * with the following ranges:
+ *
+ * [|this.maxMinBoundary|, |this.maxMinBoundary| * exp(D)]
+ * [|this.maxMinBoundary| * exp(D), |this.maxMinBoundary| * exp(2D)]
+ * [|this.maxMinBoundary| * exp(2D), |this.maxMinBoundary| * exp(3D)]
+ * ...
+ * [|this.maxMinBoundary| * exp((|binCount| - 2) * D),
+ * |this.maxMinBoundary| * exp((|binCount| - 2) * D)]
+ * [|this.maxMinBoundary| * exp((|binCount| - 1) * D),
+ * |nextMaxBinBoundary|]
+ *
+ * This method requires that the current max bin boundary is positive.
+ *
+ * @param {number} nextBinBoundary The last added bin boundary (must be
+ * greater than |this.maxMinBoundary|).
+ * @param {number} binCount Number of bins to be added (must be positive).
+ */
+ addExponentialBins(nextMaxBinBoundary, binCount) {
+ if (binCount <= 0) {
+ throw new Error('Bin count must be positive');
+ }
+ if (this.range.max <= 0) {
+ throw new Error('Current max bin boundary must be positive');
+ }
+ if (this.range.max >= nextMaxBinBoundary) {
+ throw new Error('The last added max boundary must be greater than ' +
+ 'the current max boundary boundary');
+ }
+
+ // If binRanges_ had been built, then clear them.
+ this.binRanges_ = undefined;
+ this.bins_ = undefined;
+
+ this.pushBuilderSlice_([
+ HistogramBinBoundaries.SLICE_TYPE.EXPONENTIAL,
+ nextMaxBinBoundary, binCount]);
+ this.range.addValue(nextMaxBinBoundary);
+ return this;
+ }
+ }
+
+ HistogramBinBoundaries.SLICE_TYPE = {
+ LINEAR: 0,
+ EXPONENTIAL: 1,
+ };
+
+ // This special HistogramBinBoundaries instance produces a singe binRange,
+ // allowing Histograms to have a single bin.
+ // This is the only way for Histograms to have fewer than 2 bins, since
+ // HistogramBinBoundaries.buildBinRanges_() ensures that there is always a bin
+ // whose min is -Number.MAX_VALUE, and a bin whose max is Number.MAX_VALUE.
+ // SINGULAR is the only HistogramBinBoundaries in which those bins are one and
+ // the same.
+ HistogramBinBoundaries.SINGULAR = new HistogramBinBoundaries(
+ Number.MAX_VALUE);
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.timeDurationInMs.unitName,
+ HistogramBinBoundaries.createExponential(1e-3, 1e6, 1e2));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.timeInMsAutoFormat.unitName,
+ new HistogramBinBoundaries(0)
+ .addBinBoundary(1).addExponentialBins(1e3, 3)
+ .addBinBoundary(tr.b.convertUnit(
+ 2, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ 5, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ 10, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ 30, tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ tr.b.UnitScale.TIME.MINUTE.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(2 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.MINUTE.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(5 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.MINUTE.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(10 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.MINUTE.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(30 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.MINUTE.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ tr.b.UnitScale.TIME.HOUR.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(2 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.HOUR.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(6 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.HOUR.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(12 * tr.b.convertUnit(
+ tr.b.UnitScale.TIME.HOUR.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ tr.b.UnitScale.TIME.DAY.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ tr.b.UnitScale.TIME.WEEK.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ tr.b.UnitScale.TIME.MONTH.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC))
+ .addBinBoundary(tr.b.convertUnit(
+ tr.b.UnitScale.TIME.YEAR.value,
+ tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC)));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.timeStampInMs.unitName,
+ HistogramBinBoundaries.createLinear(0, 1e10, 1e3));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.normalizedPercentage.unitName,
+ HistogramBinBoundaries.createLinear(0, 1.0, 20));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.sizeInBytes.unitName,
+ HistogramBinBoundaries.createExponential(1, 1e12, 1e2));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.energyInJoules.unitName,
+ HistogramBinBoundaries.createExponential(1e-3, 1e3, 50));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.powerInWatts.unitName,
+ HistogramBinBoundaries.createExponential(1e-3, 1, 50));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.unitlessNumber.unitName,
+ HistogramBinBoundaries.createExponential(1e-3, 1e3, 50));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.count.unitName,
+ HistogramBinBoundaries.createExponential(1, 1e3, 20));
+
+ DEFAULT_BOUNDARIES_FOR_UNIT.set(
+ tr.b.Unit.byName.sigma.unitName,
+ HistogramBinBoundaries.createLinear(-5, 5, 50));
+
+ return {
+ DEFAULT_REBINNED_COUNT,
+ DELTA,
+ Histogram,
+ HistogramBinBoundaries,
+ P_VALUE_NAME,
+ U_STATISTIC_NAME,
+ Z_SCORE_NAME,
+ percentFromString,
+ percentToString,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram.py b/chromium/third_party/catapult/tracing/tracing/value/histogram.py
new file mode 100644
index 00000000000..272468f6430
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram.py
@@ -0,0 +1,1111 @@
+# Copyright 2017 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 json
+import math
+import numbers
+import random
+
+from tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import diagnostic_ref
+from tracing.value.diagnostics import reserved_infos
+from tracing.value.diagnostics import unmergeable_diagnostic_set
+
+
+try:
+ StringTypes = basestring
+except NameError:
+ StringTypes = str
+
+
+# pylint: disable=too-many-lines
+# TODO(#3613) Split this file.
+
+
+# This should be equal to sys.float_info.max, but that value might differ
+# between platforms, whereas ECMA Script specifies this value for all platforms.
+# The specific value should not matter in normal practice.
+JS_MAX_VALUE = 1.7976931348623157e+308
+
+
+# Converts the given percent to a string in the following format:
+# 0.x produces '0x0',
+# 0.xx produces '0xx',
+# 0.xxy produces '0xx_y',
+# 1.0 produces '100'.
+def PercentToString(percent):
+ if percent < 0 or percent > 1:
+ raise ValueError('percent must be in [0,1]')
+ if percent == 0:
+ return '000'
+ if percent == 1:
+ return '100'
+ s = str(percent)
+ if s[1] != '.':
+ raise ValueError('Unexpected percent')
+ s += '0' * max(4 - len(s), 0)
+ if len(s) > 4:
+ s = s[:4] + '_' + s[4:]
+ return '0' + s[2:]
+
+
+# This variation of binary search returns the index |hi| into |ary| for which
+# callback(ary[hi]) < 0 and callback(ary[hi-1]) >= 0
+# This function assumes that map(callback, ary) is sorted descending.
+def FindHighIndexInSortedArray(ary, callback):
+ lo = 0
+ hi = len(ary)
+ while lo < hi:
+ mid = (lo + hi) >> 1
+ if callback(ary[mid]) >= 0:
+ lo = mid + 1
+ else:
+ hi = mid
+ return hi
+
+
+# Modifies |samples| in-place to reduce its length to |count|, discarding random
+# elements.
+def UniformlySampleArray(samples, count):
+ while len(samples) > count:
+ samples.pop(int(random.uniform(0, len(samples))))
+ return samples
+
+
+# When processing a stream of samples, call this method for each new sample in
+# order to decide whether to keep it in |samples|.
+# Modifies |samples| in-place such that its length never exceeds |num_samples|.
+# After |stream_length| samples have been processed, each sample has equal
+# probability of being retained in |samples|.
+# The order of samples is not preserved after |stream_length| exceeds
+# |num_samples|.
+def UniformlySampleStream(samples, stream_length, new_element, num_samples):
+ if stream_length <= num_samples:
+ if len(samples) >= stream_length:
+ samples[stream_length - 1] = new_element
+ else:
+ samples.append(new_element)
+ return
+
+ prob_keep = num_samples / stream_length
+ if random.random() > prob_keep:
+ # reject new_sample
+ return
+
+ # replace a random element
+ samples[math.floor(random.random() * num_samples)] = new_element
+
+
+# Merge two sets of samples that were assembled using UniformlySampleStream().
+# Modify |a_samples| in-place such that all of the samples in |a_samples| and
+# |b_samples| have equal probability of being retained in |a_samples|.
+def MergeSampledStreams(a_samples, a_stream_length,
+ b_samples, b_stream_length, num_samples):
+ if b_stream_length < num_samples:
+ for i in range(min(b_stream_length, len(b_samples))):
+ UniformlySampleStream(
+ a_samples, a_stream_length + i + 1, b_samples[i], num_samples)
+ return
+
+ if a_stream_length < num_samples:
+ temp_samples = list(b_samples)
+ for i in range(min(a_stream_length, len(a_samples))):
+ UniformlySampleStream(
+ temp_samples, b_stream_length + i + 1, a_samples[i], num_samples)
+ for i, temp_sample in enumerate(temp_samples):
+ a_samples[i] = temp_sample
+ return
+
+ prob_swap = b_stream_length / (a_stream_length + b_stream_length)
+ for i in range(min(num_samples, len(b_samples))):
+ if random.random() < prob_swap:
+ a_samples[i] = b_samples[i]
+
+
+def Percentile(ary, percent):
+ if percent < 0 or percent > 1:
+ raise ValueError('percent must be in [0,1]')
+ ary = list(ary)
+ ary.sort()
+ return ary[int((len(ary) - 1) * percent)]
+
+
+class Range(object):
+ __slots__ = '_empty', '_min', '_max'
+
+ def __init__(self):
+ self._empty = True
+ self._min = None
+ self._max = None
+
+ def __eq__(self, other):
+ if not isinstance(other, Range):
+ return False
+ if self.empty and other.empty:
+ return True
+ if self.empty != other.empty:
+ return False
+ return (self.min == other.min) and (self.max == other.max)
+
+ def __hash__(self):
+ return id(self)
+
+ @staticmethod
+ def FromExplicitRange(lower, upper):
+ r = Range()
+ r._min = lower
+ r._max = upper
+ r._empty = False
+ return r
+
+ @property
+ def empty(self):
+ return self._empty
+
+ @property
+ def min(self):
+ return self._min
+
+ @property
+ def max(self):
+ return self._max
+
+ @property
+ def center(self):
+ return (self._min + self._max) * 0.5
+
+ @property
+ def duration(self):
+ if self.empty:
+ return 0
+ return self._max - self._min
+
+ def AddValue(self, x):
+ if self._empty:
+ self._empty = False
+ self._min = x
+ self._max = x
+ return
+
+ self._max = max(x, self._max)
+ self._min = min(x, self._min)
+
+ def AddRange(self, other):
+ if other.empty:
+ return
+ self.AddValue(other.min)
+ self.AddValue(other.max)
+
+
+# This class computes statistics online in O(1).
+class RunningStatistics(object):
+ __slots__ = (
+ '_count', '_mean', '_max', '_min', '_sum', '_variance', '_meanlogs')
+
+ def __init__(self):
+ self._count = 0
+ self._mean = 0.0
+ self._max = -JS_MAX_VALUE
+ self._min = JS_MAX_VALUE
+ self._sum = 0.0
+ self._variance = 0.0
+ # Mean of logarithms of samples, or None if any samples were <= 0.
+ self._meanlogs = 0.0
+
+ @property
+ def count(self):
+ return self._count
+
+ @property
+ def geometric_mean(self):
+ if self._meanlogs is None:
+ return None
+ return math.exp(self._meanlogs)
+
+ @property
+ def mean(self):
+ if self._count == 0:
+ return None
+ return self._mean
+
+ @property
+ def max(self):
+ return self._max
+
+ @property
+ def min(self):
+ return self._min
+
+ @property
+ def sum(self):
+ return self._sum
+
+ # This returns the variance of the samples after Bessel's correction has
+ # been applied.
+ @property
+ def variance(self):
+ if self.count == 0:
+ return None
+ if self.count == 1:
+ return 0
+ return self._variance / (self.count - 1)
+
+ # This returns the standard deviation of the samples after Bessel's
+ # correction has been applied.
+ @property
+ def stddev(self):
+ if self.count == 0:
+ return None
+ return math.sqrt(self.variance)
+
+ def Add(self, x):
+ self._count += 1
+ x = float(x)
+ self._max = max(self._max, x)
+ self._min = min(self._min, x)
+ self._sum += x
+
+ if x <= 0.0:
+ self._meanlogs = None
+ elif self._meanlogs is not None:
+ self._meanlogs += (math.log(abs(x)) - self._meanlogs) / self.count
+
+ # The following uses Welford's algorithm for computing running mean and
+ # variance. See http://www.johndcook.com/blog/standard_deviation.
+ if self.count == 1:
+ self._mean = x
+ self._variance = 0.0
+ else:
+ old_mean = self._mean
+ old_variance = self._variance
+
+ # Using the 2nd formula for updating the mean yields better precision but
+ # it doesn't work for the case oldMean is Infinity. Hence we handle that
+ # case separately.
+ if abs(old_mean) == float('inf'):
+ self._mean = self._sum / self.count
+ else:
+ self._mean = old_mean + float(x - old_mean) / self.count
+ self._variance = old_variance + (x - old_mean) * (x - self._mean)
+
+ def Merge(self, other):
+ result = RunningStatistics()
+ result._count = self._count + other._count
+ result._sum = self._sum + other._sum
+ result._min = min(self._min, other._min)
+ result._max = max(self._max, other._max)
+ if result._count == 0:
+ result._mean = 0.0
+ result._variance = 0.0
+ result._meanlogs = 0.0
+ else:
+ # Combine the mean and the variance using the formulas from
+ # https://goo.gl/ddcAep.
+ result._mean = float(result._sum) / result._count
+ delta_mean = (self._mean or 0.0) - (other._mean or 0.0)
+ result._variance = self._variance + other._variance + (
+ self._count * other._count * delta_mean * delta_mean / result._count)
+
+ # Merge the arithmetic means of logarithms of absolute values of samples,
+ # weighted by counts.
+ if self._meanlogs is None or other._meanlogs is None:
+ result._meanlogs = None
+ else:
+ result._meanlogs = (self._count * self._meanlogs +
+ other._count * other._meanlogs) / result._count
+ return result
+
+ def AsDict(self):
+ if self._count == 0:
+ return []
+
+ # Javascript automatically converts between ints and floats.
+ # It's more efficient to serialize integers as ints than floats.
+ def FloatAsFloatOrInt(x):
+ if x is not None and x.is_integer():
+ return int(x)
+ return x
+
+ # It's more efficient to serialize these fields in an array. If you add any
+ # other fields, you should re-evaluate whether it would be more efficient to
+ # serialize as a dict.
+ return [
+ self._count,
+ FloatAsFloatOrInt(self._max),
+ FloatAsFloatOrInt(self._meanlogs),
+ FloatAsFloatOrInt(self._mean),
+ FloatAsFloatOrInt(self._min),
+ FloatAsFloatOrInt(self._sum),
+ FloatAsFloatOrInt(self._variance),
+ ]
+
+ @staticmethod
+ def FromDict(dct):
+ result = RunningStatistics()
+ if len(dct) != 7:
+ return result
+
+ def AsFloatOrNone(x):
+ if x is None:
+ return x
+ return float(x)
+ [result._count, result._max, result._meanlogs, result._mean, result._min,
+ result._sum, result._variance] = [int(dct[0])] + [
+ AsFloatOrNone(x) for x in dct[1:]]
+ return result
+
+
+class DiagnosticMap(dict):
+ __slots__ = '_allow_reserved_names',
+
+ def __init__(self, *args, **kwargs):
+ self._allow_reserved_names = True
+ dict.__init__(self, *args, **kwargs)
+
+ def DisallowReservedNames(self):
+ self._allow_reserved_names = False
+
+ def __setitem__(self, name, diag):
+ if not isinstance(name, StringTypes):
+ raise TypeError('name must be string')
+ if not isinstance(diag, (diagnostic.Diagnostic,
+ diagnostic_ref.DiagnosticRef)):
+ raise TypeError('diag must be Diagnostic or DiagnosticRef')
+ if (not self._allow_reserved_names and
+ not isinstance(diag,
+ unmergeable_diagnostic_set.UnmergeableDiagnosticSet) and
+ not isinstance(diag, diagnostic_ref.DiagnosticRef)):
+ expected_type = reserved_infos.GetTypeForName(name)
+ if expected_type and diag.__class__.__name__ != expected_type:
+ raise TypeError('Diagnostics names "%s" must be %s, not %s' %
+ (name, expected_type, diag.__class__.__name__))
+ dict.__setitem__(self, name, diag)
+
+ @staticmethod
+ def FromDict(dct):
+ dm = DiagnosticMap()
+ dm.AddDicts(dct)
+ return dm
+
+ def AddDicts(self, dct):
+ for name, diagnostic_dict in dct.items():
+ if name == 'tagmap':
+ continue
+ if isinstance(diagnostic_dict, StringTypes):
+ self[name] = diagnostic_ref.DiagnosticRef(diagnostic_dict)
+ elif diagnostic_dict['type'] not in [
+ 'RelatedHistogramMap', 'RelatedHistogramBreakdown', 'TagMap']:
+ # Ignore RelatedHistograms and TagMaps.
+ # TODO(benjhayden): Forget about them in 2019 Q2.
+ self[name] = diagnostic.Diagnostic.FromDict(diagnostic_dict)
+
+ def ResolveSharedDiagnostics(self, histograms, required=False):
+ for name, diag in self.items():
+ if not isinstance(diag, diagnostic_ref.DiagnosticRef):
+ continue
+ guid = diag.guid
+ diag = histograms.LookupDiagnostic(guid)
+ if isinstance(diag, diagnostic.Diagnostic):
+ self[name] = diag
+ elif required:
+ raise ValueError('Unable to find shared Diagnostic ' + guid)
+
+ def AsDict(self):
+ dct = {}
+ for name, diag in self.items():
+ dct[name] = diag.AsDictOrReference()
+ return dct
+
+ def Merge(self, other):
+ for name, other_diagnostic in other.items():
+ if name not in self:
+ self[name] = other_diagnostic
+ continue
+ my_diagnostic = self[name]
+ if my_diagnostic.CanAddDiagnostic(other_diagnostic):
+ my_diagnostic.AddDiagnostic(other_diagnostic)
+ continue
+ self[name] = unmergeable_diagnostic_set.UnmergeableDiagnosticSet([
+ my_diagnostic, other_diagnostic])
+
+
+MAX_DIAGNOSTIC_MAPS = 16
+
+
+class HistogramBin(object):
+ __slots__ = '_range', '_count', '_diagnostic_maps'
+
+ def __init__(self, rang):
+ self._range = rang
+ self._count = 0
+ self._diagnostic_maps = []
+
+ def AddSample(self, unused_x):
+ self._count += 1
+
+ @property
+ def count(self):
+ return self._count
+
+ @property
+ def range(self):
+ return self._range
+
+ def AddBin(self, other):
+ self._count += other.count
+
+ @property
+ def diagnostic_maps(self):
+ return self._diagnostic_maps
+
+ def AddDiagnosticMap(self, diagnostics):
+ UniformlySampleStream(
+ self._diagnostic_maps, self.count, diagnostics, MAX_DIAGNOSTIC_MAPS)
+
+ def FromDict(self, dct):
+ self._count = dct[0]
+ if len(dct) > 1:
+ for diagnostic_map_dict in dct[1]:
+ self._diagnostic_maps.append(DiagnosticMap.FromDict(
+ diagnostic_map_dict))
+
+ def AsDict(self):
+ if len(self._diagnostic_maps) == 0:
+ return [self.count]
+ return [self.count, [d.AsDict() for d in self._diagnostic_maps]]
+
+
+# TODO(#3814) Presubmit to compare with unit.html.
+UNIT_NAMES = [
+ 'ms',
+ 'msBestFitFormat',
+ 'tsMs',
+ 'n%',
+ 'sizeInBytes',
+ 'bytesPerSecond',
+ 'J', # Joule
+ 'W', # Watt
+ 'A', # Ampere
+ 'V', # Volt
+ 'Hz', # Hertz
+ 'unitless',
+ 'count',
+ 'sigma',
+]
+
+def ExtendUnitNames():
+ # Use a function in order to avoid cluttering the global namespace with a loop
+ # variable.
+ for name in list(UNIT_NAMES):
+ UNIT_NAMES.append(name + '_biggerIsBetter')
+ UNIT_NAMES.append(name + '_smallerIsBetter')
+
+ExtendUnitNames()
+
+
+class Scalar(object):
+ __slots__ = '_unit', '_value'
+
+ def __init__(self, unit, value):
+ assert unit in UNIT_NAMES, (
+ 'Unrecognized unit "%r"' % unit)
+ self._unit = unit
+ self._value = value
+
+ @property
+ def unit(self):
+ return self._unit
+
+ @property
+ def value(self):
+ return self._value
+
+ def AsDict(self):
+ return {'type': 'scalar', 'unit': self.unit, 'value': self.value}
+
+ @staticmethod
+ def FromDict(dct):
+ return Scalar(dct['unit'], dct['value'])
+
+
+DEFAULT_SUMMARY_OPTIONS = {
+ 'avg': True,
+ 'geometricMean': False,
+ 'std': True,
+ 'count': True,
+ 'sum': True,
+ 'min': True,
+ 'max': True,
+ 'nans': False,
+ # Don't include 'percentile' here. Its default value is [], which is
+ # modifiable. Callers may push to it, so there must be a different Array
+ # instance for each Histogram instance.
+}
+
+
+class Histogram(object):
+ __slots__ = (
+ '_bin_boundaries_dict',
+ '_description',
+ '_name',
+ '_diagnostics',
+ '_nan_diagnostic_maps',
+ '_num_nans',
+ '_running',
+ '_sample_values',
+ '_summary_options',
+ '_unit',
+ '_bins',
+ '_max_num_sample_values')
+
+ def __init__(self, name, unit, bin_boundaries=None):
+ assert unit in UNIT_NAMES, (
+ 'Unrecognized unit "%r"' % unit)
+
+ if bin_boundaries is None:
+ base_unit = unit.split('_')[0]
+ bin_boundaries = DEFAULT_BOUNDARIES_FOR_UNIT[base_unit]
+
+ # Serialize bin boundaries here instead of holding a reference to it in case
+ # it is modified.
+ self._bin_boundaries_dict = bin_boundaries.AsDict()
+
+ # HistogramBinBoundaries creates empty HistogramBins. Save memory by sharing
+ # those empty HistogramBin instances with other Histograms. Wait to copy
+ # HistogramBins until we need to modify it (copy-on-write).
+ self._bins = list(bin_boundaries.bins)
+ self._description = ''
+ self._name = name
+ self._diagnostics = DiagnosticMap()
+ self._diagnostics.DisallowReservedNames()
+ self._nan_diagnostic_maps = []
+ self._num_nans = 0
+ self._running = None
+ self._sample_values = []
+ self._summary_options = dict(DEFAULT_SUMMARY_OPTIONS)
+ self._summary_options['percentile'] = []
+ self._unit = unit
+
+ self._max_num_sample_values = self._GetDefaultMaxNumSampleValues()
+
+ @property
+ def nan_diagnostic_maps(self):
+ return self._nan_diagnostic_maps
+
+ @property
+ def unit(self):
+ return self._unit
+
+ @property
+ def running(self):
+ return self._running
+
+ @property
+ def max_num_sample_values(self):
+ return self._max_num_sample_values
+
+ @max_num_sample_values.setter
+ def max_num_sample_values(self, n):
+ self._max_num_sample_values = n
+ UniformlySampleArray(self._sample_values, self._max_num_sample_values)
+
+ @property
+ def sample_values(self):
+ return self._sample_values
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def bins(self):
+ return self._bins
+
+ @property
+ def diagnostics(self):
+ return self._diagnostics
+
+ @staticmethod
+ def FromDict(dct):
+ boundaries = HistogramBinBoundaries.FromDict(dct.get('binBoundaries'))
+ hist = Histogram(dct['name'], dct['unit'], boundaries)
+ if 'description' in dct:
+ hist._description = dct['description']
+ if 'diagnostics' in dct:
+ hist._diagnostics.AddDicts(dct['diagnostics'])
+ if 'allBins' in dct:
+ if isinstance(dct['allBins'], list):
+ for i, bin_dct in enumerate(dct['allBins']):
+ # Copy HistogramBin on write, share the rest with the other
+ # Histograms that use the same HistogramBinBoundaries.
+ hist._bins[i] = HistogramBin(hist._bins[i].range)
+ hist._bins[i].FromDict(bin_dct)
+ else:
+ for i, bin_dct in dct['allBins'].items():
+ i = int(i)
+ hist._bins[i] = HistogramBin(hist._bins[i].range)
+ hist._bins[i].FromDict(bin_dct)
+ if 'running' in dct:
+ hist._running = RunningStatistics.FromDict(dct['running'])
+ if 'summaryOptions' in dct:
+ hist.CustomizeSummaryOptions(dct['summaryOptions'])
+ if 'maxNumSampleValues' in dct:
+ hist._max_num_sample_values = dct['maxNumSampleValues']
+ if 'sampleValues' in dct:
+ hist._sample_values = dct['sampleValues']
+ if 'numNans' in dct:
+ hist._num_nans = dct['numNans']
+ if 'nanDiagnostics' in dct:
+ for map_dct in dct['nanDiagnostics']:
+ hist._nan_diagnostic_maps.append(DiagnosticMap.FromDict(map_dct))
+ return hist
+
+ @property
+ def num_values(self):
+ if self._running is None:
+ return 0
+ return self._running.count
+
+ @property
+ def num_nans(self):
+ return self._num_nans
+
+ @property
+ def average(self):
+ if self._running is None:
+ return None
+ return self._running.mean
+
+ @property
+ def standard_deviation(self):
+ if self._running is None:
+ return None
+ return self._running.stddev
+
+ @property
+ def geometric_mean(self):
+ if self._running is None:
+ return 0
+ return self._running.geometric_mean
+
+ @property
+ def sum(self):
+ if self._running is None:
+ return 0
+ return self._running.sum
+
+ def GetApproximatePercentile(self, percent):
+ if percent < 0 or percent > 1:
+ raise ValueError('percent must be in [0,1]')
+ if self.num_values == 0:
+ return 0
+
+ if len(self._bins) == 1:
+ sorted_sample_values = list(self._sample_values)
+ sorted_sample_values.sort()
+ return sorted_sample_values[
+ int((len(sorted_sample_values) - 1) * percent)]
+
+ values_to_skip = math.floor((self.num_values - 1) * percent)
+ for hbin in self._bins:
+ values_to_skip -= hbin.count
+ if values_to_skip >= 0:
+ continue
+ if hbin.range.min == -JS_MAX_VALUE:
+ return hbin.range.max
+ elif hbin.range.max == JS_MAX_VALUE:
+ return hbin.range.min
+ else:
+ return hbin.range.center
+ return self._bins[len(self._bins) - 1].range.min
+
+ def GetBinIndexForValue(self, value):
+ index = FindHighIndexInSortedArray(
+ self._bins, lambda b: (-1 if (value < b.range.max) else 1))
+ if 0 <= index < len(self._bins):
+ return index
+ return len(self._bins) - 1
+
+ def GetBinForValue(self, value):
+ return self._bins[self.GetBinIndexForValue(value)]
+
+ def AddSample(self, value, diagnostic_map=None):
+ if (diagnostic_map is not None and
+ not isinstance(diagnostic_map, DiagnosticMap)):
+ diagnostic_map = DiagnosticMap(diagnostic_map)
+
+ if not isinstance(value, numbers.Number) or math.isnan(value):
+ self._num_nans += 1
+ if diagnostic_map:
+ UniformlySampleStream(self._nan_diagnostic_maps, self.num_nans,
+ diagnostic_map, MAX_DIAGNOSTIC_MAPS)
+ else:
+ if self._running is None:
+ self._running = RunningStatistics()
+ self._running.Add(value)
+
+ bin_index = self.GetBinIndexForValue(value)
+ hbin = self._bins[bin_index]
+ if hbin.count == 0:
+ hbin = HistogramBin(hbin.range)
+ self._bins[bin_index] = hbin
+ hbin.AddSample(value)
+ if diagnostic_map:
+ hbin.AddDiagnosticMap(diagnostic_map)
+
+ UniformlySampleStream(self._sample_values, self.num_values + self.num_nans,
+ value, self.max_num_sample_values)
+
+ def CanAddHistogram(self, other):
+ if self.unit != other.unit:
+ return False
+ return self._bin_boundaries_dict == other._bin_boundaries_dict
+
+ def AddHistogram(self, other):
+ if not self.CanAddHistogram(other):
+ raise ValueError('Merging incompatible Histograms')
+
+ MergeSampledStreams(
+ self.sample_values, self.num_values,
+ other.sample_values, other.num_values,
+ (self.max_num_sample_values + other.max_num_sample_values) / 2)
+ self._num_nans += other._num_nans
+
+ if other.running is not None:
+ if self.running is None:
+ self._running = RunningStatistics()
+ self._running = self._running.Merge(other.running)
+
+ for i, hbin in enumerate(other.bins):
+ mybin = self._bins[i]
+ if mybin.count == 0:
+ self._bins[i] = mybin = HistogramBin(mybin.range)
+ mybin.AddBin(hbin)
+
+ self.diagnostics.Merge(other.diagnostics)
+
+ def CustomizeSummaryOptions(self, options):
+ for key, value in options.items():
+ self._summary_options[key] = value
+
+ def Clone(self):
+ return Histogram.FromDict(self.AsDict())
+
+ def CloneEmpty(self):
+ return Histogram(self.name, self.unit, HistogramBinBoundaries.FromDict(
+ self._bin_boundaries_dict))
+
+ @property
+ def statistics_scalars(self):
+ results = {}
+ for stat_name, option in self._summary_options.items():
+ if not option:
+ continue
+ if stat_name == 'percentile':
+ for percent in option:
+ percentile = self.GetApproximatePercentile(percent)
+ results['pct_' + PercentToString(percent)] = Scalar(
+ self.unit, percentile)
+ elif stat_name == 'nans':
+ results['nans'] = Scalar('count', self.num_nans)
+ else:
+ if stat_name == 'count':
+ stat_unit = 'count'
+ else:
+ stat_unit = self.unit
+ if stat_name == 'std':
+ key = 'stddev'
+ elif stat_name == 'avg':
+ key = 'mean'
+ elif stat_name == 'geometricMean':
+ key = 'geometric_mean'
+ else:
+ key = stat_name
+ if self._running is None:
+ self._running = RunningStatistics()
+ stat_value = getattr(self._running, key)
+ if isinstance(stat_value, numbers.Number):
+ results[stat_name] = Scalar(stat_unit, stat_value)
+ return results
+
+ def AsDict(self):
+ dct = {'name': self.name, 'unit': self.unit}
+ if self._bin_boundaries_dict is not None:
+ dct['binBoundaries'] = self._bin_boundaries_dict
+ if self._description:
+ dct['description'] = self._description
+ if len(self.diagnostics):
+ dct['diagnostics'] = self.diagnostics.AsDict()
+ if self.max_num_sample_values != self._GetDefaultMaxNumSampleValues():
+ dct['maxNumSampleValues'] = self.max_num_sample_values
+ if self.num_nans:
+ dct['numNans'] = self.num_nans
+ if len(self.nan_diagnostic_maps):
+ dct['nanDiagnostics'] = [m.AsDict() for m in self.nan_diagnostic_maps]
+ if self.num_values:
+ dct['sampleValues'] = list(self.sample_values)
+ dct['running'] = self._running.AsDict()
+ dct['allBins'] = self._GetAllBinsAsDict()
+ if dct['allBins'] is None:
+ del dct['allBins']
+
+ summary_options = {}
+ any_overridden_summary_options = False
+ for name, option in self._summary_options.items():
+ if name == 'percentile':
+ if len(option) == 0:
+ continue
+ elif option == DEFAULT_SUMMARY_OPTIONS[name]:
+ continue
+ summary_options[name] = option
+ any_overridden_summary_options = True
+ if any_overridden_summary_options:
+ dct['summaryOptions'] = summary_options
+ return dct
+
+ def _GetAllBinsAsDict(self):
+ num_bins = len(self._bins)
+ empty_bins = 0
+ for hbin in self._bins:
+ if hbin.count == 0:
+ empty_bins += 1
+ if empty_bins == num_bins:
+ return None
+
+ if empty_bins > (num_bins / 2):
+ all_bins_dict = {}
+ for i, hbin in enumerate(self._bins):
+ if hbin.count > 0:
+ all_bins_dict[i] = hbin.AsDict()
+ return all_bins_dict
+
+ all_bins_list = []
+ for hbin in self._bins:
+ all_bins_list.append(hbin.AsDict())
+ return all_bins_list
+
+ def _GetDefaultMaxNumSampleValues(self):
+ return len(self._bins) * 10
+
+
+class HistogramBinBoundaries(object):
+ __slots__ = '_builder', '_range', '_bin_ranges', '_bins'
+
+ CACHE = {}
+ SLICE_TYPE_LINEAR = 0
+ SLICE_TYPE_EXPONENTIAL = 1
+
+ def __init__(self, min_bin_boundary):
+ self._builder = [min_bin_boundary]
+ self._range = Range()
+ self._range.AddValue(min_bin_boundary)
+ self._bin_ranges = None
+ self._bins = None
+
+ @property
+ def range(self):
+ return self._range
+
+ @staticmethod
+ def FromDict(dct):
+ if dct is None:
+ return HistogramBinBoundaries.SINGULAR
+
+ cache_key = json.dumps(dct)
+ if cache_key in HistogramBinBoundaries.CACHE:
+ return HistogramBinBoundaries.CACHE[cache_key]
+
+ bin_boundaries = HistogramBinBoundaries(dct[0])
+ for slic in dct[1:]:
+ if not isinstance(slic, list):
+ bin_boundaries.AddBinBoundary(slic)
+ continue
+ if slic[0] == HistogramBinBoundaries.SLICE_TYPE_LINEAR:
+ bin_boundaries.AddLinearBins(slic[1], slic[2])
+ elif slic[0] == HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL:
+ bin_boundaries.AddExponentialBins(slic[1], slic[2])
+ else:
+ raise ValueError('Unrecognized HistogramBinBoundaries slice type')
+
+ HistogramBinBoundaries.CACHE[cache_key] = bin_boundaries
+ return bin_boundaries
+
+ def AsDict(self):
+ if len(self._builder) == 1 and self._builder[0] == JS_MAX_VALUE:
+ return None
+ return self._builder
+
+ @staticmethod
+ def CreateExponential(lower, upper, num_bins):
+ return HistogramBinBoundaries(lower).AddExponentialBins(upper, num_bins)
+
+ @staticmethod
+ def CreateLinear(lower, upper, num_bins):
+ return HistogramBinBoundaries(lower).AddLinearBins(upper, num_bins)
+
+ def _PushBuilderSlice(self, slic):
+ self._builder += [slic]
+
+ def AddBinBoundary(self, next_max_bin_boundary):
+ if next_max_bin_boundary <= self.range.max:
+ raise ValueError('The added max bin boundary must be larger than ' +
+ 'the current max boundary')
+
+ self._bin_ranges = None
+ self._bins = None
+
+ self._PushBuilderSlice(next_max_bin_boundary)
+ self.range.AddValue(next_max_bin_boundary)
+ return self
+
+ def AddLinearBins(self, next_max_bin_boundary, bin_count):
+ if bin_count <= 0:
+ raise ValueError('Bin count must be positive')
+ if next_max_bin_boundary <= self.range.max:
+ raise ValueError('The new max bin boundary must be greater than ' +
+ 'the previous max bin boundary')
+
+ self._bin_ranges = None
+ self._bins = None
+
+ self._PushBuilderSlice([
+ HistogramBinBoundaries.SLICE_TYPE_LINEAR,
+ next_max_bin_boundary, bin_count])
+ self.range.AddValue(next_max_bin_boundary)
+ return self
+
+ def AddExponentialBins(self, next_max_bin_boundary, bin_count):
+ if bin_count <= 0:
+ raise ValueError('Bin count must be positive')
+ if self.range.max <= 0:
+ raise ValueError('Current max bin boundary must be positive')
+ if self.range.max >= next_max_bin_boundary:
+ raise ValueError('The last added max boundary must be greater than ' +
+ 'the current max boundary boundary')
+
+ self._bin_ranges = None
+ self._bins = None
+
+ self._PushBuilderSlice([
+ HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL,
+ next_max_bin_boundary, bin_count])
+ self.range.AddValue(next_max_bin_boundary)
+ return self
+
+ @property
+ def bins(self):
+ if self._bins is None:
+ self._BuildBins()
+ return self._bins
+
+ def _BuildBins(self):
+ self._bins = [HistogramBin(r) for r in self.bin_ranges]
+
+ @property
+ def bin_ranges(self):
+ if self._bin_ranges is None:
+ self._BuildBinRanges()
+ return self._bin_ranges
+
+ def _BuildBinRanges(self):
+ if not isinstance(self._builder[0], numbers.Number):
+ raise ValueError('Invalid start of builder_')
+
+ self._bin_ranges = []
+ prev_boundary = self._builder[0]
+ if prev_boundary > -JS_MAX_VALUE:
+ # underflow bin
+ self._bin_ranges.append(Range.FromExplicitRange(
+ -JS_MAX_VALUE, prev_boundary))
+
+ for slic in self._builder[1:]:
+ if not isinstance(slic, list):
+ self._bin_ranges.append(Range.FromExplicitRange(
+ prev_boundary, slic))
+ prev_boundary = slic
+ continue
+
+ next_max_bin_boundary = float(slic[1])
+ bin_count = slic[2]
+ slice_min_bin_boundary = float(prev_boundary)
+
+ if slic[0] == self.SLICE_TYPE_LINEAR:
+ bin_width = (next_max_bin_boundary - prev_boundary) / bin_count
+ for i in range(1, bin_count):
+ boundary = slice_min_bin_boundary + (i * bin_width)
+ self._bin_ranges.append(Range.FromExplicitRange(
+ prev_boundary, boundary))
+ prev_boundary = boundary
+ elif slic[0] == self.SLICE_TYPE_EXPONENTIAL:
+ bin_exponent_width = (
+ math.log(next_max_bin_boundary / prev_boundary) / bin_count)
+ for i in range(1, bin_count):
+ boundary = slice_min_bin_boundary * math.exp(i * bin_exponent_width)
+ self._bin_ranges.append(Range.FromExplicitRange(
+ prev_boundary, boundary))
+ prev_boundary = boundary
+ else:
+ raise ValueError('Unrecognized HistogramBinBoundaries slice type')
+
+ self._bin_ranges.append(Range.FromExplicitRange(
+ prev_boundary, next_max_bin_boundary))
+ prev_boundary = next_max_bin_boundary
+
+ if prev_boundary < JS_MAX_VALUE:
+ # overflow bin
+ self._bin_ranges.append(Range.FromExplicitRange(
+ prev_boundary, JS_MAX_VALUE))
+
+
+HistogramBinBoundaries.SINGULAR = HistogramBinBoundaries(JS_MAX_VALUE)
+
+
+# The JS version computes these values using tr.b.convertUnit, which is
+# not implemented in Python, so we write them out here.
+def _CreateMsAutoFormatBins():
+ bins = [
+ 2000,
+ 5000,
+ 10000,
+ 30000,
+ 60000,
+ 120000,
+ 300000,
+ 600000,
+ 1800000,
+ 3600000,
+ 7200000,
+ 21600000,
+ 43200000,
+ 86400000,
+ 604800000,
+ 2629743840,
+ 31556926080
+ ]
+
+ boundaries = HistogramBinBoundaries(0).AddBinBoundary(1).AddExponentialBins(
+ 1e3, 3)
+
+ for b in bins:
+ boundaries.AddBinBoundary(b)
+
+ return boundaries
+
+
+DEFAULT_BOUNDARIES_FOR_UNIT = {
+ 'ms': HistogramBinBoundaries.CreateExponential(1e-3, 1e6, 100),
+ 'tsMs': HistogramBinBoundaries.CreateLinear(0, 1e10, 1000),
+ 'msBestFitFormat': _CreateMsAutoFormatBins(),
+ 'n%': HistogramBinBoundaries.CreateLinear(0, 1.0, 20),
+ 'sizeInBytes': HistogramBinBoundaries.CreateExponential(1, 1e12, 100),
+ 'bytesPerSecond': HistogramBinBoundaries.CreateExponential(1, 1e12, 100),
+ 'J': HistogramBinBoundaries.CreateExponential(1e-3, 1e3, 50),
+ 'W': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50),
+ 'A': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50),
+ 'V': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50),
+ 'Hz': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50),
+ 'unitless': HistogramBinBoundaries.CreateExponential(1e-3, 1e3, 50),
+ 'count': HistogramBinBoundaries.CreateExponential(1, 1e3, 20),
+ 'sigma': HistogramBinBoundaries.CreateLinear(-5, 5, 50),
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html
new file mode 100644
index 00000000000..9a61aa19510
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.v', function() {
+ /*
+ * HistogramGrouping objects are registered named functions that map from
+ * Histogram objects to strings.
+ *
+ * They are used to group Histograms by
+ * tr.v.HistogramSet.groupHistogramsRecursively()
+ *
+ * The tr-ui-b-grouping-table-groupby-picker module within the
+ * tr-v-ui-histogram-set-controls module allows users to select and reorder
+ * groupings.
+ */
+ class HistogramGrouping {
+ /**
+ * @param {string} key
+ * @param {!function(!tr.v.Histogram):string} callback
+ */
+ 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;
+ }
+
+ /**
+ * @param {!Set.<string>} tags
+ * @param {string} diagnosticName
+ * @return {!Array.<!HistogramGrouping>}
+ */
+ static buildFromTags(tags, diagnosticName) {
+ const booleanTags = new Set();
+ const keyValueTags = new Set();
+ for (const tag of tags) {
+ if (tag.includes(':')) {
+ const key = tag.split(':')[0];
+ if (booleanTags.has(key)) {
+ throw new Error(
+ `Tag "${key}" cannot be both boolean and key-value`);
+ }
+ keyValueTags.add(key);
+ } else {
+ if (keyValueTags.has(tag)) {
+ throw new Error(
+ `Tag "${tag}" cannot be both boolean and key-value`);
+ }
+ booleanTags.add(tag);
+ }
+ }
+
+ const groupings = [];
+ for (const tag of booleanTags) {
+ groupings.push(HistogramGrouping.buildBooleanTagGrouping_(
+ tag, diagnosticName));
+ }
+ for (const tag of keyValueTags) {
+ groupings.push(HistogramGrouping.buildKeyValueTagGrouping_(
+ tag, diagnosticName));
+ }
+ return groupings;
+ }
+
+ static buildBooleanTagGrouping_(tag, diagnosticName) {
+ return new HistogramGrouping(`${tag}Tag`, h => {
+ const tags = h.diagnostics.get(diagnosticName);
+ if (tags === undefined || !tags.has(tag)) return `~${tag}`;
+ return tag;
+ });
+ }
+
+ static buildKeyValueTagGrouping_(tag, diagnosticName) {
+ return new HistogramGrouping(`${tag}Tag`, h => {
+ const tags = h.diagnostics.get(diagnosticName);
+ if (tags === undefined) return `~${tag}`;
+ const values = new Set();
+ for (const value of tags) {
+ const kvp = value.split(':');
+ if (kvp.length < 2 || kvp[0] !== tag) continue;
+ values.add(kvp[1]);
+ }
+ if (values.size === 0) return `~${tag}`;
+ const sortedValues = Array.from(values);
+ sortedValues.sort();
+ return sortedValues.join(',');
+ }, `${tag} tag`);
+ }
+ }
+
+ HistogramGrouping.BY_KEY = new Map();
+
+ HistogramGrouping.HISTOGRAM_NAME = new HistogramGrouping('name', h => h.name);
+
+ HistogramGrouping.DISPLAY_LABEL = new HistogramGrouping(
+ 'displayLabel', hist => {
+ const labels = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.LABELS);
+ if (labels !== undefined && labels.size > 0) {
+ return Array.from(labels).join(',');
+ }
+
+ const benchmarks = hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARKS);
+ const start = hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START);
+ if (benchmarks === undefined) {
+ if (start === undefined) return 'Value';
+
+ return start.toString();
+ }
+ const benchmarksStr = Array.from(benchmarks).join('\n');
+
+ if (start === undefined) return benchmarksStr;
+
+ return benchmarksStr + '\n' + start.toString();
+ });
+
+ class GenericSetGrouping 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 '';
+ const parts = Array.from(diag);
+ parts.sort();
+ return parts.join(',');
+ }
+ }
+
+ GenericSetGrouping.NAMES = [
+ tr.v.d.RESERVED_NAMES.ARCHITECTURES,
+ tr.v.d.RESERVED_NAMES.BENCHMARKS,
+ tr.v.d.RESERVED_NAMES.BOTS,
+ tr.v.d.RESERVED_NAMES.BUILDS,
+ tr.v.d.RESERVED_NAMES.DEVICE_IDS,
+ tr.v.d.RESERVED_NAMES.MASTERS,
+ tr.v.d.RESERVED_NAMES.MEMORY_AMOUNTS,
+ tr.v.d.RESERVED_NAMES.OS_NAMES,
+ tr.v.d.RESERVED_NAMES.OS_VERSIONS,
+ tr.v.d.RESERVED_NAMES.PRODUCT_VERSIONS,
+ tr.v.d.RESERVED_NAMES.STORIES,
+ tr.v.d.RESERVED_NAMES.STORYSET_REPEATS,
+ tr.v.d.RESERVED_NAMES.STORY_TAGS,
+ tr.v.d.RESERVED_NAMES.TEST_PATH,
+ ];
+
+ for (const name of GenericSetGrouping.NAMES) {
+ // Instantiating a HistogramGrouping adds it to BY_KEY.
+ new GenericSetGrouping(name);
+ }
+
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py
new file mode 100644
index 00000000000..49eab313d49
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping.py
@@ -0,0 +1,161 @@
+# Copyright 2017 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 tracing.value.diagnostics import reserved_infos
+
+
+GROUPINGS_BY_KEY = {}
+
+
+class HistogramGrouping(object):
+ """This class wraps a registered function that maps from a Histogram to a
+ string or number in order to allow grouping together Histograms that produce
+ the same string or number. HistogramGroupings may be looked up by key in
+ GROUPINGS_BY_KEY.
+ """
+
+ def __init__(self, key, callback):
+ self._key = key
+ self._callback = callback
+ GROUPINGS_BY_KEY[key] = self
+
+ @property
+ def key(self):
+ return self._key
+
+ @property
+ def callback(self):
+ return self._callback
+
+
+def BuildFromTags(tags, diagnostic_name):
+ """Builds HistogramGroupings from a set of tags.
+
+ Builds one HistogramGrouping for each tag in tags. The HistogramGroupings wrap
+ functions (callback) that get the named diagnostic from the given Histogram.
+ If the named diagnostic is found, it is assumed to be a GenericSet containing
+ strings. The HistogramGrouping callback returns a string indicating whether
+ the tag is in the GenericSet, and, if it is a key-value tag, what its values
+ are.
+
+ Args:
+ tags: set of strings
+ diagnostic_name: string, e.g. reserved_infos.STORY_TAGS.name
+
+ Returns:
+ list of HistogramGrouping
+ """
+ boolean_tags = set()
+ key_value_tags = set()
+ for tag in tags:
+ if ':' in tag:
+ key = tag.split(':')[0]
+ assert key not in boolean_tags, key
+ key_value_tags.add(key)
+ else:
+ assert tag not in key_value_tags, tag
+ boolean_tags.add(tag)
+ groupings = [
+ _BuildBooleanTagGrouping(tag, diagnostic_name) for tag in boolean_tags]
+ groupings += [
+ _BuildKeyValueTagGrouping(key, diagnostic_name) for key in key_value_tags]
+ return groupings
+
+
+def _BuildBooleanTagGrouping(tag, diagnostic_name):
+ def Closure(hist):
+ tags = hist.diagnostics.get(diagnostic_name)
+ if not tags or tag not in tags:
+ return '~' + tag
+ return tag
+ return HistogramGrouping(tag + 'Tag', Closure)
+
+
+def _BuildKeyValueTagGrouping(key, diagnostic_name):
+ def Closure(hist):
+ tags = hist.diagnostics.get(diagnostic_name)
+ if not tags:
+ return '~' + key
+ values = set()
+ for tag in tags:
+ kvp = tag.split(':')
+ if len(kvp) < 2 or kvp[0] != key:
+ continue
+ values.add(kvp[1])
+ if len(values) == 0:
+ return '~' + key
+ return ','.join(sorted(values))
+ return HistogramGrouping(key + 'Tag', Closure)
+
+
+HISTOGRAM_NAME = HistogramGrouping('name', lambda h: h.name)
+
+
+def _DisplayLabel(hist):
+ labels = hist.diagnostics.get(reserved_infos.LABELS.name)
+ if labels and len(labels):
+ return ','.join(sorted(labels))
+
+ benchmarks = hist.diagnostics.get(reserved_infos.BENCHMARKS.name)
+ start = hist.diagnostics.get(reserved_infos.BENCHMARK_START.name)
+ if not benchmarks:
+ if not start:
+ return 'Value'
+ return str(start)
+ benchmarks = '\n'.join(benchmarks)
+ if not start:
+ return benchmarks
+ return benchmarks + '\n' + str(start)
+
+
+DISPLAY_LABEL = HistogramGrouping('displayLabel', _DisplayLabel)
+
+
+class GenericSetGrouping(HistogramGrouping):
+ """Wraps a function that looks up and formats a GenericSet by name from a
+ Histogram.
+ """
+
+ def __init__(self, name):
+ super(GenericSetGrouping, self).__init__(name, self._Compute)
+
+ def _Compute(self, hist):
+ diag = hist.diagnostics.get(self.key)
+ if not diag:
+ return ''
+ return ','.join(str(elem) for elem in sorted(diag))
+
+
+GenericSetGrouping(reserved_infos.ARCHITECTURES.name)
+GenericSetGrouping(reserved_infos.BENCHMARKS.name)
+GenericSetGrouping(reserved_infos.BOTS.name)
+GenericSetGrouping(reserved_infos.BUILDS.name)
+GenericSetGrouping(reserved_infos.DEVICE_IDS.name)
+GenericSetGrouping(reserved_infos.MASTERS.name)
+GenericSetGrouping(reserved_infos.MEMORY_AMOUNTS.name)
+GenericSetGrouping(reserved_infos.OS_NAMES.name)
+GenericSetGrouping(reserved_infos.OS_VERSIONS.name)
+GenericSetGrouping(reserved_infos.PRODUCT_VERSIONS.name)
+GenericSetGrouping(reserved_infos.STORIES.name)
+GenericSetGrouping(reserved_infos.STORYSET_REPEATS.name)
+GenericSetGrouping(reserved_infos.STORY_TAGS.name)
+
+
+class DateRangeGrouping(HistogramGrouping):
+ """Wraps a function that looks up and formats a DateRange by name from a
+ Histogram.
+ """
+
+ def __init__(self, name):
+ super(DateRangeGrouping, self).__init__(name, self._Compute)
+
+ def _Compute(self, hist):
+ diag = hist.diagnostics.get(self.key)
+ if not diag:
+ return ''
+ return str(diag)
+
+
+DateRangeGrouping(reserved_infos.BENCHMARK_START.name)
+DateRangeGrouping(reserved_infos.TRACE_START.name)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html
new file mode 100644
index 00000000000..622313b54b0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_test.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('booleanTags', function() {
+ const aHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(
+ ['video', 'audio']),
+ ]]),
+ });
+ const bHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['audio']),
+ ]]),
+ });
+ const cHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['video']),
+ ]]),
+ });
+ const dHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet([]),
+ ]]),
+ });
+
+ const groupings = tr.v.HistogramGrouping.buildFromTags(
+ ['video', 'audio'], tr.v.d.RESERVED_NAMES.STORY_TAGS);
+ assert.lengthOf(groupings, 2);
+ assert.strictEqual(groupings[0].key, 'videoTag');
+ assert.strictEqual(groupings[1].key, 'audioTag');
+ assert.strictEqual(groupings[0].callback(aHist), 'video');
+ assert.strictEqual(groupings[0].callback(bHist), '~video');
+ assert.strictEqual(groupings[0].callback(cHist), 'video');
+ assert.strictEqual(groupings[0].callback(dHist), '~video');
+ assert.strictEqual(groupings[1].callback(aHist), 'audio');
+ assert.strictEqual(groupings[1].callback(bHist), 'audio');
+ assert.strictEqual(groupings[1].callback(cHist), '~audio');
+ assert.strictEqual(groupings[1].callback(dHist), '~audio');
+ });
+
+ test('keyValueTags', function() {
+ const aHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['case:load']),
+ ]]),
+ });
+ const bHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(
+ ['case:browse']),
+ ]]),
+ });
+ const cHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet([]),
+ ]]),
+ });
+ const dHist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(
+ ['case:load', 'case:browse']),
+ ]]),
+ });
+
+ const groupings = tr.v.HistogramGrouping.buildFromTags(
+ ['case:load', 'case:browse'], tr.v.d.RESERVED_NAMES.STORY_TAGS);
+ assert.lengthOf(groupings, 1);
+ assert.strictEqual(groupings[0].key, 'caseTag');
+ assert.strictEqual(groupings[0].callback(aHist), 'load');
+ assert.strictEqual(groupings[0].callback(bHist), 'browse');
+ assert.strictEqual(groupings[0].callback(cHist), '~case');
+ assert.strictEqual(groupings[0].callback(dHist), 'browse,load');
+ });
+
+ test('histogramNameGrouping', function() {
+ const hist = tr.v.Histogram.create('name', tr.b.Unit.byName.count, []);
+ assert.strictEqual(tr.v.HistogramGrouping.HISTOGRAM_NAME.callback(hist),
+ 'name');
+ });
+
+ test('labelGrouping', function() {
+ const hist = tr.v.Histogram.create('name', tr.b.Unit.byName.count, []);
+ assert.strictEqual(tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist),
+ 'Value');
+ hist.diagnostics.set(tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['H']));
+ assert.strictEqual(tr.v.HistogramGrouping.DISPLAY_LABEL.callback(hist),
+ 'H');
+ });
+
+ test('genericSetGrouping', function() {
+ const grouping = new tr.v.GenericSetGrouping('foo');
+
+ const empty = tr.v.Histogram.create('', tr.b.Unit.byName.count, []);
+ assert.strictEqual(grouping.callback(empty), '');
+
+ const hist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([
+ ['foo', new tr.v.d.GenericSet(['baz', 'bar'])],
+ ]),
+ });
+ assert.strictEqual(grouping.callback(hist), 'bar,baz');
+ });
+
+ test('reservedGenericSetGroupings', function() {
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.ARCHITECTURES), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARKS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BOTS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BUILDS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.MASTERS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.MEMORY_AMOUNTS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.OS_NAMES), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.OS_VERSIONS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.PRODUCT_VERSIONS), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORIES), tr.v.GenericSetGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.STORYSET_REPEATS), tr.v.GenericSetGrouping);
+ });
+
+ test('dateRangeGrouping', function() {
+ const grouping = new tr.v.DateRangeGrouping('foo');
+
+ const empty = tr.v.Histogram.create('', tr.b.Unit.byName.count, []);
+ assert.strictEqual(grouping.callback(empty), '');
+
+ const hist = tr.v.Histogram.create('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([
+ ['foo', new tr.v.d.DateRange(15e11)],
+ ]),
+ });
+ assert.strictEqual(grouping.callback(hist),
+ tr.b.formatDate(new Date(15e11)));
+ });
+
+ test('reservedDateRangeGroupings', function() {
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START), tr.v.DateRangeGrouping);
+ assert.instanceOf(tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.TRACE_START), tr.v.DateRangeGrouping);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py
new file mode 100644
index 00000000000..3050121e970
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_grouping_unittest.py
@@ -0,0 +1,127 @@
+# Copyright 2017 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 tracing.value import histogram
+from tracing.value import histogram_grouping
+from tracing.value.diagnostics import date_range
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+
+class HistogramGroupingUnittest(unittest.TestCase):
+
+ def testBooleanTags(self):
+ a_hist = histogram.Histogram('', 'count')
+ a_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ ['video', 'audio'])
+ b_hist = histogram.Histogram('', 'count')
+ b_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ ['audio'])
+ c_hist = histogram.Histogram('', 'count')
+ c_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ ['video'])
+ d_hist = histogram.Histogram('', 'count')
+ d_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ [])
+ groupings = histogram_grouping.BuildFromTags(
+ ['audio', 'video'], reserved_infos.STORY_TAGS.name)
+ self.assertEqual(len(groupings), 2)
+ groupings.sort(key=lambda g: g.key)
+ self.assertEqual(groupings[0].key, 'audioTag')
+ self.assertEqual(groupings[1].key, 'videoTag')
+ self.assertEqual(groupings[0].callback(a_hist), 'audio')
+ self.assertEqual(groupings[0].callback(b_hist), 'audio')
+ self.assertEqual(groupings[0].callback(c_hist), '~audio')
+ self.assertEqual(groupings[0].callback(d_hist), '~audio')
+ self.assertEqual(groupings[1].callback(a_hist), 'video')
+ self.assertEqual(groupings[1].callback(b_hist), '~video')
+ self.assertEqual(groupings[1].callback(c_hist), 'video')
+ self.assertEqual(groupings[1].callback(d_hist), '~video')
+
+ def testKeyValueTags(self):
+ a_hist = histogram.Histogram('', 'count')
+ a_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ ['case:load'])
+ b_hist = histogram.Histogram('', 'count')
+ b_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ ['case:browse'])
+ c_hist = histogram.Histogram('', 'count')
+ c_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ [])
+ d_hist = histogram.Histogram('', 'count')
+ d_hist.diagnostics[reserved_infos.STORY_TAGS.name] = generic_set.GenericSet(
+ ['case:load', 'case:browse'])
+ groupings = histogram_grouping.BuildFromTags(
+ ['case:load', 'case:browse'], reserved_infos.STORY_TAGS.name)
+ self.assertEqual(len(groupings), 1)
+ self.assertEqual(groupings[0].key, 'caseTag')
+ self.assertEqual(groupings[0].callback(a_hist), 'load')
+ self.assertEqual(groupings[0].callback(b_hist), 'browse')
+ self.assertEqual(groupings[0].callback(c_hist), '~case')
+ self.assertEqual(groupings[0].callback(d_hist), 'browse,load')
+
+ def testName(self):
+ self.assertEqual(histogram_grouping.HISTOGRAM_NAME.callback(
+ histogram.Histogram('test', 'count')), 'test')
+
+ def testDisplayLabel(self):
+ hist = histogram.Histogram('test', 'count')
+ self.assertEqual(histogram_grouping.DISPLAY_LABEL.callback(hist), 'Value')
+ hist.diagnostics[reserved_infos.LABELS.name] = generic_set.GenericSet(['H'])
+ self.assertEqual(histogram_grouping.DISPLAY_LABEL.callback(hist), 'H')
+
+ def testGenericSet(self):
+ grouping = histogram_grouping.GenericSetGrouping('foo')
+ hist = histogram.Histogram('', 'count')
+ self.assertEqual(grouping.callback(hist), '')
+ hist.diagnostics['foo'] = generic_set.GenericSet(['baz'])
+ self.assertEqual(grouping.callback(hist), 'baz')
+ hist.diagnostics['foo'] = generic_set.GenericSet(['baz', 'bar'])
+ self.assertEqual(grouping.callback(hist), 'bar,baz')
+
+ def testReservedGenericSetGroupings(self):
+ self.assertIsInstance(
+ histogram_grouping.GROUPINGS_BY_KEY[reserved_infos.ARCHITECTURES.name],
+ histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.BENCHMARKS.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.BOTS.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.BUILDS.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.MASTERS.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(
+ histogram_grouping.GROUPINGS_BY_KEY[reserved_infos.MEMORY_AMOUNTS.name],
+ histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.OS_NAMES.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.OS_VERSIONS.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(
+ histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.PRODUCT_VERSIONS.name],
+ histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.STORIES.name], histogram_grouping.GenericSetGrouping)
+ self.assertIsInstance(
+ histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.STORYSET_REPEATS.name],
+ histogram_grouping.GenericSetGrouping)
+
+ def testDateRange(self):
+ grouping = histogram_grouping.DateRangeGrouping('foo')
+ hist = histogram.Histogram('', 'count')
+ self.assertEqual(grouping.callback(hist), '')
+ hist.diagnostics['foo'] = date_range.DateRange(15e11)
+ self.assertEqual(grouping.callback(hist), str(hist.diagnostics['foo']))
+
+ def testReservedDateRangeGroupings(self):
+ self.assertIsInstance(
+ histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.BENCHMARK_START.name],
+ histogram_grouping.DateRangeGrouping)
+ self.assertIsInstance(histogram_grouping.GROUPINGS_BY_KEY[
+ reserved_infos.TRACE_START.name], histogram_grouping.DateRangeGrouping)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html
new file mode 100644
index 00000000000..3a8c3356914
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_importer.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v', function() {
+ class HistogramImporter {
+ /**
+ * @param {!Element} loadingEl
+ */
+ constructor(loadingEl) {
+ this.loadingEl_ = loadingEl;
+ this.histograms_ = undefined;
+ this.string_ = '';
+ this.dataOffset_ = 0;
+ this.view_ = undefined;
+ this.fmpMark_ = tr.b.Timing.mark('HistogramImporter', 'fmp');
+
+ this.loadingEl_.textContent = 'Parsing HTML...';
+ // The json comment appears after this script tag in results.html, so the
+ // browser will parse them into DOM now.
+ }
+
+ /**
+ * @param {string} message
+ * @return {Promise} resolves when |message| is displayed.
+ */
+ async update_(message) {
+ this.loadingEl_.textContent = message;
+ // Use rAF callbacks only if the document is visible. If the document is
+ // hidden, then the user-agent can stop triggering the rAF callbacks. So
+ // avoid rAF callbacks when hidden.
+ if (window.document.visibilityState === 'visible') {
+ await tr.b.animationFrame();
+ }
+ }
+
+ /**
+ * The string contains a list of histograms of the following form:
+ * JSON\n
+ * JSON\n
+ * ...
+ * The |view| should have 'display:none' so that it doesn't obnoxiously
+ * display "zero Histograms" while they are being imported.
+ *
+ * @param {!String} string
+ * @param {!Element} view A histogram-set-view.
+ * @return {Promise} resolves when |view| is displayed.
+ */
+ async importHistograms(string, view) {
+ this.histograms_ = new tr.v.HistogramSet();
+ this.string_ = string;
+ this.view_ = view;
+ tr.b.Timing.instant('HistogramImporter', 'string', this.string_.length);
+
+ if (this.string_.length > 0) {
+ await this.update_('Loading Histogram 0');
+ const loadMark = tr.b.Timing.mark(
+ 'HistogramImporter', 'loadHistograms');
+ if (!this.findDataStart_()) return;
+ await this.loadSomeHistograms_();
+ loadMark.end();
+ tr.b.Timing.instant('HistogramImporter', 'nsPerJson',
+ parseInt(1e3 * loadMark.durationMs / this.histograms_.length));
+ }
+
+ await this.update_('Displaying Histogram table...');
+ await this.displayHistograms_();
+ }
+
+ findDataStart_() {
+ // Find the initial data start.
+ this.dataOffset_ = this.string_.indexOf('\n', this.dataOffset_);
+ if (this.dataOffset_ < 0) return false;
+ // Skip over newline character.
+ this.dataOffset_++;
+ return true;
+ }
+
+ async loadSomeHistograms_() {
+ // Don't spend so long on this chunk of Histograms that the user gets
+ // frustrated, but also don't call requestAnimationFrame faster than every
+ // 16ms, so that the browser doesn't have to wait for the next vsync.
+ // Powerful computers can load several hundred Histograms in 32ms.
+ // Also don't call performance.now() more often than necessary.
+ const startTime = performance.now();
+ do {
+ for (let i = 0; i < 100; i++) {
+ const endIndex = this.string_.indexOf('\n', this.dataOffset_);
+ if (endIndex < 0) return;
+ const json = this.string_.substring(this.dataOffset_, endIndex);
+ const dict = JSON.parse(json);
+ this.histograms_.importDict(dict);
+ // Continue after last found newline character.
+ this.dataOffset_ = endIndex + 1;
+ }
+ } while (performance.now() - startTime < 50);
+
+ await this.update_(`Loading Histogram ${this.histograms_.length}`);
+ await this.loadSomeHistograms_();
+ }
+
+ async displayHistograms_() {
+ this.view_.addEventListener('display-ready', async() => {
+ this.loadingEl_.style.display = 'none';
+ this.view_.style.display = 'block';
+ await tr.b.animationFrame();
+ this.fmpMark_.end();
+ });
+
+ await this.view_.build(this.histograms_, {
+ progress: message => this.update_(message),
+ helpHref: 'https://github.com/catapult-project/catapult/blob/master/docs/metrics-results-ui.md',
+ feedbackHref: 'https://docs.google.com/a/google.com/forms/d/e/1FAIpQLSfXvMvm_z2F9-khFaKyH_LHVZ6caPPkxI27BZqMnEt4XjyJ3g/viewform',
+ });
+ }
+ }
+
+ return {
+ HistogramImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html
new file mode 100644
index 00000000000..19213f74ba4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'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,
+ ];
+ // HISTOGRAM_NAME is overridden.
+ // DISPLAY_LABEL is used to define the columns, so don't allow grouping rows
+ // by it.
+ for (const group of tr.v.HistogramGrouping.BY_KEY.values()) {
+ if (EXCLUDED_GROUPING_KEYS.includes(group.key)) continue;
+ DEFAULT_POSSIBLE_GROUPS.push(group);
+ }
+
+ // This Processor collects various parameters from a set of Histograms such as
+ // their statistics, displayLabels, and grouping keys in a single pass.
+ class HistogramParameterCollector {
+ constructor() {
+ this.statisticNames_ = new Set(['avg']);
+
+ this.labelsToStartTimes_ = new Map();
+
+ // @typedef {!Map.<string,!tr.v.HistogramGrouping>}
+ this.keysToGroupings_ = new Map(DEFAULT_POSSIBLE_GROUPS.map(
+ g => [g.key, g]));
+
+ // Map from HistogramGrouping keys to Sets of return values from the
+ // HistogramGroupings' callbacks.
+ this.keysToValues_ = new Map(DEFAULT_POSSIBLE_GROUPS.map(
+ g => [g.key, new Set()]));
+
+ // Never remove 'name' from keysToGroupings.
+ 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));
+ }
+ this.labelsToStartTimes_.set(displayLabel, startTime);
+
+ for (const [groupingKey, values] of this.keysToValues_) {
+ const grouping = this.keysToGroupings_.get(groupingKey);
+ const value = grouping.callback(hist);
+ if (!value) continue;
+ values.add(value);
+ if (values.size > 1) {
+ // This grouping will definitely stay in keysToGroupings_. We don't
+ // need to see any more values in the rest of histograms. Remove
+ // this groupingKey from this.keysToValues_ so that we don't compute
+ // it for any more histograms and so that we don't delete it from
+ // keysToGroupings_.
+ this.keysToValues_.delete(groupingKey);
+ }
+ }
+
+ const storyTags = hist.diagnostics.get(
+ tr.v.d.RESERVED_NAMES.STORY_TAGS);
+ for (const tag of (storyTags || [])) {
+ allStoryTags.add(tag);
+ }
+ }
+ tr.b.Timing.instant(
+ 'HistogramParameterCollector', 'maxSampleCount', maxSampleCount);
+
+ for (const tagGrouping of tr.v.HistogramGrouping.buildFromTags(
+ allStoryTags, tr.v.d.RESERVED_NAMES.STORY_TAGS)) {
+ const values = new Set();
+ for (const hist of histograms) {
+ values.add(tagGrouping.callback(hist));
+ }
+ if (values.size > 1) {
+ this.keysToGroupings_.set(tagGrouping.key, tagGrouping);
+ this.keysToValues_.set(tagGrouping.key, values);
+ }
+ }
+
+ this.statisticNames_.add('pct_090');
+ }
+
+ get statisticNames() {
+ return Array.from(this.statisticNames_);
+ }
+
+ get labels() {
+ const displayLabels = Array.from(this.labelsToStartTimes_.keys());
+ displayLabels.sort((x, y) =>
+ this.labelsToStartTimes_.get(x) - this.labelsToStartTimes_.get(y));
+ return displayLabels;
+ }
+
+ get possibleGroupings() {
+ for (const [key, values] of this.keysToValues_) {
+ if (values.size >= 2) continue;
+ // Remove this grouping from keysToGroupings_ if there is fewer than
+ // 2 possible values.
+ this.keysToGroupings_.delete(key);
+ }
+
+ return Array.from(this.keysToGroupings_.values());
+ }
+ }
+
+ return {
+ HistogramParameterCollector,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html
new file mode 100644
index 00000000000..2e508d21c92
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_parameter_collector_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram_parameter_collector.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ test('empty', function() {
+ const collector = new tr.v.HistogramParameterCollector();
+ collector.process([]);
+ assert.lengthOf(collector.statisticNames, 2);
+ assert.strictEqual('avg', collector.statisticNames[0]);
+ assert.strictEqual('pct_090', collector.statisticNames[1]);
+ assert.strictEqual('name',
+ tr.b.getOnlyElement(collector.possibleGroupings).key);
+ assert.lengthOf(collector.labels, 0);
+ });
+
+ test('sortLabels', function() {
+ const collector = new tr.v.HistogramParameterCollector();
+ collector.process([
+ tr.v.Histogram.create('', tr.b.Unit.byName.count, 0, {
+ diagnostics: new Map([
+ [
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']),
+ ], [
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1000),
+ ],
+ ]),
+ }),
+ tr.v.Histogram.create('', tr.b.Unit.byName.count, 0, {
+ diagnostics: new Map([
+ [
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']),
+ ], [
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(3000),
+ ],
+ ]),
+ }),
+ tr.v.Histogram.create('', tr.b.Unit.byName.count, 0, {
+ diagnostics: new Map([
+ [
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['C']),
+ ], [
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(2000),
+ ],
+ ]),
+ }),
+ ]);
+ const labels = collector.labels;
+ assert.lengthOf(labels, 3);
+ assert.strictEqual(labels[0], 'A');
+ assert.strictEqual(labels[1], 'C');
+ assert.strictEqual(labels[2], 'B');
+ });
+
+ test('possibleGroupings', function() {
+ const collector = new tr.v.HistogramParameterCollector();
+ collector.process([
+ tr.v.Histogram.create('a', tr.b.Unit.byName.count, 0, {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['F'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1000)],
+ [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, new tr.v.d.GenericSet([0])],
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['R'])],
+ ]),
+ }),
+ tr.v.Histogram.create('b', tr.b.Unit.byName.count, 0, {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['F'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(3000)],
+ [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, new tr.v.d.GenericSet([1])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet(['N'])],
+ ]),
+ }),
+ tr.v.Histogram.create('c', tr.b.Unit.byName.count, 0, {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['E', 'F'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['C'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(2000)],
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['P'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet(['M'])],
+ ]),
+ }),
+ ]);
+
+ const possibleGroupingKeys = new Set(
+ collector.possibleGroupings.map(g => g.key));
+ assert.isTrue(possibleGroupingKeys.has(
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key));
+ assert.isTrue(possibleGroupingKeys.has(
+ tr.v.d.RESERVED_NAMES.BENCHMARKS));
+ assert.isTrue(possibleGroupingKeys.has(
+ tr.v.d.RESERVED_NAMES.STORYSET_REPEATS));
+ assert.isTrue(possibleGroupingKeys.has(
+ tr.v.d.RESERVED_NAMES.STORIES));
+ assert.isFalse(possibleGroupingKeys.has(
+ tr.v.HistogramGrouping.DISPLAY_LABEL.key));
+ assert.isFalse(possibleGroupingKeys.has(
+ tr.v.d.RESERVED_NAMES.TRACE_START));
+ assert.isTrue(possibleGroupingKeys.has('ETag'));
+ assert.isFalse(possibleGroupingKeys.has('FTag'));
+ assert.strictEqual(possibleGroupingKeys.size, 7);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.html
new file mode 100644
index 00000000000..1171233ebb1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.html
@@ -0,0 +1,374 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+
+<script>
+'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);
+ }
+
+ /**
+ * Create a Histogram, configure it, add samples to it, and add it to this
+ * HistogramSet.
+ *
+ * |samples| can be either
+ * 0. a number, or
+ * 1. a dictionary {value: number, diagnostics: dictionary}, or
+ * 2. an array of
+ * 2a. number, or
+ * 2b. dictionaries {value, diagnostics}.
+ *
+ * @param {string} name
+ * @param {!tr.b.Unit} unit
+ * @param {number|!Object|!Array.<(number|!Object)>} samples
+ * @param {!Object=} opt_options
+ * @param {!tr.v.HistogramBinBoundaries} opt_options.binBoundaries
+ * @param {!Object|!Map} opt_options.summaryOptions
+ * @param {!Object|!Map} opt_options.diagnostics
+ * @param {string} opt_options.description
+ * @return {!tr.v.Histogram}
+ */
+ createHistogram(name, unit, samples, opt_options) {
+ const hist = tr.v.Histogram.create(name, unit, samples, opt_options);
+ this.addHistogram(hist);
+ return hist;
+ }
+
+ /**
+ * @param {!tr.v.Histogram} hist
+ * @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics
+ */
+ 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.histograms_.add(hist);
+ }
+
+ /**
+ * Add a Diagnostic to all Histograms so that it will only be serialized
+ * once per HistogramSet rather than once per Histogram that contains it.
+ *
+ * @param {string} name
+ * @param {!tr.v.d.Diagnostic} diagnostic
+ */
+ addSharedDiagnosticToAllHistograms(name, diagnostic) {
+ this.addSharedDiagnostic(diagnostic);
+ for (const hist of this) {
+ hist.diagnostics.set(name, diagnostic);
+ }
+ }
+
+ /**
+ * Add a Diagnostic to this HistogramSet so that it will only be serialized
+ * once per HistogramSet rather than once per Histogram that contains it.
+ *
+ * @param {!tr.v.d.Diagnostic} diagnostic
+ */
+ addSharedDiagnostic(diagnostic) {
+ this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic);
+ }
+
+ get length() {
+ return this.histograms_.size;
+ }
+
+ * [Symbol.iterator]() {
+ for (const hist of this.histograms_) {
+ yield hist;
+ }
+ }
+
+ /**
+ * Filters Histograms by matching their name exactly.
+ *
+ * @param {string} name Histogram name.
+ * @return {!Array.<!tr.v.Histogram>}
+ */
+ getHistogramsNamed(name) {
+ return [...this].filter(h => h.name === name);
+ }
+
+ /**
+ * Filters to find the Histogram that matches the specified name exactly.
+ * If no Histogram with that name exists, undefined is returned. If multiple
+ * Histograms with the name exist, an error is thrown.
+ *
+ * @param {string} name Histogram name.
+ * @return {tr.v.Histogram}
+ */
+ 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];
+ }
+
+ /**
+ * Lookup a Diagnostic by its guid.
+ *
+ * @param {string} guid
+ * @return {!tr.v.d.Diagnostic|undefined}
+ */
+ lookupDiagnostic(guid) {
+ return this.sharedDiagnosticsByGuid_.get(guid);
+ }
+
+ /**
+ * Convert dicts to either Histograms or shared Diagnostics.
+ *
+ * @param {!Object} dicts
+ */
+ importDicts(dicts) {
+ for (const dict of dicts) {
+ this.importDict(dict);
+ }
+ }
+
+ /**
+ * Convert dict to either a Histogram or a shared Diagnostic.
+ *
+ * @param {!Object} dict
+ */
+ importDict(dict) {
+ if (dict.type !== undefined) {
+ // TODO(benjhayden): Forget about TagMaps in 2019Q2.
+ 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);
+ }
+ }
+
+ /**
+ * Serialize all of the Histograms and shared Diagnostics to an Array of
+ * dictionaries.
+ *
+ * @return {!Array.<!Object>}
+ */
+ 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;
+ }
+
+ /**
+ * Find the Histograms whose names are not contained in any other
+ * Histograms' RelatedNameMap diagnostics.
+ *
+ * @return {!Array.<!tr.v.Histogram>}
+ */
+ 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 HistogramSet;
+ for (const hist of this) {
+ if (!diagnosticNames.has(hist.name)) {
+ sourceHistograms.addHistogram(hist);
+ }
+ }
+ return sourceHistograms;
+ }
+
+ /**
+ * Return a nested Map, whose keys are strings and leaf values are Arrays of
+ * Histograms.
+ * See GROUPINGS for example |groupings|.
+ * Groupings are skipped when |opt_skipGroupingCallback| is specified and
+ * returns true.
+ *
+ * @typedef {!Array.<tr.v.Histogram>} HistogramArray
+ * @typedef {!Map.<string,!(HistogramArray|HistogramArrayMap)>}
+ * HistogramArrayMap
+ * @typedef {!Map.<string,!HistogramArray>} LeafHistogramArrayMap
+ *
+ * @param {!Array.<!tr.v.HistogramGrouping>} groupings
+ * @param {!function(!Grouping, !LeafHistogramArrayMap):boolean=}
+ * opt_skipGroupingCallback
+ *
+ * @return {!(HistogramArray|HistogramArrayMap)}
+ */
+ groupHistogramsRecursively(groupings, opt_skipGroupingCallback) {
+ function recurse(histograms, level) {
+ if (level === groupings.length) {
+ return histograms; // recursion base case
+ }
+
+ 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));
+ }
+
+ return groupedHistograms;
+ }
+
+ return recurse([...this], 0);
+ }
+
+ /*
+ * Histograms and Diagnostics are merged two at a time, without considering
+ * any others, so it is possible for two merged Diagnostics to be equivalent
+ * but not identical, which is inefficient. This method replaces equivalent
+ * Diagnostics with shared Diagnostics so that the HistogramSet can be
+ * serialized more efficiently and so that these Diagnostics can be compared
+ * quickly when merging relationship Diagnostics.
+ */
+ deduplicateDiagnostics() {
+ const namesToCandidates = new Map(); // string: Set<Diagnostic>
+ const diagnosticsToHistograms = new Map(); // Diagnostic: [Histogram]
+ const keysToDiagnostics = new Map(); // string: Diagnostic
+
+ for (const hist of this) {
+ for (const [name, candidate] of hist.diagnostics) {
+ // TODO(#3695): Remove this check once equality is smoke-tested.
+ if (candidate.equals === undefined) {
+ this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate);
+ continue;
+ }
+
+ const hashKey = candidate.hashKey;
+ if (candidate.hashKey !== undefined) {
+ // TODO(857283): Fall back to slow path if same name but diff type
+ if (keysToDiagnostics.has(hashKey)) {
+ hist.diagnostics.set(name, keysToDiagnostics.get(hashKey));
+ } else {
+ keysToDiagnostics.set(hashKey, candidate);
+ this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate);
+ }
+
+ continue;
+ }
+
+ if (diagnosticsToHistograms.get(candidate) === undefined) {
+ diagnosticsToHistograms.set(candidate, [hist]);
+ } else {
+ diagnosticsToHistograms.get(candidate).push(hist);
+ }
+
+ if (!namesToCandidates.has(name)) {
+ namesToCandidates.set(name, new Set());
+ }
+ namesToCandidates.get(name).add(candidate);
+ }
+ }
+
+ for (const [name, candidates] of namesToCandidates) {
+ const deduplicatedDiagnostics = new Set();
+
+ for (const candidate of candidates) {
+ let found = false;
+ for (const test of deduplicatedDiagnostics) {
+ if (candidate.equals(test)) {
+ const hists = diagnosticsToHistograms.get(candidate);
+ for (const hist of hists) {
+ hist.diagnostics.set(name, test);
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ deduplicatedDiagnostics.add(candidate);
+ }
+
+ for (const diagnostic of deduplicatedDiagnostics) {
+ this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param {!Iterable.<string>} names of GenericSet diagnostics
+ * @return {!Array.<!tr.v.HistogramGrouping>}
+ */
+ buildGroupingsFromTags(names) {
+ const tags = new Map(); // name: Set<string>
+ 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;
+ }
+ }
+
+ return {HistogramSet};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.py
new file mode 100644
index 00000000000..b9a5d795c37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set.py
@@ -0,0 +1,155 @@
+# Copyright 2017 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 collections
+
+from tracing.value import histogram as histogram_module
+from tracing.value.diagnostics import all_diagnostics
+from tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import diagnostic_ref
+from tracing.value.diagnostics import generic_set
+
+class HistogramSet(object):
+ def __init__(self, histograms=()):
+ self._histograms = set()
+ self._shared_diagnostics_by_guid = {}
+ for hist in histograms:
+ self.AddHistogram(hist)
+
+ @property
+ def shared_diagnostics(self):
+ return self._shared_diagnostics_by_guid.values()
+
+ def RemoveOrphanedDiagnostics(self):
+ orphans = set(self._shared_diagnostics_by_guid.keys())
+ for h in self._histograms:
+ for d in h.diagnostics.values():
+ if d.guid in orphans:
+ orphans.remove(d.guid)
+ for guid in orphans:
+ del self._shared_diagnostics_by_guid[guid]
+
+ def FilterHistograms(self, discard):
+ self._histograms = set(
+ hist
+ for hist in self._histograms
+ if not discard(hist))
+
+ def AddHistogram(self, hist, diagnostics=None):
+ if diagnostics:
+ for name, diag in diagnostics.items():
+ hist.diagnostics[name] = diag
+
+ self._histograms.add(hist)
+
+ def AddSharedDiagnostic(self, diag):
+ self._shared_diagnostics_by_guid[diag.guid] = diag
+
+ def AddSharedDiagnosticToAllHistograms(self, name, diag):
+ self._shared_diagnostics_by_guid[diag.guid] = diag
+
+ for hist in self:
+ hist.diagnostics[name] = diag
+
+ def GetFirstHistogram(self):
+ for histogram in self._histograms:
+ return histogram
+
+ def GetHistogramsNamed(self, name):
+ return [h for h in self if h.name == name]
+
+ def GetHistogramNamed(self, name):
+ hs = self.GetHistogramsNamed(name)
+ assert len(hs) == 1, 'Found %d Histograms names "%s"' % (len(hs), name)
+ return hs[0]
+
+ def GetSharedDiagnosticsOfType(self, typ):
+ return [d for d in self.shared_diagnostics if isinstance(d, typ)]
+
+ def LookupDiagnostic(self, guid):
+ return self._shared_diagnostics_by_guid.get(guid)
+
+ def __len__(self):
+ return len(self._histograms)
+
+ def __iter__(self):
+ for hist in self._histograms:
+ yield hist
+
+ def ImportDicts(self, dicts):
+ for d in dicts:
+ if 'type' in d:
+ # TODO(benjhayden): Forget about TagMaps in 2019Q2.
+ if d['type'] == 'TagMap':
+ continue
+
+ assert d['type'] in all_diagnostics.GetDiagnosticTypenames(), (
+ 'Unrecognized shared diagnostic type ' + d['type'])
+ diag = diagnostic.Diagnostic.FromDict(d)
+ self._shared_diagnostics_by_guid[d['guid']] = diag
+ else:
+ hist = histogram_module.Histogram.FromDict(d)
+ hist.diagnostics.ResolveSharedDiagnostics(self)
+ self.AddHistogram(hist)
+
+ def AsDicts(self):
+ dcts = []
+ for d in self._shared_diagnostics_by_guid.values():
+ dcts.append(d.AsDict())
+ for h in self:
+ dcts.append(h.AsDict())
+ return dcts
+
+ def ReplaceSharedDiagnostic(self, old_guid, new_diagnostic):
+ if not isinstance(new_diagnostic, diagnostic_ref.DiagnosticRef):
+ self._shared_diagnostics_by_guid[new_diagnostic.guid] = new_diagnostic
+
+ old_diagnostic = self._shared_diagnostics_by_guid.get(old_guid)
+
+ # Fast path, if they're both generic_sets, we overwrite the contents of the
+ # old diagnostic.
+ if isinstance(new_diagnostic, generic_set.GenericSet) and (
+ isinstance(old_diagnostic, generic_set.GenericSet)):
+ old_diagnostic.SetValues(list(new_diagnostic))
+ old_diagnostic.ResetGuid(new_diagnostic.guid)
+
+ self._shared_diagnostics_by_guid[new_diagnostic.guid] = old_diagnostic
+ del self._shared_diagnostics_by_guid[old_guid]
+
+ return
+
+ for hist in self:
+ for name, diag in hist.diagnostics.items():
+ if diag.has_guid and diag.guid == old_guid:
+ hist.diagnostics[name] = new_diagnostic
+
+ def DeduplicateDiagnostics(self):
+ names_to_candidates = {}
+ diagnostics_to_histograms = collections.defaultdict(list)
+
+ for hist in self:
+ for name, candidate in hist.diagnostics.items():
+ diagnostics_to_histograms[candidate].append(hist)
+
+ if name not in names_to_candidates:
+ names_to_candidates[name] = set()
+ names_to_candidates[name].add(candidate)
+
+ for name, candidates in names_to_candidates.items():
+ deduplicated_diagnostics = set()
+
+ for candidate in candidates:
+ found = False
+ for test in deduplicated_diagnostics:
+ if candidate == test:
+ hists = diagnostics_to_histograms.get(candidate)
+ for h in hists:
+ h.diagnostics[name] = test
+ found = True
+ break
+ if not found:
+ deduplicated_diagnostics.add(candidate)
+
+ for diag in deduplicated_diagnostics:
+ self._shared_diagnostics_by_guid[diag.guid] = diag
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html
new file mode 100644
index 00000000000..86a3765fbfe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_hierarchy.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v', function() {
+ /*
+ * See also HistogramSet.groupHistogramsRecursively().
+ * See also tr.v.ui.HistogramSetTableRow.
+ */
+ class HistogramSetHierarchy {
+ /**
+ * @param {string} name
+ */
+ 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();
+ }
+
+ /**
+ * Build table rows recursively from grouped Histograms.
+ *
+ * @param {!(HistogramArray|HistogramArrayMap)}
+ * @returns {!Array.<!HistogramSetHierarchy>}
+ */
+ 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)) {
+ row.maybeRebin_();
+ }
+
+ return rootRows;
+ }
+
+ maybeRebin_() {
+ // if all of |this| row's columns are single-bin, then re-bin all of them.
+ 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; // don't re-bin
+ if (hist.numValues === 0) continue; // ignore hist
+ 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; // don't rebin
+
+ 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) {
+ // Track the path down the grouping tree to each Histogram,
+ // but only start tracking the path at the grouping level that
+ // corresponds to the Histogram NAME Grouping.
+ 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) {
+ // There have already been unmergeable histograms.
+ existing.addHistogram(histogram);
+ continue;
+ }
+
+ if (!existing.canAddHistogram(histogram)) {
+ // TODO(benjhayden) Remove?
+ 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) {
+ // This recursion base case corresponds to the recursion base case of
+ // groupHistogramsRecursively(). The last groupingCallback is always
+ // getDisplayLabel, which defines the columns of the table and is
+ // unskippable.
+ for (const histogram of histograms) {
+ HistogramSetHierarchy.mergeHistogramDownHierarchy_(
+ histogram, hierarchy, name);
+ }
+ } else if (histograms instanceof Map) {
+ // |histograms| is actually a nested histogramArrayMap.
+ 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};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html
new file mode 100644
index 00000000000..2db75bfba45
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_test.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/diagnostics/generic_set.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ // TODO(#3812) Test groupHistogramsRecursively.
+
+ test('assertType', function() {
+ const hs = new tr.v.HistogramSet();
+ assert.throws(() => hs.importDict({type: ''}),
+ Error, 'Unrecognized shared diagnostic type ');
+ });
+
+ test('ignoreTagMap', function() {
+ const hs = new tr.v.HistogramSet();
+ hs.importDict({type: 'TagMap'});
+ });
+
+ test('importDicts', function() {
+ const n = new tr.v.Histogram('foo', tr.b.Unit.byName.unitlessNumber);
+ const histograms = new tr.v.HistogramSet([n]);
+ const histograms2 = new tr.v.HistogramSet();
+ histograms2.importDicts(histograms.asDicts());
+ assert.isDefined(histograms2.getHistogramNamed('foo'));
+ });
+
+ test('importDictsWithSampleDiagnostic', function() {
+ const n = new tr.v.Histogram('foo', tr.b.Unit.byName.count);
+ n.addSample(10, {bar: new tr.v.d.GenericSet(['baz'])});
+
+ const histograms = new tr.v.HistogramSet([n]);
+ const histograms2 = new tr.v.HistogramSet();
+ histograms2.importDicts(histograms.asDicts());
+ assert.isDefined(histograms2.getHistogramNamed('foo'));
+ const v = histograms2.getHistogramNamed('foo');
+ assert.lengthOf(v.getBinForValue(10).diagnosticMaps, 1);
+ const dm = v.getBinForValue(10).diagnosticMaps[0];
+ assert.strictEqual(dm.size, 1);
+ assert.instanceOf(dm.get('bar'), tr.v.d.GenericSet);
+ assert.strictEqual(tr.b.getOnlyElement(dm.get('bar')), 'baz');
+ });
+
+ test('sourceHistogramsWithSampleDiagnostic', function() {
+ const unit = tr.b.Unit.byName.unitlessNumber;
+ const aHist = new tr.v.Histogram('a', unit);
+ aHist.addSample(1);
+
+ const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.unitlessNumber);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', aHist.name);
+ bHist.diagnostics.set('related', related);
+ bHist.addSample(1);
+
+ const histograms = new tr.v.HistogramSet([aHist, bHist]);
+ assert.strictEqual(tr.b.getOnlyElement(histograms.sourceHistograms), bHist);
+ });
+
+ test('sourceHistogramsWithNameMap', function() {
+ const unit = tr.b.Unit.byName.unitlessNumber;
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('A', unit, []);
+ const bHist = histograms.createHistogram('B', unit, [], {diagnostics: {
+ related: tr.v.d.RelatedNameMap.fromEntries([['a', 'A']]),
+ }});
+ assert.strictEqual(tr.b.getOnlyElement(histograms.sourceHistograms), bHist);
+ });
+
+ test('sharedDiagnostic', function() {
+ // Make a single Histogram, add a shared Diagnostic.
+ const aHist = new tr.v.Histogram('aHist', tr.b.Unit.byName.count);
+ const histograms = new tr.v.HistogramSet([aHist]);
+ const diagnostic = new tr.v.d.GenericSet(['shared']);
+ histograms.addSharedDiagnosticToAllHistograms('generic', diagnostic);
+
+ // Serializing a single Histogram with a single shared diagnostic should
+ // produce 2 dicts.
+ const dicts = histograms.asDicts();
+ assert.lengthOf(dicts, 2);
+ assert.deepEqual(diagnostic.asDict(), dicts[0]);
+
+ // The serialized Histogram should refer to the shared diagnostic by its
+ // guid.
+ assert.strictEqual(dicts[1].diagnostics.generic, diagnostic.guid);
+
+ // Deserialize the dicts.
+ const histograms2 = new tr.v.HistogramSet();
+ histograms2.importDicts(dicts);
+ assert.lengthOf(histograms2, 1);
+ const aHist2 = histograms2.getHistogramNamed(aHist.name);
+
+ assert.instanceOf(aHist2.diagnostics.get('generic'), tr.v.d.GenericSet);
+ assert.strictEqual(tr.b.getOnlyElement(diagnostic),
+ tr.b.getOnlyElement(aHist2.diagnostics.get('generic')));
+ });
+
+ test('getHistogramNamed_noHistogramFound', function() {
+ const aHist = new tr.v.Histogram('aHist', tr.b.Unit.byName.count);
+ const histograms = new tr.v.HistogramSet([aHist]);
+
+ assert.isUndefined(histograms.getHistogramNamed('bHist'));
+ });
+
+ test('getHistogramNamed_oneHistogramFound', function() {
+ const aHist = new tr.v.Histogram('aHist', tr.b.Unit.byName.count);
+ const histograms = new tr.v.HistogramSet([aHist]);
+
+ assert.strictEqual(histograms.getHistogramNamed('aHist'), aHist);
+ });
+
+ test('getHistogramNamed_multipleHistogramsFound', function() {
+ const aHist1 = new tr.v.Histogram('aHist', tr.b.Unit.byName.count);
+ const aHist2 = new tr.v.Histogram('aHist', tr.b.Unit.byName.count);
+ const histograms = new tr.v.HistogramSet([aHist1, aHist2]);
+
+ assert.throws(() => histograms.getHistogramNamed('aHist'),
+ Error, 'Unexpectedly found multiple histograms named "aHist"');
+ });
+
+ test('deduplicateDiagnostics', function() {
+ const genericA = new tr.v.d.GenericSet(['A']);
+ const genericB = new tr.v.d.GenericSet(['B']);
+ const dateA = new tr.v.d.DateRange(42);
+ const dateB = new tr.v.d.DateRange(57);
+
+ const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count);
+ const generic0 = genericA.clone();
+ generic0.addDiagnostic(genericB);
+ aHist.diagnostics.set('generic', generic0);
+ const date0 = dateA.clone();
+ date0.addDiagnostic(dateB);
+ aHist.diagnostics.set('date', date0);
+
+ const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count);
+ const generic1 = genericA.clone();
+ generic1.addDiagnostic(genericB);
+ bHist.diagnostics.set('generic', generic1);
+ const date1 = dateA.clone();
+ date1.addDiagnostic(dateB);
+ bHist.diagnostics.set('date', date1);
+
+ const cHist = new tr.v.Histogram('c', tr.b.Unit.byName.count);
+ cHist.diagnostics.set('generic', generic1);
+
+ const histograms = new tr.v.HistogramSet([aHist, bHist, cHist]);
+ assert.notStrictEqual(
+ aHist.diagnostics.get('generic'), bHist.diagnostics.get('generic'));
+ assert.strictEqual(
+ bHist.diagnostics.get('generic'), cHist.diagnostics.get('generic'));
+ assert.isTrue(
+ aHist.diagnostics.get('generic').equals(
+ bHist.diagnostics.get('generic')));
+ assert.notStrictEqual(
+ aHist.diagnostics.get('date'), bHist.diagnostics.get('date'));
+ assert.isTrue(
+ aHist.diagnostics.get('date').equals(bHist.diagnostics.get('date')));
+
+ histograms.deduplicateDiagnostics();
+
+ assert.strictEqual(
+ aHist.diagnostics.get('generic'), bHist.diagnostics.get('generic'));
+ assert.strictEqual(
+ bHist.diagnostics.get('generic'), cHist.diagnostics.get('generic'));
+ assert.strictEqual(
+ aHist.diagnostics.get('date'), bHist.diagnostics.get('date'));
+
+ const histogramDicts = histograms.asDicts();
+
+ // All diagnostics should have been serialized as DiagnosticRefs.
+ for (const dict of histogramDicts) {
+ if (!('type' in dict)) {
+ for (const diagnosticDict of Object.values(dict.diagnostics)) {
+ assert.strictEqual(typeof(diagnosticDict), 'string');
+ }
+ }
+ }
+
+ const histograms2 = new tr.v.HistogramSet();
+ histograms2.importDicts(histogramDicts);
+ const aHist2 = histograms2.getHistogramNamed('a');
+ const bHist2 = histograms2.getHistogramNamed('b');
+
+ assert.strictEqual(
+ aHist2.diagnostics.get('generic'), bHist2.diagnostics.get('generic'));
+ assert.strictEqual(
+ aHist2.diagnostics.get('date'), bHist2.diagnostics.get('date'));
+ });
+
+ test('buildGroupingsFromTags', function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['a'])],
+ ]),
+ });
+ const bHist = histograms.createHistogram('', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS, new tr.v.d.GenericSet(['b'])],
+ ]),
+ });
+ const groupings = histograms.buildGroupingsFromTags([
+ tr.v.d.RESERVED_NAMES.STORY_TAGS]);
+ assert.lengthOf(groupings, 2);
+ assert.strictEqual(groupings[0].callback(aHist), 'a');
+ assert.strictEqual(groupings[0].callback(bHist), '~a');
+ assert.strictEqual(groupings[1].callback(aHist), '~b');
+ assert.strictEqual(groupings[1].callback(bHist), 'b');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py
new file mode 100644
index 00000000000..39f6e45a3ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_set_unittest.py
@@ -0,0 +1,236 @@
+# Copyright 2017 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 tracing.value import histogram
+from tracing.value import histogram_set
+from tracing.value.diagnostics import date_range
+from tracing.value.diagnostics import diagnostic_ref
+from tracing.value.diagnostics import generic_set
+
+class HistogramSetUnittest(unittest.TestCase):
+
+ def testGetSharedDiagnosticsOfType(self):
+ d0 = generic_set.GenericSet(['foo'])
+ d1 = date_range.DateRange(0)
+ hs = histogram_set.HistogramSet()
+ hs.AddSharedDiagnosticToAllHistograms('generic', d0)
+ hs.AddSharedDiagnosticToAllHistograms('generic', d1)
+ diagnostics = hs.GetSharedDiagnosticsOfType(generic_set.GenericSet)
+ self.assertEqual(len(diagnostics), 1)
+ self.assertIsInstance(diagnostics[0], generic_set.GenericSet)
+
+ def testImportDicts(self):
+ hist = histogram.Histogram('', 'unitless')
+ hists = histogram_set.HistogramSet([hist])
+ hists2 = histogram_set.HistogramSet()
+ hists2.ImportDicts(hists.AsDicts())
+ self.assertEqual(len(hists), len(hists2))
+
+ def testAssertType(self):
+ hs = histogram_set.HistogramSet()
+ with self.assertRaises(AssertionError):
+ hs.ImportDicts([{'type': ''}])
+
+ def testIgnoreTagMap(self):
+ histogram_set.HistogramSet().ImportDicts([{'type': 'TagMap'}])
+
+ def testFilterHistogram(self):
+ a = histogram.Histogram('a', 'unitless')
+ b = histogram.Histogram('b', 'unitless')
+ c = histogram.Histogram('c', 'unitless')
+ hs = histogram_set.HistogramSet([a, b, c])
+ hs.FilterHistograms(lambda h: h.name == 'b')
+
+ names = set(['a', 'c'])
+ for h in hs:
+ self.assertIn(h.name, names)
+ names.remove(h.name)
+ self.assertEqual(0, len(names))
+
+ def testRemoveOrphanedDiagnostics(self):
+ da = generic_set.GenericSet(['a'])
+ db = generic_set.GenericSet(['b'])
+ a = histogram.Histogram('a', 'unitless')
+ b = histogram.Histogram('b', 'unitless')
+ hs = histogram_set.HistogramSet([a])
+ hs.AddSharedDiagnosticToAllHistograms('a', da)
+ hs.AddHistogram(b)
+ hs.AddSharedDiagnosticToAllHistograms('b', db)
+ hs.FilterHistograms(lambda h: h.name == 'a')
+
+ dicts = hs.AsDicts()
+ self.assertEqual(3, len(dicts))
+
+ hs.RemoveOrphanedDiagnostics()
+ dicts = hs.AsDicts()
+ self.assertEqual(2, len(dicts))
+
+ def testAddSharedDiagnostic(self):
+ diags = {}
+ da = generic_set.GenericSet(['a'])
+ db = generic_set.GenericSet(['b'])
+ diags['da'] = da
+ diags['db'] = db
+ a = histogram.Histogram('a', 'unitless')
+ b = histogram.Histogram('b', 'unitless')
+ hs = histogram_set.HistogramSet()
+ hs.AddSharedDiagnostic(da)
+ hs.AddHistogram(a, {'da': da})
+ hs.AddHistogram(b, {'db': db})
+
+ # This should produce one shared diagnostic and 2 histograms.
+ dicts = hs.AsDicts()
+ self.assertEqual(3, len(dicts))
+ self.assertEqual(da.AsDict(), dicts[0])
+
+
+ # Assert that you only see the shared diagnostic once.
+ seen_once = False
+ for idx, val in enumerate(dicts):
+ if idx == 0:
+ continue
+ if 'da' in val['diagnostics']:
+ self.assertFalse(seen_once)
+ self.assertEqual(val['diagnostics']['da'], da.guid)
+ seen_once = True
+
+
+ def testSharedDiagnostic(self):
+ hist = histogram.Histogram('', 'unitless')
+ hists = histogram_set.HistogramSet([hist])
+ diag = generic_set.GenericSet(['shared'])
+ hists.AddSharedDiagnosticToAllHistograms('generic', diag)
+
+ # Serializing a single Histogram with a single shared diagnostic should
+ # produce 2 dicts.
+ ds = hists.AsDicts()
+ self.assertEqual(len(ds), 2)
+ self.assertEqual(diag.AsDict(), ds[0])
+
+ # The serialized Histogram should refer to the shared diagnostic by its
+ # guid.
+ self.assertEqual(ds[1]['diagnostics']['generic'], diag.guid)
+
+ # Deserialize ds.
+ hists2 = histogram_set.HistogramSet()
+ hists2.ImportDicts(ds)
+ self.assertEqual(len(hists2), 1)
+ hist2 = [h for h in hists2][0]
+
+ self.assertIsInstance(
+ hist2.diagnostics.get('generic'), generic_set.GenericSet)
+ self.assertEqual(list(diag), list(hist2.diagnostics.get('generic')))
+
+ def testReplaceSharedDiagnostic(self):
+ hist = histogram.Histogram('', 'unitless')
+ hists = histogram_set.HistogramSet([hist])
+ diag0 = generic_set.GenericSet(['shared0'])
+ diag1 = generic_set.GenericSet(['shared1'])
+ hists.AddSharedDiagnosticToAllHistograms('generic0', diag0)
+ hists.AddSharedDiagnosticToAllHistograms('generic1', diag1)
+
+ guid0 = diag0.guid
+ guid1 = diag1.guid
+
+ hists.ReplaceSharedDiagnostic(
+ guid0, diagnostic_ref.DiagnosticRef('fakeGuid'))
+
+ self.assertEqual(hist.diagnostics['generic0'].guid, 'fakeGuid')
+ self.assertEqual(hist.diagnostics['generic1'].guid, guid1)
+
+ def testReplaceSharedDiagnostic_NonRefAddsToMap(self):
+ hist = histogram.Histogram('', 'unitless')
+ hists = histogram_set.HistogramSet([hist])
+ diag0 = generic_set.GenericSet(['shared0'])
+ diag1 = generic_set.GenericSet(['shared1'])
+ hists.AddSharedDiagnosticToAllHistograms('generic0', diag0)
+
+ guid0 = diag0.guid
+ guid1 = diag1.guid
+
+ hists.ReplaceSharedDiagnostic(guid0, diag1)
+
+ self.assertIsNotNone(hists.LookupDiagnostic(guid1))
+
+ def testDeduplicateDiagnostics(self):
+ generic_a = generic_set.GenericSet(['A'])
+ generic_b = generic_set.GenericSet(['B'])
+ date_a = date_range.DateRange(42)
+ date_b = date_range.DateRange(57)
+
+ a_hist = histogram.Histogram('a', 'unitless')
+ generic0 = generic_set.GenericSet.FromDict(generic_a.AsDict())
+ generic0.AddDiagnostic(generic_b)
+ a_hist.diagnostics['generic'] = generic0
+ date0 = date_range.DateRange.FromDict(date_a.AsDict())
+ date0.AddDiagnostic(date_b)
+ a_hist.diagnostics['date'] = date0
+
+ b_hist = histogram.Histogram('b', 'unitless')
+ generic1 = generic_set.GenericSet.FromDict(generic_a.AsDict())
+ generic1.AddDiagnostic(generic_b)
+ b_hist.diagnostics['generic'] = generic1
+ date1 = date_range.DateRange.FromDict(date_a.AsDict())
+ date1.AddDiagnostic(date_b)
+ b_hist.diagnostics['date'] = date1
+
+ c_hist = histogram.Histogram('c', 'unitless')
+ c_hist.diagnostics['generic'] = generic1
+
+ histograms = histogram_set.HistogramSet([a_hist, b_hist, c_hist])
+ self.assertNotEqual(
+ a_hist.diagnostics['generic'].guid, b_hist.diagnostics['generic'].guid)
+ self.assertEqual(
+ b_hist.diagnostics['generic'].guid, c_hist.diagnostics['generic'].guid)
+ self.assertEqual(
+ a_hist.diagnostics['generic'], b_hist.diagnostics['generic'])
+ self.assertNotEqual(
+ a_hist.diagnostics['date'].guid, b_hist.diagnostics['date'].guid)
+ self.assertEqual(
+ a_hist.diagnostics['date'], b_hist.diagnostics['date'])
+
+ histograms.DeduplicateDiagnostics()
+
+ self.assertEqual(
+ a_hist.diagnostics['generic'].guid, b_hist.diagnostics['generic'].guid)
+ self.assertEqual(
+ b_hist.diagnostics['generic'].guid, c_hist.diagnostics['generic'].guid)
+ self.assertEqual(
+ a_hist.diagnostics['generic'], b_hist.diagnostics['generic'])
+ self.assertEqual(
+ a_hist.diagnostics['date'].guid, b_hist.diagnostics['date'].guid)
+ self.assertEqual(
+ a_hist.diagnostics['date'], b_hist.diagnostics['date'])
+
+ histogram_dicts = histograms.AsDicts()
+
+ # All diagnostics should have been serialized as DiagnosticRefs.
+ for d in histogram_dicts:
+ if 'type' not in d:
+ for diagnostic_dict in d['diagnostics'].values():
+ self.assertIsInstance(diagnostic_dict, str)
+
+ histograms2 = histogram_set.HistogramSet()
+ histograms2.ImportDicts(histograms.AsDicts())
+ a_hists = histograms2.GetHistogramsNamed('a')
+ self.assertEqual(len(a_hists), 1)
+ a_hist2 = a_hists[0]
+ b_hists = histograms2.GetHistogramsNamed('b')
+ self.assertEqual(len(b_hists), 1)
+ b_hist2 = b_hists[0]
+
+ self.assertEqual(
+ a_hist2.diagnostics['generic'].guid,
+ b_hist2.diagnostics['generic'].guid)
+ self.assertEqual(
+ a_hist2.diagnostics['generic'],
+ b_hist2.diagnostics['generic'])
+ self.assertEqual(
+ a_hist2.diagnostics['date'].guid,
+ b_hist2.diagnostics['date'].guid)
+ self.assertEqual(
+ a_hist2.diagnostics['date'],
+ b_hist2.diagnostics['date'])
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_test.html b/chromium/third_party/catapult/tracing/tracing/value/histogram_test.html
new file mode 100644
index 00000000000..16f7b948272
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_test.html
@@ -0,0 +1,809 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/generic_set.html">
+<link rel="import" href="/tracing/value/histogram.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const unitlessNumber = tr.b.Unit.byName.unitlessNumber;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+
+ const TEST_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 1000, 10);
+
+ function checkBoundaries(boundaries, expectedMinBoundary, expectedMaxBoundary,
+ expectedUnit, expectedBinRanges) {
+ assert.strictEqual(boundaries.range.min, expectedMinBoundary);
+ assert.strictEqual(boundaries.range.max, expectedMaxBoundary);
+
+ // Check that the boundaries can be used multiple times.
+ for (let i = 0; i < 3; i++) {
+ const hist = new tr.v.Histogram('', expectedUnit, boundaries);
+ assert.instanceOf(hist, tr.v.Histogram);
+ assert.strictEqual(hist.unit, expectedUnit);
+ assert.strictEqual(hist.numValues, 0);
+
+ assert.lengthOf(hist.allBins, expectedBinRanges.length);
+ for (let j = 0; j < expectedBinRanges.length; j++) {
+ const bin = hist.allBins[j];
+ assert.strictEqual(bin.count, 0);
+ assert.isTrue(bin.range.equals(expectedBinRanges[j]));
+ }
+ }
+ }
+
+ test('truncateBreakdowns', function() {
+ const hist = tr.v.Histogram.create('a', unitlessNumber, {
+ value: 1,
+ diagnostics: {b: tr.v.d.Breakdown.fromEntries([
+ ['c', 1 / 3],
+ ])},
+ }, {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ });
+ assert.strictEqual(0.3333, hist.allBins[0].diagnosticMaps[0].get(
+ 'b').get('c'));
+ });
+
+ test('createWithNameUnitNumber', function() {
+ const hist = tr.v.Histogram.create('a', unitlessNumber, 1);
+ assert.strictEqual(hist.name, 'a');
+ assert.strictEqual(hist.unit, unitlessNumber);
+ assert.lengthOf(hist.sampleValues, 1);
+ assert.strictEqual(hist.average, 1);
+ });
+
+ test('createWithSamples', function() {
+ const hist = tr.v.Histogram.create('', unitlessNumber, [
+ 1,
+ {value: 3, diagnostics: {a: new tr.v.d.GenericSet(['b'])}},
+ ]);
+ assert.lengthOf(hist.sampleValues, 2);
+ assert.strictEqual(hist.average, 2);
+
+ const bin = hist.getBinForValue(3);
+ assert.lengthOf(bin.diagnosticMaps, 1);
+ const sampleDiagnostics = tr.b.getOnlyElement(bin.diagnosticMaps);
+ assert.strictEqual(tr.b.getOnlyElement(sampleDiagnostics.get('a')), 'b');
+ });
+
+ test('createWithOptions', function() {
+ const hist = tr.v.Histogram.create('', unitlessNumber, [], {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ description: 'foo',
+ diagnostics: {
+ generic: new tr.v.d.GenericSet(['occam']),
+ },
+ summaryOptions: {
+ count: false,
+ percentile: [0.5],
+ }
+ });
+ assert.strictEqual(hist.description, 'foo');
+ assert.strictEqual(tr.b.getOnlyElement(
+ hist.diagnostics.get('generic')), 'occam');
+ assert.isFalse(hist.summaryOptions.get('count'));
+ assert.strictEqual(tr.b.getOnlyElement(
+ hist.summaryOptions.get('percentile')), 0.5);
+ });
+
+ test('getStatisticScalar', function() {
+ const hist = new tr.v.Histogram('', unitlessNumber);
+ // getStatisticScalar should work even when the statistics are disabled.
+ hist.customizeSummaryOptions({
+ avg: false,
+ count: false,
+ max: false,
+ min: false,
+ std: false,
+ sum: false,
+ });
+
+ assert.isUndefined(hist.getStatisticScalar('avg'));
+ assert.isUndefined(hist.getStatisticScalar('std'));
+ assert.strictEqual(0, hist.getStatisticScalar('geometricMean').value);
+ assert.strictEqual(Infinity, hist.getStatisticScalar('min').value);
+ assert.strictEqual(-Infinity, hist.getStatisticScalar('max').value);
+ assert.strictEqual(0, hist.getStatisticScalar('sum').value);
+ assert.strictEqual(0, hist.getStatisticScalar('nans').value);
+ assert.strictEqual(0, hist.getStatisticScalar('count').value);
+ assert.isUndefined(hist.getStatisticScalar('pct_000'));
+ assert.isUndefined(hist.getStatisticScalar('pct_050'));
+ assert.isUndefined(hist.getStatisticScalar('pct_100'));
+
+ assert.isFalse(hist.canCompare());
+ assert.throws(() => hist.getStatisticScalar(tr.v.DELTA + 'avg'));
+
+ const ref = new tr.v.Histogram('', unitlessNumber);
+ for (let i = 0; i < 10; ++i) {
+ hist.addSample(i * 10);
+ ref.addSample(i);
+ }
+
+ assert.strictEqual(45, hist.getStatisticScalar('avg').value);
+ assert.closeTo(30.277, hist.getStatisticScalar('std').value, 1e-3);
+ assert.closeTo(0, hist.getStatisticScalar('geometricMean').value, 1e-4);
+ assert.strictEqual(0, hist.getStatisticScalar('min').value);
+ assert.strictEqual(90, hist.getStatisticScalar('max').value);
+ assert.strictEqual(450, hist.getStatisticScalar('sum').value);
+ assert.strictEqual(0, hist.getStatisticScalar('nans').value);
+ assert.strictEqual(10, hist.getStatisticScalar('count').value);
+ assert.closeTo(18.371, hist.getStatisticScalar('pct_025').value, 1e-3);
+ assert.closeTo(55.48, hist.getStatisticScalar('pct_075').value, 1e-3);
+ assert.closeTo(37.108, hist.getStatisticScalar('ipr_025_075').value, 1e-3);
+
+ assert.strictEqual(40.5, hist.getStatisticScalar(
+ tr.v.DELTA + 'avg', ref).value);
+ assert.closeTo(27.249, hist.getStatisticScalar(
+ tr.v.DELTA + 'std', ref).value, 1e-3);
+ assert.closeTo(0, hist.getStatisticScalar(
+ tr.v.DELTA + 'geometricMean', ref).value, 1e-4);
+ assert.strictEqual(0, hist.getStatisticScalar(
+ tr.v.DELTA + 'min', ref).value);
+ assert.strictEqual(81, hist.getStatisticScalar(
+ tr.v.DELTA + 'max', ref).value);
+ assert.strictEqual(405, hist.getStatisticScalar(
+ tr.v.DELTA + 'sum', ref).value);
+ assert.strictEqual(0, hist.getStatisticScalar(
+ tr.v.DELTA + 'nans', ref).value);
+ assert.strictEqual(0, hist.getStatisticScalar(
+ tr.v.DELTA + 'count', ref).value);
+ assert.closeTo(16.357, hist.getStatisticScalar(
+ tr.v.DELTA + 'pct_025', ref).value, 1e-3);
+ assert.closeTo(49.396, hist.getStatisticScalar(
+ tr.v.DELTA + 'pct_075', ref).value, 1e-3);
+ assert.closeTo(33.04, hist.getStatisticScalar(
+ tr.v.DELTA + 'ipr_025_075', ref).value, 1e-3);
+
+ assert.strictEqual(9, hist.getStatisticScalar(
+ `%${tr.v.DELTA}avg`, ref).value);
+ assert.closeTo(9, hist.getStatisticScalar(
+ `%${tr.v.DELTA}std`, ref).value, 1e-3);
+ assert.isTrue(isNaN(hist.getStatisticScalar(
+ `%${tr.v.DELTA}geometricMean`, ref).value));
+ assert.isTrue(isNaN(hist.getStatisticScalar(
+ `%${tr.v.DELTA}min`, ref).value));
+ assert.strictEqual(9, hist.getStatisticScalar(
+ `%${tr.v.DELTA}max`, ref).value);
+ assert.strictEqual(9, hist.getStatisticScalar(
+ `%${tr.v.DELTA}sum`, ref).value);
+ assert.isTrue(isNaN(hist.getStatisticScalar(
+ `%${tr.v.DELTA}nans`, ref).value));
+ assert.strictEqual(0, hist.getStatisticScalar(
+ `%${tr.v.DELTA}count`, ref).value);
+ assert.closeTo(8.12, hist.getStatisticScalar(
+ `%${tr.v.DELTA}pct_025`, ref).value, 1e-3);
+ assert.closeTo(8.12, hist.getStatisticScalar(
+ `%${tr.v.DELTA}pct_075`, ref).value, 1e-3);
+ assert.closeTo(8.12, hist.getStatisticScalar(
+ `%${tr.v.DELTA}ipr_025_075`, ref).value, 1e-3);
+ });
+
+ test('rebin', function() {
+ const hist = new tr.v.Histogram('foo', unitlessNumber_smallerIsBetter,
+ tr.v.HistogramBinBoundaries.SINGULAR);
+ assert.strictEqual(400, hist.maxNumSampleValues);
+ for (let i = 0; i < 100; ++i) {
+ hist.addSample(i);
+ }
+
+ let rebinned = hist.rebin(TEST_BOUNDARIES);
+ assert.strictEqual(12, rebinned.allBins.length);
+ assert.strictEqual(100, rebinned.allBins[1].count);
+ assert.strictEqual(hist.numValues, rebinned.numValues);
+ assert.strictEqual(hist.average, rebinned.average);
+ assert.strictEqual(hist.standardDeviation, rebinned.standardDeviation);
+ assert.strictEqual(hist.geometricMean, rebinned.geometricMean);
+ assert.strictEqual(hist.sum, rebinned.sum);
+ assert.strictEqual(hist.min, rebinned.min);
+ assert.strictEqual(hist.max, rebinned.max);
+
+ for (let i = 100; i < 1000; ++i) {
+ hist.addSample(i);
+ }
+
+ rebinned = hist.rebin(TEST_BOUNDARIES);
+ assert.strictEqual(12, rebinned.allBins.length);
+ let binCountSum = 0;
+ for (let i = 1; i < 11; ++i) {
+ binCountSum += rebinned.allBins[i].count;
+ assert.isAbove(100, rebinned.allBins[i].count, i);
+ }
+ assert.strictEqual(400, binCountSum);
+ assert.strictEqual(hist.numValues, rebinned.numValues);
+ assert.strictEqual(hist.average, rebinned.average);
+ assert.strictEqual(hist.standardDeviation, rebinned.standardDeviation);
+ assert.strictEqual(hist.geometricMean, rebinned.geometricMean);
+ assert.strictEqual(hist.sum, rebinned.sum);
+ assert.strictEqual(hist.min, rebinned.min);
+ assert.strictEqual(hist.max, rebinned.max);
+ });
+
+ test('serializationSize', function() {
+ // Ensure that serialized Histograms don't take up too much more space than
+ // necessary.
+ const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES);
+
+ // You can change these numbers, but when you do, please explain in your CL
+ // description why they changed.
+ let dict = hist.asDict();
+ assert.strictEqual(61, JSON.stringify(dict).length);
+ assert.isUndefined(dict.allBins);
+ assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict());
+
+ hist.addSample(100);
+ dict = hist.asDict();
+ assert.strictEqual(142, JSON.stringify(dict).length);
+ assert.isUndefined(dict.allBins.length);
+ assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict());
+
+ hist.addSample(100);
+ dict = hist.asDict();
+ // SAMPLE_VALUES grew by "100,"
+ assert.strictEqual(146, JSON.stringify(dict).length);
+ assert.isUndefined(dict.allBins.length);
+ assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict());
+
+ hist.addSample(271, {foo: new tr.v.d.GenericSet(['bar'])});
+ dict = hist.asDict();
+ assert.strictEqual(212, JSON.stringify(dict).length);
+ assert.isUndefined(dict.allBins.length);
+ assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict());
+
+ // Add samples to most bins so that allBinsArray is more efficient than
+ // allBinsDict.
+ for (let i = 10; i < 100; ++i) {
+ hist.addSample(10 * i);
+ }
+ dict = hist.asDict();
+ assert.strictEqual(628, JSON.stringify(hist.asDict()).length);
+ assert.lengthOf(dict.allBins, 12);
+ assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict());
+
+ // Lowering maxNumSampleValues takes a random sub-sample of the existing
+ // sampleValues. We have deliberately set all samples to 3-digit numbers so
+ // that the serialized size is constant regardless of which samples are
+ // retained.
+ hist.maxNumSampleValues = 10;
+ dict = hist.asDict();
+ assert.strictEqual(320, JSON.stringify(dict).length);
+ assert.lengthOf(dict.allBins, 12);
+ assert.deepEqual(dict, tr.v.Histogram.fromDict(dict).asDict());
+ });
+
+ test('significance', function() {
+ const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 10);
+ const histA = new tr.v.Histogram(
+ '', unitlessNumber_smallerIsBetter, boundaries);
+ const histB = new tr.v.Histogram(
+ '', unitlessNumber_smallerIsBetter, boundaries);
+
+ const dontCare = new tr.v.Histogram('', unitlessNumber, boundaries);
+ assert.strictEqual(dontCare.getDifferenceSignificance(dontCare),
+ tr.b.math.Statistics.Significance.DONT_CARE);
+
+ for (let i = 0; i < 100; ++i) {
+ histA.addSample(i);
+ histB.addSample(i * 0.85);
+ }
+
+ assert.strictEqual(histA.getDifferenceSignificance(histB),
+ tr.b.math.Statistics.Significance.INSIGNIFICANT);
+ assert.strictEqual(histB.getDifferenceSignificance(histA),
+ tr.b.math.Statistics.Significance.INSIGNIFICANT);
+ assert.strictEqual(histA.getDifferenceSignificance(histB, 0.1),
+ tr.b.math.Statistics.Significance.SIGNIFICANT);
+ assert.strictEqual(histB.getDifferenceSignificance(histA, 0.1),
+ tr.b.math.Statistics.Significance.SIGNIFICANT);
+ });
+
+ test('basic', function() {
+ const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES);
+ assert.strictEqual(hist.getBinForValue(250).range.min, 200);
+ assert.strictEqual(hist.getBinForValue(250).range.max, 300);
+
+ hist.addSample(-1, {foo: new tr.v.d.GenericSet(['a'])});
+ hist.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ hist.addSample(0, {foo: new tr.v.d.GenericSet(['c'])});
+ hist.addSample(500, {foo: new tr.v.d.GenericSet(['c'])});
+ hist.addSample(999, {foo: new tr.v.d.GenericSet(['d'])});
+ hist.addSample(1000, {foo: new tr.v.d.GenericSet(['d'])});
+ assert.strictEqual(hist.allBins[0].count, 1);
+
+ assert.strictEqual(hist.getBinForValue(0).count, 2);
+ assert.deepEqual(
+ hist.getBinForValue(0).diagnosticMaps.map(dm =>
+ tr.b.getOnlyElement(dm.get('foo'))), ['b', 'c']);
+
+ assert.strictEqual(hist.getBinForValue(500).count, 1);
+ assert.strictEqual(hist.getBinForValue(999).count, 1);
+
+ assert.strictEqual(hist.allBins[hist.allBins.length - 1].count, 1);
+ assert.strictEqual(hist.numValues, 6);
+ assert.closeTo(hist.average, 416.3, 0.1);
+ });
+
+ test('nans', function() {
+ const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES);
+
+ hist.addSample(undefined, {foo: new tr.v.d.GenericSet(['b'])});
+ hist.addSample(NaN, {'foo': new tr.v.d.GenericSet(['c'])});
+ hist.addSample(undefined);
+ hist.addSample(NaN);
+
+ assert.strictEqual(hist.numNans, 4);
+ assert.deepEqual(hist.nanDiagnosticMaps.map(dm =>
+ tr.b.getOnlyElement(dm.get('foo'))), ['b', 'c']);
+
+ const hist2 = tr.v.Histogram.fromDict(hist.asDict());
+ assert.instanceOf(hist2.nanDiagnosticMaps[0], tr.v.d.DiagnosticMap);
+ assert.instanceOf(hist2.nanDiagnosticMaps[0].get('foo'), tr.v.d.GenericSet);
+ });
+
+ test('addHistogramsValid', function() {
+ const hist0 = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES);
+ const hist1 = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES);
+
+ hist0.addSample(-1, {foo: new tr.v.d.GenericSet(['a0'])});
+ hist0.addSample(0, {foo: new tr.v.d.GenericSet(['b0'])});
+ hist0.addSample(0, {foo: new tr.v.d.GenericSet(['c0'])});
+ hist0.addSample(500, {foo: new tr.v.d.GenericSet(['c0'])});
+ hist0.addSample(1000, {foo: new tr.v.d.GenericSet(['d0'])});
+ hist0.addSample(NaN, {foo: new tr.v.d.GenericSet(['e0'])});
+
+ hist1.addSample(-1, {foo: new tr.v.d.GenericSet(['a1'])});
+ hist1.addSample(0, {foo: new tr.v.d.GenericSet(['b1'])});
+ hist1.addSample(0, {foo: new tr.v.d.GenericSet(['c1'])});
+ hist1.addSample(999, {foo: new tr.v.d.GenericSet(['d1'])});
+ hist1.addSample(1000, {foo: new tr.v.d.GenericSet(['d1'])});
+ hist1.addSample(NaN, {foo: new tr.v.d.GenericSet(['e1'])});
+
+ hist0.addHistogram(hist1);
+
+ assert.strictEqual(hist0.numNans, 2);
+ assert.deepEqual(hist0.nanDiagnosticMaps.map(dmd =>
+ tr.b.getOnlyElement(dmd.get('foo'))), ['e0', 'e1']);
+
+ assert.strictEqual(hist0.allBins[0].count, 2);
+ assert.deepEqual(
+ hist0.allBins[0].diagnosticMaps.map(dmd =>
+ tr.b.getOnlyElement(dmd.get('foo'))), ['a0', 'a1']);
+
+ assert.strictEqual(hist0.getBinForValue(0).count, 4);
+ assert.deepEqual(
+ hist0.getBinForValue(0).diagnosticMaps.map(dmd =>
+ tr.b.getOnlyElement(dmd.get('foo'))), ['b0', 'c0', 'b1', 'c1']);
+
+ assert.strictEqual(hist0.getBinForValue(500).count, 1);
+ assert.deepEqual(
+ hist0.getBinForValue(500).diagnosticMaps.map(dmd =>
+ tr.b.getOnlyElement(dmd.get('foo'))), ['c0']);
+
+ assert.strictEqual(hist0.getBinForValue(999).count, 1);
+ assert.deepEqual(
+ hist0.getBinForValue(999).diagnosticMaps.map(dmd =>
+ tr.b.getOnlyElement(dmd.get('foo'))), ['d1']);
+
+ assert.strictEqual(hist0.allBins[hist0.allBins.length - 1].count, 2);
+ assert.deepEqual(hist0.allBins[hist0.allBins.length - 1].diagnosticMaps.map(
+ dmd => tr.b.getOnlyElement(dmd.get('foo'))), ['d0', 'd1']);
+
+ assert.strictEqual(hist0.numValues, 10);
+ assert.closeTo(hist0.average, 349.7, 0.1);
+
+ const hist02 = tr.v.Histogram.fromDict(hist0.asDict());
+ assert.instanceOf(hist02.allBins[0].diagnosticMaps[0],
+ tr.v.d.DiagnosticMap);
+ assert.instanceOf(hist02.allBins[0].diagnosticMaps[0].get('foo'),
+ tr.v.d.GenericSet);
+ });
+
+ test('addHistogramsInvalid', function() {
+ const hist0 = new tr.v.Histogram('', tr.b.Unit.byName.timeDurationInMs,
+ tr.v.HistogramBinBoundaries.createLinear(0, 1000, 10));
+ const hist1 = new tr.v.Histogram('', tr.b.Unit.byName.timeDurationInMs,
+ tr.v.HistogramBinBoundaries.createLinear(0, 1001, 10));
+ const hist2 = new tr.v.Histogram('', tr.b.Unit.byName.timeDurationInMs,
+ tr.v.HistogramBinBoundaries.createLinear(0, 1000, 11));
+
+ assert.isFalse(hist0.canAddHistogram(hist1));
+ assert.isFalse(hist0.canAddHistogram(hist2));
+ assert.isFalse(hist1.canAddHistogram(hist0));
+ assert.isFalse(hist1.canAddHistogram(hist2));
+ assert.isFalse(hist2.canAddHistogram(hist0));
+ assert.isFalse(hist2.canAddHistogram(hist1));
+
+ assert.throws(hist0.addHistogram.bind(hist0, hist1), Error);
+ assert.throws(hist0.addHistogram.bind(hist0, hist2), Error);
+ });
+
+ test('addHistogramWithNonDiagnosticMapThrows', function() {
+ const hist = new tr.v.Histogram('', unitlessNumber, TEST_BOUNDARIES);
+ assert.throws(hist.addSample.bind(42, 'foo'), Error);
+ });
+
+ test('getApproximatePercentile', function() {
+ function check(array, min, max, bins, precision) {
+ const boundaries = tr.v.HistogramBinBoundaries.createLinear(
+ min, max, bins);
+ const hist = new tr.v.Histogram(
+ '', tr.b.Unit.byName.timeDurationInMs, boundaries);
+ array.forEach(x => hist.addSample(
+ x, {foo: new tr.v.d.GenericSet(['x'])}));
+ [0.25, 0.5, 0.75, 0.8, 0.95, 0.99].forEach(function(percent) {
+ const expected = tr.b.math.Statistics.percentile(array, percent);
+ const actual = hist.getApproximatePercentile(percent);
+ assert.closeTo(expected, actual, precision);
+ });
+ }
+ check([1, 2, 5, 7], 0.5, 10.5, 10, 1e-3);
+ check([3, 3, 4, 4], 0.5, 10.5, 10, 1e-3);
+ check([1, 10], 0.5, 10.5, 10, 1e-3);
+ check([1, 2, 3, 4, 5], 0.5, 10.5, 10, 1e-3);
+ check([3, 3, 3, 3, 3], 0.5, 10.5, 10, 1e-3);
+ check([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3);
+ check([1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3);
+ check([0, 11], 0.5, 10.5, 10, 1);
+ check([0, 6, 11], 0.5, 10.5, 10, 1);
+ const array = [];
+ for (let i = 0; i < 1000; i++) {
+ array.push((i * i) % 10 + 1);
+ }
+ check(array, 0.5, 10.5, 10, 1e-3);
+ // If the real percentile is outside the bin range then the approximation
+ // error can be high.
+ check([-10000], 0, 10, 10, 10000);
+ check([10000], 0, 10, 10, 10000 - 10);
+ // The result is no more than the bin width away from the real percentile.
+ check([1, 1], 0, 10, 1, 10);
+ });
+
+ test('histogramBinBoundaries_addBinBoundary', function() {
+ const b = new tr.v.HistogramBinBoundaries(-100);
+ b.addBinBoundary(50);
+
+ checkBoundaries(b, -100, 50, tr.b.Unit.byName.timeDurationInMs, [
+ tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, -100),
+ tr.b.math.Range.fromExplicitRange(-100, 50),
+ tr.b.math.Range.fromExplicitRange(50, Number.MAX_VALUE)
+ ]);
+
+ b.addBinBoundary(60);
+ b.addBinBoundary(75);
+
+ checkBoundaries(b, -100, 75, tr.b.Unit.byName.timeDurationInMs, [
+ tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, -100),
+ tr.b.math.Range.fromExplicitRange(-100, 50),
+ tr.b.math.Range.fromExplicitRange(50, 60),
+ tr.b.math.Range.fromExplicitRange(60, 75),
+ tr.b.math.Range.fromExplicitRange(75, Number.MAX_VALUE)
+ ]);
+ });
+
+ test('histogramBinBoundaries_addLinearBins', function() {
+ const b = new tr.v.HistogramBinBoundaries(1000);
+ b.addLinearBins(1200, 5);
+
+ checkBoundaries(b, 1000, 1200, tr.b.Unit.byName.powerInWatts, [
+ tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, 1000),
+ tr.b.math.Range.fromExplicitRange(1000, 1040),
+ tr.b.math.Range.fromExplicitRange(1040, 1080),
+ tr.b.math.Range.fromExplicitRange(1080, 1120),
+ tr.b.math.Range.fromExplicitRange(1120, 1160),
+ tr.b.math.Range.fromExplicitRange(1160, 1200),
+ tr.b.math.Range.fromExplicitRange(1200, Number.MAX_VALUE)
+ ]);
+ });
+
+ test('histogramBinBoundaries_addExponentialBins', function() {
+ const b = new tr.v.HistogramBinBoundaries(0.5);
+ b.addExponentialBins(8, 4);
+
+ checkBoundaries(b, 0.5, 8, tr.b.Unit.byName.energyInJoules, [
+ tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, 0.5),
+ tr.b.math.Range.fromExplicitRange(0.5, 1),
+ tr.b.math.Range.fromExplicitRange(1, 2),
+ tr.b.math.Range.fromExplicitRange(2, 4),
+ tr.b.math.Range.fromExplicitRange(4, 8),
+ tr.b.math.Range.fromExplicitRange(8, Number.MAX_VALUE)
+ ]);
+ });
+
+ test('histogramBinBoundaries_combined', function() {
+ const b = new tr.v.HistogramBinBoundaries(-273.15);
+ b.addBinBoundary(-50);
+ b.addLinearBins(4, 3);
+ b.addExponentialBins(16, 2);
+ b.addLinearBins(17, 4);
+ b.addBinBoundary(100);
+
+ checkBoundaries(b, -273.15, 100, tr.b.Unit.byName.unitlessNumber, [
+ tr.b.math.Range.fromExplicitRange(-Number.MAX_VALUE, -273.15),
+ tr.b.math.Range.fromExplicitRange(-273.15, -50),
+ tr.b.math.Range.fromExplicitRange(-50, -32),
+ tr.b.math.Range.fromExplicitRange(-32, -14),
+ tr.b.math.Range.fromExplicitRange(-14, 4),
+ tr.b.math.Range.fromExplicitRange(4, 8),
+ tr.b.math.Range.fromExplicitRange(8, 16),
+ tr.b.math.Range.fromExplicitRange(16, 16.25),
+ tr.b.math.Range.fromExplicitRange(16.25, 16.5),
+ tr.b.math.Range.fromExplicitRange(16.5, 16.75),
+ tr.b.math.Range.fromExplicitRange(16.75, 17),
+ tr.b.math.Range.fromExplicitRange(17, 100),
+ tr.b.math.Range.fromExplicitRange(100, Number.MAX_VALUE)
+ ]);
+ });
+
+ test('histogramBinBoundaries_throws', function() {
+ const b0 = new tr.v.HistogramBinBoundaries(-7);
+ assert.throws(function() { b0.addBinBoundary(-10 /* must be > -7 */); });
+ assert.throws(function() { b0.addBinBoundary(-7 /* must be > -7 */); });
+ assert.throws(function() { b0.addLinearBins(-10 /* must be > -7 */, 10); });
+ assert.throws(function() { b0.addLinearBins(-7 /* must be > -7 */, 100); });
+ assert.throws(function() { b0.addLinearBins(10, 0 /* must be > 0 */); });
+ assert.throws(function() {
+ // Current max bin boundary (-7) must be positive.
+ b0.addExponentialBins(16, 4);
+ });
+
+ const b1 = new tr.v.HistogramBinBoundaries(8);
+ assert.throws(() => b1.addExponentialBins(20, 0 /* must be > 0 */));
+ assert.throws(() => b1.addExponentialBins(5 /* must be > 8 */, 3));
+ assert.throws(() => b1.addExponentialBins(8 /* must be > 8 */, 3));
+ });
+
+ test('statisticsScalars', function() {
+ const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 100);
+ let hist = new tr.v.Histogram('', unitlessNumber, boundaries);
+
+ hist.addSample(50);
+ hist.addSample(60);
+ hist.addSample(70);
+ hist.addSample('i am not a number');
+
+ hist.customizeSummaryOptions({
+ count: true,
+ min: true,
+ max: true,
+ sum: true,
+ avg: true,
+ std: true,
+ nans: true,
+ geometricMean: true,
+ percentile: [0.5, 1]
+ });
+
+ // Test round-tripping summaryOptions.
+ hist = tr.v.Histogram.fromDict(hist.asDict());
+
+ const stats = hist.statisticsScalars;
+ assert.strictEqual(stats.get('nans').unit,
+ tr.b.Unit.byName.count_smallerIsBetter);
+ assert.strictEqual(stats.get('nans').value, 1);
+ assert.strictEqual(stats.get('count').unit,
+ tr.b.Unit.byName.count_smallerIsBetter);
+ assert.strictEqual(stats.get('count').value, 3);
+ assert.strictEqual(stats.get('min').unit, hist.unit);
+ assert.strictEqual(stats.get('min').value, 50);
+ assert.strictEqual(stats.get('max').unit, hist.unit);
+ assert.strictEqual(stats.get('max').value, 70);
+ assert.strictEqual(stats.get('sum').unit, hist.unit);
+ assert.strictEqual(stats.get('sum').value, 180);
+ assert.strictEqual(stats.get('avg').unit, hist.unit);
+ assert.strictEqual(stats.get('avg').value, 60);
+ assert.strictEqual(stats.get('std').value, 10);
+ assert.strictEqual(stats.get('pct_050').unit, hist.unit);
+ assert.closeTo(stats.get('pct_050').value, 60, 1);
+ assert.strictEqual(stats.get('pct_100').unit, hist.unit);
+ assert.closeTo(stats.get('pct_100').value, 70, 1);
+ assert.strictEqual(stats.get('geometricMean').unit, hist.unit);
+ assert.closeTo(stats.get('geometricMean').value, 59.439, 1e-3);
+ });
+
+ test('statisticsScalarsNoSummaryOptions', function() {
+ const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 100);
+ const hist = new tr.v.Histogram('', unitlessNumber, boundaries);
+
+ hist.addSample(50);
+ hist.addSample(60);
+ hist.addSample(70);
+
+ hist.customizeSummaryOptions({
+ count: false,
+ min: false,
+ max: false,
+ sum: false,
+ avg: false,
+ std: false,
+ percentile: []
+ });
+
+ assert.strictEqual(hist.statisticsScalars.size, 0);
+ });
+
+ test('statisticsScalarsEmptyHistogram', function() {
+ const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 100, 100);
+ const hist = new tr.v.Histogram('', unitlessNumber, boundaries);
+ hist.customizeSummaryOptions({
+ count: true,
+ min: true,
+ max: true,
+ sum: true,
+ avg: true,
+ std: true,
+ percentile: [0, 0.01, 0.1, 0.5, 0.995, 1]
+ });
+
+ const stats = hist.statisticsScalars;
+ assert.strictEqual(stats.get('count').value, 0);
+ assert.strictEqual(stats.get('min').value, Infinity);
+ assert.strictEqual(stats.get('max').value, -Infinity);
+ assert.strictEqual(stats.get('sum').value, 0);
+ assert.strictEqual(stats.get('avg'), undefined);
+ assert.strictEqual(stats.get('std'), undefined);
+ assert.isUndefined(stats.get('pct_000'));
+ assert.isUndefined(stats.get('pct_001'));
+ assert.isUndefined(stats.get('pct_010'));
+ assert.isUndefined(stats.get('pct_050'));
+ assert.isUndefined(stats.get('pct_099_5'));
+ assert.isUndefined(stats.get('pct_100'));
+ });
+
+ test('sampleValues', function() {
+ const boundaries = tr.v.HistogramBinBoundaries.createLinear(0, 1000, 50);
+ const hist0 = new tr.v.Histogram('', unitlessNumber, boundaries);
+ const hist1 = new tr.v.Histogram('', unitlessNumber, boundaries);
+ // maxNumSampleValues defaults to numBins * 10, which, including the
+ // underflow bin and overflow bin plus this builder's 10 central bins,
+ // is 52 * 10.
+ assert.strictEqual(hist0.maxNumSampleValues, 520);
+ assert.strictEqual(hist1.maxNumSampleValues, 520);
+ const values0 = [];
+ const values1 = [];
+ for (let i = 0; i < 10; ++i) {
+ values0.push(i);
+ hist0.addSample(i);
+ }
+ for (let i = 10; i < 20; ++i) {
+ values1.push(i);
+ hist1.addSample(i);
+ }
+ assert.deepEqual(hist0.sampleValues, values0);
+ assert.deepEqual(hist1.sampleValues, values1);
+ hist0.addHistogram(hist1);
+ assert.deepEqual(hist0.sampleValues, values0.concat(values1));
+ const hist2 = tr.v.Histogram.fromDict(hist0.asDict());
+ assert.deepEqual(hist2.sampleValues, values0.concat(values1));
+
+ for (let i = 0; i < 500; ++i) {
+ hist0.addSample(i);
+ }
+ assert.strictEqual(hist0.sampleValues.length, hist0.maxNumSampleValues);
+
+ const hist3 = new tr.v.Histogram('', unitlessNumber, boundaries);
+ hist3.maxNumSampleValues = 10;
+ for (let i = 0; i < 100; ++i) {
+ hist3.addSample(i);
+ }
+ assert.strictEqual(hist3.sampleValues.length, 10);
+ });
+
+ test('singularBin', function() {
+ const hist = new tr.v.Histogram('', unitlessNumber,
+ tr.v.HistogramBinBoundaries.SINGULAR);
+ assert.lengthOf(hist.allBins, 1);
+
+ const dict = hist.asDict();
+ assert.isUndefined(dict.binBoundaries);
+ const clone = tr.v.Histogram.fromDict(dict);
+ assert.lengthOf(clone.allBins, 1);
+ assert.deepEqual(dict, clone.asDict());
+
+ assert.isUndefined(hist.getApproximatePercentile(0));
+ assert.isUndefined(hist.getApproximatePercentile(1));
+ hist.addSample(0);
+ assert.strictEqual(0, hist.getApproximatePercentile(0));
+ assert.strictEqual(0, hist.getApproximatePercentile(1));
+ hist.addSample(1);
+ assert.strictEqual(0, hist.getApproximatePercentile(0));
+ assert.strictEqual(1, hist.getApproximatePercentile(1));
+ hist.addSample(2);
+ assert.strictEqual(0, hist.getApproximatePercentile(0));
+ assert.strictEqual(1, hist.getApproximatePercentile(0.5));
+ assert.strictEqual(2, hist.getApproximatePercentile(1));
+ hist.addSample(3);
+ assert.strictEqual(0, hist.getApproximatePercentile(0));
+ assert.strictEqual(1, hist.getApproximatePercentile(0.5));
+ assert.strictEqual(2, hist.getApproximatePercentile(0.9));
+ assert.strictEqual(3, hist.getApproximatePercentile(1));
+ hist.addSample(4);
+ assert.strictEqual(0, hist.getApproximatePercentile(0));
+ assert.strictEqual(1, hist.getApproximatePercentile(0.4));
+ assert.strictEqual(2, hist.getApproximatePercentile(0.7));
+ assert.strictEqual(3, hist.getApproximatePercentile(0.9));
+ assert.strictEqual(4, hist.getApproximatePercentile(1));
+ });
+
+ test('singularBin_with_multiBin', function() {
+ const multiBin = new tr.v.Histogram('', unitlessNumber);
+ const singleBin = new tr.v.Histogram('', unitlessNumber,
+ tr.v.HistogramBinBoundaries.SINGULAR);
+ multiBin.addSample(1);
+ singleBin.addSample(3);
+ assert.strictEqual(1, multiBin.average);
+ assert.strictEqual(3, singleBin.average);
+ multiBin.addHistogram(singleBin);
+ assert.strictEqual(2, multiBin.average);
+ multiBin.addSample(1);
+ singleBin.addHistogram(multiBin);
+ assert.strictEqual(2, singleBin.average);
+ });
+
+ test('mergeSummaryOptions', function() {
+ const hist0 = new tr.v.Histogram('', unitlessNumber);
+ const hist1 = new tr.v.Histogram('', unitlessNumber);
+
+ hist0.customizeSummaryOptions({
+ sum: false,
+ percentile: [0.1, 0.9],
+ iprs: [
+ tr.b.math.Range.fromExplicitRange(0.1, 0.9),
+ tr.b.math.Range.fromExplicitRange(0.25, 0.75),
+ ],
+ });
+ hist1.customizeSummaryOptions({
+ min: false,
+ percentile: [0.1, 0.95],
+ iprs: [
+ tr.b.math.Range.fromExplicitRange(0.1, 0.9),
+ tr.b.math.Range.fromExplicitRange(0.2, 0.8),
+ ],
+ });
+
+ let merged = tr.v.Histogram.fromDict(hist0.asDict());
+ let mergedIprs = merged.summaryOptions.get('iprs');
+ assert.isTrue(merged.summaryOptions.get('min'));
+ assert.isFalse(merged.summaryOptions.get('sum'));
+ assert.deepEqual(merged.summaryOptions.get('percentile'), [0.1, 0.9]);
+ assert.lengthOf(merged.summaryOptions.get('iprs'), 2);
+ tr.b.assertRangeEquals(
+ mergedIprs[0], tr.b.math.Range.fromExplicitRange(0.1, 0.9));
+ tr.b.assertRangeEquals(
+ mergedIprs[1], tr.b.math.Range.fromExplicitRange(0.25, 0.75));
+
+ merged = tr.v.Histogram.fromDict(hist1.asDict());
+ mergedIprs = merged.summaryOptions.get('iprs');
+ assert.isFalse(merged.summaryOptions.get('min'));
+ assert.isTrue(merged.summaryOptions.get('sum'));
+ assert.deepEqual(merged.summaryOptions.get('percentile'), [0.1, 0.95]);
+ assert.lengthOf(merged.summaryOptions.get('iprs'), 2);
+ tr.b.assertRangeEquals(
+ mergedIprs[0], tr.b.math.Range.fromExplicitRange(0.1, 0.9));
+ tr.b.assertRangeEquals(
+ mergedIprs[1], tr.b.math.Range.fromExplicitRange(0.2, 0.8));
+
+ merged = hist0.clone();
+ merged.addHistogram(hist1);
+
+ assert.isTrue(merged.summaryOptions.get('min'));
+ assert.isTrue(merged.summaryOptions.get('sum'));
+ assert.deepEqual(merged.summaryOptions.get('percentile'), [0.1, 0.9, 0.95]);
+ mergedIprs = merged.summaryOptions.get('iprs');
+ assert.lengthOf(mergedIprs, 3);
+ tr.b.assertRangeEquals(
+ mergedIprs[0], tr.b.math.Range.fromExplicitRange(0.1, 0.9));
+ tr.b.assertRangeEquals(
+ mergedIprs[1], tr.b.math.Range.fromExplicitRange(0.25, 0.75));
+ tr.b.assertRangeEquals(
+ mergedIprs[2], tr.b.math.Range.fromExplicitRange(0.2, 0.8));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py
new file mode 100644
index 00000000000..ab9b473e21c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histogram_unittest.py
@@ -0,0 +1,674 @@
+# Copyright 2017 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 json
+import math
+import unittest
+
+from tracing.value import histogram
+from tracing.value.diagnostics import date_range
+from tracing.value.diagnostics import diagnostic
+from tracing.value.diagnostics import diagnostic_ref
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import related_event_set
+from tracing.value.diagnostics import related_name_map
+from tracing.value.diagnostics import reserved_infos
+from tracing.value.diagnostics import unmergeable_diagnostic_set as ums
+
+# pylint: disable=too-many-lines
+
+class PercentToStringUnittest(unittest.TestCase):
+ def testPercentToString(self):
+ with self.assertRaises(Exception) as ex:
+ histogram.PercentToString(-1)
+ self.assertEqual(str(ex.exception), 'percent must be in [0,1]')
+
+ with self.assertRaises(Exception) as ex:
+ histogram.PercentToString(2)
+ self.assertEqual(str(ex.exception), 'percent must be in [0,1]')
+
+ self.assertEqual(histogram.PercentToString(0), '000')
+ self.assertEqual(histogram.PercentToString(1), '100')
+
+ with self.assertRaises(Exception) as ex:
+ histogram.PercentToString(float('nan'))
+ self.assertEqual(str(ex.exception), 'Unexpected percent')
+
+ self.assertEqual(histogram.PercentToString(0.50), '050')
+ self.assertEqual(histogram.PercentToString(0.95), '095')
+
+
+class StatisticsUnittest(unittest.TestCase):
+ def testFindHighIndexInSortedArray(self):
+ self.assertEqual(histogram.FindHighIndexInSortedArray(
+ list(range(0, -10, -1)), lambda x: x + 5), 6)
+
+ def testUniformlySampleArray(self):
+ self.assertEqual(len(histogram.UniformlySampleArray(
+ list(range(10)), 5)), 5)
+
+ def testUniformlySampleStream(self):
+ samples = []
+ histogram.UniformlySampleStream(samples, 1, 'A', 5)
+ self.assertEqual(samples, ['A'])
+ histogram.UniformlySampleStream(samples, 2, 'B', 5)
+ histogram.UniformlySampleStream(samples, 3, 'C', 5)
+ histogram.UniformlySampleStream(samples, 4, 'D', 5)
+ histogram.UniformlySampleStream(samples, 5, 'E', 5)
+ self.assertEqual(samples, ['A', 'B', 'C', 'D', 'E'])
+ histogram.UniformlySampleStream(samples, 6, 'F', 5)
+ self.assertEqual(len(samples), 5)
+
+ samples = [0, 0, 0]
+ histogram.UniformlySampleStream(samples, 1, 'G', 5)
+ self.assertEqual(samples, ['G', 0, 0])
+
+ def testMergeSampledStreams(self):
+ samples = []
+ histogram.MergeSampledStreams(samples, 0, ['A'], 1, 5)
+ self.assertEqual(samples, ['A'])
+ histogram.MergeSampledStreams(samples, 1, ['B', 'C', 'D', 'E'], 4, 5)
+ self.assertEqual(samples, ['A', 'B', 'C', 'D', 'E'])
+ histogram.MergeSampledStreams(samples, 9, ['F', 'G', 'H', 'I', 'J'], 7, 5)
+ self.assertEqual(len(samples), 5)
+
+
+class RangeUnittest(unittest.TestCase):
+ def testAddValue(self):
+ r = histogram.Range()
+ self.assertEqual(r.empty, True)
+ r.AddValue(1)
+ self.assertEqual(r.empty, False)
+ self.assertEqual(r.min, 1)
+ self.assertEqual(r.max, 1)
+ self.assertEqual(r.center, 1)
+ r.AddValue(2)
+ self.assertEqual(r.empty, False)
+ self.assertEqual(r.min, 1)
+ self.assertEqual(r.max, 2)
+ self.assertEqual(r.center, 1.5)
+
+
+class RunningStatisticsUnittest(unittest.TestCase):
+ def _Run(self, data):
+ running = histogram.RunningStatistics()
+ for datum in data:
+ running.Add(datum)
+ return running
+
+ def testStatistics(self):
+ running = self._Run([1, 2, 3])
+ self.assertEqual(running.sum, 6)
+ self.assertEqual(running.mean, 2)
+ self.assertEqual(running.min, 1)
+ self.assertEqual(running.max, 3)
+ self.assertEqual(running.variance, 1)
+ self.assertEqual(running.stddev, 1)
+ self.assertEqual(running.geometric_mean, math.pow(6, 1./3))
+ self.assertEqual(running.count, 3)
+
+ running = self._Run([2, 4, 4, 2])
+ self.assertEqual(running.sum, 12)
+ self.assertEqual(running.mean, 3)
+ self.assertEqual(running.min, 2)
+ self.assertEqual(running.max, 4)
+ self.assertEqual(running.variance, 4./3)
+ self.assertEqual(running.stddev, math.sqrt(4./3))
+ self.assertAlmostEqual(running.geometric_mean, math.pow(64, 1./4))
+ self.assertEqual(running.count, 4)
+
+ def testMerge(self):
+ def Compare(data1, data2):
+ a_running = self._Run(data1 + data2)
+ b_running = self._Run(data1).Merge(self._Run(data2))
+ CompareRunningStatistics(a_running, b_running)
+ a_running = histogram.RunningStatistics.FromDict(a_running.AsDict())
+ CompareRunningStatistics(a_running, b_running)
+ b_running = histogram.RunningStatistics.FromDict(b_running.AsDict())
+ CompareRunningStatistics(a_running, b_running)
+
+ def CompareRunningStatistics(a_running, b_running):
+ self.assertEqual(a_running.sum, b_running.sum)
+ self.assertEqual(a_running.mean, b_running.mean)
+ self.assertEqual(a_running.min, b_running.min)
+ self.assertEqual(a_running.max, b_running.max)
+ self.assertAlmostEqual(a_running.variance, b_running.variance)
+ self.assertAlmostEqual(a_running.stddev, b_running.stddev)
+ self.assertAlmostEqual(a_running.geometric_mean, b_running.geometric_mean)
+ self.assertEqual(a_running.count, b_running.count)
+
+ Compare([], [])
+ Compare([], [1, 2, 3])
+ Compare([1, 2, 3], [])
+ Compare([1, 2, 3], [10, 20, 100])
+ Compare([1, 1, 1, 1, 1], [10, 20, 10, 40])
+
+
+def ToJSON(x):
+ return json.dumps(x, separators=(',', ':'), sort_keys=True)
+
+
+class HistogramUnittest(unittest.TestCase):
+ TEST_BOUNDARIES = histogram.HistogramBinBoundaries.CreateLinear(0, 1000, 10)
+
+ def assertDeepEqual(self, a, b):
+ self.assertEqual(ToJSON(a), ToJSON(b))
+
+ def testDefaultBoundaries(self):
+ hist = histogram.Histogram('', 'ms')
+ self.assertEqual(len(hist.bins), 102)
+
+ hist = histogram.Histogram('', 'tsMs')
+ self.assertEqual(len(hist.bins), 1002)
+
+ hist = histogram.Histogram('', 'n%')
+ self.assertEqual(len(hist.bins), 22)
+
+ hist = histogram.Histogram('', 'sizeInBytes')
+ self.assertEqual(len(hist.bins), 102)
+
+ hist = histogram.Histogram('', 'J')
+ self.assertEqual(len(hist.bins), 52)
+
+ hist = histogram.Histogram('', 'W')
+ self.assertEqual(len(hist.bins), 52)
+
+ hist = histogram.Histogram('', 'unitless')
+ self.assertEqual(len(hist.bins), 52)
+
+ hist = histogram.Histogram('', 'count')
+ self.assertEqual(len(hist.bins), 22)
+
+ hist = histogram.Histogram('', 'sigma')
+ self.assertEqual(len(hist.bins), 52)
+
+ hist = histogram.Histogram('', 'sigma_smallerIsBetter')
+ self.assertEqual(len(hist.bins), 52)
+
+ hist = histogram.Histogram('', 'sigma_biggerIsBetter')
+ self.assertEqual(len(hist.bins), 52)
+
+ def testSerializationSize(self):
+ hist = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ d = hist.AsDict()
+ self.assertEqual(61, len(ToJSON(d)))
+ self.assertIsNone(d.get('allBins'))
+ self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict())
+
+ hist.AddSample(100)
+ d = hist.AsDict()
+ self.assertEqual(152, len(ToJSON(d)))
+ self.assertIsInstance(d['allBins'], dict)
+ self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict())
+
+ hist.AddSample(100)
+ d = hist.AsDict()
+ # SAMPLE_VALUES grew by "100,"
+ self.assertEqual(156, len(ToJSON(d)))
+ self.assertIsInstance(d['allBins'], dict)
+ self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict())
+
+ hist.AddSample(271, {'foo': generic_set.GenericSet(['bar'])})
+ d = hist.AsDict()
+ self.assertEqual(222, len(ToJSON(d)))
+ self.assertIsInstance(d['allBins'], dict)
+ self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict())
+
+ # Add samples to most bins so that allBinsArray is more efficient than
+ # allBinsDict.
+ for i in range(10, 100):
+ hist.AddSample(10 * i)
+ d = hist.AsDict()
+ self.assertEqual(651, len(ToJSON(d)))
+ self.assertIsInstance(d['allBins'], list)
+ self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict())
+
+ # Lowering maxNumSampleValues takes a random sub-sample of the existing
+ # sampleValues. We have deliberately set all samples to 3-digit numbers so
+ # that the serialized size is constant regardless of which samples are
+ # retained.
+ hist.max_num_sample_values = 10
+ d = hist.AsDict()
+ self.assertEqual(343, len(ToJSON(d)))
+ self.assertIsInstance(d['allBins'], list)
+ self.assertDeepEqual(d, histogram.Histogram.FromDict(d).AsDict())
+
+ def testBasic(self):
+ hist = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ self.assertEqual(hist.GetBinForValue(250).range.min, 200)
+ self.assertEqual(hist.GetBinForValue(250).range.max, 300)
+
+ hist.AddSample(-1)
+ hist.AddSample(0)
+ hist.AddSample(0)
+ hist.AddSample(500)
+ hist.AddSample(999)
+ hist.AddSample(1000)
+ self.assertEqual(hist.bins[0].count, 1)
+
+ self.assertEqual(hist.GetBinForValue(0).count, 2)
+ self.assertEqual(hist.GetBinForValue(500).count, 1)
+ self.assertEqual(hist.GetBinForValue(999).count, 1)
+ self.assertEqual(hist.bins[-1].count, 1)
+ self.assertEqual(hist.num_values, 6)
+ self.assertAlmostEqual(hist.average, 416.3333333)
+
+ def testNans(self):
+ hist = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ hist.AddSample(None)
+ hist.AddSample(float('nan'))
+ self.assertEqual(hist.num_nans, 2)
+
+ def testAddHistogramValid(self):
+ hist0 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ hist1 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ hist0.AddSample(0)
+ hist0.AddSample(None)
+ hist1.AddSample(1)
+ hist1.AddSample(float('nan'))
+ hist0.AddHistogram(hist1)
+ self.assertEqual(hist0.num_nans, 2)
+ self.assertEqual(hist0.GetBinForValue(0).count, 2)
+
+ def testAddHistogramInvalid(self):
+ hist0 = histogram.Histogram(
+ '', 'ms', histogram.HistogramBinBoundaries.CreateLinear(0, 1000, 10))
+ hist1 = histogram.Histogram(
+ '', 'unitless', histogram.HistogramBinBoundaries.CreateLinear(
+ 0, 1000, 10))
+ hist2 = histogram.Histogram(
+ '', 'ms', histogram.HistogramBinBoundaries.CreateLinear(0, 1001, 10))
+ hist3 = histogram.Histogram(
+ '', 'ms', histogram.HistogramBinBoundaries.CreateLinear(0, 1000, 11))
+ hists = [hist0, hist1, hist2, hist3]
+ for hista in hists:
+ for histb in hists:
+ if hista is histb:
+ continue
+ self.assertFalse(hista.CanAddHistogram(histb))
+ with self.assertRaises(Exception):
+ hista.AddHistogram(histb)
+
+ def testPercentile(self):
+ def Check(ary, mn, mx, bins, precision):
+ boundaries = histogram.HistogramBinBoundaries.CreateLinear(mn, mx, bins)
+ hist = histogram.Histogram('', 'ms', boundaries)
+ for x in ary:
+ hist.AddSample(x)
+ for percent in [0.25, 0.5, 0.75, 0.8, 0.95, 0.99]:
+ self.assertLessEqual(
+ abs(histogram.Percentile(ary, percent) -
+ hist.GetApproximatePercentile(percent)), precision)
+ Check([1, 2, 5, 7], 0.5, 10.5, 10, 1e-3)
+ Check([3, 3, 4, 4], 0.5, 10.5, 10, 1e-3)
+ Check([1, 10], 0.5, 10.5, 10, 1e-3)
+ Check([1, 2, 3, 4, 5], 0.5, 10.5, 10, 1e-3)
+ Check([3, 3, 3, 3, 3], 0.5, 10.5, 10, 1e-3)
+ Check([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3)
+ Check([1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10], 0.5, 10.5, 10, 1e-3)
+ Check([0, 11], 0.5, 10.5, 10, 1)
+ Check([0, 6, 11], 0.5, 10.5, 10, 1)
+ array = []
+ for i in range(1000):
+ array.append((i * i) % 10 + 1)
+ Check(array, 0.5, 10.5, 10, 1e-3)
+ # If the real percentile is outside the bin range then the approximation
+ # error can be high.
+ Check([-10000], 0, 10, 10, 10000)
+ Check([10000], 0, 10, 10, 10000 - 10)
+ # The result is no more than the bin width away from the real percentile.
+ Check([1, 1], 0, 10, 1, 10)
+
+ def _CheckBoundaries(self, boundaries, expected_min_boundary,
+ expected_max_boundary, expected_bin_ranges):
+ self.assertEqual(boundaries.range.min, expected_min_boundary)
+ self.assertEqual(boundaries.range.max, expected_max_boundary)
+
+ # Check that the boundaries can be used multiple times.
+ for _ in range(3):
+ hist = histogram.Histogram('', 'unitless', boundaries)
+ self.assertEqual(len(expected_bin_ranges), len(hist.bins))
+ for j, hbin in enumerate(hist.bins):
+ self.assertAlmostEqual(hbin.range.min, expected_bin_ranges[j].min)
+ self.assertAlmostEqual(hbin.range.max, expected_bin_ranges[j].max)
+
+ def testAddBinBoundary(self):
+ b = histogram.HistogramBinBoundaries(-100)
+ b.AddBinBoundary(50)
+ self._CheckBoundaries(b, -100, 50, [
+ histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, -100),
+ histogram.Range.FromExplicitRange(-100, 50),
+ histogram.Range.FromExplicitRange(50, histogram.JS_MAX_VALUE),
+ ])
+
+ b.AddBinBoundary(60)
+ b.AddBinBoundary(75)
+ self._CheckBoundaries(b, -100, 75, [
+ histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, -100),
+ histogram.Range.FromExplicitRange(-100, 50),
+ histogram.Range.FromExplicitRange(50, 60),
+ histogram.Range.FromExplicitRange(60, 75),
+ histogram.Range.FromExplicitRange(75, histogram.JS_MAX_VALUE),
+ ])
+
+ def testAddLinearBins(self):
+ b = histogram.HistogramBinBoundaries(1000)
+ b.AddLinearBins(1200, 5)
+ self._CheckBoundaries(b, 1000, 1200, [
+ histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, 1000),
+ histogram.Range.FromExplicitRange(1000, 1040),
+ histogram.Range.FromExplicitRange(1040, 1080),
+ histogram.Range.FromExplicitRange(1080, 1120),
+ histogram.Range.FromExplicitRange(1120, 1160),
+ histogram.Range.FromExplicitRange(1160, 1200),
+ histogram.Range.FromExplicitRange(1200, histogram.JS_MAX_VALUE),
+ ])
+
+ def testAddExponentialBins(self):
+ b = histogram.HistogramBinBoundaries(0.5)
+ b.AddExponentialBins(8, 4)
+ self._CheckBoundaries(b, 0.5, 8, [
+ histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, 0.5),
+ histogram.Range.FromExplicitRange(0.5, 1),
+ histogram.Range.FromExplicitRange(1, 2),
+ histogram.Range.FromExplicitRange(2, 4),
+ histogram.Range.FromExplicitRange(4, 8),
+ histogram.Range.FromExplicitRange(8, histogram.JS_MAX_VALUE),
+ ])
+
+ def testBinBoundariesCombined(self):
+ b = histogram.HistogramBinBoundaries(-273.15)
+ b.AddBinBoundary(-50)
+ b.AddLinearBins(4, 3)
+ b.AddExponentialBins(16, 2)
+ b.AddLinearBins(17, 4)
+ b.AddBinBoundary(100)
+
+ self._CheckBoundaries(b, -273.15, 100, [
+ histogram.Range.FromExplicitRange(-histogram.JS_MAX_VALUE, -273.15),
+ histogram.Range.FromExplicitRange(-273.15, -50),
+ histogram.Range.FromExplicitRange(-50, -32),
+ histogram.Range.FromExplicitRange(-32, -14),
+ histogram.Range.FromExplicitRange(-14, 4),
+ histogram.Range.FromExplicitRange(4, 8),
+ histogram.Range.FromExplicitRange(8, 16),
+ histogram.Range.FromExplicitRange(16, 16.25),
+ histogram.Range.FromExplicitRange(16.25, 16.5),
+ histogram.Range.FromExplicitRange(16.5, 16.75),
+ histogram.Range.FromExplicitRange(16.75, 17),
+ histogram.Range.FromExplicitRange(17, 100),
+ histogram.Range.FromExplicitRange(100, histogram.JS_MAX_VALUE)
+ ])
+
+ def testBinBoundariesRaises(self):
+ b = histogram.HistogramBinBoundaries(-7)
+ with self.assertRaises(Exception):
+ b.AddBinBoundary(-10)
+ with self.assertRaises(Exception):
+ b.AddBinBoundary(-7)
+ with self.assertRaises(Exception):
+ b.AddLinearBins(-10, 10)
+ with self.assertRaises(Exception):
+ b.AddLinearBins(-7, 10)
+ with self.assertRaises(Exception):
+ b.AddLinearBins(10, 0)
+ with self.assertRaises(Exception):
+ b.AddExponentialBins(16, 4)
+ b = histogram.HistogramBinBoundaries(8)
+ with self.assertRaises(Exception):
+ b.AddExponentialBins(20, 0)
+ with self.assertRaises(Exception):
+ b.AddExponentialBins(5, 3)
+ with self.assertRaises(Exception):
+ b.AddExponentialBins(8, 3)
+
+ def testStatisticsScalars(self):
+ b = histogram.HistogramBinBoundaries.CreateLinear(0, 100, 100)
+ hist = histogram.Histogram('', 'unitless', b)
+ hist.AddSample(50)
+ hist.AddSample(60)
+ hist.AddSample(70)
+ hist.AddSample('i am not a number')
+ hist.CustomizeSummaryOptions({
+ 'count': True,
+ 'min': True,
+ 'max': True,
+ 'sum': True,
+ 'avg': True,
+ 'std': True,
+ 'nans': True,
+ 'geometricMean': True,
+ 'percentile': [0.5, 1],
+ })
+
+ # Test round-tripping summaryOptions
+ hist = hist.Clone()
+ stats = hist.statistics_scalars
+ self.assertEqual(stats['nans'].unit, 'count')
+ self.assertEqual(stats['nans'].value, 1)
+ self.assertEqual(stats['count'].unit, 'count')
+ self.assertEqual(stats['count'].value, 3)
+ self.assertEqual(stats['min'].unit, hist.unit)
+ self.assertEqual(stats['min'].value, 50)
+ self.assertEqual(stats['max'].unit, hist.unit)
+ self.assertEqual(stats['max'].value, 70)
+ self.assertEqual(stats['sum'].unit, hist.unit)
+ self.assertEqual(stats['sum'].value, 180)
+ self.assertEqual(stats['avg'].unit, hist.unit)
+ self.assertEqual(stats['avg'].value, 60)
+ self.assertEqual(stats['std'].unit, hist.unit)
+ self.assertEqual(stats['std'].value, 10)
+ self.assertEqual(stats['pct_050'].unit, hist.unit)
+ self.assertEqual(stats['pct_050'].value, 60.5)
+ self.assertEqual(stats['pct_100'].unit, hist.unit)
+ self.assertEqual(stats['pct_100'].value, 70.5)
+ self.assertEqual(stats['geometricMean'].unit, hist.unit)
+ self.assertLess(abs(stats['geometricMean'].value - 59.439), 1e-3)
+
+ hist.CustomizeSummaryOptions({
+ 'count': False,
+ 'min': False,
+ 'max': False,
+ 'sum': False,
+ 'avg': False,
+ 'std': False,
+ 'nans': False,
+ 'geometricMean': False,
+ 'percentile': [],
+ })
+ self.assertEqual(0, len(hist.statistics_scalars))
+
+ def testStatisticsScalarsEmpty(self):
+ b = histogram.HistogramBinBoundaries.CreateLinear(0, 100, 100)
+ hist = histogram.Histogram('', 'unitless', b)
+ hist.CustomizeSummaryOptions({
+ 'count': True,
+ 'min': True,
+ 'max': True,
+ 'sum': True,
+ 'avg': True,
+ 'std': True,
+ 'nans': True,
+ 'geometricMean': True,
+ 'percentile': [0, 0.01, 0.1, 0.5, 0.995, 1],
+ })
+ stats = hist.statistics_scalars
+ self.assertEqual(stats['nans'].value, 0)
+ self.assertEqual(stats['count'].value, 0)
+ self.assertEqual(stats['min'].value, histogram.JS_MAX_VALUE)
+ self.assertEqual(stats['max'].value, -histogram.JS_MAX_VALUE)
+ self.assertEqual(stats['sum'].value, 0)
+ self.assertNotIn('avg', stats)
+ self.assertNotIn('stddev', stats)
+ self.assertEqual(stats['pct_000'].value, 0)
+ self.assertEqual(stats['pct_001'].value, 0)
+ self.assertEqual(stats['pct_010'].value, 0)
+ self.assertEqual(stats['pct_050'].value, 0)
+ self.assertEqual(stats['pct_099_5'].value, 0)
+ self.assertEqual(stats['pct_100'].value, 0)
+
+ def testSampleValues(self):
+ hist0 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ hist1 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ self.assertEqual(hist0.max_num_sample_values, 120)
+ self.assertEqual(hist1.max_num_sample_values, 120)
+ values0 = []
+ values1 = []
+ for i in range(10):
+ values0.append(i)
+ hist0.AddSample(i)
+ values1.append(10 + i)
+ hist1.AddSample(10 + i)
+ self.assertDeepEqual(hist0.sample_values, values0)
+ self.assertDeepEqual(hist1.sample_values, values1)
+ hist0.AddHistogram(hist1)
+ self.assertDeepEqual(hist0.sample_values, values0 + values1)
+ hist2 = hist0.Clone()
+ self.assertDeepEqual(hist2.sample_values, values0 + values1)
+
+ for i in range(200):
+ hist0.AddSample(i)
+ self.assertEqual(len(hist0.sample_values), hist0.max_num_sample_values)
+
+ hist3 = histogram.Histogram('', 'unitless', self.TEST_BOUNDARIES)
+ hist3.max_num_sample_values = 10
+ for i in range(100):
+ hist3.AddSample(i)
+ self.assertEqual(len(hist3.sample_values), 10)
+
+ def testSingularBin(self):
+ hist = histogram.Histogram(
+ '', 'unitless', histogram.HistogramBinBoundaries.SINGULAR)
+ self.assertEqual(1, len(hist.bins))
+ d = hist.AsDict()
+ self.assertNotIn('binBoundaries', d)
+ clone = histogram.Histogram.FromDict(d)
+ self.assertEqual(1, len(clone.bins))
+ self.assertDeepEqual(d, clone.AsDict())
+
+ self.assertEqual(0, hist.GetApproximatePercentile(0))
+ self.assertEqual(0, hist.GetApproximatePercentile(1))
+ hist.AddSample(0)
+ self.assertEqual(0, hist.GetApproximatePercentile(0))
+ self.assertEqual(0, hist.GetApproximatePercentile(1))
+ hist.AddSample(1)
+ self.assertEqual(0, hist.GetApproximatePercentile(0))
+ self.assertEqual(1, hist.GetApproximatePercentile(1))
+ hist.AddSample(2)
+ self.assertEqual(0, hist.GetApproximatePercentile(0))
+ self.assertEqual(1, hist.GetApproximatePercentile(0.5))
+ self.assertEqual(2, hist.GetApproximatePercentile(1))
+ hist.AddSample(3)
+ self.assertEqual(0, hist.GetApproximatePercentile(0))
+ self.assertEqual(1, hist.GetApproximatePercentile(0.5))
+ self.assertEqual(2, hist.GetApproximatePercentile(0.9))
+ self.assertEqual(3, hist.GetApproximatePercentile(1))
+ hist.AddSample(4)
+ self.assertEqual(0, hist.GetApproximatePercentile(0))
+ self.assertEqual(1, hist.GetApproximatePercentile(0.4))
+ self.assertEqual(2, hist.GetApproximatePercentile(0.7))
+ self.assertEqual(3, hist.GetApproximatePercentile(0.9))
+ self.assertEqual(4, hist.GetApproximatePercentile(1))
+
+
+class DiagnosticMapUnittest(unittest.TestCase):
+ def testDisallowReservedNames(self):
+ diagnostics = histogram.DiagnosticMap()
+ with self.assertRaises(TypeError):
+ diagnostics[None] = generic_set.GenericSet(())
+ with self.assertRaises(TypeError):
+ diagnostics['generic'] = None
+ diagnostics[reserved_infos.TRACE_URLS.name] = date_range.DateRange(0)
+ diagnostics.DisallowReservedNames()
+ diagnostics[reserved_infos.TRACE_URLS.name] = generic_set.GenericSet(())
+ with self.assertRaises(TypeError):
+ diagnostics[reserved_infos.TRACE_URLS.name] = date_range.DateRange(0)
+
+ def testResetGuid(self):
+ generic = generic_set.GenericSet(['generic diagnostic'])
+ guid1 = generic.guid
+ generic.ResetGuid()
+ guid2 = generic.guid
+ self.assertNotEqual(guid1, guid2)
+
+ # TODO(eakuefner): Find a better place for these non-map tests once we
+ # break up the Python implementation more.
+ def testInlineSharedDiagnostic(self):
+ generic = generic_set.GenericSet(['generic diagnostic'])
+ hist = histogram.Histogram('', 'count')
+ _ = generic.guid # First access sets guid
+ hist.diagnostics['foo'] = generic
+ generic.Inline()
+ self.assertFalse(generic.has_guid)
+ hist_dict = hist.AsDict()
+ diag_dict = hist_dict['diagnostics']['foo']
+ self.assertIsInstance(diag_dict, dict)
+ self.assertEqual(diag_dict['type'], 'GenericSet')
+
+ def testCloneWithRef(self):
+ diagnostics = histogram.DiagnosticMap()
+ diagnostics['ref'] = diagnostic_ref.DiagnosticRef('abc')
+
+ clone = histogram.DiagnosticMap.FromDict(diagnostics.AsDict())
+ self.assertIsInstance(clone.get('ref'), diagnostic_ref.DiagnosticRef)
+ self.assertEqual(clone.get('ref').guid, 'abc')
+
+ def testDiagnosticGuidDeserialized(self):
+ d = {
+ 'type': 'GenericSet',
+ 'values': [],
+ 'guid': 'bar'
+ }
+ g = diagnostic.Diagnostic.FromDict(d)
+ self.assertEqual('bar', g.guid)
+
+ def testMerge(self):
+ events = related_event_set.RelatedEventSet()
+ events.Add({
+ 'stableId': '0.0',
+ 'title': 'foo',
+ 'start': 0,
+ 'duration': 1,
+ })
+ generic = generic_set.GenericSet(['generic diagnostic'])
+ generic2 = generic_set.GenericSet(['generic diagnostic 2'])
+ related_map = related_name_map.RelatedNameMap()
+ related_map.Set('a', 'histogram')
+
+ hist = histogram.Histogram('', 'count')
+
+ # When Histograms are merged, first an empty clone is created with an empty
+ # DiagnosticMap.
+ hist2 = histogram.Histogram('', 'count')
+ hist2.diagnostics['a'] = generic
+ hist.diagnostics.Merge(hist2.diagnostics)
+ self.assertIs(generic, hist.diagnostics['a'])
+
+ # Separate keys are not merged.
+ hist3 = histogram.Histogram('', 'count')
+ hist3.diagnostics['b'] = generic2
+ hist.diagnostics.Merge(hist3.diagnostics)
+ self.assertIs(generic, hist.diagnostics['a'])
+ self.assertIs(generic2, hist.diagnostics['b'])
+
+ # Merging unmergeable diagnostics should produce an
+ # UnmergeableDiagnosticSet.
+ hist4 = histogram.Histogram('', 'count')
+ hist4.diagnostics['a'] = related_map
+ hist.diagnostics.Merge(hist4.diagnostics)
+ self.assertIsInstance(hist.diagnostics['a'], ums.UnmergeableDiagnosticSet)
+ diagnostics = list(hist.diagnostics['a'])
+ self.assertIs(generic, diagnostics[0])
+ self.assertIs(related_map, diagnostics[1])
+
+ # UnmergeableDiagnosticSets are mergeable.
+ hist5 = histogram.Histogram('', 'count')
+ hist5.diagnostics['a'] = ums.UnmergeableDiagnosticSet([events, generic2])
+ hist.diagnostics.Merge(hist5.diagnostics)
+ self.assertIsInstance(hist.diagnostics['a'], ums.UnmergeableDiagnosticSet)
+ diagnostics = list(hist.diagnostics['a'])
+ self.assertIs(generic, diagnostics[0])
+ self.assertIs(related_map, diagnostics[1])
+ self.assertIs(events, diagnostics[2])
+ self.assertIs(generic2, diagnostics[3])
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py
new file mode 100644
index 00000000000..ce1b90f55b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv.py
@@ -0,0 +1,22 @@
+# Copyright 2017 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 vinn
+import tracing_project
+
+def HistogramsToCsv(json_path):
+ """Convert HistogramSet JSON to CSV.
+
+ Args:
+ json_path: path to a file containing HistogramSet JSON
+
+ Returns:
+ a Vinn result object whose 'returncode' indicates whether there was an
+ exception, and whose 'stdout' contains CSV.
+ """
+ return vinn.RunFile(
+ os.path.join(os.path.dirname(__file__), 'histograms_to_csv_cmdline.html'),
+ source_paths=list(tracing_project.TracingProject().source_paths),
+ js_args=[os.path.abspath(json_path)])
diff --git a/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html
new file mode 100644
index 00000000000..c3b5163544e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/histograms_to_csv_cmdline.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/value/csv_builder.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+if (tr.isHeadless) {
+ const histograms = new tr.v.HistogramSet();
+ histograms.importDicts(JSON.parse(tr.b.getSync('file://' + sys.argv[1])));
+ const csv = new tr.v.CSVBuilder(histograms);
+ csv.build();
+ console.log(csv.toString());
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py
new file mode 100755
index 00000000000..a940f3dde5c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter.py
@@ -0,0 +1,68 @@
+# 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 tracing.value import histogram
+from tracing.value import histogram_set
+from tracing.value.diagnostics import generic_set
+from tracing.value.diagnostics import reserved_infos
+
+
+def ConvertLegacyDicts(dicts):
+ """Convert legacy JSON dicts to Histograms.
+
+ Args:
+ dicts: A list of v0 JSON dicts
+
+ Returns:
+ A HistogramSet containing equivalent histograms and diagnostics
+ """
+ if len(dicts) < 1:
+ return histogram_set.HistogramSet()
+
+ first_dict = dicts[0]
+ master = first_dict['master']
+ bot = first_dict['bot']
+ suite = first_dict['test'].split('/')[0]
+
+ hs = histogram_set.HistogramSet()
+
+ for d in dicts:
+ assert d['master'] == master
+ assert d['bot'] == bot
+
+ test_parts = d['test'].split('/')
+ assert test_parts[0] == suite
+ name = test_parts[1]
+
+ # TODO(843643): Generalize this
+ assert 'units' not in d
+ # TODO(861822): Port this to CreateHistogram
+ h = histogram.Histogram(name, 'unitless')
+ h.AddSample(d['value'])
+ # TODO(876379): Support more than three components
+ if len(test_parts) == 3:
+ h.diagnostics[reserved_infos.STORIES.name] = generic_set.GenericSet(
+ [test_parts[2]])
+
+ hs.AddHistogram(h)
+
+ hs.AddSharedDiagnosticToAllHistograms(
+ reserved_infos.MASTERS.name, generic_set.GenericSet([master]))
+ hs.AddSharedDiagnosticToAllHistograms(
+ reserved_infos.BOTS.name, generic_set.GenericSet([bot]))
+ hs.AddSharedDiagnosticToAllHistograms(
+ reserved_infos.BENCHMARKS.name, generic_set.GenericSet([suite]))
+ _AddRevision(first_dict, hs)
+
+ return hs
+
+
+def _AddRevision(d, hs):
+ r_commit_pos = d.get('supplemental_columns', {}).get('r_commit_pos')
+ rev = d['revision']
+ if r_commit_pos == rev:
+ name = reserved_infos.CHROMIUM_COMMIT_POSITIONS.name
+ else:
+ name = reserved_infos.POINT_ID.name
+ hs.AddSharedDiagnosticToAllHistograms(name, generic_set.GenericSet([rev]))
diff --git a/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py
new file mode 100644
index 00000000000..84b173fd04b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/legacy_json_converter_unittest.py
@@ -0,0 +1,116 @@
+# 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.
+
+import unittest
+
+from tracing.value import legacy_json_converter
+from tracing.value.diagnostics import reserved_infos
+
+
+class LegacyJsonConverterUnittest(unittest.TestCase):
+
+ def testConvertBasic(self):
+ dicts = [{
+ 'master': 'Master',
+ 'bot': 'Bot',
+ 'test': 'Suite/Metric',
+ 'revision': 1234,
+ 'value': 42.0
+ }]
+
+ histograms = legacy_json_converter.ConvertLegacyDicts(dicts)
+
+ self.assertEqual(len(histograms), 1)
+
+ h = histograms.GetFirstHistogram()
+
+ self.assertEqual(h.name, 'Metric')
+ self.assertEqual(len(h.diagnostics), 4)
+
+ masters = h.diagnostics[reserved_infos.MASTERS.name]
+ self.assertEqual(masters.GetOnlyElement(), 'Master')
+
+ bots = h.diagnostics[reserved_infos.BOTS.name]
+ self.assertEqual(bots.GetOnlyElement(), 'Bot')
+
+ benchmarks = h.diagnostics[reserved_infos.BENCHMARKS.name]
+ self.assertEqual(benchmarks.GetOnlyElement(), 'Suite')
+
+ point_id = h.diagnostics[reserved_infos.POINT_ID.name].GetOnlyElement()
+ self.assertEqual(point_id, 1234)
+
+ self.assertEqual(h.num_values, 1)
+ self.assertEqual(h.average, 42.0)
+
+ def testConvertWithStory(self):
+ dicts = [{
+ 'master': 'Master',
+ 'bot': 'Bot',
+ 'test': 'Suite/Metric/Case',
+ 'revision': 1234,
+ 'value': 42.0
+ }]
+
+ histograms = legacy_json_converter.ConvertLegacyDicts(dicts)
+ h = histograms.GetFirstHistogram()
+
+ stories = h.diagnostics[reserved_infos.STORIES.name]
+ self.assertEqual(stories.GetOnlyElement(), 'Case')
+
+ def testConvertWithRCommitPos(self):
+ dicts = [{
+ 'master': 'Master',
+ 'bot': 'Bot',
+ 'test': 'Suite/Metric/Case',
+ 'revision': 1234,
+ 'value': 42.0,
+ 'supplemental_columns': {
+ 'r_commit_pos': 1234
+ }
+ }]
+
+ histograms = legacy_json_converter.ConvertLegacyDicts(dicts)
+ h = histograms.GetFirstHistogram()
+
+ commit_pos = h.diagnostics[reserved_infos.CHROMIUM_COMMIT_POSITIONS.name]
+ self.assertEqual(commit_pos.GetOnlyElement(), 1234)
+ self.assertNotIn(reserved_infos.POINT_ID.name, h.diagnostics)
+
+ def testConvertWithDifferentRCommitPos(self):
+ dicts = [{
+ 'master': 'Master',
+ 'bot': 'Bot',
+ 'test': 'Suite/Metric/Case',
+ 'revision': 1234,
+ 'value': 42.0,
+ 'supplemental_columns': {
+ 'r_commit_pos': 2435
+ }
+ }]
+
+ histograms = legacy_json_converter.ConvertLegacyDicts(dicts)
+ h = histograms.GetFirstHistogram()
+
+ point_id = h.diagnostics[reserved_infos.POINT_ID.name].GetOnlyElement()
+ self.assertEqual(point_id, 1234)
+ self.assertNotIn(reserved_infos.CHROMIUM_COMMIT_POSITIONS.name,
+ h.diagnostics)
+
+ def testConvertUsesPointIdIfSupplementalColumnsButNoRCommitPos(self):
+ dicts = [{
+ 'master': 'Master',
+ 'bot': 'Bot',
+ 'test': 'Suite/Metric/Case',
+ 'revision': 1234,
+ 'value': 42.0,
+ 'supplemental_columns': {
+ 'r_v8_rev': 1234
+ }
+ }]
+
+ histograms = legacy_json_converter.ConvertLegacyDicts(dicts)
+ h = histograms.GetFirstHistogram()
+
+ point_id = h.diagnostics[reserved_infos.POINT_ID.name].GetOnlyElement()
+ self.assertEqual(point_id, 1234)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html b/chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html
new file mode 100644
index 00000000000..801ae4d6533
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/legacy_unit_info.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v', function() {
+ // This information is used to convert results from chart-json format to
+ // Histograms.
+ // Improvement directions are copied from
+ // telemetry/telemetry/value/unit-info.json
+ // but can be overridden by 'improvement_direction' in chart-json.
+ const LEGACY_UNIT_INFO = new Map();
+ LEGACY_UNIT_INFO.set('%', {
+ name: 'normalizedPercentage',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.DONT_CARE,
+ });
+ LEGACY_UNIT_INFO.set('Celsius', {
+ name: 'unitlessNumber',
+ // Colder machines are faster.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('Hz', {
+ name: 'unitlessNumber',
+ // Higher frequencies are faster.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('KB', {
+ name: 'sizeInBytes',
+ conversionFactor: 1024,
+ // Less memory usage is better.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('MB', {
+ name: 'sizeInBytes',
+ conversionFactor: 1024 * 1024,
+ // Less memory usage is better.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('ObjectsAt30FPS', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('available_kB', {
+ name: 'sizeInBytes',
+ conversionFactor: 1024,
+ // More memory available is better.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('bit/s', {
+ name: 'unitlessNumber',
+ // TODO(#3815) Reconcile with char/s.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('bytes', {
+ name: 'sizeInBytes',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('chars/s', {
+ name: 'unitlessNumber',
+ // TODO(#3815) Reconcile with bit/s.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('commit_count', {
+ name: 'count',
+ // layer_tree_host_perftest
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('count', {
+ name: 'count',
+ // Processes
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('coverage%', {
+ name: 'normalizedPercentage',
+ // Used in alloy-perf-test/cts%/passed.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('dB', {
+ name: 'unitlessNumber',
+ // Decibels peak signal-to-noise ratio. Used by WebRTC quality tests.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('files', {
+ name: 'count',
+ // Static initializers
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('fps', {
+ name: 'unitlessNumber',
+ // Used by scirra benchmark.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('frame_count', {
+ name: 'count',
+ // layer_tree_host_perftest
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('frame_time', {
+ name: 'timeDurationInMs',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('frames', {
+ name: 'count',
+ // Dropped frames.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('frames-per-second', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('garbage_collections', {
+ name: 'count',
+ // Number of GCs needed to collect an object. Less is better.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('idle%', {
+ name: 'normalizedPercentage',
+ // Percentage of work done in idle time.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('janks', {
+ name: 'count',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('kb', {
+ name: 'sizeInBytes',
+ conversionFactor: 1024,
+ // Less memory usage is better.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('lines', {
+ name: 'count',
+ // More test coverage is better.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('mWh', {
+ name: 'energyInJoules',
+ conversionFactor: 3.6,
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('milliseconds', {
+ name: 'timeDurationInMs',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('milliseconds-per-frame', {
+ name: 'timeDurationInMs',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('minutes', {
+ name: 'timeInMsAutoFormat',
+ conversionFactor: 60e3,
+ // Used for NaCl build time.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('mips', {
+ name: 'unitlessNumber',
+ // More instructions processed per time unit.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('mpixels_sec', {
+ name: 'unitlessNumber',
+ // More pixels processed per time unit.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('ms', {
+ name: 'timeDurationInMs',
+ // Used in many Telemetry measurements. Fewer ms of time means faster.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('mtri_sec', {
+ name: 'unitlessNumber',
+ // More triangles processed per time unit.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('mvtx_sec', {
+ name: 'unitlessNumber',
+ // More vertices processed per time unit.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('objects (bigger is better)', {
+ name: 'count',
+ // Used in spaceport benchmark.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('packets', {
+ name: 'count',
+ // Monitors how many packets we use to accomplish something.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('percent', {
+ name: 'normalizedPercentage',
+ // Synonym for %, used in memory metric for percent fragmentation.
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('points', {
+ name: 'unitlessNumber',
+ // Synonym for score, used in ChromeOS touchpad tests.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('ports', {
+ name: 'count',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('reduction%', {
+ name: 'normalizedPercentage',
+ // Used in draw_property measurement to indicate relative improvement.
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('relocs', {
+ name: 'count',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('runs/s', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('runs_per_s', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('runs_per_second', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('score (bigger is better)', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('score', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('score_(bigger_is_better)', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('seconds', {
+ name: 'timeDurationInMs',
+ conversionFactor: 1e3,
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('tokens/s', {
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.BIGGER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('tasks', {
+ // Used by thread_times but actually indicates tasks/s, so not count.
+ name: 'unitlessNumber',
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+ LEGACY_UNIT_INFO.set('us', {
+ name: 'timeDurationInMs',
+ conversionFactor: 1e-3,
+ defaultImprovementDirection: tr.b.ImprovementDirection.SMALLER_IS_BETTER,
+ });
+
+ return {
+ LEGACY_UNIT_INFO,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py
new file mode 100644
index 00000000000..c7e4c9a95b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms.py
@@ -0,0 +1,34 @@
+# 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 json
+import os
+import sys
+
+import tracing_project
+import vinn
+
+_MERGE_HISTOGRAMS_CMD_LINE = os.path.join(
+ os.path.dirname(__file__), 'merge_histograms_cmdline.html')
+
+
+def MergeHistograms(json_path, groupby=()):
+ """Merge Histograms.
+
+ Args:
+ json_path: Path to a HistogramSet JSON file.
+ groupby: Array of grouping keys (name, benchmark, time, storyset_repeat,
+ story_repeat, story, tir, label)
+ Returns:
+ HistogramSet dicts of the merged Histograms.
+ """
+ result = vinn.RunFile(
+ _MERGE_HISTOGRAMS_CMD_LINE,
+ source_paths=list(tracing_project.TracingProject().source_paths),
+ js_args=[os.path.abspath(json_path)] + list(groupby))
+ if result.returncode != 0:
+ sys.stderr.write(result.stdout)
+ raise Exception('vinn merge_histograms_cmdline.html returned ' +
+ str(result.returncode))
+ return json.loads(result.stdout)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html
new file mode 100644
index 00000000000..090390e81e8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_cmdline.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+
+<script>
+'use strict';
+/* eslint-disable no-console */
+
+function findGrouping(key) {
+ const grouping = tr.v.HistogramGrouping.BY_KEY.get(key);
+ if (grouping === undefined) {
+ throw new Error(`Could not find grouping "${key}"`);
+ }
+ return grouping;
+}
+
+function findMergeable(hist, candidates) {
+ for (const candidate of candidates) {
+ if (candidate.canAddHistogram(hist)) return candidate;
+ }
+ return undefined;
+}
+
+function mergeLeafHistograms(groupedHistograms, mergedHistograms) {
+ for (const [name, histograms] of groupedHistograms) {
+ if (histograms instanceof Map) {
+ mergeLeafHistograms(histograms, mergedHistograms);
+ continue;
+ }
+
+ if (histograms.length === 1) {
+ mergedHistograms.addHistogram(histograms[0].clone());
+ continue;
+ }
+
+ // Merge Histograms in this leaf array and return the merged Histograms to
+ // mergedHistograms.
+ // If it isn't possible to merge all Histograms in |histograms| together,
+ // then merge them into as few merged Histograms as possible.
+ const merged = [histograms.shift().clone()];
+ for (const hist of histograms) {
+ const candidate = findMergeable(hist, merged);
+ if (candidate !== undefined) {
+ candidate.addHistogram(hist);
+ continue;
+ }
+ merged.push(hist.clone());
+ }
+ for (const hist of merged) {
+ mergedHistograms.addHistogram(hist);
+ }
+ }
+}
+
+function stripInternalDiagnostics(mergedHistograms) {
+ for (const hist of mergedHistograms) {
+ hist.diagnostics.delete(tr.v.d.RESERVED_NAMES.TEST_PATH);
+ }
+}
+
+function mergeHistograms(histogramsPath, groupingKeys) {
+ const histograms = new tr.v.HistogramSet();
+ histograms.importDicts(JSON.parse(tr.b.getSync('file://' + histogramsPath)));
+ histograms.buildGroupingsFromTags([tr.v.d.RESERVED_NAMES.STORY_TAGS]);
+ const groupings = groupingKeys.map(findGrouping);
+ const groupedHistograms = histograms.groupHistogramsRecursively(groupings);
+ const mergedHistograms = new tr.v.HistogramSet();
+ mergeLeafHistograms(groupedHistograms, mergedHistograms);
+ mergedHistograms.deduplicateDiagnostics();
+ stripInternalDiagnostics(mergedHistograms);
+ return mergedHistograms;
+}
+
+if (tr.isHeadless) {
+ const mergedHistograms = mergeHistograms(sys.argv[1], sys.argv.slice(2));
+ console.log(JSON.stringify(mergedHistograms.asDicts()));
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py
new file mode 100644
index 00000000000..376f3ac59d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/merge_histograms_unittest.py
@@ -0,0 +1,31 @@
+# Copyright 2017 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 json
+import os
+import tempfile
+import unittest
+
+from tracing.value import histogram
+from tracing.value import histogram_set
+from tracing.value import merge_histograms
+
+
+class MergeHistogramsUnittest(unittest.TestCase):
+
+ def testSingularHistogramsGetMergedFrom(self):
+ hist0 = histogram.Histogram('foo', 'count')
+ hist1 = histogram.Histogram('bar', 'count')
+ histograms = histogram_set.HistogramSet([hist0, hist1])
+ histograms_file = tempfile.NamedTemporaryFile(delete=False)
+ histograms_file.write(json.dumps(histograms.AsDicts()).encode('utf-8'))
+ histograms_file.close()
+
+ merged_dicts = merge_histograms.MergeHistograms(histograms_file.name,
+ ('name',))
+ merged_histograms = histogram_set.HistogramSet()
+ merged_histograms.ImportDicts(merged_dicts)
+ self.assertEqual(len(list(merged_histograms.shared_diagnostics)), 0)
+ self.assertEqual(len(merged_histograms), 2)
+ os.remove(histograms_file.name)
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html
new file mode 100644
index 00000000000..228afbd2891
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html
@@ -0,0 +1,350 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/fixed_color_scheme.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html">
+<link rel="import" href="/tracing/metrics/all_fixed_color_schemes.html">
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-breakdown-span">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #table_container {
+ display: flex;
+ flex: 0 0 auto;
+ }
+ #table {
+ max-height: 150px;
+ overflow-y: auto;
+ }
+ </style>
+
+ <div id="empty">(empty)</div>
+ <div id="table_container">
+ <div id="container"></div>
+ <span>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </span>
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'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() {
+ // Prevent this row from appearing in the ColumnChart.
+ 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';
+ }
+ }
+ return this.keySpan_;
+ }
+
+ get name() {
+ return 'Sum';
+ }
+
+ get displayElement() {
+ return this.displayElement_;
+ }
+
+ get stringPercent() {
+ return '100%';
+ }
+ }
+
+ class BreakdownTableRow {
+ constructor(name, value, histogramName, unit, color) {
+ this.name_ = name;
+ this.value_ = value;
+ this.histogramName_ = histogramName;
+ this.unit_ = unit;
+
+ if (typeof value !== 'number') {
+ throw new Error('unsupported value ' + value);
+ }
+
+ this.tableSum_ = undefined;
+ this.keySpan_ = undefined;
+
+ this.color_ = color;
+ const hsl = this.color.toHSL();
+ hsl.l *= 0.85;
+ this.highlightedColor_ = tr.b.Color.fromHSL(hsl);
+
+ if (this.unit_) {
+ this.displayElement_ = tr.v.ui.createScalarSpan(this.numberValue, {
+ unit: this.unit_,
+ });
+ } else {
+ this.displayElement_ = tr.ui.b.createSpan({
+ textContent: this.stringValue,
+ });
+ }
+ }
+
+ get name() {
+ return this.name_;
+ }
+
+ get color() {
+ return this.color_;
+ }
+
+ get highlightedColor() {
+ return this.highlightedColor_;
+ }
+
+ get keySpan() {
+ if (this.keySpan_ === undefined) {
+ if (this.histogramName_) {
+ this.keySpan_ = document.createElement('tr-ui-a-analysis-link');
+ this.keySpan_.setSelectionAndContent(
+ [this.histogramName_], this.name);
+ this.keySpan_.color = this.color;
+ this.keySpan_.title = this.histogramName_;
+ } else {
+ this.keySpan_ = document.createElement('span');
+ this.keySpan_.innerText = this.name;
+ this.keySpan_.style.color = this.color;
+ }
+ }
+ return this.keySpan_;
+ }
+
+ /**
+ * @return {number|undefined}
+ */
+ get numberValue() {
+ if (!isNaN(this.value_) &&
+ (this.value_ !== Infinity) &&
+ (this.value_ !== -Infinity) &&
+ (this.value_ > 0)) return this.value_;
+ // Prevent this row from appearing in the ColumnChart.
+ return undefined;
+ }
+
+ get stringValue() {
+ if ((this.unit_ !== undefined) &&
+ !isNaN(this.value_) &&
+ (this.value_ !== Infinity) &&
+ (this.value_ !== -Infinity)) {
+ return this.unit_.format(this.value_);
+ }
+ return this.value_.toString();
+ }
+
+ set tableSum(s) {
+ this.tableSum_ = s;
+ }
+
+ get stringPercent() {
+ if (this.tableSum_ === undefined) return '';
+ const num = this.numberValue;
+ if (num === undefined) return '';
+ return Math.floor(num * 100.0 / this.tableSum_) + '%';
+ }
+
+ get displayElement() {
+ return this.displayElement_;
+ }
+
+ compare(other) {
+ if (this.numberValue === undefined) {
+ if (other.numberValue === undefined) {
+ return this.name.localeCompare(other.name);
+ }
+ return 1;
+ }
+ if (other.numberValue === undefined) {
+ return -1;
+ }
+ 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;
+ // https://github.com/catapult-project/catapult/issues/2970
+ 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) {
+ 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;
+
+ // Let the row compute its percentage.
+ row.tableSum = tableSum;
+
+ // Add it to the chart.
+ chartData[row.name] = row.numberValue;
+
+ // Configure the colors.
+ const dataSeries = this.chart_.getDataSeries(row.name);
+ dataSeries.color = row.color;
+ dataSeries.highlightedColor = row.highlightedColor;
+ }
+
+ if (tableRows.length > 0) {
+ this.$.table.style.display = 'block';
+ this.$.empty.style.display = 'none';
+ this.$.table.tableRows = tableRows;
+ this.$.table.rebuild();
+ }
+
+ if (Object.keys(chartData).length > 1) {
+ this.$.container.style.display = 'block';
+ this.$.empty.style.display = 'none';
+ this.chart_.data = [chartData];
+ }
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html
new file mode 100644
index 00000000000..71079cdc856
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/fixed_color_scheme.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/breakdown.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/breakdown_span.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_Breakdown', function() {
+ let breakdown = new tr.v.d.Breakdown();
+ breakdown.colorScheme =
+ tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;
+ breakdown.set('script', 42);
+ breakdown.set('style', 57);
+
+ // Test weird numbers.
+ breakdown.set('ba---a', NaN);
+ breakdown.set('inf', Infinity);
+ breakdown.set('-inf', -Infinity);
+ breakdown.set('goose egg', 0);
+ breakdown.set('<0', -1);
+
+ // Test lots of categories
+ for (let i = 0; i < 10; ++i) {
+ breakdown.set('cat ' + i, i);
+ }
+
+ // Test round-tripping.
+ breakdown = tr.v.d.Diagnostic.fromDict(breakdown.asDict());
+
+ const span = tr.v.ui.createDiagnosticSpan(breakdown);
+ assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_BreakdownWithFixedColorScheme', function() {
+ const colorScheme = tr.b.FixedColorScheme.fromNames([
+ 'foo',
+ 'bar',
+ ]);
+ tr.b.FixedColorSchemeRegistry.register(() => colorScheme, {
+ 'name': 'myColorScheme',
+ });
+
+ let breakdown = new tr.v.d.Breakdown();
+ breakdown.colorScheme = 'myColorScheme';
+ breakdown.set('foo', 42);
+ breakdown.set('bar', 57);
+
+ // Test round-tripping.
+ breakdown = tr.v.d.Diagnostic.fromDict(breakdown.asDict());
+
+ const span = tr.v.ui.createDiagnosticSpan(breakdown);
+ span.updateContents_();
+ assert.strictEqual(
+ span.chart_.getDataSeries('foo').color, colorScheme.getColor('foo'));
+ this.addHTMLOutput(span);
+ });
+
+ test('empty', function() {
+ const breakdown = new tr.v.d.Breakdown();
+ const span = tr.v.ui.createDiagnosticSpan(breakdown);
+ assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+
+ test('emptyExceptForWeirdNumbers', function() {
+ const breakdown = new tr.v.d.Breakdown();
+ breakdown.set('ba---a', NaN);
+ breakdown.set('inf', Infinity);
+ breakdown.set('-inf', -Infinity);
+ breakdown.set('goose egg', 0);
+ breakdown.set('<0', -1);
+
+ const span = tr.v.ui.createDiagnosticSpan(breakdown);
+ assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+
+ test('correlate', function() {
+ const histograms = new tr.v.HistogramSet();
+ const sample0Breakdown = new tr.v.d.Breakdown();
+ sample0Breakdown.set('a', 5);
+ sample0Breakdown.set('b', 3);
+ sample0Breakdown.set('c', 2);
+ const sample1Breakdown = new tr.v.d.Breakdown();
+ sample1Breakdown.set('a', 50);
+ sample1Breakdown.set('b', 30);
+ sample1Breakdown.set('c', 20);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('a', histograms.createHistogram('root:a',
+ tr.b.Unit.byName.timeDurationInMs, [5, 50]).name);
+ related.set('b', tr.v.Histogram.create('root:b',
+ tr.b.Unit.byName.timeDurationInMs, [3, 30]).name);
+ related.set('c', tr.v.Histogram.create('root:c',
+ tr.b.Unit.byName.timeDurationInMs, [2, 20]).name);
+ const hist = histograms.createHistogram('root',
+ tr.b.Unit.byName.timeDurationInMs, [
+ {
+ value: 10,
+ diagnostics: new Map([['breakdown', sample0Breakdown]]),
+ },
+ {
+ value: 100,
+ diagnostics: new Map([['breakdown', sample1Breakdown]]),
+ },
+ ], {
+ diagnostics: new Map([
+ ['breakdown', related],
+ ]),
+ });
+ const span = tr.v.ui.createDiagnosticSpan(sample0Breakdown, 'breakdown',
+ hist);
+ this.addHTMLOutput(span);
+ const links = tr.ui.b.findDeepElementsMatching(span,
+ 'tr-ui-a-analysis-link');
+ assert.lengthOf(links, 4);
+ assert.strictEqual(links[0].title, '');
+ assert.strictEqual(links[1].title, 'root:a');
+ assert.strictEqual(links[2].title, 'root:b');
+ assert.strictEqual(links[3].title, 'root:c');
+ assert.strictEqual(links[0].textContent, 'Select All');
+ assert.strictEqual(links[1].textContent, 'a');
+ assert.strictEqual(links[2].textContent, 'b');
+ assert.strictEqual(links[3].textContent, 'c');
+ assert.lengthOf(links[0].selection, 3);
+ assert.strictEqual(links[0].selection[0], 'root:a');
+ assert.strictEqual(links[0].selection[1], 'root:b');
+ assert.strictEqual(links[0].selection[2], 'root:c');
+ assert.lengthOf(links[1].selection, 1);
+ assert.strictEqual(links[1].selection[0], 'root:a');
+ assert.lengthOf(links[2].selection, 1);
+ assert.strictEqual(links[2].selection[0], 'root:b');
+ assert.lengthOf(links[3].selection, 1);
+ assert.strictEqual(links[3].selection[0], 'root:c');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html
new file mode 100644
index 00000000000..08e0cc91dca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-collected-related-event-set-span">
+</dom-module>
+
+<script>
+'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 {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html
new file mode 100644
index 00000000000..c14f75a9826
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('merge', function() {
+ let aSlice;
+ let bSlice;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ aSlice = tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ title: 'a',
+ start: 0,
+ duration: 10
+ });
+ bSlice = tr.c.TestUtils.newSliceEx({
+ type: tr.model.ThreadSlice,
+ title: 'b',
+ start: 1,
+ duration: 10
+ });
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ thread.sliceGroup.pushSlice(aSlice);
+ thread.sliceGroup.pushSlice(bSlice);
+ });
+ assert.notEqual(aSlice.stableId, bSlice.stableId);
+
+ const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count);
+ const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count);
+
+ aHist.diagnostics.set('events', new tr.v.d.RelatedEventSet(aSlice));
+ bHist.diagnostics.set('events', new tr.v.d.RelatedEventSet(bSlice));
+
+ let mergedHist = aHist.clone();
+ mergedHist.addHistogram(bHist);
+ mergedHist = tr.v.Histogram.fromDict(mergedHist.asDict());
+
+ const mergedEvents = mergedHist.diagnostics.get('events');
+ const span = tr.v.ui.createDiagnosticSpan(mergedEvents);
+ assert.strictEqual(
+ 'TR-V-UI-COLLECTED-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html
new file mode 100644
index 00000000000..29773057810
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-date-range-span">
+ <template>
+ <content></content>
+ </template>
+</dom-module>
+
+<script>
+'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 {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html
new file mode 100644
index 00000000000..3e7f02f0727
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/date_range.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_one', function() {
+ const diagnostic = new tr.v.d.DateRange(1496693745398);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-DATE-RANGE-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_merged', function() {
+ const diagnostic = new tr.v.d.DateRange(1496693745398);
+ diagnostic.addDiagnostic(new tr.v.d.DateRange(1496693745399));
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-DATE-RANGE-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html
new file mode 100644
index 00000000000..3a600c00925
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<dom-module id="tr-v-ui-diagnostic-map-table">
+ <template>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'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_;
+ },
+
+ /**
+ * The |title| will be used as the heading for the column containing
+ * diagnostic-spans for |diagnosticMap|'s Diagnostics.
+ *
+ * @param {!Array.<!Object>} maps
+ * @param {!string} maps[].title
+ * @param {!tr.v.d.DiagnosticMap} maps[].diagnosticMap
+ */
+ 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) {
+ // https://github.com/catapult-project/catapult/issues/2842
+ 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 => {
+ // tr-ui-b-table requires rows to be objects.
+ return {name};
+ });
+ } else {
+ this.$.table.tableColumns = names.map(
+ name => makeColumn(name, histogram));
+ this.$.table.tableRows = this.diagnosticMaps_;
+ }
+
+ this.$.table.rebuild();
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html
new file mode 100644
index 00000000000..d5d4ac02761
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const map0 = new tr.v.d.DiagnosticMap();
+ map0.set('genericA', new tr.v.d.GenericSet([{a: 0}]));
+ map0.set('genericB', new tr.v.d.GenericSet([{b: 0}]));
+ const map1 = new tr.v.d.DiagnosticMap();
+ map1.set('genericA', new tr.v.d.GenericSet([{a: 1}]));
+ map1.set('genericB', new tr.v.d.GenericSet([{b: 1}]));
+ const table = document.createElement('tr-v-ui-diagnostic-map-table');
+ table.diagnosticMaps = [map0, map1];
+ this.addHTMLOutput(table);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html
new file mode 100644
index 00000000000..741fc07f58e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic.html">
+<link rel="import" href="/tracing/value/ui/breakdown_span.html">
+<link rel="import" href="/tracing/value/ui/collected_related_event_set_span.html">
+<link rel="import" href="/tracing/value/ui/date_range_span.html">
+<link rel="import" href="/tracing/value/ui/generic_set_span.html">
+<link rel="import" href="/tracing/value/ui/related_event_set_span.html">
+<link rel="import" href="/tracing/value/ui/scalar_diagnostic_span.html">
+<link rel="import" href="/tracing/value/ui/unmergeable_diagnostic_set_span.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ /**
+ * Find the name of a polymer element registered to display |diagnostic|
+ * or one of its base classes.
+ *
+ * @param {!tr.v.d.Diagnostic} diagnostic
+ * @return {string}
+ */
+ 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;
+ }
+
+ /**
+ * Create a visualization for |diagnostic|.
+ *
+ * @param {!tr.v.d.Diagnostic} diagnostic
+ * @param {string} name
+ * @param {!tr.v.Histogram} histogram
+ * @return {Element}
+ */
+ function createDiagnosticSpan(diagnostic, name, histogram) {
+ const tagName = findElementNameForDiagnostic(diagnostic);
+ const span = document.createElement(tagName);
+ if (span.build === undefined) throw new Error(tagName);
+ span.build(diagnostic, name, histogram);
+ return span;
+ }
+
+ return {
+ createDiagnosticSpan,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html
new file mode 100644
index 00000000000..a40c15cb1c8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html
new file mode 100644
index 00000000000..6f355e44478
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-generic-set-span">
+ <template>
+ <style>
+ a {
+ display: block;
+ }
+ </style>
+
+ <tr-ui-a-generic-object-view id="generic"></tr-ui-a-generic-object-view>
+
+ <div id="links"></div>
+ </template>
+</dom-module>
+
+<script>
+'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 (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';
+ // In case there's a listener in the hierarchy that calls
+ // preventDefault(), stop the event from propagating to it so that
+ // clicking the link always opens it in a new tab.
+ linkEl.addEventListener('click', e => e.stopPropagation());
+ }
+ this.$.links.appendChild(linkEl);
+ }
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html
new file mode 100644
index 00000000000..6bddec81aa6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/generic_set.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('link_tuple', function() {
+ const diagnostic = new tr.v.d.GenericSet([
+ ['label', 'http://example.com/'],
+ ]);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ const links = tr.ui.b.findDeepElementsMatching(span, 'a');
+ assert.lengthOf(links, diagnostic.size);
+ assert.strictEqual('label', links[0].textContent);
+ assert.strictEqual('http://example.com/', links[0].href);
+ });
+
+ test('instantiate', function() {
+ const diagnostic = new tr.v.d.GenericSet([{foo: 'bar', baz: [42]}]);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+
+ test('strings', function() {
+ const diagnostic = new tr.v.d.GenericSet([
+ 'foo', 'bar', 1, 0, Infinity, NaN,
+ ]);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ const links = tr.ui.b.findDeepElementsMatching(span, 'a');
+ assert.lengthOf(links, diagnostic.size);
+ assert.strictEqual(links[0].textContent, '0');
+ assert.strictEqual(links[0].href, '');
+ assert.strictEqual(links[1].textContent, '1');
+ assert.strictEqual(links[1].href, '');
+ assert.strictEqual(links[2].textContent, 'Infinity');
+ assert.strictEqual(links[2].href, '');
+ assert.strictEqual(links[3].textContent, 'NaN');
+ assert.strictEqual(links[3].href, '');
+ assert.strictEqual(links[4].textContent, 'bar');
+ assert.strictEqual(links[4].href, '');
+ assert.strictEqual(links[5].textContent, 'foo');
+ assert.strictEqual(links[5].href, '');
+ });
+
+ test('numbers', function() {
+ const diagnostic = new tr.v.d.GenericSet([10, 1, 0, 2, 11]);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ const links = tr.ui.b.findDeepElementsMatching(span, 'a');
+ assert.lengthOf(links, diagnostic.size);
+ assert.strictEqual(links[0].textContent, '0');
+ assert.strictEqual(links[0].href, '');
+ assert.strictEqual(links[1].textContent, '1');
+ assert.strictEqual(links[1].href, '');
+ assert.strictEqual(links[2].textContent, '2');
+ assert.strictEqual(links[2].href, '');
+ assert.strictEqual(links[3].textContent, '10');
+ assert.strictEqual(links[3].href, '');
+ assert.strictEqual(links[4].textContent, '11');
+ assert.strictEqual(links[4].href, '');
+ });
+
+ test('urls', function() {
+ const urls = [
+ 'http://google.com/',
+ 'http://cnn.com/',
+ ];
+ const span = tr.v.ui.createDiagnosticSpan(new tr.v.d.GenericSet(urls));
+ assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ const links = tr.ui.b.findDeepElementsMatching(span, 'a');
+ assert.lengthOf(links, urls.length);
+ assert.strictEqual(links[0].textContent, urls[1]);
+ assert.strictEqual(links[0].href, urls[1]);
+ assert.strictEqual(links[1].textContent, urls[0]);
+ assert.strictEqual(links[1].href, urls[0]);
+ });
+
+ test('traceUrls', function() {
+ const urls = [
+ 'https://console.developers.google.com/m/cloudstorage/b/chromium-telemetry/o/c.html',
+ 'file://d/e/f.html',
+ ];
+ const span = tr.v.ui.createDiagnosticSpan(
+ new tr.v.d.GenericSet(urls), tr.v.d.RESERVED_NAMES.TRACE_URLS);
+ assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ const links = tr.ui.b.findDeepElementsMatching(span, 'a');
+ assert.lengthOf(links, urls.length);
+ assert.strictEqual(links[0].textContent, 'f.html');
+ assert.strictEqual(links[0].href, urls[1]);
+ assert.strictEqual(links[1].textContent, 'c.html');
+ assert.strictEqual(links[1].href, urls[0]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md
new file mode 100644
index 00000000000..951e4a9918c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md
@@ -0,0 +1,71 @@
+<!-- Copyright 2017 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.
+-->
+
+# HistogramSet UI Architecture
+
+Documentation for users of this UI is in [metrics-results-ui](/docs/metrics-results-ui.md).
+
+This document outlines the MVC architecture of the implementation of the UI.
+ * Model: [HistogramSetViewState](/tracing/tracing/value/ui/histogram_set_view_state.html)
+ * searchQuery: regex filters Histogram names
+ * referenceDisplayLabel selects the reference column in the table
+ * showAll: when false, only sourceHistograms are shown in the table
+ * groupings: array of HistogramGroupings configures how the hierarchy is constructed
+ * sortColumnIndex
+ * sortDescending
+ * constrainNameColumn: whether the Name column in the table is constrained to 300px
+ * tableRowStates: Map from row name to HistogramSetTableRowState
+ * subRows: Map from row name to HistogramSetTableRowState
+ * isExpanded: whether the row is expanded to show its subRows
+ * isOverviewed: whether the overview charts are displayed
+ * cells: map from column names to HistogramSetTableCellState:
+ * isOpen: whether the cell's histogram-span is open and displaying the BarChart and Diagnostics
+ * brushedBinRange: which bins are brushed in the BarChart
+ * mergeSampleDiagnostics: whether sample diagnostics are merged
+ * Setters delegate to the main entry point, update(delta), which dispatches an update event to listeners
+ * View-Controllers:
+ * [histogram-set-view](/tracing/tracing/value/ui/histogram_set_view.html):
+ * Main entry point: build(HistogramSet, progressIndicator):Promise
+ * Displays "zero Histograms"
+ * Listens for download-csv event from [histogram-set-controls](/tracing/tracing/value/ui/histogram_set_controls.html)
+ * gets leafHistograms from the [histogram-set-table](/tracing/tracing/value/ui/histogram_set_table.html)
+ * builds a CSV using [CSVBuilder](/tracing/tracing/value/csv_builder.html)
+ * Collects possible configurations of the HistogramSet and passes them to the child elements directly (not through the HistogramSetViewState!):
+ * Possible groupings
+ * displayLabels
+ * baseStatisticNames
+ * Contains child elements:
+ * [histogram-set-controls](/tracing/tracing/value/ui/histogram_set_controls.html)
+ * visualizes and controls the top half of HistogramSetViewState:
+ * searchQuery
+ * toggle display of all isOvervieweds
+ * referenceDisplayLabel
+ * showAll
+ * groupings
+ * Displays a button to download a CSV of the leafHistograms
+ * Displays a "Help" link to [metrics-results-ui](/docs/metrics-results-ui.md)
+ * [histogram-set-table](/tracing/tracing/value/ui/histogram_set_table.html)
+ * Visualizes and controls the bottom half of HistogramSetViewState:
+ * sortColumnIndex
+ * sortDescending
+ * constrainNameColumn
+ * HistogramSetTableRowStates
+ * Builds [HistogramSetTableRow](/tracing/tracing/value/ui/histogram_set_table_row.html)s containing
+ * [histogram-set-table-name-cell](/tracing/tracing/value/ui/histogram_set_table_name_cell.html)
+ * Toggles HistogramSetTableRowState.isOverviewed
+ * Overview [NameLineChart](/tracing/tracing/ui/base/name_line_chart.html)
+ * [histogram-set-table-cell](/tracing/tracing/value/ui/histogram_set_table_cell.html)
+ * (missing) / (empty) / (unmergeable)
+ * when closed, [scalar-span](/tracing/tracing/value/ui/scalar_span.html) displays a single summary statistic
+ * when open, [histogram-span](/tracing/tracing/value/ui/histogram_span.html) contains:
+ * [NameBarChart](/tracing/tracing/ui/base/name_bar_chart.html) visualizes and controls HistogramSetTableCellState.brushedBinRange
+ * [scalar-map-table](/tracing/tracing/value/ui/scalar_map_table.html) of statistics
+ * Two [diagnostic-map-tables](/tracing/tracing/value/ui/diagnostic_map_table.html): one for Histogram.diagnostics and another for the sample diagnostics
+ * A checkbox to visualize and control HistogramSetTableCellState.mergeSampleDiagnostics
+ * Overview [NameLineChart](/tracing/tracing/ui/base/name_line_chart.html)
+ * Main entry points:
+ * build(allHistograms, sourceHistograms, displayLabels, progressIndicator):Promise
+ * onViewStateUpdate_(delta)
+ * The [HistogramSetLocation](/tracing/tracing/value/ui/histogram_set_location.html) synchronizes the HistogramSetViewState with the URL using the HTML5 history API.
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html
new file mode 100644
index 00000000000..bb4e1e11ed0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram_importer.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(() => {
+ const kHtmlString = '<script>throw new Error("oops");<' + '/script>';
+
+ function createHistogram(id) {
+ const histogram =
+ new tr.v.Histogram('name<' + id + '>', tr.b.Unit.byName.count);
+ histogram.addSample(id);
+ histogram.customizeSummaryOptions({
+ count: false,
+ max: false,
+ min: false,
+ std: false,
+ sum: false,
+ });
+ histogram.diagnostics.set('html', new tr.v.d.GenericSet([kHtmlString]));
+ return histogram;
+ }
+
+ test('importZeroHistograms', async function() {
+ const loadingEl = document.createElement('div');
+ this.addHTMLOutput(loadingEl);
+ const importer = new tr.v.HistogramImporter(loadingEl);
+ const histogramData = '\n';
+
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ view.style.display = 'none';
+ this.addHTMLOutput(view);
+
+ await importer.importHistograms(histogramData, view);
+
+ assert.strictEqual('block', view.style.display);
+ assert.strictEqual(undefined, view.histograms);
+ });
+
+ test('importOneHistogram', async function() {
+ const loadingEl = document.createElement('div');
+ this.addHTMLOutput(loadingEl);
+ const importer = new tr.v.HistogramImporter(loadingEl);
+
+ const hist = createHistogram(42);
+ const histogramData = '\n' + JSON.stringify(hist.asDict()) + '\n';
+
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ view.style.display = 'none';
+ this.addHTMLOutput(view);
+
+ await importer.importHistograms(histogramData, view);
+
+ assert.strictEqual('none', loadingEl.style.display);
+ assert.strictEqual('block', view.style.display);
+ assert.lengthOf(view.histograms, 1);
+ const histogram = view.histograms.getHistogramNamed('name<42>');
+ assert.strictEqual(kHtmlString, tr.b.getOnlyElement(
+ histogram.diagnostics.get('html')));
+ assert.deepEqual([42], histogram.sampleValues);
+ });
+
+ test('importNHistogram', async function() {
+ const loadingEl = document.createElement('div');
+ this.addHTMLOutput(loadingEl);
+ const importer = new tr.v.HistogramImporter(loadingEl);
+
+ const kNofHistograms = 1000;
+ let histogramData = '\n';
+ for (let i = 0; i < kNofHistograms; i++) {
+ const id = kNofHistograms * 100 + i;
+ const histogram = createHistogram(id);
+ histogramData += JSON.stringify(histogram.asDict()) + '\n';
+ }
+
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ view.style.display = 'none';
+ this.addHTMLOutput(view);
+
+ await importer.importHistograms(histogramData, view);
+
+ assert.strictEqual('none', loadingEl.style.display);
+ assert.strictEqual('block', view.style.display);
+ assert.lengthOf(view.histograms, kNofHistograms);
+ for (let i = 0; i < kNofHistograms; i++) {
+ const id = kNofHistograms * 100 + i;
+ const histogram = view.histograms.getHistogramNamed('name<' + id + '>');
+ assert.strictEqual(kHtmlString, tr.b.getOnlyElement(
+ histogram.diagnostics.get('html')));
+ assert.deepEqual([id], histogram.sampleValues);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html
new file mode 100644
index 00000000000..c55093a90ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html
@@ -0,0 +1,557 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_controls_export.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+
+<dom-module id="tr-v-ui-histogram-set-controls">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+
+ #help, #feedback {
+ display: none;
+ margin-left: 20px;
+ }
+
+ #search_container {
+ display: inline-flex;
+ margin-right: 20px;
+ padding-bottom: 1px;
+ border-bottom: 1px solid darkgrey;
+ }
+
+ #search {
+ border: 0;
+ max-width: 20em;
+ outline: none;
+ }
+
+ #clear_search {
+ visibility: hidden;
+ height: 1em;
+ stroke: black;
+ stroke-width: 16;
+ }
+
+ #controls {
+ white-space: nowrap;
+ }
+
+ #show_overview, #hide_overview {
+ height: 1em;
+ margin-right: 20px;
+ }
+
+ #show_overview {
+ stroke: blue;
+ stroke-width: 16;
+ }
+
+ #show_overview:hover {
+ background: blue;
+ stroke: white;
+ }
+
+ #hide_overview {
+ display: none;
+ stroke-width: 18;
+ stroke: black;
+ }
+
+ #hide_overview:hover {
+ background: black;
+ stroke: white;
+ }
+
+ #reference_display_label {
+ display: none;
+ margin-right: 20px;
+ }
+
+ #alpha, #alpha_slider_container {
+ display: none;
+ }
+
+ #alpha {
+ margin-right: 20px;
+ }
+
+ #alpha_slider_container {
+ background: white;
+ border: 1px solid black;
+ flex-direction: column;
+ padding: 0.5em;
+ position: absolute;
+ z-index: 10; /* scalar-span uses z-index :-( */
+ }
+
+ #alpha_slider {
+ -webkit-appearance: slider-vertical;
+ align-self: center;
+ height: 200px;
+ width: 30px;
+ }
+
+ #statistic {
+ display: none;
+ margin-right: 20px;
+ }
+
+ #show_visualization {
+ margin-right: 20px;
+ }
+
+ #export {
+ margin-right: 20px;
+ }
+ </style>
+
+ <div id="controls">
+ <span id="search_container">
+ <input id="search" value="{{searchQuery::keyup}}" placeholder="Find Histogram name">
+ <svg viewbox="0 0 128 128" id="clear_search" on-tap="clearSearch_">
+ <g>
+ <title>Clear search</title>
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </g>
+ </svg>
+ </span>
+
+ <svg viewbox="0 0 128 128" id="show_overview"
+ on-tap="toggleOverviewLineCharts_">
+ <g>
+ <title>Show overview charts</title>
+ <line x1="19" y1="109" x2="49" y2="49"/>
+ <line x1="49" y1="49" x2="79" y2="79"/>
+ <line x1="79" y1="79" x2="109" y2="19"/>
+ </g>
+ </svg>
+ <svg viewbox="0 0 128 128" id="hide_overview"
+ on-tap="toggleOverviewLineCharts_">
+ <g>
+ <title>Hide overview charts</title>
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </g>
+ </svg>
+
+ <select id="reference_display_label" value="{{referenceDisplayLabel::change}}">
+ <option value="">Select a reference column</option>
+ </select>
+
+ <button id="alpha" on-tap="openAlphaSlider_">&#945;=[[alphaString]]</button>
+ <div id="alpha_slider_container">
+ <input type="range" id="alpha_slider" value="{{alphaIndex::change}}" min="0" max="18" on-blur="closeAlphaSlider_" on-input="updateAlpha_">
+ </div>
+
+ <select id="statistic" value="{{displayStatisticName::change}}">
+ </select>
+
+ <button id="show_visualization" on-tap="loadVisualization_">Visualize</button>
+
+ <tr-ui-b-dropdown label="Export">
+ <tr-v-ui-histogram-set-controls-export>
+ </tr-v-ui-histogram-set-controls-export>
+ </tr-ui-b-dropdown>
+
+ <input type="checkbox" id="show_all" checked="{{showAll::change}}" title="When unchecked, less important histograms are hidden.">
+ <label for="show_all" title="When unchecked, less important histograms are hidden.">Show all</label>
+
+ <a id="help">Help</a>
+ <a id="feedback">Feedback</a>
+ </div>
+
+ <tr-ui-b-grouping-table-groupby-picker id="picker">
+ </tr-ui-b-grouping-table-groupby-picker>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const ALPHA_OPTIONS = [];
+ for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-3);
+ for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-2);
+ ALPHA_OPTIONS.push(0.1);
+
+ Polymer({
+ is: 'tr-v-ui-histogram-set-controls',
+
+ properties: {
+ searchQuery: {
+ type: String,
+ value: '',
+ observer: 'onSearchQueryChange_',
+ },
+ showAll: {
+ type: Boolean,
+ value: true,
+ observer: 'onUserChange_',
+ },
+ referenceDisplayLabel: {
+ type: String,
+ value: '',
+ observer: 'onUserChange_',
+ },
+ displayStatisticName: {
+ type: String,
+ value: '',
+ observer: 'onUserChange_',
+ },
+ alphaString: {
+ type: String,
+ computed: 'getAlphaString_(alphaIndex)',
+ },
+ alphaIndex: {
+ type: Number,
+ value: 9,
+ observer: 'onUserChange_',
+ },
+ },
+
+ created() {
+ this.viewState_ = undefined;
+ this.rowListener_ = this.onRowViewStateUpdate_.bind(this);
+ this.baseStatisticNames_ = [];
+
+ // When onViewStateUpdate_() copies multiple properties from the viewState
+ // to polymer properties, disable onUserChange_ until all properties are
+ // copied in order to prevent nested mutations to the ViewState.
+ this.isInOnViewStateUpdate_ = false;
+ this.searchQueryDebounceMs = 200;
+ },
+
+ ready() {
+ this.$.picker.addEventListener('current-groups-changed',
+ this.onGroupsChanged_.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));
+ // It would be arduous to construct a delta and call viewStateListener_
+ // here in case vs contains non-default values, so callers must set
+ // viewState first and then update it.
+ },
+
+ async onSearchQueryChange_() {
+ // Bypass debouncing for testing purpose:
+ if (this.searchQueryDebounceMs === 0) return this.onUserChange_();
+ // Limit the update rate for instance caused by typing in a search.
+ this.debounce('onSearchQueryDebounce', this.onUserChange_,
+ this.searchQueryDebounceMs);
+ },
+
+ async onUserChange_() {
+ if (!this.viewState) return;
+ if (this.isInOnViewStateUpdate_) return;
+
+ const marks = [];
+ if (this.searchQuery !== this.viewState.searchQuery) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'search'));
+ }
+ if (this.showAll !== this.viewState.showAll) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'showAll'));
+ }
+ if (this.referenceDisplayLabel !== this.viewState.referenceDisplayLabel) {
+ marks.push(tr.b.Timing.mark(
+ 'histogram-set-controls', 'referenceColumn'));
+ }
+ if (this.displayStatisticName !== this.viewState.displayStatisticName) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'statistic'));
+ }
+ if (parseInt(this.alphaIndex) !== this.getAlphaIndexFromViewState_()) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'alpha'));
+ }
+
+ this.$.clear_search.style.visibility =
+ this.searchQuery ? 'visible' : 'hidden';
+
+ let displayStatisticName = this.displayStatisticName;
+ if (this.viewState.referenceDisplayLabel === '' &&
+ this.referenceDisplayLabel !== '' &&
+ this.baseStatisticNames.length) {
+ // The user selected a reference display label.
+ displayStatisticName = `%${tr.v.DELTA}${this.displayStatisticName}`;
+ // Can't set this.displayStatisticName before updating viewState -- that
+ // would cause an infinite loop of onUserChange_().
+ }
+ if (this.referenceDisplayLabel === '' &&
+ this.viewState.referenceDisplayLabel !== '' &&
+ this.baseStatisticNames.length) {
+ // The user unset the reference display label.
+ // Ensure that displayStatisticName is not a delta statistic.
+ const deltaIndex = displayStatisticName.indexOf(tr.v.DELTA);
+ if (deltaIndex >= 0) {
+ displayStatisticName = displayStatisticName.slice(deltaIndex + 1);
+ } else if (!this.baseStatisticNames.includes(displayStatisticName)) {
+ displayStatisticName = 'avg';
+ }
+ }
+
+ // Propagate updates from the user to the view state.
+ await this.viewState.update({
+ searchQuery: this.searchQuery,
+ showAll: this.showAll,
+ referenceDisplayLabel: this.referenceDisplayLabel,
+ displayStatisticName,
+ alpha: ALPHA_OPTIONS[this.alphaIndex],
+ });
+
+ if (this.referenceDisplayLabel &&
+ this.statisticNames.length === this.baseStatisticNames.length) {
+ // When a reference column is selected, delta statistics should be
+ // available.
+ this.statisticNames = this.baseStatisticNames.concat(
+ tr.v.Histogram.getDeltaStatisticsNames(this.baseStatisticNames));
+ } else if (!this.referenceDisplayLabel &&
+ this.statisticNames.length > this.baseStatisticNames.length) {
+ // When a reference column is not selected, delta statistics should not
+ // be available.
+ this.statisticNames = this.baseStatisticNames;
+ }
+
+ for (const mark of marks) mark.end();
+ },
+
+ onViewStateUpdate_(event) {
+ this.isInOnViewStateUpdate_ = true;
+
+ if (event.delta.searchQuery) {
+ this.searchQuery = this.viewState.searchQuery;
+ }
+
+ if (event.delta.showAll) this.showAll = this.viewState.showAll;
+
+ if (event.delta.displayStatisticName) {
+ this.displayStatisticName = this.viewState.displayStatisticName;
+ }
+
+ if (event.delta.referenceDisplayLabel) {
+ this.referenceDisplayLabel = this.viewState.referenceDisplayLabel;
+ this.$.alpha.style.display = this.referenceDisplayLabel ? 'inline' : '';
+ }
+
+ if (event.delta.groupings) {
+ this.$.picker.currentGroupKeys = this.viewState.groupings.map(
+ g => g.key);
+ }
+
+ if (event.delta.tableRowStates) {
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ this.viewState.tableRowStates.values())) {
+ row.addUpdateListener(this.rowListener_);
+ }
+
+ const anyShowing = this.anyOverviewCharts_;
+ this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none';
+ this.$.show_overview.style.display = anyShowing ? 'none' : 'inline';
+ }
+
+ if (event.delta.alpha) {
+ this.alphaIndex = this.getAlphaIndexFromViewState_();
+ }
+
+ this.isInOnViewStateUpdate_ = false;
+ this.onUserChange_();
+ },
+
+ onRowViewStateUpdate_(event) {
+ if (event.delta.isOverviewed) {
+ const anyShowing = event.delta.isOverviewed.current ||
+ this.anyOverviewCharts_;
+ this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none';
+ this.$.show_overview.style.display = anyShowing ? 'none' : 'inline';
+ }
+
+ if (event.delta.subRows) {
+ for (const subRow of event.delta.subRows.previous) {
+ subRow.removeUpdateListener(this.rowListener_);
+ }
+ for (const subRow of event.delta.subRows.current) {
+ subRow.addUpdateListener(this.rowListener_);
+ }
+ }
+ },
+
+ onGroupsChanged_() {
+ if (this.$.picker.currentGroups.length === 0 &&
+ this.$.picker.possibleGroups.length > 0) {
+ // If the current groupings are now empty but there are possible
+ // groupings, then force there to be at least one grouping.
+ // The histogram-set-table requires there to be at least one grouping.
+ this.$.picker.currentGroupKeys = [this.$.picker.possibleGroups[0].key];
+ }
+ this.viewState.groupings = this.$.picker.currentGroups;
+ },
+
+ set showAllEnabled(enable) {
+ if (!enable) this.$.show_all.checked = true;
+ this.$.show_all.disabled = !enable;
+ },
+
+ set possibleGroupings(groupings) {
+ this.$.picker.possibleGroups = groupings;
+ this.$.picker.style.display = (groupings.length < 2) ? 'none' : 'block';
+ this.onGroupsChanged_();
+ },
+
+ set displayLabels(labels) {
+ this.$.reference_display_label.style.display =
+ (labels.length < 2) ? 'none' : 'inline';
+
+ while (this.$.reference_display_label.children.length > 1) {
+ this.$.reference_display_label.removeChild(
+ this.$.reference_display_label.lastChild);
+ }
+
+ for (const displayLabel of labels) {
+ const option = document.createElement('option');
+ option.textContent = displayLabel;
+ option.value = displayLabel;
+ this.$.reference_display_label.appendChild(option);
+ }
+
+ if (labels.includes(this.viewState.referenceDisplayLabel)) {
+ this.referenceDisplayLabel = this.viewState.referenceDisplayLabel;
+ } else {
+ this.viewState.referenceDisplayLabel = '';
+ }
+ },
+
+ get baseStatisticNames() {
+ return this.baseStatisticNames_;
+ },
+
+ set baseStatisticNames(names) {
+ this.baseStatisticNames_ = names;
+ this.statisticNames = names;
+ },
+
+ get statisticNames() {
+ return Array.from(this.$.statistic.options).map(o => o.value);
+ },
+
+ set statisticNames(names) {
+ this.$.statistic.style.display = (names.length < 2) ? 'none' : 'inline';
+
+ while (this.$.statistic.children.length) {
+ this.$.statistic.removeChild(this.$.statistic.lastChild);
+ }
+
+ for (const name of names) {
+ const option = document.createElement('option');
+ option.textContent = name;
+ this.$.statistic.appendChild(option);
+ }
+
+ if (names.includes(this.viewState.displayStatisticName)) {
+ this.displayStatisticName = this.viewState.displayStatisticName;
+ // Polymer doesn't reset the value when the options change, so do that
+ // manually.
+ 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) {
+ // (9 * 1e-3).toString() is "0.009000000000000001", so truncate.
+ 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 {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html
new file mode 100644
index 00000000000..98936f24bba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+
+<dom-module id="tr-v-ui-histogram-set-controls-export">
+ <template>
+ <style>
+ :host {
+ display: grid;
+ grid-gap: 1em;
+ grid-template-rows: auto auto;
+ grid-template-columns: auto auto;
+ }
+ button {
+ -webkit-appearance: none;
+ border: 0;
+ font-size: initial;
+ padding: 5px;
+ }
+ </style>
+
+ <button on-tap="exportRawCsv_">raw CSV</button>
+ <button on-tap="exportRawJson_">raw JSON</button>
+ <button on-tap="exportMergedCsv_">merged CSV</button>
+ <button on-tap="exportMergedJson_">merged JSON</button>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-histogram-set-controls-export',
+
+ exportRawCsv_() {
+ this.export_(false, 'csv');
+ },
+
+ exportRawJson_() {
+ this.export_(false, 'json');
+ },
+
+ exportMergedCsv_() {
+ this.export_(true, 'csv');
+ },
+
+ exportMergedJson_() {
+ this.export_(true, 'json');
+ },
+
+ export_(merged, format) {
+ tr.b.dispatchSimpleEvent(this, 'export', true, true, {merged, format});
+ },
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html
new file mode 100644
index 00000000000..9783059ccbe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html
@@ -0,0 +1,300 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_controls.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ function buildControls(test) {
+ const controls = document.createElement('tr-v-ui-histogram-set-controls');
+ controls.viewState = new tr.v.ui.HistogramSetViewState();
+ test.addHTMLOutput(controls);
+ return controls;
+ }
+
+ test('helpHref', function() {
+ const controls = buildControls(this);
+ controls.helpHref = 'data:text/html,hello';
+ const help = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'help');
+ assert.strictEqual(help.style.display, 'inline');
+ assert.strictEqual(help.href, 'data:text/html,hello');
+ });
+
+ test('feedbackHref', function() {
+ const controls = buildControls(this);
+ controls.feedbackHref = 'data:text/html,hello';
+ const feedback = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'feedback');
+ assert.strictEqual(feedback.style.display, 'inline');
+ assert.strictEqual(feedback.href, 'data:text/html,hello');
+ });
+
+ test('displayLabels', function() {
+ const controls = buildControls(this);
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'reference_display_label');
+ assert.strictEqual('none', getComputedStyle(selector).display);
+
+ controls.displayLabels = [];
+ assert.strictEqual('none', getComputedStyle(selector).display);
+
+ controls.displayLabels = ['Value'];
+ assert.strictEqual('none', getComputedStyle(selector).display);
+
+ controls.displayLabels = ['a', 'b\nc'];
+ assert.strictEqual('inline-block', getComputedStyle(selector).display);
+ assert.strictEqual('', selector.children[0].value);
+ assert.strictEqual('a', selector.children[1].value);
+ assert.strictEqual('a', selector.children[1].textContent);
+
+ // displayLabels can contain newlines, which <option> replace with spaces.
+ // histogram-set-controls must set option.value in order for selector.value
+ // to contain the newlines.
+ assert.strictEqual('b\nc', selector.children[2].value);
+ assert.strictEqual('b\nc', selector.children[2].textContent);
+ selector.selectedIndex = 2;
+ assert.strictEqual('b\nc', selector.value);
+
+ controls.displayLabels = ['Value'];
+ assert.strictEqual('none', getComputedStyle(selector).display);
+ });
+
+ test('baseStatisticNames', function() {
+ const controls = buildControls(this);
+ controls.baseStatisticNames = ['avg', 'std'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'statistic');
+ assert.strictEqual('inline-block', getComputedStyle(selector).display);
+ assert.lengthOf(selector.children, 2);
+ assert.strictEqual('avg', selector.children[0].value);
+ assert.strictEqual('avg', selector.children[0].textContent);
+ assert.strictEqual('std', selector.children[1].value);
+ assert.strictEqual('std', selector.children[1].textContent);
+ assert.strictEqual('avg', selector.value);
+ });
+
+ test('viewDisplayStatisticName', function() {
+ const controls = buildControls(this);
+ controls.baseStatisticNames = ['avg', 'std'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'statistic');
+ controls.viewState.displayStatisticName = 'std';
+ assert.strictEqual('std', selector.value);
+ });
+
+ test('controlDisplayStatisticName', function() {
+ const controls = buildControls(this);
+ controls.baseStatisticNames = ['avg', 'std'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'statistic');
+ selector.value = 'std';
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', false, true);
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('std', controls.viewState.displayStatisticName);
+ });
+
+ test('viewSearchQuery', function() {
+ const controls = buildControls(this);
+ controls.viewState.searchQuery = 'foo';
+ const search = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'search');
+ assert.strictEqual(search.value, 'foo');
+ });
+
+ test('controlSearchQuery', function() {
+ const controls = buildControls(this);
+ controls.searchQueryDebounceMs = 0;
+ const search = tr.ui.b.findDeepElementMatching(controls, '#search');
+ search.value = 'x';
+ const keyupEvent = document.createEvent('KeyboardEvent');
+ keyupEvent.initEvent('keyup');
+ search.dispatchEvent(keyupEvent);
+ assert.strictEqual(controls.viewState.searchQuery, 'x');
+ controls.clearSearch_();
+ assert.strictEqual(controls.viewState.searchQuery, '');
+ });
+
+ test('viewShowAll', function() {
+ const controls = buildControls(this);
+ const showAll = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'show_all');
+ assert.strictEqual(controls.viewState.showAll, true);
+ assert.strictEqual(showAll.checked, true);
+ controls.viewState.showAll = false;
+ assert.strictEqual(showAll.checked, false);
+ });
+
+ test('controlShowAll', function() {
+ const controls = buildControls(this);
+ const showAll = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'show_all');
+ assert.strictEqual(controls.viewState.showAll, true);
+ assert.strictEqual(showAll.checked, true);
+ showAll.click();
+ assert.strictEqual(showAll.checked, false);
+ assert.strictEqual(controls.viewState.showAll, false);
+ const showAllLabel = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.tagName === 'LABEL' && e.htmlFor === 'show_all');
+ showAllLabel.click();
+ assert.strictEqual(showAll.checked, true);
+ assert.strictEqual(controls.viewState.showAll, true);
+ });
+
+ test('viewReferenceDisplayLabel', function() {
+ const controls = buildControls(this);
+ controls.displayLabels = ['a', 'b'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'reference_display_label');
+
+ assert.strictEqual('', selector.value);
+ assert.strictEqual('', controls.viewState.referenceDisplayLabel);
+
+ controls.viewState.referenceDisplayLabel = 'a';
+ assert.strictEqual('a', selector.value);
+
+ controls.viewState.referenceDisplayLabel = 'b';
+ assert.strictEqual('b', selector.value);
+
+ controls.viewState.referenceDisplayLabel = '';
+ assert.strictEqual('', selector.value);
+ });
+
+ test('controlReferenceDisplayLabel', function() {
+ const controls = buildControls(this);
+ controls.displayLabels = ['a', 'b'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'reference_display_label');
+ assert.strictEqual('', selector.value);
+ assert.strictEqual('', controls.viewState.referenceDisplayLabel);
+
+ selector.value = 'a';
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', false, true);
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('a', controls.viewState.referenceDisplayLabel);
+
+ selector.value = 'b';
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('b', controls.viewState.referenceDisplayLabel);
+
+ selector.value = '';
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('', controls.viewState.referenceDisplayLabel);
+ });
+
+ test('viewGroupings', function() {
+ const controls = buildControls(this);
+ const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo');
+ const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values());
+ groupings.push(fooGrouping);
+ controls.possibleGroupings = groupings;
+ const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER');
+ assert.lengthOf(picker.currentGroupKeys, 1);
+ assert.strictEqual(picker.currentGroupKeys[0],
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+
+ controls.viewState.groupings = [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ ];
+ assert.lengthOf(picker.currentGroupKeys, 1);
+ assert.strictEqual(picker.currentGroupKeys[0],
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+ assert.strictEqual('block', picker.style.display);
+
+ controls.viewState.groupings = [
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ fooGrouping,
+ ];
+ assert.lengthOf(picker.currentGroupKeys, 2);
+ assert.strictEqual(picker.currentGroupKeys[0],
+ tr.v.d.RESERVED_NAMES.STORIES);
+ assert.strictEqual(picker.currentGroupKeys[1], 'foo');
+ });
+
+ test('controlGroupings', function() {
+ const controls = buildControls(this);
+ const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo');
+ const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values());
+ groupings.push(fooGrouping);
+ controls.possibleGroupings = groupings;
+ const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER');
+ assert.lengthOf(picker.currentGroupKeys, 1);
+ assert.strictEqual(controls.viewState.groupings[0].key,
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+
+ picker.currentGroupKeys = ['name'];
+ assert.lengthOf(controls.viewState.groupings, 1);
+ assert.strictEqual(controls.viewState.groupings[0].key,
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+
+ picker.currentGroupKeys = [tr.v.d.RESERVED_NAMES.STORIES, 'foo'];
+ assert.lengthOf(controls.viewState.groupings, 2);
+ assert.strictEqual(controls.viewState.groupings[0],
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES));
+ assert.strictEqual(controls.viewState.groupings[1],
+ fooGrouping);
+ });
+
+ test('viewIsOverviewed', function() {
+ const controls = buildControls(this);
+ const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'show_overview');
+ const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'hide_overview');
+ controls.viewState.tableRowStates = new Map([
+ ['a', new tr.v.ui.HistogramSetTableRowState()],
+ ['b', new tr.v.ui.HistogramSetTableRowState()],
+ ]);
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+
+ controls.viewState.tableRowStates.get('a').isOverviewed = true;
+ assert.strictEqual('none', showOverview.style.display);
+ assert.strictEqual('inline', hideOverview.style.display);
+
+ controls.viewState.tableRowStates.get('a').isOverviewed = false;
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+ });
+
+ test('controlIsOverviewed', async function() {
+ const controls = buildControls(this);
+ const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'show_overview');
+ const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'hide_overview');
+ controls.viewState.tableRowStates = new Map([
+ ['a', new tr.v.ui.HistogramSetTableRowState()],
+ ['b', new tr.v.ui.HistogramSetTableRowState()],
+ ]);
+ assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed);
+ assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed);
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+
+ await controls.toggleOverviewLineCharts_();
+ assert.strictEqual('none', showOverview.style.display);
+ assert.strictEqual('inline', hideOverview.style.display);
+ assert.isTrue(controls.viewState.tableRowStates.get('a').isOverviewed);
+ assert.isTrue(controls.viewState.tableRowStates.get('b').isOverviewed);
+
+ await controls.toggleOverviewLineCharts_();
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+ assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed);
+ assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html
new file mode 100644
index 00000000000..882806ba32b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/base/url_json.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ // This number is used to decide whether the tableStates can fit in the URL,
+ // and omit them if not.
+ // There is no specification for maximum URL length, so it is typically
+ // limited by hosts, not browsers.
+ // TODO(#3816) Tune this number.
+ const MAX_URL_LENGTH = 2048;
+
+ // This class wraps |window.location| and |window.history| to allow tests to
+ // mock it.
+ class Locus {
+ get origin() {
+ return window.location.origin;
+ }
+
+ get pathname() {
+ return window.location.pathname;
+ }
+
+ get search() {
+ return window.location.search;
+ }
+
+ get hash() {
+ return window.location.hash;
+ }
+
+ get state() {
+ if (this.stateMode === '#') return this.hash.substr(1);
+ return this.search.substr(1);
+ }
+
+ get stateMode() {
+ if (this.hash) return '#';
+ return '?';
+ }
+
+ buildUrlFromState(state) {
+ let url = this.origin + this.pathname;
+ if (this.stateMode === '#') url += this.search;
+ url += this.stateMode + state;
+ return url;
+ }
+
+ pushState(state) {
+ if (state === this.state) return;
+
+ // TODO(#3837) When should this actually call pushState()?
+ window.history.replaceState(null, null, this.buildUrlFromState(state));
+ }
+
+ addPopStateListener(listener) {
+ window.addEventListener('popstate', listener);
+ }
+ }
+
+ class HistogramSetLocation {
+ constructor(opt_location) {
+ // Optional dependency injection for testing.
+ this.location_ = opt_location || new Locus();
+ this.location_.addPopStateListener(this.onPopState_.bind(this));
+
+ this.viewState_ = undefined;
+ this.rowListener_ = this.onRowStateUpdate_.bind(this);
+ this.cellListener_ = this.onCellStateUpdate_.bind(this);
+
+ // pushState_ is disabled while handling onPopState_.
+ this.poppingState_ = false;
+ }
+
+ /**
+ * @return {!tr.v.ui.HistogramSetViewState}
+ */
+ get viewState() {
+ return this.viewState_;
+ }
+
+ /**
+ * @param {!tr.v.ui.HistogramSetViewState} vs
+ */
+ async build(vs) {
+ if (this.viewState !== undefined) {
+ throw new Error('viewState must be set exactly once.');
+ }
+ this.viewState_ = vs;
+ this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));
+
+ await this.onPopState_();
+ }
+
+ onViewStateUpdate_(event) {
+ if (event.delta.tableRowStates) {
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ event.delta.tableRowStates.previous.values())) {
+ row.removeUpdateListener(this.rowListener_);
+ for (const cell of row.cells.values()) {
+ cell.removeUpdateListener(this.cellListener_);
+ }
+ }
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ event.delta.tableRowStates.current.values())) {
+ row.addUpdateListener(this.rowListener_);
+ for (const cell of row.cells.values()) {
+ cell.addUpdateListener(this.cellListener_);
+ }
+ }
+ }
+
+ this.pushState_();
+ }
+
+ onRowStateUpdate_(event) {
+ // This assumes that subRows and cells are not updated.
+ this.pushState_();
+ }
+
+ onCellStateUpdate_(event) {
+ this.pushState_();
+ }
+
+ pushState_() {
+ if (this.poppingState_) return;
+ const mark = tr.b.Timing.mark('HistogramSetLocation', 'pushState');
+
+ const params = new Map();
+ if (this.viewState.searchQuery) {
+ params.set('q', this.viewState.searchQuery);
+ }
+ if (this.viewState.referenceDisplayLabel) {
+ params.set('r', this.viewState.referenceDisplayLabel);
+ }
+ params.set('s', this.viewState.displayStatisticName);
+ if (!this.viewState.showAll) params.set('m', '');
+ params.set('g', this.viewState.groupings.map(g => g.key).join('.'));
+ if (this.viewState.sortColumnIndex !== undefined) {
+ params.set('c', '' + this.viewState.sortColumnIndex);
+ }
+ if (this.viewState.sortDescending) params.set('d', '');
+ if (!this.viewState.constrainNameColumn) params.set('n', '0');
+ if (!tr.b.math.approximately(this.viewState.alpha, 0.01)) {
+ params.set('p', ('' + this.viewState.alpha).substr(0, 5));
+ }
+
+ let urlState = '';
+ for (const [key, value] of params) {
+ if (urlState) urlState += '&';
+ urlState += key + '=' + window.encodeURIComponent(value);
+ }
+
+ const rowDicts = {};
+ for (const [name, rowState] of this.viewState.tableRowStates) {
+ const dict = rowState.asCompactDict();
+ if (dict === undefined) continue;
+ rowDicts[name] = dict;
+ }
+
+ if (Object.keys(rowDicts).length > 0) {
+ const rowsParam = '&t=' + tr.b.UrlJson.stringify(rowDicts);
+
+ if (this.location_.buildUrlFromState(urlState + rowsParam).length <
+ MAX_URL_LENGTH) {
+ urlState += rowsParam;
+ }
+ }
+
+ this.location_.pushState(urlState);
+ mark.end();
+ }
+
+ async onPopState_() {
+ const mark = tr.b.Timing.mark('HistogramSetLocation', 'onPopState');
+ this.poppingState_ = true;
+
+ const params = new Map();
+ for (const kvp of this.location_.state.split('&')) {
+ const [key, value] = kvp.split('=');
+ try {
+ params.set(key, window.decodeURIComponent(value));
+ } catch (e) {
+ // If the user tampers with the params so that a value cannot be
+ // decoded, ignore it.
+ }
+ }
+
+ const delta = new Map();
+ if (params.has('q')) delta.set('searchQuery', params.get('q'));
+ if (params.has('r')) delta.set('referenceDisplayLabel', params.get('r'));
+ if (params.has('s')) delta.set('displayStatisticName', params.get('s'));
+ delta.set('showAll', !params.has('m'));
+ if (params.has('g')) {
+ delta.set('groupings', params.get('g').split('.').map(
+ k => tr.v.HistogramGrouping.BY_KEY.get(k)));
+ }
+ if (params.has('c')) {
+ delta.set('sortColumnIndex', parseInt(params.get('c')));
+ } else {
+ delta.set('sortColumnIndex', 0);
+ }
+ delta.set('sortDescending', params.has('d'));
+ delta.set('constrainNameColumn', params.get('n') !== '0');
+ if (params.has('p')) {
+ delta.set('alpha', parseFloat(params.get('p')));
+ }
+
+ await this.viewState.update(delta);
+
+ if (params.has('t')) {
+ let rowDicts;
+ try {
+ rowDicts = tr.b.UrlJson.parse(params.get('t'));
+ } catch (e) {
+ // If the user tampers with the params so that rowDicts cannot be
+ // parsed, ignore it.
+ }
+
+ if (rowDicts) {
+ for (const [name, rowDict] of Object.entries(rowDicts)) {
+ const rowState = this.viewState.tableRowStates.get(name);
+ if (rowState === undefined) continue;
+ await rowState.updateFromCompactDict(rowDict);
+ }
+ }
+ }
+
+ this.poppingState_ = false;
+ mark.end();
+ }
+ }
+
+ HistogramSetLocation.Locus = Locus;
+
+ return {
+ HistogramSetLocation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html
new file mode 100644
index 00000000000..d9987e7b097
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/value/ui/histogram_set_location.html">
+
+<script>
+'use strict';
+/* eslint-disable max-len */
+tr.b.unittest.testSuite(function() {
+ class TestLocus extends tr.v.ui.HistogramSetLocation.Locus {
+ constructor() {
+ super();
+ this.search_ = '';
+ this.hash_ = '';
+ this.listener_ = undefined;
+ }
+
+ get origin() {
+ return 'http://example.com';
+ }
+
+ get pathname() {
+ return '/pathname';
+ }
+
+ get search() {
+ return this.search_;
+ }
+
+ set search(s) {
+ this.search_ = s;
+ }
+
+ get hash() {
+ return this.hash_;
+ }
+
+ set hash(h) {
+ this.hash_ = h;
+ }
+
+ pushState(state) {
+ if (this.hash) {
+ this.hash = '#' + state;
+ } else {
+ this.search = '?' + state;
+ }
+ }
+
+ get stateMode() {
+ if (this.hash) return '#';
+ return '?';
+ }
+
+ addPopStateListener(listener) {
+ this.listener_ = listener;
+ }
+
+ async popState(state) {
+ if (state[0] === '?') {
+ this.search = state;
+ } else if (state[0] === '#') {
+ this.hash = state;
+ }
+ await this.listener_();
+ }
+ }
+
+ test('viewStateUpdateHashAndSearch', async function() {
+ const locus = new TestLocus();
+ locus.search = '?';
+ locus.hash = '#';
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await hsl.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: undefined,
+ });
+ assert.strictEqual(locus.hash, '#s=avg&g=name.stories');
+ });
+
+ test('viewStateUpdateHash', async function() {
+ const locus = new TestLocus();
+ locus.hash = '#';
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await hsl.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: undefined,
+ });
+ assert.strictEqual(locus.hash, '#s=avg&g=name.stories');
+
+ await hsl.viewState.update({searchQuery: 'foo'});
+ assert.strictEqual(locus.hash, '#q=foo&s=avg&g=name.stories');
+
+ await hsl.viewState.update({referenceDisplayLabel: 'bar'});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&g=name.stories');
+
+ await hsl.viewState.update({showAll: false});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories');
+
+ await hsl.viewState.update({sortColumnIndex: 2});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2');
+
+ await hsl.viewState.update({sortDescending: true});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=');
+
+ await hsl.viewState.update({constrainNameColumn: false});
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ const rowState = new tr.v.ui.HistogramSetTableRowState();
+ rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState());
+ await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])});
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true});
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)');
+
+ await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({
+ isOpen: true,
+ });
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))');
+ });
+
+ test('viewStateUpdateSearch', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await hsl.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: undefined,
+ });
+ assert.strictEqual(locus.search, '?s=avg&g=name.stories');
+
+ await hsl.viewState.update({searchQuery: 'foo'});
+ assert.strictEqual(locus.search, '?q=foo&s=avg&g=name.stories');
+
+ await hsl.viewState.update({referenceDisplayLabel: 'bar'});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&g=name.stories');
+
+ await hsl.viewState.update({showAll: false});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories');
+
+ await hsl.viewState.update({sortColumnIndex: 2});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2');
+
+ await hsl.viewState.update({sortDescending: true});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=');
+
+ await hsl.viewState.update({constrainNameColumn: false});
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ const rowState = new tr.v.ui.HistogramSetTableRowState();
+ rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState());
+ await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])});
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true});
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)');
+
+ await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({
+ isOpen: true,
+ });
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))');
+ });
+
+ test('popStateSearch', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await locus.popState('?q=foo&r=bar&s=qux&m=&c=2&d=&g=name.stories');
+ assert.strictEqual('foo', hsl.viewState.searchQuery);
+ assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel);
+ assert.strictEqual('qux', hsl.viewState.displayStatisticName);
+ assert.isFalse(hsl.viewState.showAll);
+ assert.lengthOf(hsl.viewState.groupings, 2);
+ assert.strictEqual('name', hsl.viewState.groupings[0].key);
+ assert.strictEqual('stories', hsl.viewState.groupings[1].key);
+ assert.strictEqual(2, hsl.viewState.sortColumnIndex);
+ assert.isTrue(hsl.viewState.sortDescending);
+
+ // onPopState_ should ignore missing rows and cells
+ await locus.popState('?t=f%3Am_p-(o-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.size);
+
+ await hsl.viewState.update({tableRowStates: new Map([
+ ['f:m_p', new tr.v.ui.HistogramSetTableRowState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await locus.popState('?t=f%3Am_p-(e-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size);
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([
+ ['Value', new tr.v.ui.HistogramSetTableCellState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+
+ await locus.popState('?t=f%3Am_p-(c-(Value-(o-1)))');
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+ });
+
+ test('popStateHashAndSearch', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await locus.popState('?q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories');
+ assert.strictEqual('foo', hsl.viewState.searchQuery);
+ assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel);
+ assert.strictEqual('qux', hsl.viewState.displayStatisticName);
+ assert.isTrue(hsl.viewState.showAll);
+ assert.lengthOf(hsl.viewState.groupings, 2);
+ assert.strictEqual('name', hsl.viewState.groupings[0].key);
+ assert.strictEqual('stories', hsl.viewState.groupings[1].key);
+ assert.strictEqual(2, hsl.viewState.sortColumnIndex);
+ assert.isTrue(hsl.viewState.sortDescending);
+
+ await locus.popState('#q=q');
+ assert.strictEqual('q', hsl.viewState.searchQuery);
+ });
+
+ test('popStateHash', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await locus.popState('#q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories');
+ assert.strictEqual('foo', hsl.viewState.searchQuery);
+ assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel);
+ assert.strictEqual('qux', hsl.viewState.displayStatisticName);
+ assert.isTrue(hsl.viewState.showAll);
+ assert.lengthOf(hsl.viewState.groupings, 2);
+ assert.strictEqual('name', hsl.viewState.groupings[0].key);
+ assert.strictEqual('stories', hsl.viewState.groupings[1].key);
+ assert.strictEqual(2, hsl.viewState.sortColumnIndex);
+ assert.isTrue(hsl.viewState.sortDescending);
+
+ // onPopState_ should ignore missing rows and cells
+ await locus.popState('#t=f%3Am_p-(o-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.size);
+
+ await hsl.viewState.update({tableRowStates: new Map([
+ ['f:m_p', new tr.v.ui.HistogramSetTableRowState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await locus.popState('#t=f%3Am_p-(e-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size);
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([
+ ['Value', new tr.v.ui.HistogramSetTableCellState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+
+ await locus.popState('#t=f%3Am_p-(c-(Value-(o-1)))');
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html
new file mode 100644
index 00000000000..9ac3046c5d3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html
@@ -0,0 +1,459 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/histogram_set_hierarchy.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_table_row.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+
+<dom-module id="tr-v-ui-histogram-set-table">
+ <template>
+ <style>
+ :host {
+ min-height: 0px;
+ overflow: auto;
+ }
+ #table {
+ margin-top: 5px;
+ }
+ </style>
+
+ <tr-ui-b-table id="table"/>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const MIDLINE_HORIZONTAL_ELLIPSIS = String.fromCharCode(0x22ef);
+
+ // http://stackoverflow.com/questions/3446170
+ 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.filteredHistograms_ = undefined;
+ this.groupedHistograms_ = undefined;
+ this.hierarchies_ = undefined;
+ this.tableRows_ = undefined;
+
+ // Store this listener so it can be removed while updateContents_ modifies
+ // sortColumnIndex and sortDescending, then re-added.
+ 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));
+ // It would be arduous to construct a delta and call onViewStateUpdate_
+ // here in case vs contains non-default values, so callers must set
+ // viewState first and then update it.
+ },
+
+ get histograms() {
+ return this.histograms_;
+ },
+
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.v.HistogramSet} sourceHistograms
+ * @param {!Array.<string>} displayLabels
+ * @param {function(string, function())=} opt_progress
+ */
+ 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);
+
+ // updateContents_() displays its own progress.
+ await this.updateContents_();
+
+ // Building some elements requires being able to measure them, which is
+ // impossible until they are displayed. If clients hide this table while
+ // it is being built, then they must display it when this event fires.
+ 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';
+
+ // Wrap the string in a span instead of using createTextNode() so that the
+ // span can be styled later.
+ 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_() {
+ 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) {
+ // Wait to set this.$.table.tableRows until we're ready for it to build
+ // DOM. When tableRows are set on it, tr-ui-b-table calls
+ // setTimeout(..., 0) to schedule rebuild for the next interpreter tick,
+ // but that can happen in between the next await, which is too early.
+ 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);
+
+ // Temporarily stop listening for this event in order to prevent the
+ // listener from updating viewState unnecessarily.
+ 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_);
+
+ // Each name-cell listens to this.viewState for updates to
+ // constrainNameColumn.
+ // Each table-cell listens to this.viewState for updates to
+ // displayStatisticName and referenceDisplayLabel.
+
+ if (tableRowsDirty) {
+ await this.progress_('Building DOM...');
+ this.$.table.tableRows = this.tableRows_;
+
+ // Try to restore previous row state.
+ // Wait to do this until after the base table has the new rows so that
+ // setExpandedForTableRow doesn't get confused.
+ for (const row of this.tableRows_) {
+ const previousState = previousRowStates.get(row.name);
+ if (!previousState) continue;
+ await row.restoreState(previousState);
+ }
+ }
+
+ // It's always safe to call this, it will only recompute what is dirty.
+ // We want to make sure that the table is up to date when this async
+ // function resolves.
+ 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'));
+
+ // When the user expands a row, the table builds subRows' name-cells.
+ // If a subRow's name isOverflowing even though none of the top-level rows
+ // are constrained, show the dots to allow the user to unconstrain the
+ // name column.
+ // Each name-cell.isOverflowing would force layout if we don't await
+ // animationFrame here, which would be inefficient.
+ 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';
+
+ // Size the span containing 'Name' so that the dots align with the
+ // ellipses in the name-cells.
+ 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) {
+ // Never skip meaningful groupings.
+ if (groupedHistograms.size > 1) return false;
+
+ // Never skip the zero-th grouping.
+ if (grouping.key === groupings[0].key) return false;
+
+ // Never skip the grouping that defines the table columns.
+ if (grouping.key === tr.v.HistogramGrouping.DISPLAY_LABEL.key) {
+ return false;
+ }
+
+ // Skip meaningless groupings.
+ return true;
+ }
+
+ this.groupedHistograms_ =
+ this.filteredHistograms_.groupHistogramsRecursively(
+ groupings, canSkipGrouping);
+
+ this.hierarchies_ = undefined;
+ },
+
+ /**
+ * @param {!tr.b.Event} event
+ * @param {!Object} event.delta
+ * @param {!Object} event.delta.searchQuery
+ * @param {!Object} event.delta.referenceDisplayLabel
+ * @param {!Object} event.delta.displayStatisticName
+ * @param {!Object} event.delta.showAll
+ * @param {!Object} event.delta.groupings
+ * @param {!Object} event.delta.sortColumnIndex
+ * @param {!Object} event.delta.sortDescending
+ * @param {!Object} event.delta.constrainNameColumn
+ * @param {!Object} event.delta.tableRowStates
+ */
+ 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) {
+ // Force re-sort.
+ this.$.table.sortColumnIndex = undefined;
+ }
+
+ if (event.delta.referenceDisplayLabel !== undefined ||
+ event.delta.displayStatisticName !== undefined) {
+ // Force this.$.table.bodyDirty_ = true;
+ this.$.table.tableRows = this.$.table.tableRows;
+ }
+
+ // updateContents_() always copies sortColumnIndex and sortDescending
+ // from the viewState to the table. The table will only re-sort if
+ // they change.
+
+ // Name-cells listen to this.viewState to handle updates to
+ // constrainNameColumn.
+
+ 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');
+ }
+ }
+ return; // No need to re-enter updateContents_().
+ }
+
+ await this.updateContents_();
+ },
+
+ onSortColumnChanged_(event) {
+ tr.b.Timing.instant('histogram-set-table', 'sortColumn');
+ this.viewState.update({
+ sortColumnIndex: event.sortColumnIndex,
+ sortDescending: event.sortDescending,
+ });
+ },
+
+ onRequestSelectionChange_(event) {
+ // This event may reference an EventSet or an array of Histogram names.
+ // If EventSet, let the BrushingStateController handle it.
+ 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})$`,
+ });
+ },
+
+ /**
+ * @return {!tr.v.HistogramSet}
+ */
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html
new file mode 100644
index 00000000000..8a1d158d021
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html
@@ -0,0 +1,396 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+<link rel="import" href="/tracing/value/ui/histogram_span.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id="tr-v-ui-histogram-set-table-cell">
+ <template>
+ <style>
+ #histogram_container {
+ display: flex;
+ flex-direction: row;
+ }
+
+ #missing, #empty, #unmergeable, #scalar {
+ flex-grow: 1;
+ }
+
+ #open_histogram, #close_histogram, #open_histogram svg, #close_histogram svg {
+ height: 1em;
+ }
+
+ #open_histogram svg {
+ margin-left: 4px;
+ stroke-width: 0;
+ stroke: blue;
+ fill: blue;
+ }
+ :host(:hover) #open_histogram svg {
+ background: blue;
+ stroke: white;
+ fill: white;
+ }
+
+ #scalar {
+ flex-grow: 1;
+ white-space: nowrap;
+ }
+
+ #histogram {
+ flex-grow: 1;
+ }
+
+ #close_histogram svg line {
+ stroke-width: 18;
+ stroke: black;
+ }
+ #close_histogram:hover svg {
+ background: black;
+ }
+ #close_histogram:hover svg line {
+ stroke: white;
+ }
+
+ #overview_container {
+ display: none;
+ }
+ </style>
+
+ <div id="histogram_container">
+ <span id="missing">(missing)</span>
+ <span id="empty">(empty)</span>
+ <span id="unmergeable">(unmergeable)</span>
+
+ <tr-v-ui-scalar-span id="scalar" on-click="openHistogram_"></tr-v-ui-scalar-span>
+
+ <span id="open_histogram" on-click="openHistogram_">
+ <svg viewbox="0 0 128 128">
+ <rect x="16" y="24" width="32" height="16"/>
+ <rect x="16" y="56" width="96" height="16"/>
+ <rect x="16" y="88" width="64" height="16"/>
+ </svg>
+ </span>
+
+ <span id="histogram"></span>
+
+ <span id="close_histogram" on-click="closeHistogram_">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </div>
+
+ <div id="overview_container">
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'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_);
+ // Don't need to removeUpdateListener for the row and cells; their
+ // lifetimes are the same as |this|.
+ },
+
+ 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 is undefined when this.histogram_ is undefined.
+ // In that case, onViewStateUpdate_ wouldn't be able to do anything
+ // anyway.
+ 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.histogram_ and this.referenceHistogram might be undefined,
+ // a HistogramSet of unmergeable Histograms, or a Histogram.
+ 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;
+ }
+
+ // Unfortunately, we can't use a css attribute for this since this stuff
+ // is tied up in all the possible states of this.histogram. See
+ // updateContents_().
+
+ 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';
+
+ // Wait to create the histogram-span until the user wants to display it
+ // in order to speed up creating lots of histogram-set-table-cells when
+ // building the table.
+ 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_);
+ }
+
+ this.viewState.isOpen = open;
+ },
+
+ onViewStateUpdate_(event) {
+ if (event.delta.isOpen) {
+ this.isHistogramOpen = this.viewState.isOpen;
+ }
+ },
+
+ onRowStateUpdate_(event) {
+ if (event.delta.isOverviewed === undefined) return;
+ if (this.row.viewState.isOverviewed) {
+ this.showOverview();
+ } else {
+ this.hideOverview();
+ }
+ },
+
+ onRootStateUpdate_(event) {
+ if (event.delta.referenceDisplayLabel &&
+ this.histogramSpan_) {
+ this.histogramSpan_.build(this.histogram, this.referenceHistogram);
+ }
+
+ if (event.delta.displayStatisticName ||
+ event.delta.referenceDisplayLabel) {
+ this.updateMwu_();
+ this.updateContents_();
+ } else if (event.delta.alpha && this.mwuResult_) {
+ this.mwuResult_.compare(this.row.rootViewState.alpha);
+ this.updateSignificance_();
+ }
+
+ if (this.row.viewState.isOverviewed &&
+ (event.delta.sortColumnIndex ||
+ event.delta.sortDescending ||
+ event.delta.displayStatisticName ||
+ event.delta.referenceDisplayLabel)) {
+ if (this.overviewChart_ !== undefined) {
+ this.$.overview_container.removeChild(this.overviewChart_);
+ this.overviewChart_ = undefined;
+ }
+ this.showOverview();
+ }
+ },
+
+ onClick_(event) {
+ // Since the histogram-set-table's table doesn't support any kind of
+ // selection, clicking anywhere within a row that has subRows will
+ // expand/collapse that row, which can relayout the table and move things
+ // around. Prevent table relayout by preventing the tr-ui-b-table from
+ // receiving the click event.
+ event.stopPropagation();
+ },
+
+ openHistogram_() {
+ this.isHistogramOpen = true;
+ tr.b.Timing.instant('histogram-set-table-cell', 'open');
+ },
+
+ closeHistogram_() {
+ this.isHistogramOpen = false;
+ tr.b.Timing.instant('histogram-set-table-cell', 'close');
+ },
+
+ updateContents_() {
+ const isOpen = this.isHistogramOpen;
+
+ this.$.empty.style.display = 'none';
+ this.$.unmergeable.style.display = 'none';
+ this.$.scalar.style.display = 'none';
+ this.$.histogram.style.display = 'none';
+ this.$.close_histogram.style.display = 'none';
+ this.$.open_histogram.style.visibility = 'hidden';
+
+ if (!this.histogram) {
+ this.$.missing.style.display = 'block';
+ return;
+ }
+
+ this.$.missing.style.display = 'none';
+
+ if (this.histogram instanceof tr.v.HistogramSet) {
+ this.$.unmergeable.style.display = 'block';
+ return;
+ }
+
+ if (!(this.histogram instanceof tr.v.Histogram)) {
+ throw new Error('Invalid Histogram: ' + this.histogram);
+ }
+
+ if (this.histogram.numValues === 0) {
+ this.$.empty.style.display = 'block';
+ return;
+ }
+
+ this.$.open_histogram.style.display = 'block';
+ this.$.open_histogram.style.visibility = 'visible';
+ this.$.scalar.style.display = 'flex';
+
+ this.updateSignificance_();
+
+ const referenceHistogram = this.referenceHistogram;
+ const statName = this.histogram.getAvailableStatisticName(
+ this.row.rootViewState.displayStatisticName, referenceHistogram);
+ const statisticScalar = this.histogram.getStatisticScalar(
+ statName, referenceHistogram);
+ this.$.scalar.setValueAndUnit(
+ statisticScalar.value, statisticScalar.unit);
+
+ this.isHistogramOpen = isOpen;
+ },
+
+ showOverview() {
+ this.$.overview_container.style.display = 'block';
+ if (this.overviewChart_ !== undefined) return;
+
+ this.row.sortSubRows();
+ let referenceDisplayLabel =
+ this.row.rootViewState.referenceDisplayLabel;
+ if (referenceDisplayLabel === this.displayLabel_) {
+ referenceDisplayLabel = undefined;
+ }
+ const displayStatisticName = this.row.rootViewState.displayStatisticName;
+ const data = [];
+ let unit;
+
+ for (const subRow of this.row.subRows) {
+ const subHist = subRow.columns.get(this.displayLabel_);
+ if (!(subHist instanceof tr.v.Histogram)) continue;
+
+ if (unit === undefined) {
+ unit = subHist.unit;
+ } else if (unit !== subHist.unit) {
+ // The subrows have different units, so the overview chart cannot
+ // use a single unit to format all of the values, so don't display
+ // an overview chart at all.
+ data.splice(0);
+ break;
+ }
+
+ const refHist = subRow.columns.get(referenceDisplayLabel);
+ const statName = subHist.getAvailableStatisticName(
+ displayStatisticName, refHist);
+ const statScalar = subHist.getStatisticScalar(
+ statName, refHist);
+
+ if (statScalar !== undefined) {
+ data.push({
+ x: subRow.name,
+ y: statScalar.value,
+ });
+ }
+ }
+ if (data.length < 2) return;
+
+ this.overviewChart_ = new tr.ui.b.NameLineChart();
+ this.$.overview_container.appendChild(this.overviewChart_);
+ this.overviewChart_.displayXInHover = true;
+ this.overviewChart_.hideLegend = true;
+ this.overviewChart_.unit = unit;
+ this.overviewChart_.overrideDataRange = this.row.overviewDataRange;
+ this.overviewChart_.data = data;
+ },
+
+ hideOverview() {
+ this.$.overview_container.style.display = 'none';
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html
new file mode 100644
index 00000000000..f0dec062018
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html
@@ -0,0 +1,361 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+
+<dom-module id="tr-v-ui-histogram-set-table-name-cell">
+ <template>
+ <style>
+ #name_container {
+ display: flex;
+ }
+
+ #name {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ #show_overview, #hide_overview, #show_overview svg, #hide_overview svg {
+ height: 1em;
+ margin-left: 5px;
+ }
+
+ #show_overview svg {
+ stroke: blue;
+ stroke-width: 16;
+ }
+
+ #show_overview:hover svg {
+ background: blue;
+ stroke: white;
+ }
+
+ #hide_overview {
+ display: none;
+ }
+
+ #hide_overview svg {
+ stroke-width: 18;
+ stroke: black;
+ }
+
+ #hide_overview:hover svg {
+ background: black;
+ stroke: white;
+ }
+
+ #open_histograms, #close_histograms, #open_histograms svg, #close_histograms svg {
+ height: 1em;
+ }
+
+ #close_histograms {
+ display: none;
+ }
+
+ #open_histograms svg {
+ margin-left: 4px;
+ stroke-width: 0;
+ stroke: blue;
+ fill: blue;
+ }
+ #open_histograms:hover svg {
+ background: blue;
+ stroke: white;
+ fill: white;
+ }
+
+ #close_histograms line {
+ stroke-width: 18;
+ stroke: black;
+ }
+ #close_histograms:hover {
+ background: black;
+ }
+ #close_histograms:hover line {
+ stroke: white;
+ }
+
+ #overview_container {
+ display: none;
+ }
+ </style>
+
+ <div id="name_container">
+ <span id="name"></span>
+
+ <span id="show_overview" on-click="showOverview_">
+ <svg viewbox="0 0 128 128">
+ <line x1="19" y1="109" x2="49" y2="49"/>
+ <line x1="49" y1="49" x2="79" y2="79"/>
+ <line x1="79" y1="79" x2="109" y2="19"/>
+ </svg>
+ </span>
+
+ <span id="hide_overview" on-click="hideOverview_">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+
+ <span id="open_histograms" on-click="openHistograms_">
+ <svg viewbox="0 0 128 128">
+ <rect x="16" y="24" width="32" height="16"/>
+ <rect x="16" y="56" width="96" height="16"/>
+ <rect x="16" y="88" width="64" height="16"/>
+ </svg>
+ </span>
+
+ <span id="close_histograms" on-click="closeHistograms_">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </div>
+
+ <div id="overview_container">
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const NAME_COLUMN_WIDTH_PX = 300;
+
+ Polymer({
+ is: 'tr-v-ui-histogram-set-table-name-cell',
+
+ created() {
+ this.row_ = undefined;
+ this.overviewChart_ = undefined;
+ this.cellListener_ = this.onCellStateUpdate_.bind(this);
+ this.rootListener_ = this.onRootStateUpdate_.bind(this);
+ },
+
+ attached() {
+ if (this.row) {
+ this.row.rootViewState.addUpdateListener(this.rootListener_);
+ }
+ },
+
+ detached() {
+ this.row.rootViewState.removeUpdateListener(this.rootListener_);
+ // Don't need to removeUpdateListener for the row and cells; their
+ // lifetimes are the same as |this|.
+ },
+
+ get row() {
+ return this.row_;
+ },
+
+ build(row) {
+ if (this.row_ !== undefined) {
+ throw new Error('row must be set exactly once.');
+ }
+ this.row_ = row;
+ this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this));
+ this.constrainWidth = this.row.rootViewState.constrainNameColumn;
+ if (this.isAttached) {
+ this.row.rootViewState.addUpdateListener(this.rootListener_);
+ }
+
+ for (const cellState of this.row.viewState.cells.values()) {
+ cellState.addUpdateListener(this.cellListener_);
+ }
+
+ Polymer.dom(this.$.name).textContent = this.row.name;
+
+ this.title = this.row.name;
+ if (this.row.description) {
+ this.title += '\n' + this.row.description;
+ }
+
+ if (this.row.overviewDataRange.isEmpty ||
+ this.row.overviewDataRange.min === this.row.overviewDataRange.max) {
+ // TODO(#3744) Also hide this button when column or subrow units don't
+ // match.
+ this.$.show_overview.style.display = 'none';
+ }
+
+ let histogramCount = 0;
+ for (const cell of this.row.columns.values()) {
+ if (cell instanceof tr.v.Histogram &&
+ cell.numValues > 0) {
+ ++histogramCount;
+ }
+ }
+ if (histogramCount <= 1) {
+ this.$.open_histograms.style.display = 'none';
+ }
+ },
+
+ set constrainWidth(constrain) {
+ this.$.name.style.maxWidth = constrain ?
+ (this.nameWidthPx + 'px') : 'none';
+ },
+
+ get nameWidthPx() {
+ // tr-ui-b-table adds 16px of padding for each additional level of subRows
+ // nesting, so outer nameDivs can be wider than inner nameDivs.
+ return NAME_COLUMN_WIDTH_PX - (16 * this.row.depth);
+ },
+
+ get isOverflowing() {
+ return this.$.name.style.maxWidth !== 'none' &&
+ this.$.name.getBoundingClientRect().width === this.nameWidthPx;
+ },
+
+ get isOverviewed() {
+ return this.$.overview_container.style.display === 'block';
+ },
+
+ set isOverviewed(isOverviewed) {
+ if (isOverviewed === this.isOverviewed) return;
+ if (isOverviewed) {
+ this.showOverview_();
+ } else {
+ this.hideOverview_();
+ }
+ },
+
+ hideOverview_(opt_event) {
+ this.$.overview_container.style.display = 'none';
+ this.$.hide_overview.style.display = 'none';
+ this.$.show_overview.style.display = 'block';
+
+ if (opt_event !== undefined) {
+ opt_event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'hideOverview');
+ this.row.viewState.isOverviewed = this.isOverviewed;
+ }
+ },
+
+ showOverview_(opt_event) {
+ if (opt_event !== undefined) {
+ opt_event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'showOverview');
+ this.row.viewState.isOverviewed = true;
+ }
+
+ this.$.overview_container.style.display = 'block';
+ this.$.hide_overview.style.display = 'block';
+ this.$.show_overview.style.display = 'none';
+
+ if (this.overviewChart_ === undefined) {
+ const displayStatisticName =
+ this.row.rootViewState.displayStatisticName;
+ const data = [];
+ let unit;
+
+ for (const [displayLabel, hist] of this.row.sortedColumns()) {
+ if (!(hist instanceof tr.v.Histogram)) continue;
+
+ if (unit === undefined) {
+ unit = hist.unit;
+ } else if (unit !== hist.unit) {
+ // The columns have different units, so the overview chart cannot
+ // use a single unit to format all of the values, so don't display
+ // an overview chart at all.
+ data.splice(0);
+ break;
+ }
+
+ const statName = hist.getAvailableStatisticName(displayStatisticName);
+ const statScalar = hist.getStatisticScalar(statName);
+
+ if (statScalar !== undefined) {
+ data.push({
+ x: displayLabel,
+ y: statScalar.value,
+ });
+ }
+ }
+ if (data.length < 2) {
+ return;
+ }
+
+ this.overviewChart_ = new tr.ui.b.NameLineChart();
+ this.$.overview_container.appendChild(this.overviewChart_);
+ this.overviewChart_.displayXInHover = true;
+ this.overviewChart_.hideLegend = true;
+ this.overviewChart_.unit = unit;
+ this.overviewChart_.overrideDataRange = this.row.overviewDataRange;
+ this.overviewChart_.data = data;
+ }
+ },
+
+ openHistograms_(event) {
+ event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'openHistograms');
+ for (const cell of this.row.cells.values()) {
+ cell.isHistogramOpen = true;
+ }
+ this.$.close_histograms.style.display = 'block';
+ this.$.open_histograms.style.display = 'none';
+ },
+
+ closeHistograms_(event) {
+ event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'closeHistograms');
+ for (const cell of this.row.cells.values()) {
+ cell.isHistogramOpen = false;
+ }
+ this.$.open_histograms.style.display = 'block';
+ this.$.close_histograms.style.display = 'none';
+ },
+
+ onRootStateUpdate_(event) {
+ if (event.delta.constrainNameColumn) {
+ this.constrainWidth = this.row.rootViewState.constrainNameColumn;
+ }
+ if (this.row.viewState.isOverviewed &&
+ event.delta.displayStatisticName) {
+ this.row.resetOverviewDataRange();
+ if (this.overviewChart_ !== undefined) {
+ this.$.overview_container.removeChild(this.overviewChart_);
+ this.overviewChart_ = undefined;
+ }
+ this.showOverview_();
+ }
+ },
+
+ onRowStateUpdate_(event) {
+ if (event.delta.isOverviewed) {
+ this.isOverviewed = this.row.viewState.isOverviewed;
+ }
+ // This assumes that cell states are not updated.
+ },
+
+ onCellStateUpdate_(event) {
+ if (!event.delta.isOpen) return;
+
+ let cellCount = 0;
+ let openCellCount = 0;
+ for (const cell of this.row.cells.values()) {
+ if (!(cell.histogram instanceof tr.v.Histogram) ||
+ (cell.histogram.numValues === 0)) {
+ continue;
+ }
+ ++cellCount;
+ if (cell.isHistogramOpen) ++openCellCount;
+ }
+ if (cellCount <= 1) return;
+ const mostlyOpen = openCellCount > (cellCount / 2);
+ this.$.open_histograms.style.display = mostlyOpen ? 'none' : 'block';
+ this.$.close_histograms.style.display = mostlyOpen ? 'block' : 'none';
+ }
+ });
+
+ return {
+ NAME_COLUMN_WIDTH_PX,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html
new file mode 100644
index 00000000000..b4cb1a54020
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html
@@ -0,0 +1,299 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/ui/histogram_set_table_cell.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_table_name_cell.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ class HistogramSetTableRow {
+ /**
+ * @param {!tr.v.HistogramSetHierarchy} hierarchy
+ * @param {!Element} baseTable tr-ui-b-table
+ * @param {!tr.v.ui.HistogramSetViewState} rootViewState
+ */
+ constructor(hierarchy, baseTable, rootViewState) {
+ this.hierarchy_ = hierarchy;
+ this.baseTable_ = baseTable;
+ this.rootViewState_ = rootViewState;
+ this.viewState_ = new tr.v.ui.HistogramSetTableRowState();
+ this.viewState_.addUpdateListener(this.onViewStateUpdate_.bind(this));
+ this.overviewDataRange_ = undefined;
+ this.nameCell_ = undefined;
+ this.cells_ = new Map();
+ this.subRows_ = [];
+
+ // Don't assign viewState.subRows or cells. There can't be anything
+ // listening to viewState, so avoid the overhead of dispatching an event.
+ for (const subHierarchy of hierarchy.subRows) {
+ const subRow = new HistogramSetTableRow(
+ subHierarchy, baseTable, rootViewState);
+ this.subRows_.push(subRow);
+ this.viewState.subRows.set(subRow.name, subRow.viewState);
+ }
+ for (const columnName of this.columns.keys()) {
+ this.viewState.cells.set(
+ columnName, new tr.v.ui.HistogramSetTableCellState());
+ }
+ }
+
+ /**
+ * @return {string}
+ */
+ get name() {
+ return this.hierarchy_.name;
+ }
+
+ /**
+ * @return {number}
+ */
+ get depth() {
+ return this.hierarchy_.depth;
+ }
+
+ /**
+ * @return {string}
+ */
+ get description() {
+ return this.hierarchy_.description;
+ }
+
+ /**
+ * @return {!Map.<string, !(undefined|tr.v.Histogram|tr.v.HistogramSet)>}
+ */
+ get columns() {
+ return this.hierarchy_.columns;
+ }
+
+ * sortedColumns() {
+ for (const col of this.baseTable_.tableColumns) {
+ yield [
+ col.displayLabel,
+ this.hierarchy_.columns.get(col.displayLabel),
+ ];
+ }
+ }
+
+ /**
+ * @return {!tr.b.Range}
+ */
+ get overviewDataRange() {
+ if (this.overviewDataRange_ === undefined) {
+ this.overviewDataRange_ = new tr.b.math.Range();
+
+ const displayStatisticName =
+ this.rootViewState.displayStatisticName;
+ const referenceDisplayLabel =
+ this.rootViewState.referenceDisplayLabel;
+
+ for (const [displayLabel, hist] of this.columns) {
+ if (hist instanceof tr.v.Histogram) {
+ const statName = hist.getAvailableStatisticName(
+ displayStatisticName);
+ const statScalar = hist.getStatisticScalar(statName);
+ if (statScalar !== undefined) {
+ this.overviewDataRange_.addValue(statScalar.value);
+ }
+ }
+
+ for (const subRow of this.subRows) {
+ const subHist = subRow.columns.get(displayLabel);
+ if (!(subHist instanceof tr.v.Histogram)) continue;
+
+ const refHist = subRow.columns.get(referenceDisplayLabel);
+ const statName = subHist.getAvailableStatisticName(
+ displayStatisticName, refHist);
+ const statScalar = subHist.getStatisticScalar(
+ statName, refHist);
+
+ if (statScalar !== undefined) {
+ this.overviewDataRange_.addValue(statScalar.value);
+ }
+ }
+ }
+ }
+ return this.overviewDataRange_;
+ }
+
+ /**
+ * overviewDataRange is used by histogram-set-table-cell (hstc) and
+ * histogram-set-table-name-cell (hstnc) to display overview line charts
+ * with consistent y-axes.
+ * overviewDataRange depends on HistogramSetViewState.displayStatisticName
+ * and referenceDisplayLabel, so it must be recomputed when either of those
+ * changes.
+ * overviewDataRange should not be recomputed for each hstc in the row; it
+ * should only be computed once when necessary, and cached.
+ * HistogramSetTableRow (HSTR) cannot listen to HistogramSetViewState
+ * (HSVS) updates because there is no way for it to remove the listener.
+ * However, Polymer has detached callbacks, so dom-modules can listen to
+ * HSVS updates without leaking memory.
+ * overviewDataRange should be recomputed only once whenever
+ * displayStatisticName or referenceDisplayLabel changes.
+ * There is exactly one hstnc per row.
+ * histogram-set-table-name-cell resets overviewDataRange when
+ * displayStatisticName or referenceDisplayLabel changes.
+ */
+ resetOverviewDataRange() {
+ this.overviewDataRange_ = undefined;
+ }
+
+ /**
+ * @return {!tr.v.ui.HistogramSetViewState}
+ */
+ get rootViewState() {
+ return this.rootViewState_;
+ }
+
+ /**
+ * @return {!Map.<string, !Element>} tr-v-ui-histogram-set-table-cell
+ */
+ get cells() {
+ return this.cells_;
+ }
+
+ /**
+ * @return {!Array.<tr.v.ui.HistogramSetTableRow>}
+ */
+ get subRows() {
+ return this.subRows_;
+ }
+
+ /**
+ * @return {!Array.<tr.v.ui.HistogramSetTableRowState>}
+ */
+ get viewState() {
+ return this.viewState_;
+ }
+
+ * walk() {
+ yield this;
+ for (const row of this.subRows) yield* row.walk();
+ }
+
+ static* walkAll(rootRows) {
+ for (const rootRow of rootRows) yield* rootRow.walk();
+ }
+
+ get nameCell() {
+ if (this.nameCell_ === undefined) {
+ this.nameCell_ = document.createElement(
+ 'tr-v-ui-histogram-set-table-name-cell');
+ this.nameCell_.build(this);
+ }
+ return this.nameCell_;
+ }
+
+ getCell(columnName) {
+ if (this.cells.has(columnName)) return this.cells.get(columnName);
+ const cell = document.createElement('tr-v-ui-histogram-set-table-cell');
+ cell.build(this, columnName, this.viewState.cells.get(columnName));
+ this.cells.set(columnName, cell);
+ return cell;
+ }
+
+ compareNames(other) {
+ return this.name.localeCompare(other.name);
+ }
+
+ compareCells(other, displayLabel) {
+ // If a reference column is selected, compare the absolute deltas
+ // between the two cells and their references.
+ const referenceDisplayLabel = this.rootViewState.referenceDisplayLabel;
+ let referenceCellA;
+ let referenceCellB;
+ if (referenceDisplayLabel &&
+ referenceDisplayLabel !== displayLabel) {
+ referenceCellA = this.columns.get(referenceDisplayLabel);
+ referenceCellB = other.columns.get(referenceDisplayLabel);
+ }
+
+ const cellA = this.columns.get(displayLabel);
+ let valueA = 0;
+ if (cellA instanceof tr.v.Histogram) {
+ const statisticA = cellA.getAvailableStatisticName(
+ this.rootViewState.displayStatisticName, referenceCellA);
+ const scalarA = cellA.getStatisticScalar(statisticA, referenceCellA);
+ if (scalarA) {
+ valueA = scalarA.value;
+ }
+ }
+
+ const cellB = other.columns.get(displayLabel);
+ let valueB = 0;
+ if (cellB instanceof tr.v.Histogram) {
+ const statisticB = cellB.getAvailableStatisticName(
+ this.rootViewState.displayStatisticName, referenceCellB);
+ const scalarB = cellB.getStatisticScalar(statisticB, referenceCellB);
+ if (scalarB) {
+ valueB = scalarB.value;
+ }
+ }
+
+ return valueA - valueB;
+ }
+
+ onViewStateUpdate_(event) {
+ if (event.delta.isExpanded) {
+ this.baseTable_.setExpandedForTableRow(this, this.viewState.isExpanded);
+ }
+
+ if (event.delta.subRows) {
+ throw new Error('HistogramSetTableRow.subRows must not be reassigned.');
+ }
+
+ if (event.delta.cells) {
+ // Only validate the cells that have already been built.
+ // Cells may not have been built yet, so only validate the cells that
+ // have been built.
+ for (const [displayLabel, cell] of this.cells) {
+ if (cell.viewState !== this.viewState.cells.get(displayLabel)) {
+ throw new Error('Only HistogramSetTableRow may update cells');
+ }
+ }
+ }
+ }
+
+ async restoreState(vs) {
+ // Don't use updateFromViewState() because it would overwrite cells and
+ // subRows, but we just want to restore them.
+ await this.viewState.update({
+ isExpanded: vs.isExpanded,
+ isOverviewed: vs.isOverviewed,
+ });
+
+ // If cells haven't been built yet, then their state will be restored when
+ // they are built.
+ for (const [displayLabel, cell] of this.cells) {
+ const previousState = vs.cells.get(displayLabel);
+ if (!previousState) continue;
+ await cell.viewState.updateFromViewState(previousState);
+ }
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html
new file mode 100644
index 00000000000..f8d76afc72b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html
@@ -0,0 +1,1679 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ // TODO(#3811) Clean up these tests.
+
+ const TEST_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 20);
+
+ async function buildTable(test, histograms) {
+ // This should mirror HistogramImporter in order to be as similar to
+ // results.html as possible.
+ const table = document.createElement('tr-v-ui-histogram-set-table');
+
+ table.viewState = new tr.v.ui.HistogramSetViewState();
+ await table.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [tr.v.HistogramGrouping.HISTOGRAM_NAME],
+ });
+
+ table.style.display = 'none';
+ test.addHTMLOutput(table);
+
+ table.addEventListener('display-ready', () => {
+ table.style.display = '';
+ });
+
+ const collector = new tr.v.HistogramParameterCollector();
+ collector.process(histograms);
+
+ await table.build(
+ histograms,
+ histograms.sourceHistograms,
+ collector.labels,
+ async message => {
+ await tr.b.animationFrame();
+ });
+ return table;
+ }
+
+ function range(start, end) {
+ const result = [];
+ for (let i = start; i < end; ++i) result.push(i);
+ return result;
+ }
+
+ function getBaseTable(table) {
+ return tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.tagName === 'TR-UI-B-TABLE');
+ }
+
+ function getNameCells(table) {
+ return tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL');
+ }
+
+ function getTableCells(table) {
+ return tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL');
+ }
+
+ test('viewSearchQuery', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+
+ await table.viewState.update({searchQuery: 'a'});
+ let cells = getTableCells(table);
+ assert.lengthOf(cells, 1);
+
+ await table.viewState.update({searchQuery: '[z-'});
+ cells = getTableCells(table);
+ assert.lengthOf(cells, 2);
+
+ await table.viewState.update({searchQuery: 'x'});
+ cells = getTableCells(table);
+ assert.lengthOf(cells, 0);
+
+ await table.viewState.update({searchQuery: ''});
+ cells = getTableCells(table);
+ assert.lengthOf(cells, 2);
+ });
+
+ test('controlSearchQuery', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count,
+ {value: 1, diagnostics: {r: tr.v.d.Breakdown.fromEntries([['0', 1]])}});
+ const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', bHist.name);
+ aHist.diagnostics.set('r', related);
+ const table = await buildTable(this, histograms);
+ await table.viewState.tableRowStates.get('a').cells.get('Value').update(
+ {isOpen: true});
+ const link = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-A-ANALYSIS-LINK');
+ link.click();
+ assert.strictEqual('^(b)$', table.viewState.searchQuery);
+ });
+
+ test('viewReferenceDisplayLabel', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])
+ ]]),
+ });
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])
+ ]]),
+ });
+ const table = await buildTable(this, histograms);
+ const baseTable = getBaseTable(table);
+ assert.isUndefined(baseTable.selectedTableColumnIndex);
+
+ await table.viewState.update({referenceDisplayLabel: 'A'});
+ assert.strictEqual(1, baseTable.selectedTableColumnIndex);
+
+ await table.viewState.update({referenceDisplayLabel: 'B'});
+ assert.strictEqual(2, baseTable.selectedTableColumnIndex);
+ });
+
+ test('viewDisplayStatisticName', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 10), {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])
+ ]]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(10, 20), {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])
+ ]]),
+ });
+ const table = await buildTable(this, histograms);
+ let scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-SCALAR-SPAN');
+ assert.lengthOf(scalarSpans, 2);
+ assert.strictEqual('5', scalarSpans[0].unit.format(scalarSpans[0].value));
+ assert.strictEqual('15', scalarSpans[1].unit.format(scalarSpans[1].value));
+
+ await table.viewState.update({displayStatisticName: 'std'});
+ scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-SCALAR-SPAN');
+ assert.lengthOf(scalarSpans, 2);
+ assert.strictEqual('3', scalarSpans[0].unit.format(scalarSpans[0].value));
+ assert.strictEqual('3', scalarSpans[1].unit.format(scalarSpans[1].value));
+ });
+
+ test('autoShowAll', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', bHist.name);
+ aHist.diagnostics.set('r', related);
+ const table = await buildTable(this, histograms);
+
+ let cells = getNameCells(table);
+ assert.lengthOf(cells, 2);
+ assert.strictEqual('a', cells[0].row.name);
+
+ await table.viewState.update({searchQuery: 'b'});
+ assert.isTrue(table.viewState.showAll);
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 1);
+ assert.strictEqual('b', cells[0].row.name);
+ });
+
+ test('viewShowAll', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', bHist.name);
+ aHist.diagnostics.set('r', related);
+ const table = await buildTable(this, histograms);
+
+ let cells = getNameCells(table);
+ assert.lengthOf(cells, 2);
+ assert.strictEqual('a', cells[0].row.name);
+ assert.strictEqual('b', cells[1].row.name);
+
+ await table.viewState.update({showAll: false});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 1);
+ assert.strictEqual('a', cells[0].row.name);
+ });
+
+ test('viewSortColumnIndex', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+ const baseTable = getBaseTable(table);
+ assert.strictEqual(baseTable.sortColumnIndex, 0);
+ assert.isFalse(baseTable.sortDescending);
+
+ await table.viewState.update({sortColumnIndex: 1, sortDescending: true});
+ assert.isTrue(baseTable.sortDescending);
+ assert.strictEqual(baseTable.sortColumnIndex, 1);
+ });
+
+ test('controlSortColumnIndex', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+
+ assert.strictEqual(0, table.viewState.sortColumnIndex);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click();
+ assert.strictEqual(0, table.viewState.sortColumnIndex);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[1].click();
+ assert.strictEqual(1, table.viewState.sortColumnIndex);
+ });
+
+ test('viewSortDescending', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+
+ await table.viewState.update({sortColumnIndex: 0});
+
+ await table.viewState.update({sortDescending: true});
+
+ await table.viewState.update({sortDescending: false});
+ });
+
+ test('controlSortDescending', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({sortColumnIndex: 0});
+
+ assert.isFalse(table.viewState.sortDescending);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click();
+ assert.isTrue(table.viewState.sortDescending);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click();
+ assert.isFalse(table.viewState.sortDescending);
+ });
+
+ test('sortUndefinedStatistics', async function() {
+ // The 'avg' statistic Scalar of an empty histogram is undefined, so
+ // HistogramSetTableRow.compareCells must not throw when it encounters
+ // undefined Scalars.
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({sortColumnIndex: 1});
+ });
+
+ test('sortByDeltaStatistic', async function() {
+ const histograms0 = new tr.v.HistogramSet();
+ const histograms1 = new tr.v.HistogramSet();
+ const a0Hist = histograms0.createHistogram(
+ 'a', tr.b.Unit.byName.count, [0]);
+ const b0Hist = histograms0.createHistogram(
+ 'b', tr.b.Unit.byName.count, [0]);
+ const c0Hist = histograms0.createHistogram(
+ 'c', tr.b.Unit.byName.count, [3]);
+ const a1Hist = histograms1.createHistogram(
+ 'a', tr.b.Unit.byName.count, [1]);
+ const b1Hist = histograms1.createHistogram(
+ 'b', tr.b.Unit.byName.count, [2]);
+ const c1Hist = histograms1.createHistogram(
+ 'c', tr.b.Unit.byName.count, [3]);
+ histograms0.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L0']));
+ histograms0.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(0));
+ histograms1.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L1']));
+ histograms1.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1));
+
+ const table = await buildTable(this, new tr.v.HistogramSet(
+ Array.from(histograms0).concat(Array.from(histograms1))));
+ await table.viewState.update({
+ displayStatisticName: tr.v.DELTA + 'avg',
+ referenceDisplayLabel: 'L0',
+ sortColumnIndex: 2,
+ });
+ const nameCells = getNameCells(table);
+ assert.strictEqual('c', nameCells[0].row.name);
+ assert.strictEqual('a', nameCells[1].row.name);
+ assert.strictEqual('b', nameCells[2].row.name);
+ });
+
+ test('sortMissing', async function() {
+ // Missing cells should be treated as zero for sorting purposes. The
+ // comparator must not return undefined or NaN.
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])],
+ ]),
+ });
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])],
+ ]),
+ });
+ // 'c','x' intentionally missing
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])],
+ ]),
+ });
+ // 'b','y' intentionally missing
+ histograms.createHistogram('c', tr.b.Unit.byName.count, [-1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({sortColumnIndex: 2});
+ let cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('c', cells[0].row.name);
+ assert.strictEqual('b', cells[1].row.name);
+ assert.strictEqual('a', cells[2].row.name);
+ await table.viewState.update({sortDescending: true});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('a', cells[0].row.name);
+ assert.strictEqual('b', cells[1].row.name);
+ assert.strictEqual('c', cells[2].row.name);
+ await table.viewState.update({sortColumnIndex: 1});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('b', cells[0].row.name);
+ assert.strictEqual('a', cells[1].row.name);
+ assert.strictEqual('c', cells[2].row.name);
+ await table.viewState.update({sortDescending: false});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('c', cells[0].row.name);
+ assert.strictEqual('a', cells[1].row.name);
+ assert.strictEqual('b', cells[2].row.name);
+ });
+
+ test('viewConstrainNameColumn', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ assert.isTrue(table.viewState.constrainNameColumn);
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('block', dots.style.display);
+
+ await table.viewState.update({constrainNameColumn: false});
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+
+ await table.viewState.update({constrainNameColumn: true});
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('controlConstrainNameColumn', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ assert.isTrue(table.viewState.constrainNameColumn);
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('block', dots.style.display);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click();
+ assert.isFalse(table.viewState.constrainNameColumn);
+ await tr.b.animationFrame();
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click();
+ assert.isTrue(table.viewState.constrainNameColumn);
+ await tr.b.animationFrame();
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('viewRowExpanded', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['A']));
+ const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+ assert.lengthOf(getTableCells(table), 1);
+
+ await table.viewState.tableRowStates.get('a').update({isExpanded: true});
+ assert.lengthOf(getTableCells(table), 3);
+
+ await table.viewState.tableRowStates.get('a').update({isExpanded: false});
+ assert.lengthOf(getTableCells(table), 1);
+ });
+
+ test('controlRowExpanded', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['A']));
+ const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+ assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded);
+
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ nameCell.click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').isExpanded);
+
+ nameCell.click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded);
+ });
+
+ test('viewIsOverviewed', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ const nameCells = getNameCells(table);
+ const cells = getTableCells(table);
+ assert.isFalse(nameCells[0].isOverviewed);
+
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: true});
+ assert.isTrue(nameCells[0].isOverviewed);
+
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: false});
+ assert.isFalse(nameCells[0].isOverviewed);
+ });
+
+ test('overviewSorted', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [4], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['D'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [2], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [3], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['C'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: 0,
+ sortDescending: true,
+ });
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: true});
+
+ let cells = getTableCells(table);
+ let chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.strictEqual('D', chart.data[0].x);
+ assert.strictEqual('C', chart.data[1].x);
+ assert.strictEqual('B', chart.data[2].x);
+ assert.strictEqual('A', chart.data[3].x);
+
+ await table.viewState.update({
+ sortDescending: false,
+ });
+ cells = getTableCells(table);
+ chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.strictEqual('A', chart.data[0].x);
+ assert.strictEqual('B', chart.data[1].x);
+ assert.strictEqual('C', chart.data[2].x);
+ assert.strictEqual('D', chart.data[3].x);
+ });
+
+ test('controlIsOverviewed', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed);
+
+ const nameCells = getNameCells(table);
+ tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e =>
+ e.id === 'show_overview').click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').isOverviewed);
+
+ tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e =>
+ e.id === 'hide_overview').click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed);
+ });
+
+ test('overviewStatistic', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ displayStatisticName: 'count',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ });
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: true});
+
+ let charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.lengthOf(charts, 3);
+ assert.lengthOf(charts[0].data, 2);
+ assert.lengthOf(charts[1].data, 2);
+ assert.lengthOf(charts[2].data, 2);
+ assert.strictEqual(charts[0].data[0].y, 3);
+ assert.strictEqual(charts[0].data[1].y, 7);
+ assert.strictEqual(charts[1].data[0].y, 1);
+ assert.strictEqual(charts[1].data[1].y, 2);
+ assert.strictEqual(charts[2].data[0].y, 3);
+ assert.strictEqual(charts[2].data[1].y, 4);
+
+ await table.viewState.update({
+ displayStatisticName: tr.v.DELTA + 'count',
+ referenceDisplayLabel: 'A',
+ });
+ charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.lengthOf(charts, 3);
+ assert.lengthOf(charts[0].data, 2);
+ assert.lengthOf(charts[1].data, 2);
+ assert.lengthOf(charts[2].data, 2);
+ assert.strictEqual(charts[0].data[0].y, 3);
+ assert.strictEqual(charts[0].data[1].y, 7);
+ assert.strictEqual(charts[1].data[0].y, 1);
+ assert.strictEqual(charts[1].data[1].y, 2);
+ assert.strictEqual(charts[2].data[0].y, 2);
+ assert.strictEqual(charts[2].data[1].y, 2);
+ });
+
+ test('overviewUnits', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ histograms.createHistogram(
+ 'a', tr.b.Unit.byName.unitlessNumber, [1, 1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ displayStatisticName: 'count',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ });
+ await table.viewState.tableRowStates.get('a').update({
+ isExpanded: true,
+ isOverviewed: true,
+ });
+ await table.viewState.tableRowStates.get('a').subRows.get('X').update({
+ isOverviewed: true,
+ });
+
+ const nameCells = getNameCells(table);
+
+ // Check there is no overviewChart in name-cell when column units mismatch.
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[0],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[2],
+ '#overview_container svg'));
+
+ // When column units match, the overview chart should exist and have the
+ // correct unit.
+ const nameOverviewChart = tr.ui.b.findDeepElementMatching(nameCells[1],
+ '#overview_container svg');
+ assert.isDefined(nameOverviewChart);
+ assert.strictEqual(nameOverviewChart.unit, tr.b.Unit.byName.count);
+
+ const cells = getTableCells(table);
+
+ // When subrow units match, the overview chart should exist and have the
+ // correct unit.
+ const overviewChart = tr.ui.b.findDeepElementMatching(cells[0],
+ '#overview_container svg');
+ assert.isDefined(overviewChart);
+ assert.strictEqual(overviewChart.unit, tr.b.Unit.byName.count);
+
+ // Check there is no overviewChart in table-cell when subrow units mismatch.
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[1],
+ '#overview_container svg'));
+
+ // Check there is no overviewChart in table-cell when there are no subrows.
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[2],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[3],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[4],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[5],
+ '#overview_container svg'));
+ });
+
+ test('viewCellOpen', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ const table = await buildTable(this, histograms);
+ const cell = tr.b.getOnlyElement(getTableCells(table));
+ assert.isFalse(cell.isHistogramOpen);
+
+ await table.viewState.tableRowStates.get('a').cells.get('Value').update(
+ {isOpen: true});
+ assert.isTrue(cell.isHistogramOpen);
+
+ await table.viewState.tableRowStates.get('a').cells.get('Value').update(
+ {isOpen: false});
+ assert.isFalse(cell.isHistogramOpen);
+ });
+
+ test('controlCellOpen', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+ const cells = getTableCells(table);
+
+ tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.tagName === 'TR-V-UI-SCALAR-SPAN').click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+
+ tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.id === 'close_histogram').click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.id === 'open_histograms').click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+ assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('B')
+ .isOpen);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.id === 'close_histograms').click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('B')
+ .isOpen);
+ });
+
+ test('rebin', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 100), {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(50, 150), {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+
+ const cells = getTableCells(table);
+ assert.lengthOf(cells, 2);
+ assert.lengthOf(cells[0].histogram.allBins,
+ 2 + tr.v.DEFAULT_REBINNED_COUNT);
+ assert.lengthOf(cells[1].histogram.allBins,
+ 2 + tr.v.DEFAULT_REBINNED_COUNT);
+ assert.strictEqual(cells[0].histogram.allBins[0].range.max, 0);
+ assert.strictEqual(cells[1].histogram.allBins[0].range.max, 0);
+ assert.strictEqual(cells[0].histogram.allBins[41].range.min, 200);
+ assert.strictEqual(cells[1].histogram.allBins[41].range.min, 200);
+ });
+
+ test('leafHistograms', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ assert.lengthOf(table.leafHistograms, 1);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+ assert.lengthOf(table.leafHistograms, 2);
+ });
+
+ test('nameCellOverflow', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('block', dots.style.display);
+ dots.click();
+
+ await tr.b.animationFrame();
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('nameCellOverflowOnExpand', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['0'.repeat(100)]));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['1'.repeat(100)]));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('none', dots.style.display);
+
+ const baseTable = getBaseTable(table);
+ await table.viewState.tableRowStates.get('a').update({isExpanded: true});
+
+ const nameCell = tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL' &&
+ e.row.name === '0'.repeat(100));
+ await tr.b.animationFrame();
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+
+ assert.strictEqual('block', dots.style.display);
+ dots.click();
+
+ await tr.b.animationFrame();
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('overviewCharts', async function() {
+ const binBoundaries = tr.v.HistogramBinBoundaries.createLinear(0, 150, 10);
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [0], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [10], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [100], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [110], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [0], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [9], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [90], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [99], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ const now = new Date().getTime();
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ table.viewState.tableRowStates.values())) {
+ await row.update({isOverviewed: true});
+ }
+
+ let charts = tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => ((e.id === 'overview_container') &&
+ (e.style.display !== 'none')));
+ charts = charts.map(div => div.children[0]);
+ assert.lengthOf(charts, 6);
+
+ assert.deepEqual(JSON.stringify(charts[0].data),
+ JSON.stringify([{x: '0', y: 45}, {x: '1', y: 54}]));
+ tr.b.assertRangeEquals(
+ charts[0].dataRange, tr.b.math.Range.fromExplicitRange(0, 99));
+
+ assert.deepEqual(
+ charts[1].data, [{x: 'story0', y: 0}, {x: 'story1', y: 90}]);
+ tr.b.assertRangeEquals(
+ charts[1].dataRange, tr.b.math.Range.fromExplicitRange(0, 99));
+
+ assert.deepEqual(
+ charts[2].data, [{x: 'story0', y: 9}, {x: 'story1', y: 99}]);
+ tr.b.assertRangeEquals(
+ charts[2].dataRange, tr.b.math.Range.fromExplicitRange(0, 99));
+
+ assert.deepEqual(charts[3].data, [{x: '0', y: 50}, {x: '1', y: 60}]);
+ tr.b.assertRangeEquals(
+ charts[3].dataRange, tr.b.math.Range.fromExplicitRange(0, 110));
+
+ assert.deepEqual(
+ charts[4].data, [{x: 'story0', y: 0}, {x: 'story1', y: 100}]);
+ tr.b.assertRangeEquals(
+ charts[4].dataRange, tr.b.math.Range.fromExplicitRange(0, 110));
+
+ assert.deepEqual(
+ charts[5].data, [{x: 'story0', y: 10}, {x: 'story1', y: 110}]);
+ tr.b.assertRangeEquals(
+ charts[5].dataRange, tr.b.math.Range.fromExplicitRange(0, 110));
+
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ table.viewState.tableRowStates.values())) {
+ await row.update({isOverviewed: false});
+ }
+
+ charts = tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => ((e.id === 'overview_container') &&
+ (e.style.display !== 'none')));
+ assert.lengthOf(charts, 0);
+ });
+
+ test('sortByDisplayStatistic', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram(
+ 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [0, 10], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [5], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ sortColumnIndex: 1,
+ sortDescending: false,
+ displayStatisticName: 'min',
+ });
+
+ let nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'bar');
+ assert.strictEqual(nameCells[1].row.name, 'foo');
+
+ await table.viewState.update({sortDescending: true});
+
+ nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'foo');
+ assert.strictEqual(nameCells[1].row.name, 'bar');
+
+ await table.viewState.update({displayStatisticName: 'max'});
+
+ nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'bar');
+ assert.strictEqual(nameCells[1].row.name, 'foo');
+
+ await table.viewState.update({sortDescending: false});
+
+ nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'foo');
+ assert.strictEqual(nameCells[1].row.name, 'bar');
+ });
+
+ test('displayStatistic', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const now = new Date().getTime();
+ const barHist = histograms.createHistogram(
+ 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1, 2, 3], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['bar'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+ const fooHist = histograms.createHistogram(
+ 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [10, 20, 30], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ // Add a Histogram with another name so that the table displays the scalars.
+ const quxHist = histograms.createHistogram(
+ 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const table = await buildTable(this, histograms);
+
+ function histogramsEqual(a, b) {
+ // This is not an exhaustive equality check. This only tests the fields
+ // that are distinguishing for this test().
+ if (a.name !== b.name) return false;
+ return tr.v.HistogramGrouping.DISPLAY_LABEL.callback(a) ===
+ tr.v.HistogramGrouping.DISPLAY_LABEL.callback(b);
+ }
+
+ let fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, fooHist)));
+ assert.isDefined(fooCell);
+
+ let fooContent = tr.ui.b.findDeepElementMatchingPredicate(
+ fooCell, elem => elem.id === 'content');
+ assert.isDefined(fooContent);
+
+ let barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, barHist)));
+ assert.isDefined(barCell);
+
+ let barContent = tr.ui.b.findDeepElementMatchingPredicate(
+ barCell, elem => elem.id === 'content');
+ assert.isDefined(barContent);
+
+ assert.strictEqual(table.viewState.displayStatisticName, 'avg');
+ assert.strictEqual('20.000 ms', fooContent.textContent);
+ assert.strictEqual('2.000 ms', barContent.textContent);
+
+ await table.viewState.update({referenceDisplayLabel: 'foo'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, fooHist)));
+ assert.isDefined(fooCell);
+
+ fooContent = tr.ui.b.findDeepElementMatchingPredicate(
+ fooCell, elem => elem.id === 'content');
+ assert.isDefined(fooContent);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, barHist)));
+ assert.isDefined(barCell);
+
+ barContent = tr.ui.b.findDeepElementMatchingPredicate(
+ barCell, elem => elem.id === 'content');
+ assert.isDefined(barContent);
+
+ await table.viewState.update({displayStatisticName: `${tr.v.DELTA}avg`});
+ assert.strictEqual('20.000 ms', fooContent.textContent);
+ assert.strictEqual('-18.000 ms', barContent.textContent);
+
+ await table.viewState.update({displayStatisticName: `%${tr.v.DELTA}avg`});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, fooHist)));
+ assert.isDefined(fooCell);
+
+ fooContent = tr.ui.b.findDeepElementMatchingPredicate(
+ fooCell, elem => elem.id === 'content');
+ assert.isDefined(fooContent);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, barHist)));
+ assert.isDefined(barCell);
+
+ barContent = tr.ui.b.findDeepElementMatchingPredicate(
+ barCell, elem => elem.id === 'content');
+ assert.isDefined(barContent);
+
+ assert.strictEqual(table.viewState.displayStatisticName,
+ `%${tr.v.DELTA}avg`);
+ assert.strictEqual('20.000 ms', fooContent.textContent);
+ assert.strictEqual('-90.0%', barContent.textContent);
+ });
+
+ test('requestSelectionChange', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const barHist = histograms.createHistogram(
+ 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const fooHist = histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, {
+ value: 1,
+ diagnostics: {
+ breakdown: tr.v.d.Breakdown.fromEntries([
+ ['bar', 1],
+ ['qux', 0],
+ ]),
+ },
+ }, {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const quxHist = histograms.createHistogram(
+ 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ const breakdown = new tr.v.d.RelatedNameMap();
+ breakdown.set('bar', barHist.name);
+ breakdown.set('qux', quxHist.name);
+ fooHist.diagnostics.set('breakdown', breakdown);
+
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({showAll: false});
+
+ let fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ let barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isUndefined(barCell);
+
+ fooCell.isHistogramOpen = true;
+
+ const barLink = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => elem.tagName === 'TR-UI-A-ANALYSIS-LINK');
+ assert.isDefined(barLink);
+ barLink.click();
+
+ await tr.b.animationFrame();
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ await table.viewState.update({searchQuery: ''});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ fooCell.isHistogramOpen = true;
+
+ const selectAllLink = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-UI-A-ANALYSIS-LINK') &&
+ (elem.textContent === 'Select All')));
+ assert.isDefined(selectAllLink);
+ selectAllLink.click();
+
+ assert.strictEqual(table.viewState.searchQuery, '^(bar|qux)$');
+
+ await tr.b.animationFrame();
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isUndefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ const quxCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'qux')));
+ assert.isDefined(quxCell);
+ });
+
+ test('search', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram(
+ 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const table = await buildTable(this, histograms);
+
+ let fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ let barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ await table.viewState.update({searchQuery: 'bar'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isUndefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ await table.viewState.update({searchQuery: 'foo'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isUndefined(barCell);
+
+ // As users type in regexes, some intermediate forms may be invalid regexes.
+ // When the search is an invalid regex, just ignore it.
+ await table.viewState.update({searchQuery: '[a-'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+ });
+
+ test('emptyAndMissing', async function() {
+ const now = new Date().getTime();
+ const histograms = new tr.v.HistogramSet();
+
+ const histA = histograms.createHistogram(
+ 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['iteration A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const histB = histograms.createHistogram(
+ 'histogram B', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['iteration B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const histC = histograms.createHistogram(
+ 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['iteration B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const table = await buildTable(this, histograms);
+
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === '(empty)'));
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === '(missing)'));
+ });
+
+ test('instantiate_1x1', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const hist = histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ const table = await buildTable(this, histograms);
+
+ const baseTable = getBaseTable(table);
+ assert.strictEqual(baseTable.tableRows.length, 1);
+
+ const cell = tr.ui.b.findDeepElementMatchingPredicate(table, elem =>
+ elem.tagName === 'TR-V-UI-SCALAR-SPAN');
+ cell.click();
+
+ const yAxisText = tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.tagName === 'text' && e.textContent === '<0.000 ms');
+ assert.isBelow(0, yAxisText.getBBox().width);
+ });
+
+ test('merge_unmergeable', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [], {
+ binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 21),
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ assert.strictEqual(table.viewState.tableRowStates.size, 1);
+ assert.instanceOf(tr.b.getOnlyElement(getTableCells(table)).histogram,
+ tr.v.HistogramSet);
+ });
+
+ test('instantiate_1x5', async function() {
+ const histograms = new tr.v.HistogramSet();
+
+ for (let i = 0; i < 5; ++i) {
+ histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['' + i])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ }
+ const table = await buildTable(this, histograms);
+ });
+
+ test('instantiate_2x2', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('foo', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ const baseTable = getBaseTable(table);
+
+ assert.lengthOf(baseTable.tableColumns, 3);
+ assert.strictEqual('Name',
+ baseTable.tableColumns[0].title.children[0].textContent);
+ assert.strictEqual('A',
+ baseTable.tableColumns[1].title.textContent);
+ assert.strictEqual('B',
+ baseTable.tableColumns[2].title.textContent);
+
+ await table.viewState.update({referenceDisplayLabel: 'A'});
+ baseTable.rebuild();
+ assert.strictEqual(1, baseTable.selectedTableColumnIndex);
+ let cells = getTableCells(table);
+ assert.strictEqual(cells[1].referenceHistogram, cells[0].histogram);
+ assert.strictEqual(cells[3].referenceHistogram, cells[2].histogram);
+
+ await table.viewState.update({referenceDisplayLabel: 'B'});
+ cells = getTableCells(table);
+ assert.strictEqual(2, baseTable.selectedTableColumnIndex);
+ assert.strictEqual(cells[0].referenceHistogram, cells[1].histogram);
+ assert.strictEqual(cells[2].referenceHistogram, cells[3].histogram);
+
+ // Test sorting by the reference column when the displayStatistic is a delta
+ // statistic.
+ await table.viewState.update({sortColumnIndex: 2});
+ let nameCell = getNameCells(table)[0];
+ const originalFirstRow = nameCell.row.name;
+ // This is either 'foo' or 'bar' depending on Math.random() above.
+
+ await table.viewState.update({
+ sortDescending: !table.viewState.sortDescending,
+ });
+ baseTable.rebuild();
+ nameCell = getNameCells(table)[0];
+ assert.notEqual(originalFirstRow, nameCell.row.name);
+ });
+
+ test('merged', async function() {
+ const histograms = new tr.v.HistogramSet();
+ // Add 2^8=256 Histograms, all with the same name, with different metadata.
+ const benchmarkNames = ['bm A', 'bm B'];
+ const storyGroupingKeys0 = ['A', 'B'];
+ const storyGroupingKeys1 = ['C', 'D'];
+ const storyNames = ['story A', 'story B'];
+ const starts = [1439708400000, 1439794800000];
+ const labels = ['label A', 'label B'];
+ const name = 'name '.repeat(20);
+ const unit = tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;
+
+ for (const benchmarkName of benchmarkNames) {
+ for (const storyGroupingKey0 of storyGroupingKeys0) {
+ for (const storyGroupingKey1 of storyGroupingKeys1) {
+ for (const storyName of storyNames) {
+ for (const startMs of starts) {
+ for (let storysetCounter = 0; storysetCounter < 2;
+ ++storysetCounter) {
+ for (const label of labels) {
+ const samples = range(0, 100).map(i => {
+ return {
+ value: Math.random() * 1e3,
+ diagnostics: {i: new tr.v.d.GenericSet([i])},
+ };
+ });
+ histograms.createHistogram(name, unit, samples, {
+ description: 'The best description.',
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet([label])],
+ [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS,
+ new tr.v.d.GenericSet([storysetCounter])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARKS,
+ new tr.v.d.GenericSet([benchmarkName])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(startMs)],
+ [tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet([storyName])],
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS,
+ new tr.v.d.GenericSet([
+ `storyGroupingKey0:${storyGroupingKey0}`,
+ `storyGroupingKey1:${storyGroupingKey1}`,
+ ])],
+ ]),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ histograms.buildGroupingsFromTags([tr.v.d.RESERVED_NAMES.STORY_TAGS]);
+
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARKS),
+ tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey0Tag'),
+ tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey1Tag'),
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START),
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORYSET_REPEATS),
+ ]});
+ const baseTable = getBaseTable(table);
+
+ assert.lengthOf(baseTable.tableColumns, 3);
+ const nameHeaderCell = baseTable.tableColumns[0].title;
+ assert.strictEqual('Name', nameHeaderCell.children[0].textContent);
+ assert.strictEqual('label A', baseTable.tableColumns[1].title.textContent);
+ assert.strictEqual('label B', baseTable.tableColumns[2].title.textContent);
+
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.closeTo(346, nameCell.getBoundingClientRect().width, 1);
+
+ nameHeaderCell.children[1].click();
+ // toggleNameColumnWidth_ does not await viewState.update()
+ await tr.b.animationFrame();
+ assert.isBelow(322, nameCell.getBoundingClientRect().width);
+
+ nameHeaderCell.children[1].click();
+ await tr.b.animationFrame();
+ assert.closeTo(346, nameCell.getBoundingClientRect().width, 1);
+
+ assert.lengthOf(baseTable.tableRows, 1);
+ assert.strictEqual(name, baseTable.tableRows[0].name);
+ assert.lengthOf(baseTable.tableRows[0].subRows, 2);
+
+ // assertions only report their arguments, which is not enough information
+ // to diagnose problems with nested structures like tableRows -- the path to
+ // the particular row is needed. This code would be a bit simpler if each
+ // row were given a named variable, but the path to each subRow would still
+ // need to be tracked in order to provide for diagnosing.
+ const subRowPath = [];
+ function getSubRow() {
+ let row = baseTable.tableRows[0];
+ for (const index of subRowPath) {
+ row = row.subRows[index];
+ }
+ return row;
+ }
+
+ for (let i = 0; i < benchmarkNames.length; ++i) {
+ subRowPath.push(i);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(benchmarkNames[i], getSubRow().name, subRowPath);
+
+ for (let s = 0; s < storyGroupingKeys0.length; ++s) {
+ subRowPath.push(s);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(storyGroupingKeys0[s], getSubRow().name, subRowPath);
+
+ for (let t = 0; t < storyGroupingKeys1.length; ++t) {
+ subRowPath.push(t);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(storyGroupingKeys1[t], getSubRow().name,
+ subRowPath);
+
+ for (let j = 0; j < storyNames.length; ++j) {
+ subRowPath.push(j);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(storyNames[j], getSubRow().name, subRowPath);
+
+ for (let k = 0; k < starts.length; ++k) {
+ subRowPath.push(k);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(tr.b.formatDate(new Date(starts[k])),
+ getSubRow().name, subRowPath);
+
+ for (let l = 0; l < 2; ++l) {
+ subRowPath.push(l);
+ assert.lengthOf(getSubRow().subRows, 0, subRowPath);
+ assert.strictEqual('' + l, getSubRow().name, subRowPath);
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html
new file mode 100644
index 00000000000..18ba824811f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html
@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/null_brushing_state_controller.html">
+<link rel="import" href="/tracing/value/csv_builder.html">
+<link rel="import" href="/tracing/value/histogram_parameter_collector.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_controls.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_table.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<dom-module id="tr-v-ui-histogram-set-view">
+ <template>
+ <style>
+ :host {
+ font-family: sans-serif;
+ }
+
+ #zero {
+ color: red;
+ /* histogram-set-table is used by both metrics-side-panel and results.html.
+ * This font-size rule has no effect in results.html, but improves
+ * legibility in the metrics-side-panel, which sets font-size in order to
+ * make this table denser.
+ */
+ font-size: initial;
+ }
+
+ #container {
+ display: none;
+ }
+
+ #visualizations{
+ display: none;
+ }
+ </style>
+
+ <div id="zero">zero Histograms</div>
+
+ <div id="container">
+ <tr-v-ui-histogram-set-controls id="controls">
+ </tr-v-ui-histogram-set-controls>
+
+ <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>
+ </template>
+</dom-module>
+
+<script>
+'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;
+ },
+
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!Object=} opt_options
+ * @param {function(string):!Promise=} opt_options.progressconst
+ * @param {string=} opt_options.helpHref
+ * @param {string=} opt_options.feedbackHref
+ */
+ 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();
+ // Disable show_all if all values are sourceHistograms.
+ 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();
+
+ // Only show visualizer button if values are from rendering benchmarks
+ 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;
+
+ // Table.build() displays its own progress.
+ 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.visualizationLoaded_) { // Initial loading
+ this.$.visualizations.style.display = 'block';
+ this.$.visualizations.build(this.$.table.leafHistograms,
+ this.histograms);
+ this.visualizationLoaded_ = true;
+ } else if (this.$.visualizations.style.display === 'none') {
+ // Toggle to hide
+ this.$.visualizations.style.display = 'block';
+ } else {
+ this.$.visualizations.style.display = 'none';
+ }
+ },
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html
new file mode 100644
index 00000000000..ade4ef2a9c4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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.
+-->
+
+<link rel="import" href="/tracing/base/view_state.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ class HistogramSetViewState extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('searchQuery', '');
+ this.define('referenceDisplayLabel', '');
+ this.define('displayStatisticName', '');
+ this.define('showAll', true);
+ this.define('groupings', []);
+ this.define('sortColumnIndex', 0);
+ this.define('sortDescending', false);
+ this.define('constrainNameColumn', true);
+ this.define('tableRowStates', new Map());
+ this.define('alpha', 0.01);
+ }
+ }
+
+ tr.b.ViewState.register(HistogramSetViewState);
+
+ class HistogramSetTableRowState extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('isExpanded', false);
+ this.define('isOverviewed', false);
+ this.define('cells', new Map());
+ this.define('subRows', new Map());
+ this.define('diagnosticsTab', '');
+ }
+
+ asCompactDict() {
+ const result = {};
+ if (this.isExpanded) result.e = '1';
+ if (this.isOverviewed) result.o = '1';
+ if (this.diagnosticsTab) result.d = this.diagnosticsTab;
+ const cells = {};
+ for (const [name, cell] of this.cells) {
+ const cellDict = cell.asCompactDict();
+ if (cellDict === undefined) continue;
+ cells[name] = cellDict;
+ }
+ if (Object.keys(cells).length > 0) result.c = cells;
+
+ const subRows = {};
+ for (const [name, row] of this.subRows) {
+ const rowDict = row.asCompactDict();
+ if (rowDict === undefined) continue;
+ subRows[name] = rowDict;
+ }
+ if (Object.keys(subRows).length > 0) result.r = subRows;
+
+ if (Object.keys(result).length === 0) return undefined;
+
+ return result;
+ }
+
+ async updateFromCompactDict(dict) {
+ await this.update({
+ isExpanded: dict.e === '1',
+ isOverviewed: dict.o === '1',
+ diagnosticsTab: dict.d || '',
+ });
+
+ for (const [name, cellDict] of Object.entries(dict.c || {})) {
+ const cell = this.cells.get(name);
+ if (cell === undefined) continue;
+ await cell.updateFromCompactDict(cellDict);
+ }
+
+ for (const [name, subRowDict] of Object.entries(dict.r || {})) {
+ const subRow = this.subRows.get(name);
+ if (subRow === undefined) continue;
+ await subRow.updateFromCompactDict(subRowDict);
+ }
+ }
+
+ * walk() {
+ yield this;
+ for (const row of this.subRows.values()) yield* row.walk();
+ }
+
+ static* walkAll(rootRows) {
+ for (const rootRow of rootRows) yield* rootRow.walk();
+ }
+ }
+
+ tr.b.ViewState.register(HistogramSetTableRowState);
+
+ class HistogramSetTableCellState extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('isOpen', false);
+ this.define('brushedBinRange', new tr.b.math.Range());
+ this.define('mergeSampleDiagnostics', true);
+ }
+
+ asCompactDict() {
+ const result = {};
+ if (this.isOpen) result.o = '1';
+ if (!this.mergeSampleDiagnostics) result.m = '0';
+ if (!this.brushedBinRange.isEmpty) {
+ result.b = this.brushedBinRange.min + '_' + this.brushedBinRange.max;
+ }
+ if (Object.keys(result).length === 0) return undefined;
+ return result;
+ }
+
+ async updateFromCompactDict(dict) {
+ let binRange = this.brushedBinRange;
+ if (dict.b) {
+ let [bMin, bMax] = dict.b.split('_');
+ bMin = parseInt(bMin);
+ bMax = parseInt(bMax);
+ if (bMin !== binRange.min || bMax !== binRange.max) {
+ binRange = tr.b.math.Range.fromExplicitRange(bMin, bMax);
+ }
+ }
+ await this.update({
+ isOpen: dict.o === '1',
+ brushedBinRange: binRange,
+ mergeSampleDiagnostics: dict.m !== '0',
+ });
+ }
+ }
+
+ tr.b.ViewState.register(HistogramSetTableCellState);
+
+ return {
+ HistogramSetTableCellState,
+ HistogramSetTableRowState,
+ HistogramSetViewState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html
new file mode 100644
index 00000000000..ede486c98d5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate0', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ const histograms = new tr.v.HistogramSet();
+
+ const hist = new tr.v.Histogram('a', tr.b.Unit.byName.normalizedPercentage);
+ for (let i = 0; i < 1e2; ++i) {
+ hist.addSample(Math.random());
+ }
+ histograms.addHistogram(hist);
+
+ this.addHTMLOutput(view);
+ await view.build(histograms);
+
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+
+ test('implicitUndefinedHistogramSet', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ this.addHTMLOutput(view);
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+
+ test('explicitUndefinedHistogramSet', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ this.addHTMLOutput(view);
+ view.build(undefined);
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+
+ test('emptyHistogramSet', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ this.addHTMLOutput(view);
+ view.build(new tr.v.HistogramSet());
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html
new file mode 100644
index 00000000000..c0382f66b93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html
@@ -0,0 +1,599 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/box_chart.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/name_bar_chart.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+<link rel="import" href="/tracing/value/ui/scalar_map_table.html">
+
+<dom-module id="tr-v-ui-histogram-span">
+ <template>
+ <style>
+ #container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ #chart {
+ flex-grow: 1;
+ display: none;
+ }
+ #drag_handle, #diagnostics_tab_templates {
+ display: none;
+ }
+ #chart svg {
+ display: block;
+ }
+ #stats_container {
+ overflow-y: auto;
+ }
+ </style>
+
+ <div id="container">
+ <div id="chart"></div>
+ <div id="stats_container">
+ <tr-v-ui-scalar-map-table id="stats"></tr-v-ui-scalar-map-table>
+ </div>
+ </div>
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
+
+ <tr-ui-b-tab-view id="diagnostics"></tr-ui-b-tab-view>
+
+ <div id="diagnostics_tab_templates">
+ <tr-v-ui-diagnostic-map-table id="metric_diagnostics"></tr-v-ui-diagnostic-map-table>
+
+ <tr-v-ui-diagnostic-map-table id="metadata_diagnostics"></tr-v-ui-diagnostic-map-table>
+
+ <div id="sample_diagnostics_container">
+ <div id="merge_sample_diagnostics_container">
+ <input type="checkbox" id="merge_sample_diagnostics" checked on-change="updateDiagnostics_">
+ <label for="merge_sample_diagnostics">Merge Sample Diagnostics</label>
+ </div>
+ <tr-v-ui-diagnostic-map-table id="sample_diagnostics"></tr-v-ui-diagnostic-map-table>
+ </div>
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const DEFAULT_BAR_HEIGHT_PX = 5;
+ const TRUNCATE_BIN_MARGIN = 0.15;
+ const IGNORE_DELTA_STATISTICS_NAMES = [
+ `${tr.v.DELTA}min`,
+ `%${tr.v.DELTA}min`,
+ `${tr.v.DELTA}max`,
+ `%${tr.v.DELTA}max`,
+ `${tr.v.DELTA}sum`,
+ `%${tr.v.DELTA}sum`,
+ `${tr.v.DELTA}count`,
+ `%${tr.v.DELTA}count`,
+ ];
+
+ Polymer({
+ is: 'tr-v-ui-histogram-span',
+
+ created() {
+ this.viewStateListener_ = this.onViewStateUpdate_.bind(this);
+ this.viewState = new tr.v.ui.HistogramSetTableCellState();
+ this.rowStateListener_ = this.onRowStateUpdate_.bind(this);
+ this.rowState = new tr.v.ui.HistogramSetTableRowState();
+ this.rootStateListener_ = this.onRootStateUpdate_.bind(this);
+ this.rootState = new tr.v.ui.HistogramSetViewState();
+
+ this.histogram_ = undefined;
+ this.referenceHistogram_ = undefined;
+ this.graphWidth_ = undefined;
+ this.graphHeight_ = undefined;
+ this.mouseDownBin_ = undefined;
+ this.prevBrushedBinRange_ = new tr.b.math.Range();
+ this.anySampleDiagnostics_ = false;
+ this.canMergeSampleDiagnostics_ = true;
+ this.mwuResult_ = undefined;
+ },
+
+ get rowState() {
+ return this.rowState_;
+ },
+
+ set rowState(rs) {
+ if (this.rowState) {
+ this.rowState.removeUpdateListener(this.rowStateListener_);
+ }
+ this.rowState_ = rs;
+ this.rowState.addUpdateListener(this.rowStateListener_);
+ if (this.isAttached) this.updateContents_();
+ },
+
+ get viewState() {
+ return this.viewState_;
+ },
+
+ set viewState(vs) {
+ if (this.viewState) {
+ this.viewState.removeUpdateListener(this.viewStateListener_);
+ }
+ this.viewState_ = vs;
+ this.viewState.addUpdateListener(this.viewStateListener_);
+ if (this.isAttached) this.updateContents_();
+ },
+
+ get rootState() {
+ return this.rootState_;
+ },
+
+ set rootState(vs) {
+ if (this.rootState) {
+ this.rootState.removeUpdateListener(this.rootStateListener_);
+ }
+ this.rootState_ = vs;
+ this.rootState.addUpdateListener(this.rootStateListener_);
+ if (this.isAttached) this.updateContents_();
+ },
+
+ build(histogram, opt_referenceHistogram) {
+ this.histogram_ = histogram;
+ this.$.metric_diagnostics.histogram = histogram;
+ this.$.sample_diagnostics.histogram = histogram;
+ this.referenceHistogram_ = opt_referenceHistogram;
+
+ if (this.histogram.canCompare(this.referenceHistogram)) {
+ this.mwuResult_ = tr.b.math.Statistics.mwu(
+ this.histogram.sampleValues,
+ this.referenceHistogram.sampleValues,
+ this.rootState.alpha);
+ }
+
+ this.anySampleDiagnostics_ = false;
+ for (const bin of this.histogram.allBins) {
+ if (bin.diagnosticMaps.length > 0) {
+ this.anySampleDiagnostics_ = true;
+ break;
+ }
+ }
+
+ if (this.isAttached) this.updateContents_();
+ },
+
+ onViewStateUpdate_(event) {
+ if (event.delta.brushedBinRange) {
+ if (this.chart_ !== undefined) {
+ this.chart_.brushedRange = this.viewState.brushedBinRange;
+ }
+ this.updateDiagnostics_();
+ }
+
+ if (event.delta.mergeSampleDiagnostics &&
+ (this.viewState.mergeSampleDiagnostics !==
+ this.$.merge_sample_diagnostics.checked)) {
+ this.$.merge_sample_diagnostics.checked =
+ this.canMergeSampleDiagnostics &&
+ this.viewState.mergeSampleDiagnostics;
+ this.updateDiagnostics_();
+ }
+ },
+
+ updateSignificance_() {
+ if (!this.mwuResult_) return;
+ this.$.stats.setSignificanceForKey(
+ `${tr.v.DELTA}avg`, this.mwuResult_.significance);
+ },
+
+ onRootStateUpdate_(event) {
+ if (event.delta.alpha && this.mwuResult_) {
+ this.mwuResult_.compare(this.rootState.alpha);
+ this.updateSignificance_();
+ }
+ },
+
+ onRowStateUpdate_(event) {
+ if (event.delta.diagnosticsTab) {
+ if (this.rowState.diagnosticsTab ===
+ this.$.sample_diagnostics_container.tabLabel) {
+ this.updateDiagnostics_();
+ } else {
+ for (const tab of this.$.diagnostics.subViews) {
+ if (this.rowState.diagnosticsTab === tab.tabLabel) {
+ this.$.diagnostics.selectedSubView = tab;
+ break;
+ }
+ }
+ }
+ }
+ },
+
+ ready() {
+ this.$.metric_diagnostics.tabLabel = 'histogram diagnostics';
+ this.$.sample_diagnostics_container.tabLabel = 'sample diagnostics';
+ this.$.metadata_diagnostics.tabLabel = 'metadata';
+ this.$.metadata_diagnostics.isMetadata = true;
+ this.$.diagnostics.addEventListener(
+ 'selected-tab-change', this.onSelectedDiagnosticsChanged_.bind(this));
+ this.$.drag_handle.target = this.$.container;
+ this.$.drag_handle.addEventListener(
+ 'drag-handle-resize', this.onResize_.bind(this));
+ },
+
+ attached() {
+ if (this.histogram_ !== undefined) this.updateContents_();
+ },
+
+ get canMergeSampleDiagnostics() {
+ return this.canMergeSampleDiagnostics_;
+ },
+
+ set canMergeSampleDiagnostics(merge) {
+ this.canMergeSampleDiagnostics_ = merge;
+ if (!merge) this.viewState.mergeSampleDiagnostics = false;
+ this.$.merge_sample_diagnostics_container.style.display = (
+ merge ? '' : 'none');
+ },
+
+ onResize_(event) {
+ event.stopPropagation();
+ let heightPx = parseInt(this.$.container.style.height);
+ if (heightPx < this.defaultGraphHeight) {
+ heightPx = this.defaultGraphHeight;
+ this.$.container.style.height = this.defaultGraphHeight + 'px';
+ }
+ this.chart_.graphHeight = heightPx - (this.chart_.margin.top +
+ this.chart_.margin.bottom);
+ this.$.stats_container.style.maxHeight =
+ this.chart_.getBoundingClientRect().height + 'px';
+ },
+
+ /**
+ * Get the width in pixels of the widest bar in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @return {number}
+ */
+ get graphWidth() {
+ return this.graphWidth_ || this.defaultGraphWidth;
+ },
+
+ /**
+ * Set the width in pixels of the widest bar in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @param {number} width
+ */
+ set graphWidth(width) {
+ this.graphWidth_ = width;
+ },
+
+ /**
+ * Get the height in pixels of the bars in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @return {number}
+ */
+ get graphHeight() {
+ return this.graphHeight_ || this.defaultGraphHeight;
+ },
+
+ /**
+ * Set the height in pixels of the bars in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @param {number} height
+ */
+ set graphHeight(height) {
+ this.graphHeight_ = height;
+ },
+
+ /**
+ * Get the height in pixels of one bar in the bar chart.
+ *
+ * @return {number}
+ */
+ get barHeight() {
+ return this.chart_.barHeight;
+ },
+
+ /**
+ * Set the height in pixels of one bar in the bar chart.
+ *
+ * @param {number} px
+ */
+ set barHeight(px) {
+ this.graphHeight = this.computeChartHeight_(px);
+ },
+
+ computeChartHeight_(barHeightPx) {
+ return (this.chart_.margin.top +
+ this.chart_.margin.bottom +
+ (barHeightPx * this.histogram.allBins.length));
+ },
+
+ get defaultGraphHeight() {
+ if (this.histogram && this.histogram.allBins.length === 1) {
+ return 150;
+ }
+ return this.computeChartHeight_(DEFAULT_BAR_HEIGHT_PX);
+ },
+
+ get defaultGraphWidth() {
+ if (this.histogram.allBins.length === 1) {
+ return 100;
+ }
+ return 300;
+ },
+
+ get brushedBins() {
+ const bins = [];
+ if (this.histogram && !this.viewState.brushedBinRange.isEmpty) {
+ for (let i = this.viewState.brushedBinRange.min;
+ i < this.viewState.brushedBinRange.max; ++i) {
+ bins.push(this.histogram.allBins[i]);
+ }
+ }
+ return bins;
+ },
+
+ async updateBrushedRange_(binIndex) {
+ const brushedBinRange = new tr.b.math.Range();
+ brushedBinRange.addValue(tr.b.math.clamp(
+ this.mouseDownBinIndex_, 0, this.histogram.allBins.length - 1));
+ brushedBinRange.addValue(tr.b.math.clamp(
+ binIndex, 0, this.histogram.allBins.length - 1));
+ brushedBinRange.max += 1;
+ await this.viewState.update({brushedBinRange});
+ },
+
+ onMouseDown_(chartEvent) {
+ chartEvent.stopPropagation();
+ if (!this.histogram) return;
+ this.prevBrushedBinRange_ = this.viewState.brushedBinRange;
+ this.mouseDownBinIndex_ = chartEvent.y;
+ this.updateBrushedRange_(chartEvent.y);
+ },
+
+ onMouseMove_(chartEvent) {
+ chartEvent.stopPropagation();
+ if (!this.histogram) return;
+ this.updateBrushedRange_(chartEvent.y);
+ },
+
+ onMouseUp_(chartEvent) {
+ chartEvent.stopPropagation();
+ if (!this.histogram) return;
+ this.updateBrushedRange_(chartEvent.y);
+ if (this.prevBrushedBinRange_.range === 1 &&
+ this.viewState.brushedBinRange.range === 1 &&
+ (this.prevBrushedBinRange_.min ===
+ this.viewState.brushedBinRange.min)) {
+ tr.b.Timing.instant('histogram-span', 'clearBrushedBins');
+ this.viewState.update({brushedBinRange: new tr.b.math.Range()});
+ } else {
+ tr.b.Timing.instant('histogram-span', 'brushBins');
+ }
+ this.mouseDownBinIndex_ = undefined;
+ },
+
+ async onSelectedDiagnosticsChanged_() {
+ await this.rowState.update({
+ diagnosticsTab: this.$.diagnostics.selectedSubView.tabLabel,
+ });
+ if ((this.$.diagnostics.selectedSubView ===
+ this.$.sample_diagnostics_container) &&
+ this.histogram &&
+ this.viewState.brushedBinRange.isEmpty) {
+ // When the user selects the sample diagnostics tab, if they haven't
+ // already brushed any bins, then automatically brush all bins.
+ const brushedBinRange = tr.b.math.Range.fromExplicitRange(
+ 0, this.histogram.allBins.length);
+ await this.viewState.update({brushedBinRange});
+ this.updateDiagnostics_();
+ }
+ },
+
+ updateDiagnostics_() {
+ let maps = [];
+ for (const bin of this.brushedBins) {
+ for (const map of bin.diagnosticMaps) {
+ maps.push(map);
+ }
+ }
+
+ if (this.$.merge_sample_diagnostics.checked !==
+ this.viewState.mergeSampleDiagnostics) {
+ this.viewState.update({
+ mergeSampleDiagnostics: this.$.merge_sample_diagnostics.checked});
+ }
+
+ if (this.viewState.mergeSampleDiagnostics) {
+ const merged = new tr.v.d.DiagnosticMap();
+ for (const map of maps) {
+ merged.addDiagnostics(map);
+ }
+ 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) {
+ // Hide implementation details.
+ 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);
+ }
+ this.$.diagnostics.resetSubViews(diagnosticTabs);
+ this.$.diagnostics.set('tabsHidden', diagnosticTabs.length < 2);
+
+ if (this.histogram.numValues <= 1) {
+ await this.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(
+ 0, this.histogram.allBins.length)});
+ this.$.container.style.justifyContent = 'flex-end';
+ return;
+ }
+
+ this.$.chart.style.display = 'block';
+ this.$.drag_handle.style.display = 'block';
+
+ if (this.histogram.allBins.length === 1) {
+ if (this.histogram.min !== this.histogram.max) {
+ this.chart_ = new tr.ui.b.BoxChart();
+ Polymer.dom(this.$.chart).appendChild(this.chart_);
+ this.chart_.graphWidth = this.graphWidth;
+ this.chart_.graphHeight = this.graphHeight;
+ this.chart_.hideXAxis = true;
+ this.chart_.data = [
+ {
+ x: '',
+ color: 'blue',
+ percentile_0: this.histogram.running.min,
+ percentile_25: this.histogram.getApproximatePercentile(0.25),
+ percentile_50: this.histogram.getApproximatePercentile(0.5),
+ percentile_75: this.histogram.getApproximatePercentile(0.75),
+ percentile_100: this.histogram.running.max,
+ }
+ ];
+ }
+ this.$.stats_container.style.maxHeight =
+ this.chart_.getBoundingClientRect().height + 'px';
+ await this.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(
+ 0, this.histogram.allBins.length)});
+ return;
+ }
+
+ this.chart_ = new tr.ui.b.NameBarChart();
+ Polymer.dom(this.$.chart).appendChild(this.chart_);
+ this.chart_.graphWidth = this.graphWidth;
+ this.chart_.graphHeight = this.graphHeight;
+ this.chart_.addEventListener('item-mousedown',
+ this.onMouseDown_.bind(this));
+ this.chart_.addEventListener('item-mousemove',
+ this.onMouseMove_.bind(this));
+ this.chart_.addEventListener('item-mouseup',
+ this.onMouseUp_.bind(this));
+ this.chart_.hideLegend = true;
+ this.chart_.getDataSeries('y').color = 'blue';
+ this.chart_.xAxisLabel = '#';
+ this.chart_.brushedRange = this.viewState.brushedBinRange;
+ if (!this.viewState.brushedBinRange.isEmpty) {
+ this.updateDiagnostics_();
+ }
+
+ const chartData = [];
+ const binCounts = [];
+ for (const bin of this.histogram.allBins) {
+ let x = bin.range.min;
+ if (x === -Number.MAX_VALUE) {
+ x = '<' + new tr.b.Scalar(
+ this.histogram.unit, bin.range.max).toString();
+ } else {
+ x = new tr.b.Scalar(this.histogram.unit, x).toString();
+ }
+ chartData.push({x, y: bin.count});
+ binCounts.push(bin.count);
+ }
+
+ // If the largest 1 or 2 bins are more than twice as large as the next
+ // largest bin, then set the dataRange max to TRUNCATE_BIN_MARGIN% more
+ // than that next largest bin.
+ binCounts.sort((x, y) => y - x);
+ const dataRange = tr.b.math.Range.fromExplicitRange(0, binCounts[0]);
+ if (binCounts[1] > 0 && binCounts[0] > (binCounts[1] * 2)) {
+ dataRange.max = binCounts[1] * (1 + TRUNCATE_BIN_MARGIN);
+ }
+ if (binCounts[2] > 0 && binCounts[1] > (binCounts[2] * 2)) {
+ dataRange.max = binCounts[2] * (1 + TRUNCATE_BIN_MARGIN);
+ }
+ this.chart_.overrideDataRange = dataRange;
+
+ this.chart_.data = chartData;
+ this.$.stats_container.style.maxHeight =
+ this.chart_.getBoundingClientRect().height + 'px';
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html
new file mode 100644
index 00000000000..2ca360b3348
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html
@@ -0,0 +1,300 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/histogram_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ h.addSample(-1, {foo: new tr.v.d.GenericSet(['a'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['c'])});
+ h.addSample(500, {foo: new tr.v.d.GenericSet(['c'])});
+ h.addSample(999, {foo: new tr.v.d.GenericSet(['d'])});
+ h.addSample(1000, {foo: new tr.v.d.GenericSet(['d'])});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('emptyHistogram', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('viewBrushedBinRange', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count,
+ [0, 1, 2, 3, 4].map(value => {
+ return {value, diagnostics: new Map([
+ ['i', new tr.v.d.GenericSet([value])]])};
+ })));
+ assert.isTrue(span.viewState.brushedBinRange.isEmpty);
+
+ await span.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(5, 10),
+ });
+ const chart = tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.tagName === 'svg');
+ assert.strictEqual(5, chart.brushedRange.min);
+ assert.strictEqual(10, chart.brushedRange.max);
+ });
+
+ test('controlBrushedBinRange', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count,
+ [0, 1, 2, 3, 4]));
+ assert.isTrue(span.viewState.brushedBinRange.isEmpty);
+
+ span.onMouseDown_({
+ stopPropagation: () => undefined,
+ y: 21,
+ });
+ span.onMouseUp_({
+ stopPropagation: () => undefined,
+ y: 0,
+ });
+ tr.b.assertRangeEquals(span.viewState.brushedBinRange,
+ tr.b.math.Range.fromExplicitRange(0, 22));
+ });
+
+ test('viewMergeSampleDiagnostics', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ const samples = [];
+ for (let i = 0; i < 5; ++i) {
+ samples.push({
+ value: i,
+ diagnostics: {
+ breakdown: tr.v.d.Breakdown.fromDict({
+ values: {
+ a: 5 - i,
+ b: i + 5,
+ c: i,
+ },
+ }),
+ },
+ });
+ }
+ span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples));
+ await span.viewState.update({brushedBinRange:
+ tr.b.math.Range.fromExplicitRange(0, 10)});
+ const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e =>
+ e.id === 'merge_sample_diagnostics');
+ assert.isTrue(merge.checked);
+
+ await span.viewState.update({mergeSampleDiagnostics: false});
+ assert.isFalse(merge.checked);
+
+ await span.viewState.update({mergeSampleDiagnostics: true});
+ assert.isTrue(merge.checked);
+ });
+
+ test('controlMergeSampleDiagnostics', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ const samples = [];
+ for (let i = 0; i < 5; ++i) {
+ samples.push({
+ value: i,
+ diagnostics: {
+ breakdown: tr.v.d.Breakdown.fromDict({
+ values: {
+ a: 5 - i,
+ b: i + 5,
+ c: i,
+ },
+ }),
+ },
+ });
+ }
+ span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples));
+ await span.viewState.update({brushedBinRange:
+ tr.b.math.Range.fromExplicitRange(0, 10)});
+ const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e =>
+ e.id === 'merge_sample_diagnostics');
+ assert.isTrue(merge.checked);
+
+ merge.click();
+ assert.isFalse(span.viewState.mergeSampleDiagnostics);
+
+ merge.click();
+ assert.isTrue(span.viewState.mergeSampleDiagnostics);
+ });
+
+ test('mergeSampleDiagnostics', async function() {
+ // Add several samples with sample diagnostics to a Histogram, brush all of
+ // the bins, test that the sample diagnostics are merged.
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage);
+ h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})});
+ h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})});
+ h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})});
+ h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})});
+ h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ await span.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)});
+ let breakdowns = tr.ui.b.findDeepElementsMatchingPredicate(
+ span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN');
+ assert.lengthOf(breakdowns, 1);
+
+ const merge = tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.id === 'merge_sample_diagnostics');
+ merge.click();
+ breakdowns = tr.ui.b.findDeepElementsMatchingPredicate(
+ span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN');
+ assert.lengthOf(breakdowns, 5);
+ });
+
+ test('cannotMergeSampleDiagnostics', async function() {
+ // Add several samples with sample diagnostics to a Histogram, brush all of
+ // the bins, test that the sample diagnostics are not merged.
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage);
+ h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})});
+ h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})});
+ h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})});
+ h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})});
+ h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ span.canMergeSampleDiagnostics = false;
+ this.addHTMLOutput(span);
+ span.build(h);
+ await span.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)});
+ const breakdowns = tr.ui.b.findDeepElementsMatchingPredicate(
+ span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN');
+ assert.lengthOf(breakdowns, 5);
+ });
+
+ test('singleSample', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ h.addSample(100, {
+ sample_diagnostic_0: new tr.v.d.GenericSet(['foo']),
+ sample_diagnostic_1: new tr.v.d.GenericSet(['bar']),
+ });
+ h.diagnostics.set('histogram diagnostic 0', new tr.v.d.GenericSet(['baz']));
+ h.diagnostics.set('histogram diagnostic 1', new tr.v.d.GenericSet(['qux']));
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('nans', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ h.addSample(undefined, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(NaN, {foo: new tr.v.d.GenericSet(['c'])});
+ h.customizeSummaryOptions({nans: true});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('singleBin', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber,
+ tr.v.HistogramBinBoundaries.SINGULAR);
+ h.addSample(0);
+ h.addSample(25);
+ h.addSample(100);
+ h.addSample(100);
+ h.addSample(25);
+ h.addSample(50);
+ h.addSample(75);
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('referenceHistogram', function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, [1, 10, 100], {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ }), tr.v.Histogram.create('', tr.b.Unit.byName.count, [2, 20, 200], {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ }));
+ this.addHTMLOutput(span);
+ });
+
+ test('breakdownUnit', async function() {
+ const root = new tr.v.Histogram('root', tr.b.Unit.byName.sizeInBytes);
+ const sampleBreakdown = new tr.v.d.Breakdown();
+ sampleBreakdown.set('x', 30 << 20);
+ sampleBreakdown.set('y', 70 << 20);
+ root.addSample(100 << 20, {sampleBreakdown});
+ const rhb = new tr.v.d.RelatedNameMap();
+ root.diagnostics.set('rhb', rhb);
+ const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.sizeInBytes);
+ rhb.set('a', aHist.name);
+ aHist.addSample(10 << 20);
+ const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.sizeInBytes);
+ rhb.set('b', bHist.name);
+ bHist.addSample(90 << 20);
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(root);
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.textContent === '100.0 MiB'));
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.textContent === '30.0 MiB'));
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.textContent === '70.0 MiB'));
+ });
+
+ test('diagnosticsTabs', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ span.build(tr.v.Histogram.create(
+ '', tr.b.Unit.byName.count, [
+ {value: 1, diagnostics: new Map([
+ ['sample diagnostic', new tr.v.d.GenericSet(['value1'])],
+ ])},
+ {value: 10, diagnostics: new Map([
+ ['sample diagnostic', new tr.v.d.GenericSet(['value10'])],
+ ])},
+ ], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet([
+ 'system_health.common_desktop'])],
+ ]),
+ }));
+ this.addHTMLOutput(span);
+
+ const sample = tr.ui.b.findDeepElementMatching(
+ span, '#sample_diagnostics_container');
+ assert.strictEqual(span.rowState.diagnosticsTab, sample.tabLabel);
+ const tabs = tr.ui.b.findDeepElementMatching(
+ span, 'TR-UI-B-TAB-VIEW');
+ assert.strictEqual(tabs.selectedSubView, sample);
+
+ const metadata = tr.ui.b.findDeepElementMatching(
+ span, '#metadata_diagnostics');
+ await span.rowState.update({diagnosticsTab: metadata.tabLabel});
+ assert.strictEqual(tabs.selectedSubView, metadata);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html
new file mode 100644
index 00000000000..1bc6ea696b3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html
@@ -0,0 +1,353 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/running_statistics.html">
+<link rel="import" href="/tracing/ui/base/name_bar_chart.html">
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+<dom-module id='tr-v-ui-metrics-visualization'>
+ <template>
+ <style>
+ button {
+ padding: 5px;
+ font-size: 14px;
+ }
+
+ .text_input {
+ width: 50px;
+ padding: 4px;
+ font-size: 14px;
+ }
+
+ .error {
+ color: red;
+ display: none;
+ }
+
+ .container {
+ position: relative;
+ display: inline-block;
+ margin-left: 15px;
+ }
+
+ #title {
+ font-size: 20px;
+ font-weight: bold;
+ padding-bottom: 5px;
+ }
+
+ #selectors {
+ display: block;
+ padding-bottom: 10px;
+ }
+
+ #search_page {
+ width: 200px;
+ margin-left: 30px;
+ }
+
+ #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" class="container">
+ </span>
+ <span id="pageByPageContainer" class="container">
+ <span id="selectors">
+ <span id="percentile_label">Percentile Range:</span>
+ <input id="start" class="text_input" placeholder="0">
+ <input id="end" class="text_input" placeholder="100">
+ <button id="filter" on-tap="filterByPercentile_">Filter</button>
+ <input id="search_page" class="text_input" placeholder="Page Name">
+ <button id="search" on-tap="searchByPage_">Search</button>
+ <span id="search_error" class="error">Sorry, could not find that page!</span>
+ </span>
+ </span>
+ <div id="submetricsContainer" display="block">
+ <span id="close">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </div>
+ </template>
+</dom-module>
+<script>
+'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;
+
+ // build aggregate chart
+ 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);
+
+ // build page by page
+ 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);
+ },
+
+ // Assign color gradient to series in chart
+ 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 = '';
+ }
+ }
+ }
+ },
+
+ // Element creation
+ 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;
+ },
+
+ // Create child chart and populate it
+ openChildChart_(rect) {
+ // Find main metric and corresponding sub-metrics
+ 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;
+ }
+ }
+
+ // Create child chart
+ 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);
+
+ // Get breakdown data for main step
+ 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};
+ },
+
+ // Handle filtering by start and end percentiles
+ 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);
+ },
+
+ // Get index of x percentile value
+ 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_;
+ },
+
+ // Handle searching by page name
+ 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;
+ }
+
+ // Create child chart with breakdown data
+ 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;
+ },
+
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html
new file mode 100644
index 00000000000..0d062557f63
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/value/ui/metrics_visualization.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ function generateChartBar(metrics, benchmark, page) {
+ const data = {x: benchmark, group: page};
+ for (const metric of metrics) {
+ const key = metric + '-' + benchmark;
+ const mean = Math.random() * 100;
+ data[key] = Math.round(mean * 100) / 100;
+ }
+ return data;
+ }
+
+ function generateSubmetricBar(submetric, benchmark, page,
+ metricToSubmetricMap) {
+ let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric);
+ if (!submetricToBenchmarkMap) {
+ submetricToBenchmarkMap = [];
+ metricToSubmetricMap.set(submetric, submetricToBenchmarkMap);
+ }
+ const data = {x: submetric, hide: 0, group: benchmark};
+ const mean = Math.random() * 100;
+ data[submetric + '-' + benchmark] = Math.round(mean * 100) / 100;
+ submetricToBenchmarkMap.push(data);
+ }
+
+ test('instantiate', function() {
+ const mv = document.createElement('tr-v-ui-metrics-visualization');
+ this.addHTMLOutput(mv);
+
+ const testMetrics = tr.v.ui.METRICS.get('Thread');
+
+ // generate aggregate chart
+ const aggregateChart = [];
+ for (let i = 1; i <= 5; i++) {
+ aggregateChart.push(generateChartBar(testMetrics,
+ 'Run ' + i, 'aggregate'));
+ }
+
+ // generate chart with individual page metrics
+ const chartData = [];
+ for (let i = 1; i <= 5; i++) {
+ for (let j = 1; j <= 5; j++) {
+ chartData.push(generateChartBar(testMetrics,
+ 'Run ' + i, 'Page ' + j));
+ }
+ }
+
+ // generate submetrics
+ const submetricsData = new Map();
+ for (const metric in testMetrics) {
+ const testSubmetrics = [metric + 'a', metric + 'b', metric + 'c'];
+ for (let i = 1; i <= 5; i++) {
+ const page = 'Page ' + i;
+ const pageToMetricMap = tr.v.ui.getValueFromMap(page,
+ submetricsData);
+ const metricToSubmetricMap = tr.v.ui.getValueFromMap(metric,
+ pageToMetricMap);
+ for (let j = 1; j <= 5; j++) {
+ for (const submetric in testSubmetrics) {
+ generateSubmetricBar(submetric, 'Run ' + j, page,
+ metricToSubmetricMap);
+ }
+ }
+ }
+ }
+
+ mv.build({
+ title: 'Thread',
+ aggregate: aggregateChart,
+ page: chartData,
+ submetrics: submetricsData
+ });
+ });
+});
+</script> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html
new file mode 100644
index 00000000000..b546041de50
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+ 'use strict';
+ Polymer({
+ is: 'tr-v-ui-preferred-display-unit',
+
+ ready() {
+ this.preferredTimeDisplayMode_ = undefined;
+ },
+
+ attached() {
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ },
+
+ detached() {
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ },
+
+ // null means no-preference
+ get preferredTimeDisplayMode() {
+ return this.preferredTimeDisplayMode_;
+ },
+
+ set preferredTimeDisplayMode(v) {
+ if (this.preferredTimeDisplayMode_ === v) return;
+ this.preferredTimeDisplayMode_ = v;
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ }
+
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html
new file mode 100644
index 00000000000..382dc5963fe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/value/ui/preferred_display_unit.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const unit = document.createElement('tr-v-ui-preferred-display-unit');
+ const ms = tr.b.TimeDisplayModes.ms;
+ unit.preferredDisplayUnit = ms;
+ assert.strictEqual(unit.preferredDisplayUnit, ms);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html
new file mode 100644
index 00000000000..d3253d8eff9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<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 id="search_page" class="text_input" placeholder="Page Name">
+ <button id="search_button">Search</button>
+ <div id="search_error" class="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" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </span>
+ </template>
+</dom-module>
+<script>
+'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) {
+ // Find main metric and corresponding sub-metrics
+ const benchmarkIndex = Math.floor(rect.index / tr.v.ui.FRAME.length);
+ const title = rect.datum.x;
+
+ // Create child chart with breakdown data
+ 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_() {
+ // Create child chart with breakdown data
+ 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;
+ }
+
+ // Create child chart with breakdown data
+ 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;
+ },
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html
new file mode 100644
index 00000000000..eaf3ee61842
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/value/ui/raster_visualization.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function generateBars(page, benchmark) {
+ const benchmarkData = [];
+ for (const [threadName, thread] of tr.v.ui.FRAME.entries()) {
+ const data = {x: benchmark, hide: 0};
+ if (page !== tr.v.ui.AGGREGATE_KEY) data.group = page;
+ for (const metric of thread.keys()) {
+ const key = metric + '-' + data.x + '-' + threadName;
+ const mean = Math.random() * 100;
+ data[key] = Math.round(mean * 100) / 100;
+ }
+ benchmarkData.push(data);
+ }
+ return benchmarkData;
+ }
+
+ test('instantiate', function() {
+ const rv = document.createElement('tr-v-ui-raster-visualization');
+ this.addHTMLOutput(rv);
+
+ const allChartData = new Map();
+
+ // generate aggregate data
+ let aggregateData = [];
+ for (let i = 1; i <= 5; i++) {
+ aggregateData = aggregateData.concat(generateBars(tr.v.ui.AGGREGATE_KEY,
+ 'Run ' + i));
+ }
+ allChartData.set(tr.v.ui.AGGREGATE_KEY, aggregateData);
+
+ // generate data per page
+ for (let i = 1; i <= 5; i++) {
+ const page = 'Page ' + i;
+ let chartData = [];
+ for (let j = 1; j <= 5; j++) {
+ chartData = chartData.concat(generateBars(page, 'Run ' + j));
+ }
+ allChartData.set(page, chartData);
+ }
+
+ rv.build(allChartData);
+ });
+});
+</script> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html
new file mode 100644
index 00000000000..ac52e51aba2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-related-event-set-span">
+</dom-module>
+
+<script>
+'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 {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html
new file mode 100644
index 00000000000..b0058a3c97b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/related_event_set.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_RelatedEventSet0', function() {
+ const diagnostic = new tr.v.d.RelatedEventSet();
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ assert.strictEqual('0 events', span.textContent);
+ });
+
+ test('instantiate_RelatedEventSet1', function() {
+ const diagnostic = new tr.v.d.RelatedEventSet();
+ tr.c.TestUtils.newModel(function(model) {
+ const proc = model.getOrCreateProcess(1);
+ const thread = proc.getOrCreateThread(2);
+ const event = tr.c.TestUtils.newSliceEx(
+ {title: 'a', start: 0, duration: 1});
+ thread.sliceGroup.pushSlice(event);
+ diagnostic.add(event);
+ });
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ assert.strictEqual('a 1.000 ms', span.textContent);
+ });
+
+ test('instantiate_RelatedEventSet2', function() {
+ const diagnostic = new tr.v.d.RelatedEventSet();
+ tr.c.TestUtils.newModel(function(model) {
+ const proc = model.getOrCreateProcess(1);
+ const thread = proc.getOrCreateThread(2);
+ let event = tr.c.TestUtils.newSliceEx({start: 0, duration: 1});
+ thread.sliceGroup.pushSlice(event);
+ diagnostic.add(event);
+ event = tr.c.TestUtils.newSliceEx({start: 1, duration: 1});
+ thread.sliceGroup.pushSlice(event);
+ diagnostic.add(event);
+ });
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ assert.strictEqual('2 events', span.textContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html
new file mode 100644
index 00000000000..ac71cd7a1d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+
+<dom-module id="tr-v-ui-scalar-context-controller">
+ <template></template>
+</dom-module>
+
+<!--
+@fileoverview Polymer element for controlling common context across scalar
+spans. To facilitate multiple separate contexts (e.g. a separate context for
+each table column), each scalar span has to specify which "context group"
+it belongs to:
+
+ +============ some container element (e.g. <div>) ============+
+ | |
+ | <tr-v-ui-scalar-context-controller> |
+ | ^ ^ |
+ | | | |
+ | v v |
+ | .... Context group 1 .... .... Context group 2 .... |
+ | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : |
+ | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : . . . |
+ | : . . . : : . . . : |
+ | :.......................: :.......................: |
+ +=============================================================+
+
+An element can find its enclosing context controller using the
+getScalarContextControllerForElement(node) defined in this file. Scalar spans
+can push their state to the controller using the following three methods:
+
+ 1. onScalarSpanAdded(contextGroup, span)
+ This method should be called when a span is attached to the DOM tree (or
+ afterwards when added to a context group).
+
+ 2. onScalarSpanRemoved(contextGroup, span)
+ This method should be called when a span is detached from the DOM tree (or
+ beforehand when removed from a context group).
+
+ 3. onScalarSpanUpdated(contextGroup, span)
+ This method should be called when the value of a span changes.
+
+Note: If a span wants to change its context group, it should first call
+onScalarSpanRemoved with the old group and then onScalarSpanAdded with the new
+group.
+
+If one or more group contexts are modified (due to one of the three methods
+above), the controller will asynchronously (at the next RAF) update them and
+fire a 'context-updated' event. Scalar spans can listen for this event and
+update their UI accordingly.
+
+The context currently consists of the range of values of the associated spans.
+This allows automatic display of relative sizes using sparklines.
+
+The controller design is based on:
+https://docs.google.com/document/d/16ih8yYK8kF8MMlPnB-5KlyfS_AjjtbyAfi3pkxoZ8xs/edit?usp=sharing
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-scalar-context-controller',
+
+ created() {
+ this.host_ = undefined;
+ this.groupToContext_ = new Map();
+ this.dirtyGroups_ = new Set();
+ },
+
+ attached() {
+ if (this.host_) {
+ throw new Error(
+ 'Scalar context controller is already attached to a host');
+ }
+
+ const host = findParentOrHost(this);
+ if (host.__scalarContextController) {
+ throw new Error(
+ 'Multiple scalar context controllers attached to this host');
+ }
+
+ host.__scalarContextController = this;
+ this.host_ = host;
+ },
+
+ detached() {
+ if (!this.host_) {
+ throw new Error('Scalar context controller is not attached to a host');
+ }
+ if (this.host_.__scalarContextController !== this) {
+ throw new Error(
+ 'Scalar context controller is not attached to its host');
+ }
+
+ delete this.host_.__scalarContextController;
+ this.host_ = undefined;
+ },
+
+ getContext(group) {
+ return this.groupToContext_.get(group);
+ },
+
+ onScalarSpanAdded(group, span) {
+ let context = this.groupToContext_.get(group);
+ if (context === undefined) {
+ context = {
+ spans: new Set(),
+ range: new tr.b.math.Range()
+ };
+ this.groupToContext_.set(group, context);
+ }
+ if (context.spans.has(span)) {
+ throw new Error('Scalar span already registered with group: ' + group);
+ }
+ context.spans.add(span);
+ this.markGroupDirtyAndScheduleUpdate_(group);
+ },
+
+ onScalarSpanRemoved(group, span) {
+ const context = this.groupToContext_.get(group);
+ if (!context.spans.has(span)) {
+ throw new Error('Scalar span not registered with group: ' + group);
+ }
+ context.spans.delete(span);
+ this.markGroupDirtyAndScheduleUpdate_(group);
+ },
+
+ onScalarSpanUpdated(group, span) {
+ const context = this.groupToContext_.get(group);
+ if (!context.spans.has(span)) {
+ throw new Error('Scalar span not registered with group: ' + group);
+ }
+ this.markGroupDirtyAndScheduleUpdate_(group);
+ },
+
+ markGroupDirtyAndScheduleUpdate_(group) {
+ const alreadyDirty = this.dirtyGroups_.size > 0;
+ this.dirtyGroups_.add(group);
+ if (!alreadyDirty) {
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContext, this);
+ }
+ },
+
+ updateContext() {
+ const groups = this.dirtyGroups_;
+ if (groups.size === 0) return;
+ this.dirtyGroups_ = new Set();
+
+ for (const group of groups) {
+ this.updateGroup_(group);
+ }
+
+ const event = new tr.b.Event('context-updated');
+ event.groups = groups;
+ this.dispatchEvent(event);
+ },
+
+ updateGroup_(group) {
+ const context = this.groupToContext_.get(group);
+ if (context.spans.size === 0) {
+ this.groupToContext_.delete(group);
+ return;
+ }
+ context.range.reset();
+ for (const span of context.spans) {
+ context.range.addValue(span.value);
+ }
+ }
+ });
+
+ function getScalarContextControllerForElement(element) {
+ while (element) {
+ if (element.__scalarContextController) {
+ return element.__scalarContextController;
+ }
+ element = findParentOrHost(element);
+ }
+ return undefined;
+ }
+
+ function findParentOrHost(node) {
+ if (node.parentElement) {
+ return node.parentElement;
+ }
+ while (Polymer.dom(node).parentNode) {
+ node = Polymer.dom(node).parentNode;
+ }
+ return node.host;
+ }
+
+ return {
+ getScalarContextControllerForElement,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html
new file mode 100644
index 00000000000..ccb3f61d6f4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html
@@ -0,0 +1,312 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<dom-module id="tr-v-ui-scalar-context-controller-mock-host">
+ <template>
+ <tr-v-ui-scalar-context-controller id="controller">
+ </tr-v-ui-scalar-context-controller>
+ <content></content>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const getScalarContextControllerForElement =
+ tr.v.ui.getScalarContextControllerForElement;
+
+ Polymer({
+ is: 'tr-v-ui-scalar-context-controller-mock-host'
+ });
+
+ test('getScalarContextControllerForElement', function() {
+ const root = document.createElement('div');
+ Polymer.dom(document.body).appendChild(root);
+ try {
+ assert.isUndefined(getScalarContextControllerForElement(root));
+
+ // <div> root
+ // |__<div> host1
+ // |__<tr-v-ui-scalar-context-controller> c1
+ const host1 = document.createElement('div');
+ Polymer.dom(root).appendChild(host1);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1));
+ const c1 = document.createElement('tr-v-ui-scalar-context-controller');
+ Polymer.dom(host1).appendChild(c1);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+
+ // <div> root
+ // |__<div> host1
+ // | |__<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // :..<tr-v-ui-scalar-context-controller> c2
+ const host2 = document.createElement(
+ 'tr-v-ui-scalar-context-controller-mock-host');
+ const c2 = host2.$.controller;
+ Polymer.dom(root).appendChild(host2);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+
+ // <div> root
+ // |__<div> host1
+ // | |__<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :..<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // |__<div> divB
+ const divA = document.createElement('div');
+ Polymer.dom(host2).appendChild(divA);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c2);
+ const divB = document.createElement('div');
+ Polymer.dom(divA).appendChild(divB);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c2);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :.-<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ // :..<tr-v-ui-scalar-context-controller> c3
+ const host3 = document.createElement(
+ 'tr-v-ui-scalar-context-controller-mock-host');
+ Polymer.dom(divB).appendChild(host3);
+ const c3 = host3.$.controller;
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c2);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c3);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :.-<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ // :..<tr-v-ui-scalar-context-controller> c3
+ const c4 = document.createElement('tr-v-ui-scalar-context-controller');
+ Polymer.dom(divA).appendChild(c4);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c3);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :.-<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ Polymer.dom(host3.root).removeChild(c3);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c4);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ Polymer.dom(host2.root).removeChild(c2);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.isUndefined(getScalarContextControllerForElement(host2));
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c4);
+
+ // <div> root
+ // | :.<tr-v-ui-scalar-context-controller> c3
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ Polymer.dom(root).appendChild(c3);
+ assert.strictEqual(getScalarContextControllerForElement(root), c3);
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c3);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c4);
+ } finally {
+ Polymer.dom(document.body).removeChild(root);
+ }
+ });
+
+ function contextTest(name, testCallback) {
+ test('context_' + name, function() {
+ const root = document.createElement('div');
+ Polymer.dom(document.body).appendChild(root);
+ try {
+ const c = document.createElement('tr-v-ui-scalar-context-controller');
+ Polymer.dom(root).appendChild(c);
+
+ let updatedGroups = []; // Fail if event fires unexpectedly.
+ c.addEventListener('context-updated', function(e) {
+ if (updatedGroups) {
+ assert.fail('Unexpected context-updated event fired.');
+ }
+ updatedGroups = Array.from(e.groups);
+ });
+
+ c.expectContextUpdatedEventForTesting =
+ function(expectedUpdatedGroups) {
+ updatedGroups = undefined;
+ tr.b.forceAllPendingTasksToRunForTest();
+ assert.sameMembers(updatedGroups, expectedUpdatedGroups);
+ };
+
+ testCallback.call(this, c);
+ } finally {
+ Polymer.dom(document.body).removeChild(root);
+ }
+ });
+ }
+
+ contextTest('singleGroup', function(c) {
+ assert.isUndefined(c.getContext('G'));
+
+ const s1 = {value: 10};
+ c.onScalarSpanAdded('G', s1);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(10, 10)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1]);
+
+ const s2 = {value: 15};
+ c.onScalarSpanAdded('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(10, 15)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]);
+
+ s1.value = 5;
+ c.onScalarSpanUpdated('G', s1);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(5, 15)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]);
+
+ c.onScalarSpanRemoved('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(5, 5)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1]);
+
+ const s3 = {value: 0};
+ c.onScalarSpanAdded('G', s3);
+ s2.value = 14;
+ c.onScalarSpanAdded('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(0, 14)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2, s3]);
+
+ c.onScalarSpanRemoved('G', s1);
+ c.onScalarSpanRemoved('G', s2);
+ c.onScalarSpanRemoved('G', s3);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isUndefined(c.getContext('G'));
+
+ c.onScalarSpanAdded('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(14, 14)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s2]);
+ });
+
+ contextTest('multipleGroups', function(c) {
+ assert.isUndefined(c.getContext('G1'));
+ assert.isUndefined(c.getContext('G2'));
+
+ const s1 = {value: 0};
+ c.onScalarSpanAdded('G1', s1);
+ c.expectContextUpdatedEventForTesting(['G1']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(0, 0)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1]);
+
+ const s2 = {value: 1};
+ c.onScalarSpanAdded('G2', s2);
+ c.expectContextUpdatedEventForTesting(['G2']);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 1)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]);
+
+ const s3 = {value: 2};
+ const s4 = {value: -1};
+ c.onScalarSpanAdded('G2', s3);
+ c.onScalarSpanAdded('G1', s4);
+ c.expectContextUpdatedEventForTesting(['G1', 'G2']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(-1, 0)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s4]);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 2)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2, s3]);
+
+ c.onScalarSpanRemoved('G2', s3);
+ c.onScalarSpanAdded('G1', s3);
+ c.expectContextUpdatedEventForTesting(['G1', 'G2']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(-1, 2)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 1)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]);
+
+ s4.value = 3;
+ c.onScalarSpanUpdated('G1', s4);
+ s1.value = 1;
+ c.onScalarSpanUpdated('G1', s1);
+ c.expectContextUpdatedEventForTesting(['G1']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 3)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 1)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]);
+
+ c.onScalarSpanRemoved('G2', s2);
+ c.expectContextUpdatedEventForTesting(['G2']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 3)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]);
+ assert.isUndefined(c.getContext('G2'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html
new file mode 100644
index 00000000000..631c3696f8c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<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>
+
+<script>
+'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 {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html
new file mode 100644
index 00000000000..9f6167f1acb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/scalar.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const diagnostic = new tr.v.d.Scalar(new tr.b.Scalar(
+ tr.b.Unit.byName.timeDurationInMs, 123.456));
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-SCALAR-DIAGNOSTIC-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html
new file mode 100644
index 00000000000..24a5cf3ac0f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id="tr-v-ui-scalar-map-table">
+ <template>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-v-ui-scalar-map-table',
+
+ created() {
+ /** @type {!Map.<string, !tr.b.Scalar>} */
+ this.scalarMap_ = new Map();
+
+ /** @type {!Map.<string, !tr.b.math.Statistics.Significance>} */
+ this.significance_ = new Map();
+ },
+
+ ready() {
+ this.$.table.showHeader = false;
+ this.$.table.tableColumns = [
+ {
+ value(row) {
+ return row.name;
+ }
+ },
+ {
+ value(row) {
+ const span = tr.v.ui.createScalarSpan(row.value);
+ if (row.significance !== undefined) {
+ span.significance = row.significance;
+ } else if (row.anyRowsHaveSignificance) {
+ // Ensure vertical alignment.
+ span.style.marginRight = '18px';
+ }
+ span.style.whiteSpace = 'nowrap';
+ return span;
+ }
+ }
+ ];
+ },
+
+ get scalarMap() {
+ return this.scalarMap_;
+ },
+
+ /**
+ * @param {!Map.<string,!tr.b.Scalar>} map
+ */
+ set scalarMap(map) {
+ this.scalarMap_ = map;
+ this.updateContents_();
+ },
+
+ /**
+ * @param {string} key
+ * @param {!tr.b.math.Statistics.Significance} significance
+ */
+ setSignificanceForKey(key, significance) {
+ this.significance_.set(key, significance);
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const rows = [];
+ for (const [key, scalar] of this.scalarMap) {
+ rows.push({
+ name: key,
+ value: scalar,
+ significance: this.significance_.get(key),
+ anyRowsHaveSignificance: (this.significance_.size > 0)
+ });
+ }
+ this.$.table.tableRows = rows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html
new file mode 100644
index 00000000000..4c9a50c2314
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/scalar_map_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const span = document.createElement('tr-v-ui-scalar-map-table');
+
+ const histogram = new tr.v.Histogram('', tr.b.Unit.byName.energyInJoules);
+ for (let i = 0; i < 1e2; ++i) {
+ histogram.addSample(Math.random() * 1000);
+ }
+
+ histogram.addSample('foo');
+ histogram.customizeSummaryOptions({nans: true});
+
+ span.scalarMap = histogram.statisticsScalars;
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html
new file mode 100644
index 00000000000..50d89653ed1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html
@@ -0,0 +1,626 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ /**
+ * One common simple way to use this function is
+ * createScalarSpan(number, {unit: tr.b.Unit.byName.whatever})
+ *
+ * This function can also take a Scalar, undefined, or a Histogram plus
+ * significance, contextGroup, customContextRange, leftAlign and/or inline.
+ *
+ * @param {undefined|tr.b.Scalar|tr.v.Histogram} value
+ * @param {Object=} opt_config
+ * @param {!tr.b.math.Range=} opt_config.customContextRange
+ * @param {boolean=} opt_config.leftAlign
+ * @param {boolean=} opt_config.inline
+ * @param {!tr.b.Unit=} opt_config.unit
+ * @param {tr.b.math.Statistics.Significance=} opt_config.significance
+ * @param {string=} opt_config.contextGroup
+ * @return {(string|!HTMLElement)}
+ */
+ function createScalarSpan(value, opt_config) {
+ if (value === undefined) return '';
+
+ const config = opt_config || {};
+ const ownerDocument = config.ownerDocument || document;
+
+ const span = ownerDocument.createElement('tr-v-ui-scalar-span');
+
+ let numericValue;
+ if (value instanceof tr.b.Scalar) {
+ span.value = value;
+ numericValue = value.value;
+ } else if (value instanceof tr.v.Histogram) {
+ numericValue = value.average;
+ if (numericValue === undefined) return '';
+ span.setValueAndUnit(numericValue, value.unit);
+ } else {
+ const unit = config.unit;
+ if (unit === undefined) {
+ throw new Error(
+ 'Unit must be provided in config when value is a number');
+ }
+ span.setValueAndUnit(value, unit);
+ numericValue = value;
+ }
+
+ if (config.context) {
+ span.context = config.context;
+ }
+
+ if (config.customContextRange) {
+ span.customContextRange = config.customContextRange;
+ }
+
+ if (config.leftAlign) {
+ span.leftAlign = true;
+ }
+
+ if (config.inline) {
+ span.inline = true;
+ }
+
+ if (config.significance !== undefined) {
+ span.significance = config.significance;
+ }
+
+ if (config.contextGroup !== undefined) {
+ span.contextGroup = config.contextGroup;
+ }
+
+ return span;
+ }
+
+ return {
+ createScalarSpan,
+ };
+});
+</script>
+
+<dom-module id="tr-v-ui-scalar-span">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ position: relative;
+ /* Limit the sparkline's negative z-index to the span only. */
+ isolation: isolate;
+ }
+
+ :host(.left-align) {
+ justify-content: flex-start;
+ }
+
+ :host(.inline) {
+ display: inline-flex;
+ }
+
+ #sparkline {
+ width: 0%;
+ position: absolute;
+ bottom: 0;
+ display: none;
+ height: 100%;
+ background-color: hsla(216, 100%, 94.5%, .75);
+ border-color: hsl(216, 100%, 89%);
+ box-sizing: border-box;
+ z-index: -1;
+ }
+ #sparkline.positive {
+ border-right-style: solid;
+ /* The border width must be kept in sync with buildSparklineStyle_(). */
+ border-right-width: 1px;
+ }
+ #sparkline:not(.positive) {
+ border-left-style: solid;
+ /* The border width must be kept in sync with buildSparklineStyle_(). */
+ border-left-width: 1px;
+ }
+ #sparkline.better {
+ background-color: hsla(115, 100%, 93%, .75);
+ border-color: hsl(118, 60%, 80%);
+ }
+ #sparkline.worse {
+ background-color: hsla(0, 100%, 88%, .75);
+ border-color: hsl(0, 100%, 80%);
+ }
+
+ #content {
+ white-space: nowrap;
+ }
+ #content, #significance, #warning {
+ flex-grow: 0;
+ }
+ #content.better {
+ color: green;
+ }
+ #content.worse {
+ color: red;
+ }
+
+ #significance svg {
+ margin-left: 4px;
+ display: none;
+ height: 1em;
+ vertical-align: text-top;
+ stroke-width: 4;
+ fill: rgba(0, 0, 0, 0);
+ }
+ #significance #insignificant {
+ stroke: black;
+ }
+ #significance #significantly_better {
+ stroke: green;
+ }
+ #significance #significantly_worse {
+ stroke: red;
+ }
+
+ #warning {
+ display: none;
+ margin-left: 4px;
+ height: 1em;
+ vertical-align: text-top;
+ stroke-width: 0;
+ }
+ #warning path {
+ fill: rgb(255, 185, 185);
+ }
+ #warning rect {
+ fill: red;
+ }
+ </style>
+
+ <span id="sparkline"></span>
+
+ <span id="content"></span>
+
+ <span id="significance">
+ <!-- Neutral face -->
+ <svg viewbox="0 0 128 128" id="insignificant">
+ <circle r="60" cx="64" cy="64"/>
+ <circle r="4" cx="44" cy="44"/>
+ <circle r="4" cx="84" cy="44"/>
+ <line x1="36" x2="92" y1="80" y2="80"/>
+ </svg>
+
+ <!-- Smiling face -->
+ <svg viewbox="0 0 128 128" id="significantly_better">
+ <circle r="60" cx="64" cy="64"/>
+ <circle r="4" cx="44" cy="44"/>
+ <circle r="4" cx="84" cy="44"/>
+ <path d="M 28 64 Q 64 128 100 64"/>
+ </svg>
+
+ <!-- Frowning face -->
+ <svg viewbox="0 0 128 128" id="significantly_worse">
+ <circle r="60" cx="64" cy="64"/>
+ <circle r="4" cx="44" cy="44"/>
+ <circle r="4" cx="84" cy="44"/>
+ <path d="M 36 96 Q 64 48 92 96"/>
+ </svg>
+ </span>
+
+ <svg viewbox="0 0 128 128" id="warning">
+ <path d="M 64 0 L 128 128 L 0 128 L 64 0"/>
+ <rect x="60" width="8" y="0" height="84"/>
+ <rect x="60" width="8" y="100" height="24"/>
+ </svg>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-v-ui-scalar-span',
+
+ properties: {
+ /**
+ * String identifier for grouping scalar spans with common context (e.g.
+ * all scalar spans in a single table column would typically share a common
+ * context and, thus, have the same context group identifier). If falsy,
+ * the scalar span will NOT be associated with any context.
+ */
+ contextGroup: {
+ type: String,
+ reflectToAttribute: true,
+ observer: 'contextGroupChanged_'
+ }
+ },
+
+ created() {
+ this.value_ = undefined;
+ this.unit_ = undefined;
+
+ // TODO(petrcermak): Merge this into the context controller.
+ this.context_ = undefined;
+
+ this.warning_ = undefined;
+ this.significance_ = tr.b.math.Statistics.Significance.DONT_CARE;
+
+ // To avoid unnecessary DOM traversal, search for the context controller
+ // only when necessary (when the span is attached and has a context group).
+ this.shouldSearchForContextController_ = false;
+ this.lazyContextController_ = undefined;
+ this.onContextUpdated_ = this.onContextUpdated_.bind(this);
+ this.updateContents_ = this.updateContents_.bind(this);
+
+ // The span can specify a custom context range, which will override the
+ // values from the context controller.
+ this.customContextRange_ = undefined;
+ },
+
+ get significance() {
+ return this.significance_;
+ },
+
+ set significance(s) {
+ this.significance_ = s;
+ this.updateContents_();
+ },
+
+ set contentTextDecoration(deco) {
+ this.$.content.style.textDecoration = deco;
+ },
+
+ get value() {
+ return this.value_;
+ },
+
+ set value(value) {
+ if (value instanceof tr.b.Scalar) {
+ this.value_ = value.value;
+ this.unit_ = value.unit;
+ } else {
+ this.value_ = value;
+ }
+ this.updateContents_();
+ if (this.hasContext_(this.contextGroup)) {
+ this.contextController_.onScalarSpanUpdated(this.contextGroup, this);
+ } else {
+ this.updateSparkline_();
+ }
+ },
+
+ get contextController_() {
+ if (this.shouldSearchForContextController_) {
+ this.lazyContextController_ =
+ tr.v.ui.getScalarContextControllerForElement(this);
+ this.shouldSearchForContextController_ = false;
+ }
+ return this.lazyContextController_;
+ },
+
+ hasContext_(contextGroup) {
+ // The ordering here is important. It ensures that we avoid a DOM traversal
+ // when the span doesn't have a context group.
+ return !!(contextGroup && this.contextController_);
+ },
+
+ contextGroupChanged_(newContextGroup, oldContextGroup) {
+ this.detachFromContextControllerIfPossible_(oldContextGroup);
+ if (!this.attachToContextControllerIfPossible_(newContextGroup)) {
+ // If the span failed to attach to a controller, it won't receive a
+ // context-updated event, so we trigger it manually.
+ this.onContextUpdated_();
+ }
+ },
+
+ attachToContextControllerIfPossible_(contextGroup) {
+ if (!this.hasContext_(contextGroup)) return false;
+
+ this.contextController_.addEventListener(
+ 'context-updated', this.onContextUpdated_);
+ this.contextController_.onScalarSpanAdded(contextGroup, this);
+ return true;
+ },
+
+ detachFromContextControllerIfPossible_(contextGroup) {
+ if (!this.hasContext_(contextGroup)) return;
+
+ this.contextController_.removeEventListener(
+ 'context-updated', this.onContextUpdated_);
+ this.contextController_.onScalarSpanRemoved(contextGroup, this);
+ },
+
+ attached() {
+ tr.b.Unit.addEventListener(
+ 'display-mode-changed', this.updateContents_);
+ this.shouldSearchForContextController_ = true;
+ this.attachToContextControllerIfPossible_(this.contextGroup);
+ },
+
+ detached() {
+ tr.b.Unit.removeEventListener(
+ 'display-mode-changed', this.updateContents_);
+ this.detachFromContextControllerIfPossible_(this.contextGroup);
+ this.shouldSearchForContextController_ = false;
+ this.lazyContextController_ = undefined;
+ },
+
+ onContextUpdated_() {
+ this.updateSparkline_();
+ },
+
+ get context() {
+ return this.context_;
+ },
+
+ set context(context) {
+ this.context_ = context;
+ this.updateContents_();
+ },
+
+ get unit() {
+ return this.unit_;
+ },
+
+ set unit(unit) {
+ this.unit_ = unit;
+ this.updateContents_();
+ this.updateSparkline_();
+ },
+
+ setValueAndUnit(value, unit) {
+ this.value_ = value;
+ this.unit_ = unit;
+ this.updateContents_();
+ },
+
+ get customContextRange() {
+ return this.customContextRange_;
+ },
+
+ set customContextRange(customContextRange) {
+ this.customContextRange_ = customContextRange;
+ this.updateSparkline_();
+ },
+
+ get inline() {
+ return Polymer.dom(this).classList.contains('inline');
+ },
+
+ set inline(inline) {
+ if (inline) {
+ Polymer.dom(this).classList.add('inline');
+ } else {
+ Polymer.dom(this).classList.remove('inline');
+ }
+ },
+
+ get leftAlign() {
+ return Polymer.dom(this).classList.contains('left-align');
+ },
+
+ set leftAlign(leftAlign) {
+ if (leftAlign) {
+ Polymer.dom(this).classList.add('left-align');
+ } else {
+ Polymer.dom(this).classList.remove('left-align');
+ }
+ },
+
+ updateSparkline_() {
+ Polymer.dom(this.$.sparkline).classList.remove('positive');
+ Polymer.dom(this.$.sparkline).classList.remove('better');
+ Polymer.dom(this.$.sparkline).classList.remove('worse');
+ Polymer.dom(this.$.sparkline).classList.remove('same');
+ this.$.sparkline.style.display = 'none';
+ this.$.sparkline.style.left = '0';
+ this.$.sparkline.style.width = '0';
+
+ // Custom context range takes precedence over controller context range.
+ let range = this.customContextRange_;
+ if (!range && this.hasContext_(this.contextGroup)) {
+ const context = this.contextController_.getContext(this.contextGroup);
+ if (context) {
+ range = context.range;
+ }
+ }
+ if (!range || range.isEmpty) return;
+
+ const leftPoint = Math.min(range.min, 0);
+ const rightPoint = Math.max(range.max, 0);
+ const pointDistance = rightPoint - leftPoint;
+ if (pointDistance === 0) {
+ // This can happen, for example, when all spans within the context have
+ // zero values (so |range| is [0, 0]).
+ return;
+ }
+
+ // Draw the sparkline.
+ this.$.sparkline.style.display = 'block';
+ let left;
+ let width;
+ if (this.value > 0) {
+ width = Math.min(this.value, rightPoint);
+ left = -leftPoint;
+ Polymer.dom(this.$.sparkline).classList.add('positive');
+ } else if (this.value <= 0) {
+ width = -Math.max(this.value, leftPoint);
+ left = (-leftPoint) - width;
+ }
+ this.$.sparkline.style.left = this.buildSparklineStyle_(
+ left / pointDistance, false);
+ this.$.sparkline.style.width = this.buildSparklineStyle_(
+ width / pointDistance, true);
+
+ // Set the sparkline color (if applicable).
+ const changeClass = this.changeClassName_;
+ if (changeClass) {
+ Polymer.dom(this.$.sparkline).classList.add(changeClass);
+ }
+ },
+
+ buildSparklineStyle_(ratio, isWidth) {
+ // To avoid visual glitches around the zero value bar, we subtract 1 pixel
+ // from the width of the element and multiply the remainder (100% - 1px) by
+ // |ratio|. The extra pixel is used for the sparkline border. This allows
+ // us to align zero sparklines with both positive and negative values:
+ //
+ // ::::::::::| +10 MiB
+ // :::::| +5 MiB
+ // | 0 MiB
+ // |::::: -5 MiB
+ // |:::::::::: -10 MiB
+ //
+ let position = 'calc(' + ratio + ' * (100% - 1px)';
+ if (isWidth) {
+ position += ' + 1px'; // Extra pixel for sparkline border.
+ }
+ position += ')';
+ return position;
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.content).textContent = '';
+ Polymer.dom(this.$.content).classList.remove('better');
+ Polymer.dom(this.$.content).classList.remove('worse');
+ Polymer.dom(this.$.content).classList.remove('same');
+ this.$.insignificant.style.display = '';
+ this.$.significantly_better.style.display = '';
+ this.$.significantly_worse.style.display = '';
+
+ if (this.unit_ === undefined) return;
+
+ this.$.content.title = '';
+ Polymer.dom(this.$.content).textContent =
+ this.unit_.format(this.value, this.context);
+ this.updateDelta_();
+ },
+
+ updateDelta_() {
+ let changeClass = this.changeClassName_;
+ if (!changeClass) {
+ this.$.significance.style.display = 'none';
+ return; // Not a delta or we don't care.
+ }
+
+ this.$.significance.style.display = 'inline';
+
+ let title;
+ switch (changeClass) {
+ case 'better':
+ title = 'improvement';
+ break;
+
+ case 'worse':
+ title = 'regression';
+ break;
+
+ case 'same':
+ title = 'no change';
+ break;
+
+ default:
+ throw new Error('Unknown change class: ' + changeClass);
+ }
+
+ // Set the content class separately from the significance class so that
+ // the Neutral face is always a neutral color.
+ Polymer.dom(this.$.content).classList.add(changeClass);
+
+ switch (this.significance) {
+ case tr.b.math.Statistics.Significance.DONT_CARE:
+ break;
+
+ case tr.b.math.Statistics.Significance.INSIGNIFICANT:
+ if (changeClass !== 'same') title = 'insignificant ' + title;
+ this.$.insignificant.style.display = 'inline';
+ changeClass = 'same';
+ break;
+
+ case tr.b.math.Statistics.Significance.SIGNIFICANT:
+ if (changeClass === 'same') {
+ throw new Error('How can no change be significant?');
+ }
+ this.$['significantly_' + changeClass].style.display = 'inline';
+ title = 'significant ' + title;
+ break;
+
+ default:
+ throw new Error('Unknown significance ' + this.significance);
+ }
+
+ this.$.significance.title = title;
+ this.$.content.title = title;
+ },
+
+ get changeClassName_() {
+ if (!this.unit_ || !this.unit_.isDelta) return undefined;
+
+ switch (this.unit_.improvementDirection) {
+ case tr.b.ImprovementDirection.DONT_CARE:
+ return undefined;
+
+ case tr.b.ImprovementDirection.BIGGER_IS_BETTER:
+ if (this.value === 0) return 'same';
+ return this.value > 0 ? 'better' : 'worse';
+
+ case tr.b.ImprovementDirection.SMALLER_IS_BETTER:
+ if (this.value === 0) return 'same';
+ return this.value < 0 ? 'better' : 'worse';
+
+ default:
+ throw new Error('Unknown improvement direction: ' +
+ this.unit_.improvementDirection);
+ }
+ },
+
+ get warning() {
+ return this.warning_;
+ },
+
+ set warning(warning) {
+ this.warning_ = warning;
+ const warningEl = this.$.warning;
+ if (this.warning_) {
+ warningEl.title = warning;
+ warningEl.style.display = 'inline';
+ } else {
+ warningEl.title = '';
+ warningEl.style.display = '';
+ }
+ },
+
+ // tr-v-ui-time-stamp-span property
+ get timestamp() {
+ return this.value;
+ },
+
+ set timestamp(timestamp) {
+ if (timestamp instanceof tr.b.u.TimeStamp) {
+ this.value = timestamp;
+ return;
+ }
+ this.setValueAndUnit(timestamp, tr.b.u.Units.timeStampInMs);
+ },
+
+ // tr-v-ui-time-duration-span property
+ get duration() {
+ return this.value;
+ },
+
+ set duration(duration) {
+ if (duration instanceof tr.b.u.TimeDuration) {
+ this.value = duration;
+ return;
+ }
+ this.setValueAndUnit(duration, tr.b.u.Units.timeDurationInMs);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html
new file mode 100644
index 00000000000..56ca6917d71
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html
@@ -0,0 +1,1027 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Scalar = tr.b.Scalar;
+ const Unit = tr.b.Unit;
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const EXAMPLE_MEMORY_FORMATTING_CONTEXT = {
+ unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI,
+ };
+ const EXAMPLE_MEMORY_NUMERIC = new Scalar(
+ Unit.byName.sizeInBytesDelta_smallerIsBetter, 256 * 1024 * 1024);
+
+ function checkSignificance(span, expectedSignificance) {
+ assert.strictEqual(span.$.insignificant.style.display,
+ expectedSignificance === 'insignificant' ? 'inline' : '');
+ assert.strictEqual(span.$.significantly_better.style.display,
+ expectedSignificance === 'significantly_better' ? 'inline' : '');
+ assert.strictEqual(span.$.significantly_worse.style.display,
+ expectedSignificance === 'significantly_worse' ? 'inline' : '');
+ }
+
+ function checkScalarSpan(test, value, unit, expectedContent, opt_options) {
+ const options = opt_options || {};
+ const span = tr.v.ui.createScalarSpan(new tr.b.Scalar(unit, value),
+ {significance: options.significance});
+
+ test.addHTMLOutput(span);
+ assert.strictEqual(
+ Polymer.dom(span.$.content).textContent, expectedContent);
+ assert.strictEqual(window.getComputedStyle(span.$.content).color,
+ options.expectedColor || 'rgb(0, 0, 0)');
+
+ if (options.expectedTitle) {
+ assert.strictEqual(span.$.content.title, options.expectedTitle);
+ }
+
+ if (options.significance !== undefined) {
+ checkSignificance(span, options.expectedEmoji);
+ if (options.expectedTitle) {
+ assert.strictEqual(span.$.significance.title, options.expectedTitle);
+ }
+ }
+ }
+
+ test('instantiate_significance', function() {
+ const countD = Unit.byName.count.correspondingDeltaUnit;
+ const countSIBD = Unit.byName.count_smallerIsBetter.correspondingDeltaUnit;
+ const countBIBD = Unit.byName.count_biggerIsBetter.correspondingDeltaUnit;
+
+ const zero = String.fromCharCode(177) + '0';
+
+ checkScalarSpan(this, 0, countSIBD, zero, {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedTitle: 'no change',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 0, countSIBD, zero, {
+ expectedEmoji: 'insignificant',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'no change'
+ });
+
+ assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero,
+ {significance: tr.b.math.Statistics.Significance.SIGNIFICANT}));
+
+ checkScalarSpan(this, 0, countBIBD, zero, {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedTitle: 'no change',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 0, countBIBD, zero, {
+ expectedEmoji: 'insignificant',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'no change'
+ });
+
+ assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero,
+ {significance: tr.b.math.Statistics.Significance.SIGNIFICANT}));
+
+ checkScalarSpan(this, 1, countSIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedTitle: 'regression',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countSIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant regression'
+ });
+
+ checkScalarSpan(this, 1, countSIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'significantly_worse',
+ expectedEmojiColor: 'rgb(255, 0, 0)',
+ expectedTitle: 'significant regression'
+ });
+
+ checkScalarSpan(this, 1, countBIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedTitle: 'improvement',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countBIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant improvement'
+ });
+
+ checkScalarSpan(this, 1, countBIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'significantly_better',
+ expectedEmojiColor: 'rgb(0, 128, 0)',
+ expectedTitle: 'significant improvement'
+ });
+
+ checkScalarSpan(this, -1, countSIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: '',
+ expectedEmojiColor: '',
+ expectedTitle: 'improvement'
+ });
+
+ checkScalarSpan(this, -1, countSIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant improvement'
+ });
+
+ checkScalarSpan(this, -1, countSIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'significantly_better',
+ expectedEmojiColor: 'rgb(0, 128, 0)',
+ expectedTitle: 'significant improvement'
+ });
+
+ checkScalarSpan(this, -1, countBIBD, '-1', {
+ expectedColor: 'rgb(255, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countBIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant regression'
+ });
+
+ checkScalarSpan(this, -1, countBIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'significantly_worse',
+ expectedEmojiColor: 'rgb(255, 0, 0)',
+ expectedTitle: 'significant regression'
+ });
+
+ checkScalarSpan(this, 1, countD, '+1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countD, '+1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countD, '+1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countD, '-1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countD, '-1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countD, '-1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedEmoji: ''
+ });
+ });
+
+ test('instantiate', function() {
+ checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMs,
+ '123.457 ms');
+ checkScalarSpan(this, 0, Unit.byName.normalizedPercentage, '0.0%');
+ checkScalarSpan(this, 1, Unit.byName.normalizedPercentage, '100.0%');
+ checkScalarSpan(this, -2560, Unit.byName.sizeInBytes, '-2.5 KiB');
+ });
+
+ test('instantiate_smallerIsBetter', function() {
+ checkScalarSpan(this, 45097156608, Unit.byName.sizeInBytes_smallerIsBetter,
+ '42.0 GiB');
+ checkScalarSpan(this, 0, Unit.byName.energyInJoules_smallerIsBetter,
+ '0.000 J');
+ checkScalarSpan(this, -0.25, Unit.byName.unitlessNumber_smallerIsBetter,
+ '-0.250');
+ });
+
+ test('instantiate_biggerIsBetter', function() {
+ checkScalarSpan(this, 0.07, Unit.byName.powerInWatts_smallerIsBetter,
+ '70.000 mW');
+ checkScalarSpan(this, 0, Unit.byName.timeStampInMs_biggerIsBetter,
+ '0.000 ms');
+ checkScalarSpan(this, -0.003,
+ Unit.byName.normalizedPercentage_biggerIsBetter, '-0.3%');
+ });
+
+ test('instantiate_delta', function() {
+ checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMsDelta,
+ '+123.457 ms');
+ checkScalarSpan(this, 0, Unit.byName.normalizedPercentageDelta,
+ '\u00B10.0%');
+ checkScalarSpan(this, -2560, Unit.byName.sizeInBytesDelta,
+ '-2.5 KiB');
+ });
+
+ test('instantiate_delta_smallerIsBetter', function() {
+ checkScalarSpan(this, 45097156608,
+ Unit.byName.sizeInBytesDelta_smallerIsBetter, '+42.0 GiB',
+ {expectedColor: 'rgb(255, 0, 0)'});
+ checkScalarSpan(this, 0, Unit.byName.energyInJoulesDelta_smallerIsBetter,
+ '\u00B10.000 J');
+ checkScalarSpan(this, -0.25,
+ Unit.byName.unitlessNumberDelta_smallerIsBetter, '-0.250',
+ {expectedColor: 'rgb(0, 128, 0)'});
+ });
+
+ test('instantiate_delta_biggerIsBetter', function() {
+ checkScalarSpan(this, 0.07, Unit.byName.powerInWattsDelta_biggerIsBetter,
+ '+70.000 mW', {expectedColor: 'rgb(0, 128, 0)'});
+ checkScalarSpan(this, 0, Unit.byName.timeStampInMsDelta_biggerIsBetter,
+ '\u00B10.000 ms');
+ checkScalarSpan(this, -0.003,
+ Unit.byName.normalizedPercentageDelta_biggerIsBetter, '-0.3%',
+ {expectedColor: 'rgb(255, 0, 0)'});
+ });
+
+ test('createScalarSpan', function() {
+ // No config.
+ let span = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.powerInWatts, 3.14));
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '3.140 W');
+ assert.strictEqual(span.ownerDocument, document);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 3.14);
+ assert.strictEqual(span.unit, Unit.byName.powerInWatts);
+ assert.isUndefined(span.context);
+ assert.isUndefined(span.customContextRange);
+ assert.isUndefined(span.warning);
+ assert.isFalse(span.leftAlign);
+ this.addHTMLOutput(span);
+
+ // Inline.
+ const div = document.createElement('div');
+ this.addHTMLOutput(div);
+ const inlineSpan = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.powerInWatts, 3.14),
+ {inline: true});
+ assert.strictEqual(Polymer.dom(inlineSpan.$.content).textContent,
+ '3.140 W');
+ assert.strictEqual(inlineSpan.ownerDocument, document);
+ assert.strictEqual(inlineSpan.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(inlineSpan.value, 3.14);
+ assert.strictEqual(inlineSpan.unit, Unit.byName.powerInWatts);
+ assert.isUndefined(inlineSpan.context);
+ assert.isUndefined(inlineSpan.customContextRange);
+ assert.isUndefined(inlineSpan.warning);
+ assert.isFalse(inlineSpan.leftAlign);
+ div.appendChild(document.createTextNode('prefix '));
+ div.appendChild(inlineSpan);
+ div.appendChild(document.createTextNode(' suffix'));
+ assert.isBelow(inlineSpan.getBoundingClientRect().width,
+ span.getBoundingClientRect().width);
+
+ // Custom owner document and right align.
+ span = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.energyInJoules, 2.72),
+ { ownerDocument: THIS_DOC, leftAlign: true});
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '2.720 J');
+ assert.strictEqual(span.ownerDocument, THIS_DOC);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 2.72);
+ assert.strictEqual(span.unit, Unit.byName.energyInJoules);
+ assert.isUndefined(span.context);
+ assert.isUndefined(span.customContextRange);
+ assert.isUndefined(span.warning);
+ assert.isTrue(span.leftAlign);
+ this.addHTMLOutput(span);
+
+ // Unit and sparkline set via config.
+ span = tr.v.ui.createScalarSpan(1.62, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: tr.b.math.Range.fromExplicitRange(0, 3.24)
+ });
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '1.620 ms');
+ assert.strictEqual(span.ownerDocument, document);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 1.62);
+ assert.strictEqual(span.unit, Unit.byName.timeStampInMs);
+ assert.isUndefined(span.context);
+ assert.isTrue(tr.b.math.Range.fromExplicitRange(0, 3.24).equals(
+ span.customContextRange));
+ assert.isUndefined(span.warning);
+ assert.isFalse(span.leftAlign);
+ this.addHTMLOutput(span);
+
+ // Custom context.
+ span = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ 256 * 1024 * 1024), { context: {
+ unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI,
+ minimumFractionDigits: 2
+ } });
+ assert.strictEqual(
+ Polymer.dom(span.$.content).textContent, '+262,144.00 KiB');
+ assert.strictEqual(span.ownerDocument, document);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 256 * 1024 * 1024);
+ assert.strictEqual(span.unit, Unit.byName.sizeInBytesDelta_smallerIsBetter);
+ assert.deepEqual(span.context, {
+ unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI,
+ minimumFractionDigits: 2
+ });
+ assert.isUndefined(span.customContextRange);
+ assert.isUndefined(span.warning);
+ assert.isFalse(span.leftAlign);
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withWarning', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = 400000000;
+ span.unit = Unit.byName.sizeInBytes;
+ span.warning = 'There is a problem with this size';
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withCustomContextRange', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = new Scalar(Unit.byName.unitlessNumber, 0.99);
+ span.customContextRange = tr.b.math.Range.fromExplicitRange(0, 3);
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withRightAlign', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = new Scalar(Unit.byName.timeStampInMs, 5.777);
+ span.leftAlign = true;
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withContext', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = new Scalar(
+ Unit.byName.unitlessNumberDelta_smallerIsBetter, 42);
+ span.context = { maximumFractionDigits: 2 };
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '+42.00');
+ this.addHTMLOutput(span);
+ });
+
+ test('deltaAndNonDeltaHaveSimilarHeights', function() {
+ const spanA = document.createElement('tr-v-ui-scalar-span');
+ spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs);
+ checkSignificance(spanA, '');
+
+ const spanB = document.createElement('tr-v-ui-scalar-span');
+ spanB.setValueAndUnit(400, Unit.byName.timeDurationInMsDelta);
+ checkSignificance(spanB, '');
+
+ const spanC = document.createElement('tr-v-ui-scalar-span');
+ spanC.setValueAndUnit(
+ 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter);
+ spanC.significance = tr.b.math.Statistics.Significance.SIGNIFICANT;
+ checkSignificance(spanC, 'significantly_worse');
+
+ const spanD = document.createElement('tr-v-ui-scalar-span');
+ spanD.setValueAndUnit(
+ 400, Unit.byName.timeDurationInMsDelta_biggerIsBetter);
+ spanD.significance = tr.b.math.Statistics.Significance.SIGNIFICANT;
+ checkSignificance(spanD, 'significantly_better');
+
+ const spanE = document.createElement('tr-v-ui-scalar-span');
+ spanE.setValueAndUnit(
+ 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter);
+ spanE.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT;
+ checkSignificance(spanE, 'insignificant');
+
+ const overall = document.createElement('div');
+ overall.style.display = 'flex';
+ // These spans must be on separate lines so that Chrome has the option of
+ // making their heights different. The point of the test is that Chrome
+ // shouldn't have to make their heights different even when it could.
+ overall.style.flexDirection = 'column';
+ Polymer.dom(overall).appendChild(spanA);
+ Polymer.dom(overall).appendChild(spanB);
+ Polymer.dom(overall).appendChild(spanC);
+ Polymer.dom(overall).appendChild(spanD);
+ Polymer.dom(overall).appendChild(spanE);
+ this.addHTMLOutput(overall);
+
+ const expectedHeight = spanA.getBoundingClientRect().height;
+ assert.strictEqual(expectedHeight, spanB.getBoundingClientRect().height);
+ assert.strictEqual(expectedHeight, spanC.getBoundingClientRect().height);
+ assert.strictEqual(expectedHeight, spanD.getBoundingClientRect().height);
+ assert.strictEqual(expectedHeight, spanE.getBoundingClientRect().height);
+ });
+
+ test('warningAndNonWarningHaveSimilarHeights', function() {
+ const spanA = document.createElement('tr-v-ui-scalar-span');
+ spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs);
+
+ const spanB = document.createElement('tr-v-ui-scalar-span');
+ spanB.setValueAndUnit(400, Unit.byName.timeDurationInMs);
+ spanB.warning = 'There is a problem with this time';
+
+ const overall = document.createElement('div');
+ overall.style.display = 'flex';
+ // These spans must be on separate lines so that Chrome has the option of
+ // making their heights different. The point of the test is that Chrome
+ // shouldn't have to make their heights different even when it could.
+ overall.style.flexDirection = 'column';
+ Polymer.dom(overall).appendChild(spanA);
+ Polymer.dom(overall).appendChild(spanB);
+ this.addHTMLOutput(overall);
+
+ const rectA = spanA.getBoundingClientRect();
+ const rectB = spanB.getBoundingClientRect();
+ assert.strictEqual(rectA.height, rectB.height);
+ });
+
+ test('respectCurrentDisplayUnit', function() {
+ try {
+ Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ns;
+
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.setValueAndUnit(73, Unit.byName.timeStampInMs);
+ this.addHTMLOutput(span);
+
+ assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ns') > 0);
+ Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ms;
+ assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ms') > 0);
+ } finally {
+ Unit.reset();
+ }
+ });
+
+ function checkSparkline(span, expectation) {
+ tr.b.forceAllPendingTasksToRunForTest();
+ const sparklineEl = span.$.sparkline;
+ const computedStyle = getComputedStyle(sparklineEl);
+
+ const expectedDisplay = expectation.display || 'block';
+ assert.strictEqual(computedStyle.display, expectedDisplay);
+ if (expectedDisplay === 'none') {
+ // Test expectation sanity check.
+ assert.notProperty(expectation, 'left');
+ assert.notProperty(expectation, 'width');
+ assert.notProperty(expectation, 'classList');
+ return;
+ }
+
+ assert.closeTo(parseFloat(computedStyle.left), expectation.left, 0.1);
+ assert.closeTo(parseFloat(computedStyle.width), expectation.width, 0.1);
+ assert.sameMembers(Array.from(sparklineEl.classList),
+ expectation.classList || []);
+ }
+
+ test('customContextRange', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+
+ // No custom context range.
+ const span1 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs
+ });
+ Polymer.dom(div).appendChild(span1);
+ checkSparkline(span1, {display: 'none'});
+ const span2 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: undefined
+ });
+ Polymer.dom(div).appendChild(span2);
+ checkSparkline(span2, {display: 'none'});
+ const span3 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: new tr.b.math.Range() // Empty range.
+ });
+ Polymer.dom(div).appendChild(span3);
+ checkSparkline(span3, {display: 'none'});
+
+ const range = tr.b.math.Range.fromExplicitRange(-15, 15);
+
+ // Values inside custom context range.
+ const span4 = tr.v.ui.createScalarSpan(-15, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span4);
+ checkSparkline(span4, {left: 0, width: 51});
+ const span5 = tr.v.ui.createScalarSpan(-14, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span5);
+ checkSparkline(span5, {left: 3.33, width: 47.67});
+ const span6 = tr.v.ui.createScalarSpan(-10, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span6);
+ checkSparkline(span6, {left: 16.67, width: 34.33});
+ const span7 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span7);
+ checkSparkline(span7, {left: 50, width: 1});
+ const span8 = tr.v.ui.createScalarSpan(10, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span8);
+ checkSparkline(span8, {left: 50, width: 34.33, classList: ['positive']});
+ const span9 = tr.v.ui.createScalarSpan(14, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span9);
+ checkSparkline(span9, {left: 50, width: 47.67, classList: ['positive']});
+ const span10 = tr.v.ui.createScalarSpan(15, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span10);
+ checkSparkline(span10, {left: 50, width: 51, classList: ['positive']});
+
+ // Values outside custom context range.
+ const span11 = tr.v.ui.createScalarSpan(-20, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span11);
+ checkSparkline(span11, {left: 0, width: 51});
+ const span12 = tr.v.ui.createScalarSpan(20, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span12);
+ checkSparkline(span12, {left: 50, width: 51, classList: ['positive']});
+ });
+
+ test('emptyNumeric', function() {
+ assert.strictEqual(tr.v.ui.createScalarSpan(), '');
+ });
+
+ test('contextControllerChanges', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const s1 = tr.v.ui.createScalarSpan(10, {
+ unit: Unit.byName.powerInWatts
+ });
+ Polymer.dom(div).appendChild(s1);
+ checkSparkline(s1, {display: 'none'});
+
+ const s2 = tr.v.ui.createScalarSpan(20, {
+ unit: Unit.byName.powerInWatts,
+ contextGroup: 'A'
+ });
+ Polymer.dom(div).appendChild(s2);
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+
+ const s3 = tr.v.ui.createScalarSpan(30, {
+ unit: Unit.byName.powerInWatts,
+ contextGroup: 'A'
+ });
+ Polymer.dom(div).appendChild(s3);
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+
+ const s4 = tr.v.ui.createScalarSpan(40, {
+ unit: Unit.byName.powerInWatts,
+ contextGroup: 'B'
+ });
+ Polymer.dom(div).appendChild(s4);
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s3.contextGroup = 'B';
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s1.setAttribute('context-group', 'A');
+ checkSparkline(s1, {left: 0, width: 51, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s1.value = 50;
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 150);
+ checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s4.contextGroup = null;
+ checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ s1.customContextRange = undefined;
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ s4.value = 0;
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ div.removeChild(s1);
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ s1.contextGroup = 'B';
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ div.appendChild(s1);
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 61, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+
+ s1.removeAttribute('context-group');
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {display: 'none'});
+
+ s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 100);
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {left: 0, width: 51, classList: ['positive']});
+
+ s3.value = 0;
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {display: 'none'});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {left: 0, width: 51, classList: ['positive']});
+ });
+
+ test('deltaSparkline_noImprovementDirection', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const span1 = tr.v.ui.createScalarSpan(20971520, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span1);
+ const span2 = tr.v.ui.createScalarSpan(15728640, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span2);
+ const span3 = tr.v.ui.createScalarSpan(12582912, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span3);
+ const span4 = tr.v.ui.createScalarSpan(11534336, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span4);
+ const span5 = tr.v.ui.createScalarSpan(10485760, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span5);
+ const span6 = tr.v.ui.createScalarSpan(9437184, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span6);
+ const span7 = tr.v.ui.createScalarSpan(8388608, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span7);
+ const span8 = tr.v.ui.createScalarSpan(5242880, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span8);
+
+ // We must check the sparklines *after* all spans are appended because new
+ // values can change the context range.
+ checkSparkline(span1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(span2, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(span3, {left: 0, width: 61, classList: ['positive']});
+ checkSparkline(span4, {left: 0, width: 56, classList: ['positive']});
+ checkSparkline(span5, {left: 0, width: 51, classList: ['positive']});
+ checkSparkline(span6, {left: 0, width: 46, classList: ['positive']});
+ checkSparkline(span7, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(span8, {left: 0, width: 26, classList: ['positive']});
+ });
+
+ test('deltaSparkline_smallerIsBetter', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const span1 = tr.v.ui.createScalarSpan(5242880, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span1);
+ const span2 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span2);
+ const span3 = tr.v.ui.createScalarSpan(-3145728, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span3);
+ const span4 = tr.v.ui.createScalarSpan(-4194304, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span4);
+ const span5 = tr.v.ui.createScalarSpan(-5242880, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span5);
+ const span6 = tr.v.ui.createScalarSpan(-6291456, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span6);
+ const span7 = tr.v.ui.createScalarSpan(-7340032, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span7);
+ const span8 = tr.v.ui.createScalarSpan(-15728640, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span8);
+
+ // We must check the sparklines *after* all spans are appended because new
+ // values can change the context range.
+ checkSparkline(span1,
+ {left: 75, width: 26, classList: ['positive', 'worse']});
+ checkSparkline(span2, {left: 75, width: 1, classList: ['same']});
+ checkSparkline(span3, {left: 60, width: 16, classList: ['better']});
+ checkSparkline(span4, {left: 55, width: 21, classList: ['better']});
+ checkSparkline(span5, {left: 50, width: 26, classList: ['better']});
+ checkSparkline(span6, {left: 45, width: 31, classList: ['better']});
+ checkSparkline(span7, {left: 40, width: 36, classList: ['better']});
+ checkSparkline(span8, {left: 0, width: 76, classList: ['better']});
+ });
+
+ test('deltaSparkline_biggerIsBetter', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const span1 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span1);
+ const span2 = tr.v.ui.createScalarSpan(-5242880, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span2);
+ const span3 = tr.v.ui.createScalarSpan(-8388608, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span3);
+ const span4 = tr.v.ui.createScalarSpan(-9437184, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span4);
+ const span5 = tr.v.ui.createScalarSpan(-10485760, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span5);
+ const span6 = tr.v.ui.createScalarSpan(-11534336, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span6);
+ const span7 = tr.v.ui.createScalarSpan(-12582912, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span7);
+ const span8 = tr.v.ui.createScalarSpan(-20971520, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span8);
+
+ // We must check the sparklines *after* all spans are appended because new
+ // values can change the context range.
+ checkSparkline(span1, {left: 100, width: 1, classList: ['same']});
+ checkSparkline(span2, {left: 75, width: 26, classList: ['worse']});
+ checkSparkline(span3, {left: 60, width: 41, classList: ['worse']});
+ checkSparkline(span4, {left: 55, width: 46, classList: ['worse']});
+ checkSparkline(span5, {left: 50, width: 51, classList: ['worse']});
+ checkSparkline(span6, {left: 45, width: 56, classList: ['worse']});
+ checkSparkline(span7, {left: 40, width: 61, classList: ['worse']});
+ checkSparkline(span8, {left: 0, width: 101, classList: ['worse']});
+ });
+
+ test('classListChanges', function() {
+ const div = document.createElement('div');
+ div.style.width = '200px';
+ this.addHTMLOutput(div);
+
+ const span = tr.v.ui.createScalarSpan(10, {
+ unit: Unit.byName.energyInJoulesDelta_smallerIsBetter,
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-20, 20)
+ });
+ Polymer.dom(div).appendChild(span);
+
+ assert.sameMembers(Array.from(span.$.content.classList), ['worse']);
+ checkSignificance(span, 'significantly_worse');
+
+ span.significance = tr.b.math.Statistics.Significance.DONT_CARE;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'worse']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['worse']);
+ checkSignificance(span, '');
+
+ span.value = -5;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), ['better']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['better']);
+ checkSignificance(span, '');
+
+ span.unit = Unit.byName.energyInJoules;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), []);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+
+ span.value = 20;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+
+ span.unit = Unit.byName.energyInJoulesDelta_biggerIsBetter;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'better']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['better']);
+ checkSignificance(span, '');
+
+ span.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'better']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['better']);
+ checkSignificance(span, 'insignificant');
+
+ span.unit = Unit.byName.energyInJoulesDelta_smallerIsBetter;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'worse']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['worse']);
+ checkSignificance(span, 'insignificant');
+
+ span.unit = Unit.byName.energyInJoulesDelta;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+
+ span.value = 0;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), []);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+ });
+
+ test('sparkline_uncentered', function() {
+ const div = document.createElement('div');
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ });
+
+ test('sparkline_centered', function() {
+ const div = document.createElement('div');
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ });
+
+ timedPerfTest('memory_scalar_spans', function() {
+ tr.v.ui.createScalarSpan(EXAMPLE_MEMORY_NUMERIC, {
+ context: EXAMPLE_MEMORY_FORMATTING_CONTEXT,
+ inline: true,
+ });
+ }, {
+ iterations: 1000,
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md
new file mode 100644
index 00000000000..3846af6ee0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md
@@ -0,0 +1,78 @@
+This document describes the Google Analytics metrics reported by results.html.
+
+Measures are recorded by the performance web API, and are visible in the User
+Timing track in the devtools Performance timeline.
+
+Instant events are recorded using console.timestamp(), and are visible as orange
+ticks at the top of the devtools Performance timeline.
+
+Both measures and instant events are recorded as Events in Google Analytics.
+[Access these metrics here](https://analytics.google.com/analytics/web/#embed/report-home/a98760012w145165698p149871853/) if you have been granted access.
+
+ * histogram-set-controls
+ * `alpha` measures response latency of changing the statistical significance
+ threshold, alpha.
+ * `hideOverviewCharts` measures response latency of hiding all overview
+ charts.
+ * `referenceColumn` measures response latency of changing the reference
+ column.
+ * `search` measures response latency of changing the search query to filter
+ rows.
+ * `showAll` measures response latency of toggling showing all rows versus
+ source Histograms only.
+ * `showOverviewCharts` measures response latency of showing all overview
+ charts.
+ * `statistic` measures response latency of changing the statistic that is
+ displayed in histogram-set-table-cells.
+ * HistogramSetLocation
+ * `onPopState` measures response latency of the browser back button.
+ * `pushState` measures latency of serializing the view state and pushing it
+ to the HTML5 history API. This happens automatically whenever any part of
+ the ViewState is updated.
+ * histogram-set-table
+ * `columnCount` instant event contains the number of columns, recorded when the
+ table is built.
+ * `nameColumnConstrained` instant event recorded when the name column width
+ is constrained.
+ * `nameColumnUnconstrained` instant event recorded when the name column width
+ is unconstrained.
+ * `rootRowCount` instant event contains the number of root rows, recorded
+ whenever it changes or the table is built.
+ * `rowCollapsed` instant event recorded whenever a row is collapsed.
+ * `rowExpanded` instant event recorded whenever a row is expanded.
+ * `selectHistogramNames` instant event recorded whenever a breakdown related
+ histogram name link is clicked.
+ * `sortColumn` instant event recorded whenever the user changes the sort
+ column.
+ * histogram-set-table-cell
+ * `close` instant event recorded when the cell is closed.
+ * `open` instant event recorded when the cell is opened.
+ * histogram-set-table-name-cell
+ * `closeHistograms` instant event recorded when the user clicks the button to
+ close all histogram-set-table-cells in the row.
+ * `hideOverview` instant event recorded when the user clicks the button to
+ hide the overview line charts for the row.
+ * `openHistograms` instant event recorded when the user clicks the button to
+ open all histogram-set-table-cells in the row.
+ * `showOverview` instant event recorded when the user clicks the button to
+ show the overview line charts for the row.
+ * histogram-set-view
+ * `build` measures latency to find source Histograms, collect parameters,
+ configure the controls and build the table. Does not include parsing
+ Histograms from json.
+ * `sourceHistograms` measures latency to find source Histograms in the
+ relationship graphical model.
+ * `collectParameters` measures latency to collect display labels, statistic
+ names, and possible groupings.
+ * `export{Raw,Merged}{CSV,JSON}` measures latency to download a CSV/JSON file
+ of raw/merged Histograms.
+ * histogram-span
+ * `brushBins` instant event recorded when the user finishes brushing bins.
+ * `clearBrushedBins` instant event recorded when the user clears brushed
+ bins.
+ * `mergeSampleDiagnostics` measures latency of displaying the table of merged
+ sample diagnostics.
+ * `splitSampleDiagnostics` measures latency of displaying the table of
+ unmerged sample diagnostics.
+ * HistogramParameterCollector
+ * `maxSampleCount` instant event records maximum Histogram.numValues
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html
new file mode 100644
index 00000000000..de68e0cfa57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 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 file only depends on diagnostic_span.html, but it must be imported from
+ diagnostic_span.html.
+ Fortunately, this file is only imported from diagnostic_span.html, so it can
+ just not import anything.
+-->
+
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-unmergeable-diagnostic-set-span">
+</dom-module>
+
+<script>
+'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 {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html
new file mode 100644
index 00000000000..29e48966fc0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const event = tr.c.TestUtils.newSliceEx({
+ title: 'event',
+ start: 0,
+ duration: 1,
+ });
+ event.parentContainer = {
+ sliceGroup: {
+ stableId: 'fake_thread',
+ slices: [event],
+ },
+ };
+ const diagnostics = new tr.v.d.UnmergeableDiagnosticSet([
+ new tr.v.d.GenericSet(['generic diagnostic']),
+ new tr.v.d.RelatedNameMap(),
+ new tr.v.d.RelatedEventSet([
+ event,
+ ]),
+ ]);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostics);
+ assert.strictEqual('TR-V-UI-UNMERGEABLE-DIAGNOSTIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html
new file mode 100644
index 00000000000..0adeaf55e3c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html
@@ -0,0 +1,410 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+<link rel="import" href="/tracing/value/ui/metrics_visualization.html">
+<link rel="import" href="/tracing/value/ui/raster_visualization.html">
+<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 id="data_error" class="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 id="metricsVisualization" class="sample">
+ </tr-v-ui-metrics-visualization>
+ </div>
+ </template>
+</dom-module>
+<script>
+'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() {
+ // from earliest to latest
+ this.orderedBenchmarks_ = [];
+ // aggregate/page -> benchmark -> metric -> statistics/submetrics
+ 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()) {
+ // process aggregate data
+ 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]]));
+ }
+ }
+ }
+
+ // process data per page
+ 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);
+
+ // retrieving submetric _ta
+ 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) {
+ // pages are ordered from smallest to largest by their total
+ // values for the first benchmark
+ 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({});
+ }
+ 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);
+ },
+
+ // Get data for breakdown of a main step
+ processSubmetricsData_(chartName) {
+ // page -> metric -> submetric ->
+ // array of submetrics across all benchmarks
+ 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,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html
new file mode 100644
index 00000000000..1199d8ed731
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 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.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function getHistogram(name) {
+ const samples = [];
+ for (let i = 0; i < 5; ++i) {
+ const total = Math.random();
+ const values = {};
+ values[name + 'a'] = total / 2.0;
+ values[name + 'b'] = total / 4.0;
+ values[name + 'c'] = total / 4.0;
+ samples.push({
+ value: total,
+ diagnostics: new Map([
+ [
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(Date.now()),
+ ], [
+ 'breakdown', tr.v.d.Breakdown.fromDict({values}),
+ ],
+ ]),
+ });
+ }
+ return tr.v.Histogram.create(name, tr.b.Unit.byName.count, samples);
+ }
+
+ function getHistogramSet(displayLabel, story, containsRasterStart = true) {
+ const histograms = new tr.v.HistogramSet();
+ let metrics = [];
+ for (const category of tr.v.ui.METRICS.values()) {
+ metrics = metrics.concat(category);
+ }
+ for (const metric of metrics) {
+ histograms.addHistogram(getHistogram(metric));
+ }
+
+ if (containsRasterStart) {
+ histograms.addHistogram(
+ getHistogram('pipeline:begin_frame_to_raster_start'));
+ histograms.addHistogram(
+ getHistogram('pipeline:begin_frame_to_raster_end'));
+ }
+ histograms.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet([displayLabel]));
+ histograms.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet([story]));
+ return histograms;
+ }
+
+ test('instantiate', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'test.com');
+
+ const histograms2 = getHistogramSet('Run 2', 'test.com');
+ histograms.importDicts(histograms2.asDicts());
+
+ const histograms3 = getHistogramSet('Run 1', 'abc.com');
+ histograms.importDicts(histograms3.asDicts());
+
+ const histograms4 = getHistogramSet('Run 2', 'abc.com');
+ histograms.importDicts(histograms4.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+
+ test('instantiateWithRepeat', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'repeat.com');
+ const histograms2 = getHistogramSet('Run 1', 'repeat.com');
+ histograms.importDicts(histograms2.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+
+ test('instantiateWithoutRasterTasks', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'test.com', false);
+
+ const histograms2 = getHistogramSet('Run 2', 'test.com', false);
+ histograms.importDicts(histograms2.asDicts());
+
+ const histograms3 = getHistogramSet('Run 1', 'abc.com');
+ histograms.importDicts(histograms3.asDicts());
+
+ const histograms4 = getHistogramSet('Run 2', 'abc.com');
+ histograms.importDicts(histograms4.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+
+ test('instantiateWithDifferentStorySets', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'test.com');
+
+ const histograms2 = getHistogramSet('Run 1', 'abc.com');
+ histograms.importDicts(histograms2.asDicts());
+
+ const histograms3 = getHistogramSet('Run 2', 'abc.com');
+ histograms.importDicts(histograms3.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing_build/__init__.py b/chromium/third_party/catapult/tracing/tracing_build/__init__.py
new file mode 100644
index 00000000000..22060c5dca2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) 2012 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 tracing_project
+tracing_project.UpdateSysPathIfNeeded()
diff --git a/chromium/third_party/catapult/tracing/tracing_build/check_common.py b/chromium/third_party/catapult/tracing/tracing_build/check_common.py
new file mode 100644
index 00000000000..28dd49edc2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/check_common.py
@@ -0,0 +1,90 @@
+# Copyright (c) 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.
+
+import os
+
+import tracing_project
+
+
+FILE_GROUPS = ["tracing_css_files",
+ "tracing_js_html_files",
+ "tracing_img_files"]
+
+
+def GetFileGroupFromFileName(filename):
+ extension = os.path.splitext(filename)[1]
+ return {
+ '.css': 'tracing_css_files',
+ '.html': 'tracing_js_html_files',
+ '.js': 'tracing_js_html_files',
+ '.png': 'tracing_img_files'
+ }[extension]
+
+
+def CheckListedFilesSorted(src_file, group_name, listed_files):
+ sorted_files = sorted(listed_files)
+ if sorted_files != listed_files:
+ mismatch = ''
+ for i, f in enumerate(listed_files):
+ if f != sorted_files[i]:
+ mismatch = f
+ break
+ what_is = ' ' + '\n '.join(listed_files)
+ what_should_be = ' ' + '\n '.join(sorted_files)
+ return '''In group {0} from file {1}, filenames aren't sorted.
+
+First mismatch:
+ {2}
+
+Current listing:
+{3}
+
+Correct listing:
+{4}\n\n'''.format(group_name, src_file, mismatch, what_is, what_should_be)
+ else:
+ return ''
+
+
+def GetKnownFiles():
+ project = tracing_project.TracingProject()
+
+ vulcanizer = project.CreateVulcanizer()
+ m = vulcanizer.loader.LoadModule(
+ module_name='tracing.ui.extras.about_tracing.about_tracing')
+ absolute_filenames = m.GetAllDependentFilenamesRecursive(
+ include_raw_scripts=False)
+
+ return list(set([os.path.relpath(f, project.tracing_root_path)
+ for f in absolute_filenames]))
+
+
+def CheckCommon(file_name, listed_files):
+ known_files = GetKnownFiles()
+ u = set(listed_files).union(set(known_files))
+ i = set(listed_files).intersection(set(known_files))
+ diff = list(u - i)
+
+ if len(diff) == 0:
+ return ''
+
+ error = 'Entries in ' + file_name + ' do not match files on disk:\n'
+ in_file_only = list(set(listed_files) - set(known_files))
+ in_known_only = list(set(known_files) - set(listed_files))
+
+ if len(in_file_only) > 0:
+ error += ' In file only:\n ' + '\n '.join(sorted(in_file_only))
+ if len(in_known_only) > 0:
+ if len(in_file_only) > 0:
+ error += '\n\n'
+ error += ' On disk only:\n ' + '\n '.join(sorted(in_known_only))
+
+ if in_file_only:
+ error += (
+ '\n\n'
+ ' Note: only files actually used in about:tracing should\n'
+ ' be listed in the build files. Try running \n'
+ ' tracing/bin/update_gni\n'
+ ' to update the files automatically.')
+
+ return error
diff --git a/chromium/third_party/catapult/tracing/tracing_build/check_common_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/check_common_unittest.py
new file mode 100644
index 00000000000..b3af7b6fb68
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/check_common_unittest.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2014 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 tracing_build import check_common
+
+
+class CheckCommonUnitTest(unittest.TestCase):
+
+ def testFilesSorted(self):
+ error = check_common.CheckListedFilesSorted('foo.gyp', 'tracing_pdf_files',
+ ['/dir/file.pdf',
+ '/dir/another_file.pdf'])
+ expected_error = '''In group tracing_pdf_files from file foo.gyp,\
+ filenames aren't sorted.
+
+First mismatch:
+ /dir/file.pdf
+
+Current listing:
+ /dir/file.pdf
+ /dir/another_file.pdf
+
+Correct listing:
+ /dir/another_file.pdf
+ /dir/file.pdf\n\n'''
+ assert error == expected_error
diff --git a/chromium/third_party/catapult/tracing/tracing_build/check_gni.py b/chromium/third_party/catapult/tracing/tracing_build/check_gni.py
new file mode 100644
index 00000000000..af68ca4e667
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/check_gni.py
@@ -0,0 +1,29 @@
+# Copyright (c) 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.
+
+import re
+import os
+
+from tracing_build import check_common
+
+_TRACING_BUILD = os.path.abspath(os.path.dirname(__file__))
+GNI_FILE = os.path.join(_TRACING_BUILD, '..', 'trace_viewer.gni')
+
+
+def GniCheck():
+ gni = open(GNI_FILE).read()
+ listed_files = []
+ error = ''
+ for group in check_common.FILE_GROUPS:
+ filenames = re.search(r'\n%s = \[([^\]]+)\]' % group, gni).groups(1)[0]
+ filenames = re.findall(r'"([^"]+)"', filenames)
+ filenames = [os.path.normpath(filename) for filename in filenames]
+ error += check_common.CheckListedFilesSorted(GNI_FILE, group, filenames)
+ listed_files.extend(filenames)
+
+ return error + check_common.CheckCommon(GNI_FILE, listed_files)
+
+
+if __name__ == '__main__':
+ print GniCheck()
diff --git a/chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents.py b/chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents.py
new file mode 100644
index 00000000000..e5aece0871f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2014 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 codecs
+import argparse
+import os
+import sys
+
+import py_vulcanize
+
+import tracing_project
+
+
+def Main(args):
+ parser = argparse.ArgumentParser(usage='%(prog)s --outdir=<directory>')
+ parser.add_argument('--outdir', dest='out_dir',
+ help='Where to place generated content')
+ parser.add_argument('--no-min', default=False, action='store_true',
+ help='Skip minification')
+ args = parser.parse_args(args)
+
+ if not args.out_dir:
+ sys.stderr.write('ERROR: Must specify --outdir=<directory>')
+ parser.print_help()
+ return 1
+
+ names = ['tracing.ui.extras.about_tracing.about_tracing']
+ project = tracing_project.TracingProject()
+
+ vulcanizer = project.CreateVulcanizer()
+ load_sequence = vulcanizer.CalcLoadSequenceForModuleNames(names)
+
+ olddir = os.getcwd()
+ try:
+ if not os.path.exists(args.out_dir):
+ os.makedirs(args.out_dir)
+ o = codecs.open(os.path.join(args.out_dir, 'about_tracing.html'), 'w',
+ encoding='utf-8')
+ try:
+ py_vulcanize.GenerateStandaloneHTMLToFile(
+ o,
+ load_sequence,
+ title='chrome://tracing',
+ flattened_js_url='tracing.js',
+ minify=not args.no_min)
+ except py_vulcanize.module.DepsException as ex:
+ sys.stderr.write('Error: %s\n\n' % str(ex))
+ return 255
+ o.close()
+
+ o = codecs.open(os.path.join(args.out_dir, 'about_tracing.js'), 'w',
+ encoding='utf-8')
+ assert o.encoding == 'utf-8'
+ py_vulcanize.GenerateJSToFile(
+ o,
+ load_sequence,
+ use_include_tags_for_scripts=False,
+ dir_for_include_tag_root=args.out_dir,
+ minify=not args.no_min)
+ o.close()
+
+ finally:
+ os.chdir(olddir)
+
+ return 0
diff --git a/chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents_unittest.py
new file mode 100644
index 00000000000..7ebe3ddc861
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/generate_about_tracing_contents_unittest.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2014 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 shutil
+import sys
+import tempfile
+
+if sys.version_info < (3,):
+ from tracing_build import generate_about_tracing_contents
+
+
+@unittest.skipIf(sys.version_info >= (3,),
+ 'py_vulcanize is not ported to python3')
+class GenerateAboutTracingContentsUnittTest(unittest.TestCase):
+
+ def testSmoke(self):
+ try:
+ tmpdir = tempfile.mkdtemp()
+ res = generate_about_tracing_contents.Main(['--outdir', tmpdir])
+ assert res == 0
+ finally:
+ shutil.rmtree(tmpdir)
diff --git a/chromium/third_party/catapult/tracing/tracing_build/histograms_viewer.html b/chromium/third_party/catapult/tracing/tracing_build/histograms_viewer.html
new file mode 100644
index 00000000000..8e86bdc695e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/histograms_viewer.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<script>
+'use strict';
+window.ga = window.ga || function() {
+ ga.q = ga.q || [];
+ ga.q.push(arguments);
+};
+ga.l = new Date();
+ga('create', 'UA-98760012-1', 'auto');
+
+// Supporting file: URIs requires disabling protocol checking, cookie storage
+// checking, and history checking.
+ga('set', 'checkProtocolTask', null);
+ga('set', 'checkStorageTask', null);
+ga('set', 'historyImportTask', null);
+
+(function() {
+ // Write this script tag at runtime instead of in HTML in order to bypass the
+ // vulcanizer.
+ const script = document.createElement('script');
+ script.src = 'https://www.google-analytics.com/analytics.js';
+ script.type = 'text/javascript';
+ script.async = true;
+ document.head.appendChild(script);
+})();
+</script>
+
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<div id="loading">Loading framework...</div>
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/value/histogram_importer.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_location.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<script>
+'use strict';
+(function() {
+ ga('send', {
+ hitType: 'pageview',
+ page: '/results.html',
+ });
+
+ const domContentLoadedMark = tr.b.Timing.mark('results2', 'domContentLoaded');
+ let importer = new tr.v.HistogramImporter(document.getElementById('loading'));
+
+ document.addEventListener('DOMContentLoaded', async() => {
+ domContentLoadedMark.end();
+
+ const jsonData = document.getElementById('histogram-json-data');
+ const view = document.getElementById('histograms');
+ const locus = new tr.v.ui.HistogramSetLocation();
+
+ const data = jsonData.childNodes[0].textContent;
+ await importer.importHistograms(data, view);
+ await locus.build(view.viewState);
+
+ // Free the objects and DOM referenced by the importer.
+ document.body.removeChild(jsonData);
+ importer = undefined;
+ });
+})();
+</script>
+
+<tr-v-ui-histogram-set-view id="histograms" style="display: none;"></tr-v-ui-histogram-set-view>
diff --git a/chromium/third_party/catapult/tracing/tracing_build/html2trace.py b/chromium/third_party/catapult/tracing/tracing_build/html2trace.py
new file mode 100644
index 00000000000..0dfa63fa2c4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/html2trace.py
@@ -0,0 +1,106 @@
+# 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 base64
+import gzip
+import io
+import json
+import re
+
+
+GZIP_HEADER_BYTES = b'\x1f\x8b'
+
+
+# Regular expressions for matching the beginning and end of trace data in HTML
+# traces. See tracing/extras/importer/trace2html_importer.html.
+TRACE_DATA_START_LINE_RE = re.compile(
+ r'^<\s*script id="viewer-data" type="(application\/json|text\/plain)">\r?$')
+TRACE_DATA_END_LINE_RE = re.compile(r'^<\/\s*script>\r?$')
+
+
+def IsHTMLTrace(trace_file_handle):
+ trace_file_handle.seek(0)
+ for line in trace_file_handle:
+ line = line.strip()
+ if not line:
+ continue
+ return line == '<!DOCTYPE html>'
+
+
+def CopyTraceDataFromHTMLFilePath(html_file_handle, trace_path,
+ gzipped_output=False):
+ """Copies trace data from an existing HTML file into new trace file(s).
+
+ If |html_file_handle| doesn't contain any trace data blocks, this function
+ throws an exception. If |html_file_handle| contains more than one trace data
+ block, the first block will be extracted into |trace_path| and the rest will
+ be extracted into separate files |trace_path|.1, |trace_path|.2, etc.
+
+ The contents of each trace data block is decoded and, if |gzipped_output| is
+ false, inflated before it's stored in a trace file.
+
+ This function returns a list of paths of the saved trace files ([|trace_path|,
+ |trace_path|.1, |trace_path|.2, ...]).
+ """
+ trace_data_list = _ExtractTraceDataFromHTMLFile(html_file_handle,
+ unzip_data=not gzipped_output)
+ saved_paths = []
+ for i, trace_data in enumerate(trace_data_list):
+ saved_path = trace_path if i == 0 else '%s.%d' % (trace_path, i)
+ saved_paths.append(saved_path)
+ with open(saved_path, 'wb' if gzipped_output else 'w') as trace_file:
+ trace_file.write(trace_data.read())
+ return saved_paths
+
+
+def ReadTracesFromHTMLFile(file_handle):
+ """Returns a list of inflated JSON traces extracted from an HTML file."""
+ return [json.load(t) for t in _ExtractTraceDataFromHTMLFile(file_handle)]
+
+
+def _ExtractTraceDataFromHTMLFile(html_file_handle, unzip_data=True):
+ assert IsHTMLTrace(html_file_handle)
+ html_file_handle.seek(0)
+ lines = html_file_handle.readlines()
+
+ start_indices = [i for i in range(len(lines))
+ if TRACE_DATA_START_LINE_RE.match(lines[i])]
+ if not start_indices:
+ raise Exception('File %r does not contain trace data')
+
+ decoded_data_list = []
+ for start_index in start_indices:
+ end_index = next(i for i in range(start_index + 1, len(lines))
+ if TRACE_DATA_END_LINE_RE.match(lines[i]))
+ encoded_data = '\n'.join(lines[start_index + 1:end_index]).strip()
+ decoded_data_list.append(io.BytesIO(base64.b64decode(encoded_data)))
+
+ if unzip_data:
+ return map(_UnzipFileIfNecessary, decoded_data_list)
+ else:
+ return map(_ZipFileIfNecessary, decoded_data_list)
+
+
+def _UnzipFileIfNecessary(original_file):
+ if _IsFileZipped(original_file):
+ return gzip.GzipFile(fileobj=original_file)
+ else:
+ return original_file
+
+
+def _ZipFileIfNecessary(original_file):
+ if _IsFileZipped(original_file):
+ return original_file
+ else:
+ zipped_file = io.BytesIO()
+ with gzip.GzipFile(fileobj=zipped_file, mode='wb') as gzip_wrapper:
+ gzip_wrapper.write(original_file.read())
+ zipped_file.seek(0)
+ return zipped_file
+
+
+def _IsFileZipped(f):
+ is_gzipped = f.read(len(GZIP_HEADER_BYTES)) == GZIP_HEADER_BYTES
+ f.seek(0)
+ return is_gzipped
diff --git a/chromium/third_party/catapult/tracing/tracing_build/merge_traces.py b/chromium/third_party/catapult/tracing/tracing_build/merge_traces.py
new file mode 100644
index 00000000000..127e4db7ebb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/merge_traces.py
@@ -0,0 +1,687 @@
+# 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.
+
+from __future__ import print_function
+
+import argparse
+import codecs
+import collections
+import gzip
+import itertools
+import json
+import logging
+import os
+import sys
+
+# Add tracing/ to the path.
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+from tracing_build import html2trace, trace2html
+
+
+try:
+ StringTypes = basestring
+except NameError:
+ StringTypes = str
+
+
+GZIP_FILENAME_SUFFIX = '.gz'
+HTML_FILENAME_SUFFIX = '.html'
+
+
+# Relevant trace event phases. See
+# https://code.google.com/p/chromium/codesearch#chromium/src/base/trace_event/common/trace_event_common.h.
+METADATA_PHASE = 'M'
+MEMORY_DUMP_PHASE = 'v'
+BEGIN_PHASE = 'B'
+END_PHASE = 'E'
+CLOCK_SYNC_EVENT_PHASE = 'c'
+
+
+# Minimum gap between two consecutive merged traces in microseconds.
+MIN_TRACE_GAP_IN_US = 1000000
+
+
+# Rule for matching IDs in an IdMap. For a given level, |match| should be a
+# named tuple class where its fields determine the importance of |entry._items|
+# for the purposes of matching pairs of IdMap entries.
+IdMapLevel = collections.namedtuple('IdMapLevel', ['name', 'match'])
+
+
+class IdMap(object):
+ """Abstract class for merging and mapping IDs from multiple sources."""
+
+ # Sub-classes must provide a tuple of |IdMapLevel| objects.
+ LEVELS = NotImplemented
+
+ def __init__(self, depth=0):
+ assert 0 <= depth <= len(self.LEVELS)
+ self._depth = depth
+
+ if depth > 0:
+ # Non-root node.
+ self._canonical_id = None
+ self._items = collections.defaultdict(set)
+ self._sources = set()
+
+ if depth < len(self.LEVELS):
+ # Non-leaf node.
+ self._entry_map = {} # (Source, Entry ID) -> Entry.
+
+ @property
+ def max_mapped_id(self):
+ """The maximum mapped ID of this map's entries."""
+ if not self._entry_map:
+ return 0
+ return max(e._canonical_id for e in self._entry_map.values())
+
+ def AddEntry(self, source, path, **items):
+ """Add a source-specific entry path to the map.
+
+ Args:
+ source: The source of the entry (e.g. trace filename).
+ path: A path of source-specific entry IDs in the map (e.g. [PID, TID]).
+ **items: Dictionary of items (or sets of items) to be appended to the
+ target entry's |_items|.
+ """
+ if path:
+ return self._GetSubMapEntry(source, path[0]).AddEntry(source, path[1:],
+ **items)
+ assert 'id' not in items # ID is set according to the path.
+ for key, value in items.items():
+ value_set = self._items[key]
+ if (isinstance(value, collections.Iterable) and
+ not isinstance(value, StringTypes)):
+ value_set.update(value)
+ else:
+ value_set.add(value)
+
+ def MapEntry(self, source, path):
+ """Map an source-specific entry ID path to a canonical entry ID path.
+
+ Args:
+ source: The source of the entry (e.g. trace filename).
+ path: A path of source-specific entry IDs in the map (e.g. [PID, TID]).
+
+ Returns:
+ A path of canonical entry IDs in the map to which the provided path of
+ source-specific entry IDs is mapped.
+ """
+ if not path:
+ return ()
+ entry = self._entry_map[(source, path[0])]
+ return (entry._canonical_id,) + entry.MapEntry(source, path[1:])
+
+ def MergeEntries(self):
+ """Recursively merge the entries in this map.
+
+ Example: Suppose that the following entries were added to the map:
+
+ map.AddEntry(source='trace_A.json', path=[10], name='Browser')
+ map.AddEntry(source='trace_A.json', path=[20], name='Renderer')
+ map.AddEntry(source='trace_B.json', path=[30], name='Browser')
+
+ Before merging, |map._entry_map| will contain three separate items:
+
+ ('trace_A.json', 10) -> IdMap(_items={id: {10}, name: {'Browser'}},
+ _sources={'trace_A.json'})
+ ('trace_A.json', 20) -> IdMap(_items={id: {20}, name: {'Renderer'}},
+ _sources={'trace_A.json'})
+ ('trace_B.json', 30) -> IdMap(_items={id: {30}, name: {'Browser'}},
+ _sources={'trace_B.json'})
+
+ Since the first two entries come from the same source, they cannot be
+ merged. On the other hand, the third entry could be merged with either of
+ the first two. Since it has a common name with the first entry, it will be
+ merged with it in this method:
+
+ ('trace_A.json', 10) -> IdMap(_items={id: {10, 30}, name: {'Browser'}},
+ _sources={'trace_A.json', 'trace_B.json'})
+ ('trace_A.json', 20) -> IdMap(_items={id: {20}, name: {Renderer}},
+ _sources={'trace_A.json'})
+ ('trace_B.json', 30) -> <same IdMap as ('trace_A.json', 10)>
+
+ Pairs of entries will be merged in a descending order of sizes of
+ pair-wise intersections of |entry._items| until there are no two entries
+ such that (1) they have at least one value in |entry._items| in common and
+ (2) they are mergeable (i.e. have no common source). Afterwards, unique IDs
+ are assigned to the resulting "canonical" entries and their sub-entries are
+ merged recursively.
+ """
+ # pylint: disable=unsubscriptable-object
+ if self._depth == len(self.LEVELS):
+ return
+
+ logging.debug('Merging %r entries in %s...', self.LEVELS[self._depth].name,
+ self)
+
+ canonical_entries = self._CanonicalizeEntries()
+ self._AssignIdsToCanonicalEntries(canonical_entries)
+
+ for entry in canonical_entries:
+ entry.MergeEntries()
+
+ def _GetSubMapEntry(self, source, entry_id):
+ full_id = (source, entry_id)
+ entry = self._entry_map.get(full_id)
+ if entry is None:
+ entry = type(self)(self._depth + 1)
+ entry._sources.add(source)
+ entry._items['id'].add(entry_id)
+ self._entry_map[full_id] = entry
+ return entry
+
+ def _CalculateUnmergeableMapFromEntrySources(self):
+ entry_ids_by_source = collections.defaultdict(set)
+ for entry_id, entry in self._entry_map.items():
+ for source in entry._sources:
+ entry_ids_by_source[source].add(entry_id)
+
+ unmergeable_map = collections.defaultdict(set)
+ for unmergeable_set in entry_ids_by_source.values():
+ for entry_id in unmergeable_set:
+ unmergeable_map[entry_id].update(unmergeable_set - {entry_id})
+
+ return unmergeable_map
+
+ def _IsMergeableWith(self, other):
+ return self._sources.isdisjoint(other._sources)
+
+ def _GetMatch(self, other):
+ # pylint: disable=unsubscriptable-object
+ match_cls = self.LEVELS[self._depth - 1].match
+ return match_cls(*(self._items[f] & other._items[f]
+ for f in match_cls._fields))
+
+ def _MergeFrom(self, source):
+ if self._depth > 0:
+ # This is NOT a ROOT node, so we need to merge fields and sources from
+ # the source node.
+ for key, values in source._items.items():
+ self._items[key].update(values)
+ self._sources.update(source._sources)
+
+ if self._depth < len(self.LEVELS):
+ # This is NOT a LEAF node, so we need to copy over entries from the
+ # source node's entry map.
+ assert not set(self._entry_map) & set(source._entry_map)
+ self._entry_map.update(source._entry_map)
+
+ def _CanonicalizeEntries(self):
+ canonical_entries = self._entry_map.copy()
+
+ # {ID1, ID2} -> Match between the two entries.
+ matches = {frozenset([full_id1, full_id2]): entry1._GetMatch(entry2)
+ for full_id1, entry1 in canonical_entries.items()
+ for full_id2, entry2 in canonical_entries.items()
+ if entry1._IsMergeableWith(entry2)}
+
+ while matches:
+ # Pop the maximum match from the dictionary.
+ max_match_set, max_match = max(
+ matches.items(), key=lambda pair: [len(v) for v in pair[1]])
+ del matches[max_match_set]
+ canonical_full_id, merged_full_id = max_match_set
+
+ # Skip pairs of entries that have nothing in common.
+ if not any(max_match):
+ continue
+
+ # Merge the entries and update the map to reflect this.
+ canonical_entry = canonical_entries[canonical_full_id]
+ merged_entry = canonical_entries.pop(merged_full_id)
+ logging.debug('Merging %s into %s [match=%s]...', merged_entry,
+ canonical_entry, max_match)
+ canonical_entry._MergeFrom(merged_entry)
+ del merged_entry
+ self._entry_map[merged_full_id] = canonical_entry
+
+ for match_set in matches.keys():
+ if merged_full_id in match_set:
+ # Remove other matches with the merged entry.
+ del matches[match_set]
+ elif canonical_full_id in match_set:
+ [other_full_id] = match_set - {canonical_full_id}
+ other_entry = canonical_entries[other_full_id]
+ if canonical_entry._IsMergeableWith(other_entry):
+ # Update other matches with the canonical entry which are still
+ # mergeable.
+ matches[match_set] = canonical_entry._GetMatch(other_entry)
+ else:
+ # Remove other matches with the canonical entry which have become
+ # unmergeable.
+ del matches[match_set]
+
+ return canonical_entries.values()
+
+ def _AssignIdsToCanonicalEntries(self, canonical_entries):
+ assigned_ids = set()
+ canonical_entries_without_assigned_ids = set()
+
+ # Try to assign each canonical entry to one of the IDs from which it was
+ # merged.
+ for canonical_entry in canonical_entries:
+ candidate_ids = canonical_entry._items['id']
+ try:
+ assigned_id = next(candidate_id for candidate_id in candidate_ids
+ if candidate_id not in assigned_ids)
+ except StopIteration:
+ canonical_entries_without_assigned_ids.add(canonical_entry)
+ continue
+ assigned_ids.add(assigned_id)
+ canonical_entry._canonical_id = assigned_id
+
+ # For canonical entries where this cannot be done (highly unlikely), scan
+ # from the minimal merged ID upwards for the first unassigned ID.
+ for canonical_entry in canonical_entries_without_assigned_ids:
+ assigned_id = next(candidate_id for candidate_id in
+ itertools.count(min(canonical_entry._items['id']))
+ if candidate_id not in assigned_ids)
+ assigned_ids.add(assigned_id)
+ canonical_entry._canonical_id = assigned_id
+
+ def __repr__(self):
+ # pylint: disable=unsubscriptable-object
+ cls_name = type(self).__name__
+ if self._depth == 0:
+ return '%s root' % cls_name
+ else:
+ return '%s %s entry(%s)' % (cls_name, self.LEVELS[self._depth - 1].name,
+ self._items)
+
+
+class ProcessIdMap(IdMap):
+ """Class for merging and mapping PIDs and TIDs from multiple sources."""
+
+ LEVELS = (
+ IdMapLevel(name='process',
+ match=collections.namedtuple('ProcessMatch',
+ ['name', 'id', 'label'])),
+ IdMapLevel(name='thread',
+ match=collections.namedtuple('ThreadMatch', ['name', 'id']))
+ )
+
+
+def LoadTrace(filename):
+ """Load a trace from a (possibly gzipped) file and return its parsed JSON."""
+ logging.info('Loading trace %r...', filename)
+ if filename.endswith(HTML_FILENAME_SUFFIX):
+ return LoadHTMLTrace(filename)
+ elif filename.endswith(GZIP_FILENAME_SUFFIX):
+ with gzip.open(filename, 'rb') as f:
+ return json.load(f)
+ else:
+ with open(filename, 'r') as f:
+ return json.load(f)
+
+
+def LoadHTMLTrace(filename):
+ """Load a trace from a vulcanized HTML trace file."""
+ trace_components = collections.defaultdict(list)
+
+ with open(filename) as file_handle:
+ for sub_trace in html2trace.ReadTracesFromHTMLFile(file_handle):
+ for name, component in TraceAsDict(sub_trace).items():
+ trace_components[name].append(component)
+
+ trace = {}
+ for name, components in trace_components.items():
+ if len(components) == 1:
+ trace[name] = components[0]
+ elif all(isinstance(component, list) for component in components):
+ trace[name] = [e for component in components for e in component]
+ else:
+ trace[name] = components[0]
+ logging.warning(
+ 'Values of repeated trace component %r in HTML trace %r are not '
+ 'lists. The first defined value of the component will be used.',
+ filename, name)
+
+ return trace
+
+
+def SaveTrace(trace, filename):
+ """Save a JSON trace to a (possibly gzipped) file."""
+ if filename is None:
+ logging.info('Dumping trace to standard output...')
+ print(json.dumps(trace))
+ else:
+ logging.info('Saving trace %r...', filename)
+ if filename.endswith(HTML_FILENAME_SUFFIX):
+ with codecs.open(filename, mode='w', encoding='utf-8') as f:
+ trace2html.WriteHTMLForTraceDataToFile([trace], 'Merged trace', f)
+ elif filename.endswith(GZIP_FILENAME_SUFFIX):
+ with gzip.open(filename, 'wb') as f:
+ json.dump(trace, f)
+ else:
+ with open(filename, 'w') as f:
+ json.dump(trace, f)
+
+
+def TraceAsDict(trace):
+ """Ensure that a trace is a dictionary."""
+ if isinstance(trace, list):
+ return {'traceEvents': trace}
+ return trace
+
+
+def MergeTraceFiles(input_trace_filenames, output_trace_filename):
+ """Merge a collection of input trace files into an output trace file."""
+ logging.info('Loading %d input traces...', len(input_trace_filenames))
+ input_traces = collections.OrderedDict()
+ for input_trace_filename in input_trace_filenames:
+ input_traces[input_trace_filename] = LoadTrace(input_trace_filename)
+
+ logging.info('Merging traces...')
+ output_trace = MergeTraces(input_traces)
+
+ logging.info('Saving output trace...')
+ SaveTrace(output_trace, output_trace_filename)
+
+ logging.info('Finished.')
+
+
+def MergeTraces(traces):
+ """Merge a collection of JSON traces into a single JSON trace."""
+ trace_components = collections.defaultdict(collections.OrderedDict)
+
+ for filename, trace in traces.items():
+ for name, component in TraceAsDict(trace).items():
+ trace_components[name][filename] = component
+
+ merged_trace = {}
+ for component_name, components_by_filename in trace_components.items():
+ logging.info('Merging %d %r components...', len(components_by_filename),
+ component_name)
+ merged_trace[component_name] = MergeComponents(component_name,
+ components_by_filename)
+
+ return merged_trace
+
+
+def MergeComponents(component_name, components_by_filename):
+ """Merge a component of multiple JSON traces into a single component."""
+ if component_name == 'traceEvents':
+ return MergeTraceEvents(components_by_filename)
+ else:
+ return MergeGenericTraceComponents(component_name, components_by_filename)
+
+
+def MergeTraceEvents(events_by_filename):
+ """Merge trace events from multiple traces into a single list of events."""
+ # Remove strings from the list of trace events
+ # (https://github.com/catapult-project/catapult/issues/2497).
+ events_by_filename = collections.OrderedDict(
+ (filename, [e for e in events if not isinstance(e, StringTypes)])
+ for filename, events in events_by_filename.items())
+
+ timestamp_range_by_filename = _AdjustTimestampRanges(events_by_filename)
+ process_map = _CreateProcessMapFromTraceEvents(events_by_filename)
+ merged_events = _CombineTraceEvents(events_by_filename, process_map)
+ _RemoveSurplusClockSyncEvents(merged_events)
+ merged_events.extend(
+ _BuildInjectedTraceMarkerEvents(timestamp_range_by_filename, process_map))
+ return merged_events
+
+
+def _RemoveSurplusClockSyncEvents(events):
+ """Remove all clock sync events except for the first one."""
+ # TODO(petrcermak): Figure out how to handle merging multiple clock sync
+ # events.
+ clock_sync_event_indices = [i for i, e in enumerate(events)
+ if e['ph'] == CLOCK_SYNC_EVENT_PHASE]
+ # The indices need to be traversed from largest to smallest (hence the -1).
+ for i in clock_sync_event_indices[:0:-1]:
+ del events[i]
+
+
+def _AdjustTimestampRanges(events_by_filename):
+ logging.info('Adjusting timestamp ranges of traces...')
+
+ previous_trace_max_timestamp = 0
+ timestamp_range_by_filename = collections.OrderedDict()
+
+ for index, (filename, events) in enumerate(events_by_filename.items(), 1):
+ # Skip metadata events, the timestamps of which are always zero.
+ non_metadata_events = [e for e in events if e['ph'] != METADATA_PHASE]
+ if not non_metadata_events:
+ logging.warning('Trace %r (%d/%d) only contains metadata events.',
+ filename, index, len(events_by_filename))
+ timestamp_range_by_filename[filename] = None
+ continue
+
+ min_timestamp = min(e['ts'] for e in non_metadata_events)
+ max_timestamp = max(e['ts'] for e in non_metadata_events)
+
+ # Determine by how much the timestamps should be shifted.
+ injected_timestamp_shift = max(
+ previous_trace_max_timestamp + MIN_TRACE_GAP_IN_US - min_timestamp, 0)
+ logging.info('Injected timestamp shift in trace %r (%d/%d): %d ms '
+ '[min=%d, max=%d, duration=%d].', filename, index,
+ len(events_by_filename), injected_timestamp_shift,
+ min_timestamp, max_timestamp, max_timestamp - min_timestamp)
+
+ if injected_timestamp_shift > 0:
+ # Shift the timestamps.
+ for event in non_metadata_events:
+ event['ts'] += injected_timestamp_shift
+
+ # Adjust the range.
+ min_timestamp += injected_timestamp_shift
+ max_timestamp += injected_timestamp_shift
+
+ previous_trace_max_timestamp = max_timestamp
+
+ timestamp_range_by_filename[filename] = min_timestamp, max_timestamp
+
+ return timestamp_range_by_filename
+
+
+def _CreateProcessMapFromTraceEvents(events_by_filename):
+ logging.info('Creating process map from trace events...')
+
+ process_map = ProcessIdMap()
+ for filename, events in events_by_filename.items():
+ for event in events:
+ pid, tid = event['pid'], event['tid']
+ process_map.AddEntry(source=filename, path=(pid, tid))
+ if event['ph'] == METADATA_PHASE:
+ if event['name'] == 'process_name':
+ process_map.AddEntry(source=filename, path=(pid,),
+ name=event['args']['name'])
+ elif event['name'] == 'process_labels':
+ process_map.AddEntry(source=filename, path=(pid,),
+ label=event['args']['labels'].split(','))
+ elif event['name'] == 'thread_name':
+ process_map.AddEntry(source=filename, path=(pid, tid),
+ name=event['args']['name'])
+
+ process_map.MergeEntries()
+ return process_map
+
+
+def _CombineTraceEvents(events_by_filename, process_map):
+ logging.info('Combining trace events from all traces...')
+
+ type_name_event_by_pid = {}
+ combined_events = []
+
+ for index, (filename, events) in enumerate(events_by_filename.items(), 1):
+ for event in events:
+ if _UpdateTraceEventForMerge(event, process_map, filename, index,
+ type_name_event_by_pid):
+ combined_events.append(event)
+
+ return combined_events
+
+
+def _UpdateTraceEventForMerge(event, process_map, filename, index,
+ type_name_event_by_pid):
+ pid, tid = process_map.MapEntry(source=filename,
+ path=(event['pid'], event['tid']))
+ event['pid'], event['tid'] = pid, tid
+
+ if event['ph'] == METADATA_PHASE:
+ # Update IDs in 'stackFrames' and 'typeNames' metadata events.
+ if event['name'] == 'stackFrames':
+ _UpdateDictIds(index, event['args'], 'stackFrames')
+ for frame in event['args']['stackFrames'].values():
+ _UpdateFieldId(index, frame, 'parent')
+ elif event['name'] == 'typeNames':
+ _UpdateDictIds(index, event['args'], 'typeNames')
+ existing_type_name_event = type_name_event_by_pid.get(pid)
+ if existing_type_name_event is None:
+ type_name_event_by_pid[pid] = event
+ else:
+ existing_type_name_event['args']['typeNames'].update(
+ event['args']['typeNames'])
+ # Don't add the event to the merged trace because it has been merged
+ # into an existing 'typeNames' metadata event for the given process.
+ return False
+
+ elif event['ph'] == MEMORY_DUMP_PHASE:
+ # Update stack frame and type name IDs in heap dump entries in process
+ # memory dumps.
+ for heap_dump in event['args']['dumps'].get('heaps', {}).values():
+ for heap_entry in heap_dump['entries']:
+ _UpdateFieldId(index, heap_entry, 'bt', ignored_values=[''])
+ _UpdateFieldId(index, heap_entry, 'type')
+
+ return True # Events should be added to the merged trace by default.
+
+
+def _ConvertId(index, original_id):
+ return '%d#%s' % (index, original_id)
+
+
+def _UpdateDictIds(index, parent_dict, key):
+ parent_dict[key] = {
+ _ConvertId(index, original_id): value
+ for original_id, value in parent_dict[key].items()}
+
+
+def _UpdateFieldId(index, parent_dict, key, ignored_values=()):
+ original_value = parent_dict.get(key)
+ if original_value is not None and original_value not in ignored_values:
+ parent_dict[key] = _ConvertId(index, original_value)
+
+
+def _BuildInjectedTraceMarkerEvents(timestamp_range_by_filename, process_map):
+ logging.info('Building injected trace marker events...')
+
+ injected_pid = process_map.max_mapped_id + 1
+
+ # Inject a mock process with a thread.
+ injected_events = [
+ {
+ 'pid': injected_pid,
+ 'tid': 0,
+ 'ph': METADATA_PHASE,
+ 'ts': 0,
+ 'name': 'process_sort_index',
+ 'args': {'sort_index': -1000} # Show the process at the top.
+ },
+ {
+ 'pid': injected_pid,
+ 'tid': 0,
+ 'ph': METADATA_PHASE,
+ 'ts': 0,
+ 'name': 'process_name',
+ 'args': {'name': 'Merged traces'}
+ },
+ {
+ 'pid': injected_pid,
+ 'tid': 0,
+ 'ph': METADATA_PHASE,
+ 'ts': 0,
+ 'name': 'thread_name',
+ 'args': {'name': 'Trace'}
+ }
+ ]
+
+ # Inject slices for each sub-trace denoting its beginning and end.
+ for index, (filename, timestamp_range) in enumerate(
+ timestamp_range_by_filename.items(), 1):
+ if timestamp_range is None:
+ continue
+ min_timestamp, max_timestamp = timestamp_range
+ name = 'Trace %r (%d/%d)' % (filename, index,
+ len(timestamp_range_by_filename))
+ slice_id = 'INJECTED_TRACE_MARKER_%d' % index
+ injected_events.extend([
+ {
+ 'pid': injected_pid,
+ 'tid': 0,
+ 'ph': BEGIN_PHASE,
+ 'cat': 'injected',
+ 'name': name,
+ 'id': slice_id,
+ 'ts': min_timestamp
+ },
+ {
+ 'pid': injected_pid,
+ 'tid': 0,
+ 'ph': END_PHASE,
+ 'cat': 'injected',
+ 'name': name,
+ 'id': slice_id,
+ 'ts': max_timestamp
+ }
+ ])
+
+ return injected_events
+
+
+def MergeGenericTraceComponents(component_name, components_by_filename):
+ """Merge a generic component of multiple JSON traces into a single component.
+
+ This function is only used for components that don't have a component-specific
+ merging function (see MergeTraceEvents). It just returns the component's first
+ provided value (in some trace).
+ """
+ components = components_by_filename.values()
+ first_component = next(components)
+ if not all(c == first_component for c in components):
+ logging.warning(
+ 'Values of trace component %r differ across the provided traces. '
+ 'The first defined value of the component will be used.',
+ component_name)
+ return first_component
+
+
+def Main(argv):
+ parser = argparse.ArgumentParser(description='Merge multiple traces.',
+ add_help=False)
+ parser.add_argument('input_traces', metavar='INPUT_TRACE', nargs='+',
+ help='Input trace filename.')
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit.')
+ parser.add_argument('-o', '--output_trace', help='Output trace filename. If '
+ 'not provided, the merged trace will be written to '
+ 'the standard output.')
+ parser.add_argument('-v', '--verbose', action='count', dest='verbosity',
+ help='Increase verbosity level.')
+ args = parser.parse_args(argv[1:])
+
+ # Set verbosity level.
+ if args.verbosity >= 2:
+ logging_level = logging.DEBUG
+ elif args.verbosity == 1:
+ logging_level = logging.INFO
+ else:
+ logging_level = logging.WARNING
+ logging.getLogger().setLevel(logging_level)
+
+ try:
+ MergeTraceFiles(args.input_traces, args.output_trace)
+ return 0
+ except Exception: # pylint: disable=broad-except
+ logging.exception('Something went wrong:')
+ return 1
+ finally:
+ logging.warning('This is an EXPERIMENTAL TOOL! If you encounter any '
+ 'issues, please file a Catapult bug '
+ '(https://github.com/catapult-project/catapult/issues/new) '
+ 'with your current Catapult commit hash, a description of '
+ 'the problem and any error messages, attach the input '
+ 'traces and notify petrcermak@chromium.org. Thank you!')
diff --git a/chromium/third_party/catapult/tracing/tracing_build/merge_traces_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/merge_traces_unittest.py
new file mode 100644
index 00000000000..f036fcd718e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/merge_traces_unittest.py
@@ -0,0 +1,48 @@
+# 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.
+
+import json
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+from tracing.trace_data import trace_data
+if sys.version_info < (3,):
+ from tracing_build import merge_traces
+
+
+def _FakeDumpEvent(pid, tid):
+ return {'ph': 'v', 'ts': 100, 'pid': pid, 'tid': tid, 'args': {'dumps': {}}}
+
+
+@unittest.skipIf(sys.version_info >= (3,),
+ 'py_vulcanize is not ported to python3')
+class MergeTracesTest(unittest.TestCase):
+ def setUp(self):
+ self.test_dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.test_dir)
+
+ def _SerializeTrace(self, filename, trace):
+ filepath = os.path.join(self.test_dir, filename)
+ ri = trace_data.CreateTraceDataFromRawData(trace)
+ ri.Serialize(filepath)
+ return filepath
+
+ def testSimple(self):
+ """Simple integration test for the main MergeTraceFiles function."""
+ trace1 = self._SerializeTrace(
+ 'trace1.html', {'traceEvents': [_FakeDumpEvent(pid=1, tid=3)]})
+ trace2 = self._SerializeTrace(
+ 'trace2.html', {'traceEvents': [_FakeDumpEvent(pid=2, tid=4)]})
+ merged = os.path.join(self.test_dir, 'merged.json')
+ merge_traces.MergeTraceFiles([trace1, trace2], merged)
+ with open(merged) as f:
+ events = json.load(f)['traceEvents']
+ # Check that both dumps are found in the merged trace.
+ dump_pids = [e['pid'] for e in events if e['ph'] == 'v']
+ self.assertEquals([1, 2], dump_pids)
diff --git a/chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer.py b/chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer.py
new file mode 100644
index 00000000000..a5ad91a4e20
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer.py
@@ -0,0 +1,81 @@
+# 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 json
+import logging
+import re
+
+_DATA_START = '<div id="histogram-json-data" style="display:none;"><!--'
+_DATA_END = '--!></div>'
+
+
+def ExtractJSON(results_html):
+ results = []
+ flags = re.MULTILINE | re.DOTALL
+ start = re.search("^%s" % re.escape(_DATA_START), results_html, flags)
+ if start is None:
+ logging.warn('Could not find histogram data start: %s', _DATA_START)
+ return []
+ pattern = '^((%s)|(.*?))$' % re.escape(_DATA_END)
+ # Find newlines and parse each line as separate JSON data.
+ for match in re.compile(pattern, flags).finditer(results_html, start.end()+1):
+ try:
+ # Check if the end tag in group(2) got a match.
+ if match.group(2):
+ return results
+ results.append(json.loads(match.group(3)))
+ except ValueError:
+ logging.warn(
+ 'Found existing results json, but failed to parse it: %s',
+ match.group(1))
+ return []
+ return results
+
+
+def ReadExistingResults(results_html):
+ if not results_html:
+ return []
+
+ histogram_dicts = ExtractJSON(results_html)
+
+ if not histogram_dicts:
+ logging.warn('Failed to extract previous results from HTML output')
+ return histogram_dicts
+
+
+def RenderHistogramsViewer(histogram_dicts, output_stream, reset_results=False,
+ vulcanized_html=''):
+ """Renders a Histograms viewer to output_stream containing histogram_dicts.
+
+ Requires a Histograms viewer to have already been vulcanized.
+ The vulcanized viewer can be provided either as a string or a file.
+
+ Args:
+ histogram_dicts: list of dictionaries containing Histograms.
+ output_stream: file-like stream to read existing results and write HTML.
+ reset_results: whether to drop existing results.
+ vulcanized_html: HTML string of vulcanized histograms viewer.
+ """
+ output_stream.seek(0)
+
+ if not reset_results:
+ results_html = output_stream.read()
+ output_stream.seek(0)
+ histogram_dicts += ReadExistingResults(results_html)
+
+ output_stream.write(vulcanized_html)
+ # Put all the serialized histograms nodes inside an html comment to avoid
+ # unecessary stress on html parsing and avoid creating throw-away dom nodes.
+ output_stream.write(_DATA_START)
+ for histogram in histogram_dicts:
+ hist_json = json.dumps(histogram, separators=(',', ':'))
+ output_stream.write('\n')
+ # No escaping is necessary since the data is stored inside an html comment.
+ # This assumes that {hist_json} doesn't contain an html comment itself.
+ output_stream.write(hist_json)
+ output_stream.write('\n%s\n' % _DATA_END)
+
+ # If the output file already existed and was longer than the new contents,
+ # discard the old contents after this point.
+ output_stream.truncate()
diff --git a/chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer_unittest.py
new file mode 100644
index 00000000000..48c8dcbe33e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/render_histograms_viewer_unittest.py
@@ -0,0 +1,95 @@
+# 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 codecs
+import json
+import unittest
+import os
+import tempfile
+
+from tracing_build import render_histograms_viewer
+
+
+class ResultsRendererTest(unittest.TestCase):
+
+ # Renamed between Python 2 and Python 3.
+ try:
+ assertCountEqual = unittest.TestCase.assertItemsEqual
+ except AttributeError:
+ pass
+
+ def setUp(self):
+ tmp = tempfile.NamedTemporaryFile(delete=False)
+ tmp.close()
+ self.output_file = tmp.name
+ self.output_stream = codecs.open(self.output_file,
+ mode='r+',
+ encoding='utf-8')
+
+ def GetOutputFileContent(self):
+ self.output_stream.seek(0)
+ return self.output_stream.read()
+
+ def tearDown(self):
+ self.output_stream.close()
+ os.remove(self.output_file)
+
+ def testBasic(self):
+ value0 = {'foo': 0}
+ value0_json = json.dumps(value0, separators=(',', ':'))
+
+ render_histograms_viewer.RenderHistogramsViewer(
+ [], self.output_stream, False)
+ self.output_stream.seek(0)
+ self.assertCountEqual([], render_histograms_viewer.ReadExistingResults(
+ self.output_stream.read()))
+ render_histograms_viewer.RenderHistogramsViewer(
+ [value0], self.output_stream, False)
+ self.output_stream.seek(0)
+ self.assertCountEqual(
+ [value0],
+ render_histograms_viewer.ReadExistingResults(self.output_stream.read()))
+ self.assertIn(value0_json, self.GetOutputFileContent())
+
+ def testExistingResults(self):
+ value0 = {'foo': 0}
+ value0_json = json.dumps(value0, separators=(',', ':'))
+
+ value1 = {'bar': 1}
+ value1_json = json.dumps(value1, separators=(',', ':'))
+
+ render_histograms_viewer.RenderHistogramsViewer(
+ [value0], self.output_stream, False)
+ render_histograms_viewer.RenderHistogramsViewer(
+ [value1], self.output_stream, False)
+ self.output_stream.seek(0)
+ self.assertCountEqual(
+ [value0, value1],
+ render_histograms_viewer.ReadExistingResults(self.output_stream.read()))
+ self.assertIn(value0_json, self.GetOutputFileContent())
+ self.assertIn(value1_json, self.GetOutputFileContent())
+
+ def testExistingResultsReset(self):
+ value0 = {'foo': 0}
+ value0_json = json.dumps(value0, separators=(',', ':'))
+
+ value1 = {'bar': 1}
+ value1_json = json.dumps(value1, separators=(',', ':'))
+
+ render_histograms_viewer.RenderHistogramsViewer(
+ [value0], self.output_stream, False)
+ render_histograms_viewer.RenderHistogramsViewer(
+ [value1], self.output_stream, True)
+ self.output_stream.seek(0)
+ self.assertCountEqual(
+ [value1],
+ render_histograms_viewer.ReadExistingResults(self.output_stream.read()))
+ self.assertNotIn(value0_json, self.GetOutputFileContent())
+ self.assertIn(value1_json, self.GetOutputFileContent())
+
+ def testHtmlEscape(self):
+ # No escaping is needed since data is stored in an html comment.
+ render_histograms_viewer.RenderHistogramsViewer(
+ [{'name': '<a><b>'}], self.output_stream, False)
+ self.assertIn('<a><b>', self.GetOutputFileContent())
diff --git a/chromium/third_party/catapult/tracing/tracing_build/run_profile.py b/chromium/third_party/catapult/tracing/tracing_build/run_profile.py
new file mode 100644
index 00000000000..458f8f5f453
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/run_profile.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2012 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 print_function
+
+import argparse
+import cProfile
+import pstats
+import inspect
+import sys
+
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+
+class Bench(object):
+
+ def SetUp(self):
+ pass
+
+ def Run(self):
+ pass
+
+ def TearDown(self):
+ pass
+
+
+def Main(args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--repeat-count', type=int, default=10)
+ parser.add_argument('bench_name')
+ args = parser.parse_args(args)
+
+ benches = [g for g in globals().values()
+ if g != Bench and inspect.isclass(g) and
+ Bench in inspect.getmro(g)]
+
+ # pylint: disable=undefined-loop-variable
+ b = [b for b in benches if b.__name__ == args.bench_name]
+ if len(b) != 1:
+ sys.stderr.write('Bench %r not found.' % args.bench_name)
+ return 1
+
+ bench = b[0]()
+ bench.SetUp()
+ try:
+ pr = cProfile.Profile()
+ pr.enable(builtins=False)
+ for _ in range(args.repeat_count):
+ bench.Run()
+ pr.disable()
+ s = StringIO()
+
+ sortby = 'cumulative'
+ ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
+ ps.print_stats()
+ print(s.getvalue())
+ return 0
+ finally:
+ bench.TearDown()
+
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv[1:]))
diff --git a/chromium/third_party/catapult/tracing/tracing_build/run_vinn_tests.py b/chromium/third_party/catapult/tracing/tracing_build/run_vinn_tests.py
new file mode 100644
index 00000000000..f8c79e0de35
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/run_vinn_tests.py
@@ -0,0 +1,47 @@
+#!/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 argparse
+import os
+import sys
+
+from hooks import install
+import tracing_project
+import vinn
+
+
+def _RelPathToUnixPath(p):
+ return p.replace(os.sep, '/')
+
+
+def RunTests():
+ project = tracing_project.TracingProject()
+ headless_test_module_filenames = [
+ '/' + _RelPathToUnixPath(x)
+ for x in project.FindAllD8TestModuleRelPaths()]
+ headless_test_module_filenames.sort()
+
+ cmd = """
+ HTMLImportsLoader.loadHTML('/tracing/base/headless_tests.html');
+ tr.b.unittest.loadAndRunTests(sys.argv.slice(1));
+ """
+ res = vinn.RunJsString(
+ cmd, source_paths=list(project.source_paths),
+ js_args=headless_test_module_filenames,
+ stdout=sys.stdout, stdin=sys.stdin)
+ return res.returncode
+
+
+def Main(argv):
+ parser = argparse.ArgumentParser(
+ description='Run d8 tests.')
+ parser.add_argument(
+ '--no-install-hooks', dest='install_hooks', action='store_false')
+ parser.set_defaults(install_hooks=True)
+ args = parser.parse_args(argv[1:])
+ if args.install_hooks:
+ install.InstallHooks()
+
+ sys.exit(RunTests())
diff --git a/chromium/third_party/catapult/tracing/tracing_build/slim_trace.py b/chromium/third_party/catapult/tracing/tracing_build/slim_trace.py
new file mode 100644
index 00000000000..44c2737ea91
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/slim_trace.py
@@ -0,0 +1,112 @@
+# 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 argparse
+import codecs
+import json
+import os
+import logging
+
+
+from tracing_build import html2trace
+from tracing_build import trace2html
+
+
+def GetFileSizeInMb(path):
+ return os.path.getsize(path) >> 20
+
+
+def Main(argv):
+ parser = argparse.ArgumentParser(
+ description='Slim a trace to a more managable size')
+ parser.add_argument('trace_path', metavar='TRACE_PATH', type=str,
+ help='trace file path (input).')
+ options = parser.parse_args(argv[1:])
+
+ trace_path = os.path.abspath(options.trace_path)
+
+ orignal_trace_name = os.path.splitext(os.path.basename(trace_path))[0]
+ slimmed_trace_path = os.path.join(
+ os.path.dirname(trace_path), 'slimmed_%s.html' % orignal_trace_name)
+
+ with codecs.open(trace_path, mode='r', encoding='utf-8') as f:
+ SlimTrace(f, slimmed_trace_path)
+
+ print('Original trace %s (%s Mb)' % (
+ trace_path, GetFileSizeInMb(trace_path)))
+ print('Slimmed trace file://%s (%s Mb)' % (
+ slimmed_trace_path, GetFileSizeInMb(slimmed_trace_path)))
+
+def SlimTraceEventsList(events_list):
+ filtered_events = []
+ # Filter out all events of phase complete that takes less than 20
+ # microseconds.
+ for e in events_list:
+ dur = e.get('dur', 0)
+ if e['ph'] != 'X' or dur >= 20:
+ filtered_events.append(e)
+ return filtered_events
+
+
+def SlimSingleTrace(trace_data):
+ if isinstance(trace_data, dict):
+ trace_data['traceEvents'] = SlimTraceEventsList(trace_data['traceEvents'])
+ elif isinstance(trace_data, list) and isinstance(trace_data[0], dict):
+ trace_data = SlimTraceEventsList(trace_data)
+ else:
+ logging.warning('Cannot slim trace %s...', trace_data[:10])
+ return trace_data
+
+
+class TraceExtractor(object):
+ def CanExtractFile(self, trace_file_handle):
+ raise NotImplementedError
+
+ def ExtractTracesFromFile(self, trace_file_handle):
+ raise NotImplementedError
+
+
+class HTMLTraceExtractor(TraceExtractor):
+ def CanExtractFile(self, trace_file_handle):
+ return html2trace.IsHTMLTrace(trace_file_handle)
+
+ def ExtractTracesFromFile(self, trace_file_handle):
+ return html2trace.ReadTracesFromHTMLFile(trace_file_handle)
+
+
+class JsonTraceExtractor(TraceExtractor):
+ def CanExtractFile(self, trace_file_handle):
+ trace_file_handle.seek(0)
+ begin_char = trace_file_handle.read(1)
+ trace_file_handle.seek(-1, 2)
+ end_char = trace_file_handle.read(1)
+ return ((begin_char == '{' and end_char == '}') or
+ (begin_char == '[' and end_char == ']'))
+
+ def ExtractTracesFromFile(self, trace_file_handle):
+ trace_file_handle.seek(0)
+ return [json.load(trace_file_handle)]
+
+
+ALL_TRACE_EXTRACTORS = [
+ HTMLTraceExtractor(),
+ JsonTraceExtractor()
+]
+
+
+def SlimTrace(trace_file_handle, slimmed_trace_path):
+ traces = None
+ for extractor in ALL_TRACE_EXTRACTORS:
+ if extractor.CanExtractFile(trace_file_handle):
+ traces = extractor.ExtractTracesFromFile(trace_file_handle)
+ break
+
+ if traces is None:
+ raise Exception('Cannot extrac trace from %s' % trace_file_handle.name)
+
+ slimmed_traces = map(SlimSingleTrace, traces)
+
+ with codecs.open(slimmed_trace_path, mode='w', encoding='utf-8') as f:
+ trace2html.WriteHTMLForTraceDataToFile(
+ slimmed_traces, title='', output_file=f)
diff --git a/chromium/third_party/catapult/tracing/tracing_build/strip_memory_infra_trace.py b/chromium/third_party/catapult/tracing/tracing_build/strip_memory_infra_trace.py
new file mode 100755
index 00000000000..4e897727f3e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/strip_memory_infra_trace.py
@@ -0,0 +1,102 @@
+# 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.
+
+"""Filters a big trace keeping only the last memory-infra dumps."""
+
+from __future__ import print_function
+
+import collections
+import gzip
+import json
+
+
+def FormatBytes(value):
+ units = ['B', 'KiB', 'MiB', 'GiB']
+ while abs(value) >= 1024 and len(units) > 1:
+ value /= 1024
+ units = units.pop(0)
+ return '%3.1f %s' % (value, units[0])
+
+
+def Main(argv):
+ if len(argv) < 2:
+ print('Usage: %s trace.json[.gz]' % argv[0])
+ return 1
+
+ in_path = argv[1]
+ if in_path.lower().endswith('.gz'):
+ fin = gzip.open(in_path, 'rb')
+ else:
+ fin = open(in_path, 'r')
+ with fin:
+ print('Loading trace (can take 1 min on a z620 for a 1GB trace)...')
+ trace = json.load(fin)
+ print('Done. Read ' + FormatBytes(fin.tell()))
+
+ print('Filtering events')
+ phase_count = collections.defaultdict(int)
+ out_events = []
+ global_dumps = collections.OrderedDict()
+ if isinstance(trace, dict):
+ in_events = trace.get('traceEvents', [])
+ elif isinstance(trace, list) and isinstance(trace[0], dict):
+ in_events = trace
+
+ for evt in in_events:
+ phase = evt.get('ph', '?')
+ phase_count[phase] += 1
+
+ # Drop all diagnostic events for memory-infra debugging.
+ if phase not in ('v', 'V') and evt.get('cat', '').endswith('memory-infra'):
+ continue
+
+ # pass-through all the other non-memory-infra events
+ if phase != 'v':
+ out_events.append(evt)
+ continue
+
+ # Recreate the global dump groups
+ event_id = evt['id']
+ global_dumps.setdefault(event_id, [])
+ global_dumps[event_id].append(evt)
+
+
+ print('Detected %d memory-infra global dumps' % len(global_dumps))
+ if global_dumps:
+ max_procs = max(len(x) for x in global_dumps.values())
+ print('Max number of processes seen: %d' % max_procs)
+
+ ndumps = 2
+ print('Preserving the last %d memory-infra dumps' % ndumps)
+ detailed_dumps = []
+ non_detailed_dumps = []
+ for global_dump in global_dumps.values():
+ try:
+ level_of_detail = global_dump[0]['args']['dumps']['level_of_detail']
+ except KeyError:
+ level_of_detail = None
+ if level_of_detail == 'detailed':
+ detailed_dumps.append(global_dump)
+ else:
+ non_detailed_dumps.append(global_dump)
+
+ dumps_to_preserve = detailed_dumps[-ndumps:]
+ ndumps -= len(dumps_to_preserve)
+ if ndumps:
+ dumps_to_preserve += non_detailed_dumps[-ndumps:]
+
+ for global_dump in dumps_to_preserve:
+ out_events += global_dump
+
+ print('\nEvents histogram for the original trace (count by phase)')
+ print('--------------------------------------------------------')
+ for phase, count in sorted(phase_count.items(), key=lambda x: x[1]):
+ print('%s %d' % (phase, count))
+
+ out_path = in_path.split('.json')[0] + '-filtered.json'
+ print('\nWriting filtered trace to ' + out_path, end='')
+ with open(out_path, 'w') as fout:
+ json.dump({'traceEvents': out_events}, fout)
+ num_bytes_written = fout.tell()
+ print(' (%s written)' % FormatBytes(num_bytes_written))
diff --git a/chromium/third_party/catapult/tracing/tracing_build/trace2html.py b/chromium/third_party/catapult/tracing/tracing_build/trace2html.py
new file mode 100644
index 00000000000..b26cc2ea0ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/trace2html.py
@@ -0,0 +1,128 @@
+# Copyright (c) 2014 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 print_function
+
+import argparse
+import codecs
+import base64
+import gzip
+import io
+import json
+import os
+
+import tracing_project
+
+from py_vulcanize import generate
+
+
+try:
+ StringTypes = basestring
+except NameError:
+ StringTypes = str
+
+
+def Main(argv):
+
+ parser = argparse.ArgumentParser(
+ usage='%(prog)s <options> trace_file1 [trace_file2 ...]',
+ epilog='Takes the provided trace file and produces a standalone HTML\n'
+ 'file that contains both the trace and the trace viewer.')
+
+ project = tracing_project.TracingProject()
+ project.AddConfigNameOptionToParser(parser)
+
+ parser.add_argument(
+ '--output', dest='output',
+ help='Where to put the generated result. If not '
+ 'given, the trace filename is used, with an html suffix.')
+ parser.add_argument(
+ '--quiet', action='store_true',
+ help='Dont print the output file name')
+ parser.add_argument(
+ '--title', type=str,
+ help='The title to put in trace viewer top panel.')
+ parser.add_argument('trace_files', nargs='+')
+ args = parser.parse_args(argv[1:])
+
+ if args.output:
+ output_filename = args.output
+ elif len(args.trace_files) > 1:
+ parser.error('Must specify --output if there are multiple trace files.')
+ else:
+ name_part = os.path.splitext(args.trace_files[0])[0]
+ output_filename = name_part + '.html'
+
+ with codecs.open(output_filename, mode='w', encoding='utf-8') as f:
+ WriteHTMLForTracesToFile(args.trace_files, f, args.title,
+ config_name=args.config_name)
+
+ if not args.quiet:
+ print(output_filename)
+ return 0
+
+
+class ViewerDataScript(generate.ExtraScript):
+
+ def __init__(self, trace_data_string, mime_type):
+ super(ViewerDataScript, self).__init__()
+ self._trace_data_string = trace_data_string
+ self._mime_type = mime_type
+
+ def WriteToFile(self, output_file):
+ output_file.write('<script id="viewer-data" type="%s">\n' % self._mime_type)
+ compressed_trace = io.BytesIO()
+ with gzip.GzipFile(fileobj=compressed_trace, mode='w', mtime=0) as f:
+ f.write(self._trace_data_string)
+ b64_content = base64.b64encode(compressed_trace.getvalue())
+ output_file.write(b64_content)
+ output_file.write('\n</script>\n')
+
+
+def WriteHTMLForTraceDataToFile(trace_data_list,
+ title, output_file,
+ config_name=None):
+ project = tracing_project.TracingProject()
+
+ if config_name is None:
+ config_name = project.GetDefaultConfigName()
+
+ modules = [
+ 'tracing.trace2html',
+ project.GetModuleNameForConfigName(config_name),
+ ]
+ vulcanizer = project.CreateVulcanizer()
+ load_sequence = vulcanizer.CalcLoadSequenceForModuleNames(modules)
+
+ scripts = []
+ for trace_data in trace_data_list:
+ # If the object was previously decoded from valid JSON data (e.g., in
+ # WriteHTMLForTracesToFile), it will be a JSON object at this point and we
+ # should re-serialize it into a string. Other types of data will be already
+ # be strings.
+ if not isinstance(trace_data, StringTypes):
+ trace_data = json.dumps(trace_data)
+ mime_type = 'application/json'
+ else:
+ mime_type = 'text/plain'
+ scripts.append(ViewerDataScript(trace_data, mime_type))
+ generate.GenerateStandaloneHTMLToFile(
+ output_file, load_sequence, title, extra_scripts=scripts)
+
+
+def WriteHTMLForTracesToFile(trace_filenames, output_file, title='',
+ config_name=None):
+ trace_data_list = []
+ for filename in trace_filenames:
+ try:
+ with gzip.GzipFile(filename, 'r') as f:
+ # If filename is not gzipped, then read() will raise IOError.
+ trace_data = f.read()
+ except IOError:
+ with open(filename, 'r') as f:
+ trace_data = f.read()
+ trace_data_list.append(trace_data)
+ if not title:
+ title = "Trace from %s" % ','.join(trace_filenames)
+ WriteHTMLForTraceDataToFile(trace_data_list, title, output_file, config_name)
diff --git a/chromium/third_party/catapult/tracing/tracing_build/trace2html_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/trace2html_unittest.py
new file mode 100644
index 00000000000..b0d07b02801
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/trace2html_unittest.py
@@ -0,0 +1,84 @@
+# Copyright (c) 2014 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 codecs
+import gzip
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+if sys.version_info < (3,):
+ from tracing_build import trace2html
+
+
+@unittest.skipIf(sys.version_info >= (3,),
+ 'py_vulcanize is not ported to python3')
+class Trace2HTMLTests(unittest.TestCase):
+ SIMPLE_TRACE_PATH = os.path.join(
+ os.path.dirname(__file__),
+ '..', 'test_data', 'simple_trace.json')
+ BIG_TRACE_PATH = os.path.join(
+ os.path.dirname(__file__),
+ '..', 'test_data', 'big_trace.json')
+ NON_JSON_TRACE_PATH = os.path.join(
+ os.path.dirname(__file__),
+ '..', 'test_data', 'android_systrace.txt')
+
+ def setUp(self):
+ self._tmpdir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
+
+ def testGzippedTraceIsntDoubleGzipped(self):
+ # |input_filename| will contain plain JSON data at one point, then gzipped
+ # JSON data at another point for reasons that will be explained below.
+ input_filename = os.path.join(self._tmpdir, 'GzippedTraceIsntDoubleGzipped')
+
+ output_filename = os.path.join(
+ self._tmpdir, 'GzippedTraceIsntDoubleGzipped.html')
+
+ # trace2html-ify SIMPLE_TRACE, but from a controlled input filename so
+ # that when ViewerDataScript gzips it, it uses the same filename for both
+ # the unzipped SIMPLE_TRACE here and the gzipped SIMPLE_TRACE below.
+ file(input_filename, 'w').write(file(self.SIMPLE_TRACE_PATH).read())
+ with codecs.open(output_filename, 'w', encoding='utf-8') as output_file:
+ trace2html.WriteHTMLForTracesToFile([input_filename], output_file)
+
+ # Hash the contents of the output file that was generated from an unzipped
+ # json input file.
+ unzipped_hash = hash(file(output_filename).read())
+
+ os.unlink(output_filename)
+
+ # Gzip SIMPLE_TRACE into |input_filename|.
+ # trace2html should automatically gunzip it and start building the html from
+ # the same input as if the input weren't gzipped.
+ with gzip.GzipFile(input_filename, mode='w') as input_gzipfile:
+ input_gzipfile.write(file(self.SIMPLE_TRACE_PATH).read())
+
+ # trace2html-ify the zipped version of SIMPLE_TRACE from the same input
+ # filename as the unzipped version so that the gzipping process is stable.
+ with codecs.open(output_filename, 'w', encoding='utf-8') as output_file:
+ trace2html.WriteHTMLForTracesToFile([input_filename], output_file)
+
+ # Hash the contents of the output file that was generated from the zipped
+ # json input file.
+ zipped_hash = hash(file(output_filename).read())
+
+ # Compare the hashes, not the file contents directly so that, if they are
+ # different, python shouldn't try to print megabytes of html.
+ self.assertEqual(unzipped_hash, zipped_hash)
+
+ def testWriteHTMLForTracesToFile(self):
+ output_filename = os.path.join(
+ self._tmpdir, 'WriteHTMLForTracesToFile.html')
+ with codecs.open(output_filename, 'w', encoding='utf-8') as output_file:
+ trace2html.WriteHTMLForTracesToFile([
+ self.BIG_TRACE_PATH,
+ self.SIMPLE_TRACE_PATH,
+ self.NON_JSON_TRACE_PATH
+ ], output_file)
diff --git a/chromium/third_party/catapult/tracing/tracing_build/tracing_dev_server_config.py b/chromium/third_party/catapult/tracing/tracing_build/tracing_dev_server_config.py
new file mode 100644
index 00000000000..fcd0a1a9f1f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/tracing_dev_server_config.py
@@ -0,0 +1,57 @@
+# 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 json
+import os
+
+import tracing_project
+
+
+import webapp2
+from webapp2 import Route
+
+
+def _RelPathToUnixPath(p):
+ return p.replace(os.sep, '/')
+
+
+class TestListHandler(webapp2.RequestHandler):
+
+ def get(self, *args, **kwargs): # pylint: disable=unused-argument
+ project = tracing_project.TracingProject()
+ test_relpaths = ['/' + _RelPathToUnixPath(x)
+ for x in project.FindAllTestModuleRelPaths()]
+
+ tests = {'test_relpaths': test_relpaths}
+ tests_as_json = json.dumps(tests)
+ self.response.content_type = 'application/json'
+ return self.response.write(tests_as_json)
+
+
+class TracingDevServerConfig(object):
+
+ def __init__(self):
+ self.project = tracing_project.TracingProject()
+
+ def GetName(self):
+ return 'tracing'
+
+ def GetRunUnitTestsUrl(self):
+ return '/tracing/tests.html'
+
+ def AddOptionstToArgParseGroup(self, g):
+ g.add_argument('-d', '--data-dir', default=self.project.test_data_path)
+ g.add_argument('-s', '--skp-data-dir', default=self.project.skp_data_path)
+
+ def GetRoutes(self, args): # pylint: disable=unused-argument
+ return [Route('/tracing/tests', TestListHandler)]
+
+ def GetSourcePaths(self, args): # pylint: disable=unused-argument
+ return list(self.project.source_paths)
+
+ def GetTestDataPaths(self, args): # pylint: disable=unused-argument
+ return [
+ ('/tracing/test_data/', os.path.expanduser(args.data_dir)),
+ ('/tracing/skp_data/', os.path.expanduser(args.skp_data_dir)),
+ ]
diff --git a/chromium/third_party/catapult/tracing/tracing_build/update_gni.py b/chromium/third_party/catapult/tracing/tracing_build/update_gni.py
new file mode 100644
index 00000000000..e31487831c7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/update_gni.py
@@ -0,0 +1,123 @@
+# 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 collections
+import os
+import re
+
+import tracing_project
+from tracing_build import check_common
+
+
+class _Token(object):
+
+ def __init__(self, data, token_id=None):
+ self.data = data
+ if token_id:
+ self.token_id = token_id
+ else:
+ self.token_id = 'plain'
+
+
+class BuildFile(object):
+
+ def __init__(self, text, file_groups):
+ self._file_groups = file_groups
+ self._tokens = [token for token in self._Tokenize(text)]
+
+ def _Tokenize(self, text):
+ rest = text
+ token_regex = self._TokenRegex()
+ while len(rest):
+ m = token_regex.search(rest)
+ if not m:
+ # In `rest', we couldn't find a match.
+ # So, lump the entire `rest' into a token
+ # and stop producing any more tokens.
+ yield _Token(rest)
+ return
+ min_index, end_index, matched_token = self._ProcessMatch(m)
+
+ if min_index > 0:
+ yield _Token(rest[:min_index])
+
+ yield matched_token
+ rest = rest[end_index:]
+
+ def Update(self, files_by_group):
+ for token in self._tokens:
+ if token.token_id in files_by_group:
+ token.data = self._GetReplacementListAsString(
+ token.data,
+ files_by_group[token.token_id])
+
+ def Write(self, f):
+ for token in self._tokens:
+ f.write(token.data)
+
+ def _ProcessMatch(self, match):
+ raise NotImplementedError
+
+ def _TokenRegex(self):
+ raise NotImplementedError
+
+ def _GetReplacementListAsString(self, existing_list_as_string, filelist):
+ raise NotImplementedError
+
+
+class GniFile(BuildFile):
+
+ def _ProcessMatch(self, match):
+ min_index = match.start(2)
+ end_index = match.end(2)
+ token = _Token(match.string[min_index:end_index],
+ token_id=match.groups()[0])
+ return min_index, end_index, token
+
+ def _TokenRegex(self):
+ # regexp to match the following:
+ # file_group_name = [
+ # "path/to/one/file.extension",
+ # "another/file.ex",
+ # ]
+ # In the match,
+ # group 1 is : 'file_group_name'
+ # group 2 is : ' "path/to/one/file.extension",\n "another/file.ex",\n'
+ regexp_str = r'(%s) = \[\n(.+?) +\],?\n' % '|'.join(self._file_groups)
+ return re.compile(regexp_str, re.MULTILINE | re.DOTALL)
+
+ def _GetReplacementListAsString(self, existing_list_as_string, filelist):
+ list_entry = existing_list_as_string.splitlines()[0]
+ prefix, _, suffix = list_entry.split('"')
+ return ''.join(['"'.join([prefix, filename, suffix + '\n'])
+ for filename in filelist])
+
+
+def _GroupFiles(file_name_to_group_name_func, filenames):
+ file_groups = collections.defaultdict(lambda: [])
+ for filename in filenames:
+ file_groups[file_name_to_group_name_func(filename)].append(filename)
+ for group in file_groups:
+ file_groups[group].sort()
+ return file_groups
+
+
+def _UpdateBuildFile(filename, build_file_class):
+ with open(filename, 'r') as f:
+ build_file = build_file_class(f.read(), check_common.FILE_GROUPS)
+ files_by_group = _GroupFiles(check_common.GetFileGroupFromFileName,
+ check_common.GetKnownFiles())
+ build_file.Update(files_by_group)
+ with open(filename, 'w') as f:
+ build_file.Write(f)
+
+
+def UpdateGni():
+ tvp = tracing_project.TracingProject()
+ _UpdateBuildFile(
+ os.path.join(tvp.tracing_root_path, 'trace_viewer.gni'), GniFile)
+
+
+def Update():
+ UpdateGni()
diff --git a/chromium/third_party/catapult/tracing/tracing_build/update_gni_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/update_gni_unittest.py
new file mode 100644
index 00000000000..049e9749442
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/update_gni_unittest.py
@@ -0,0 +1,37 @@
+# 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 unittest
+
+from tracing_build.update_gni import GniFile
+
+
+class UpdateGniTests(unittest.TestCase):
+
+ def setUp(self):
+ self.file_groups = ['group1', 'group2']
+
+ def testGniTokenizer(self):
+ content = ("useless data\ngroup1 = [\n <file list goes here>\n"
+ " ]\nNote the four spaces before the ] above")
+ gni_files = GniFile(content, self.file_groups)
+ self.assertEqual(3, len(gni_files._tokens))
+ self.assertEqual('plain', gni_files._tokens[0].token_id)
+ self.assertEqual(
+ "useless data\ngroup1 = [\n", gni_files._tokens[0].data)
+ self.assertEqual('group1', gni_files._tokens[1].token_id)
+ self.assertEqual(" <file list goes here>\n", gni_files._tokens[1].data)
+ self.assertEqual('plain', gni_files._tokens[2].token_id)
+ self.assertEqual(
+ " ]\nNote the four spaces before the ] above",
+ gni_files._tokens[2].data)
+
+ def testGniFileListBuilder(self):
+ gni_file = GniFile('', self.file_groups)
+ existing_list = (' "/four/spaces/indent",\n"'
+ ' "/five/spaces/but/only/first/line/matters",\n')
+ new_list = ['item1', 'item2', 'item3']
+ self.assertEqual(
+ ' "item1",\n "item2",\n "item3",\n',
+ gni_file._GetReplacementListAsString(existing_list, new_list))
diff --git a/chromium/third_party/catapult/tracing/tracing_build/vulcanize_histograms_viewer.py b/chromium/third_party/catapult/tracing/tracing_build/vulcanize_histograms_viewer.py
new file mode 100644
index 00000000000..c9d4122c444
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/vulcanize_histograms_viewer.py
@@ -0,0 +1,27 @@
+# Copyright 2017 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 tracing_project
+from py_vulcanize import generate
+
+from tracing_build import render_histograms_viewer
+
+
+def VulcanizeHistogramsViewer():
+ """Vulcanizes Histograms viewer with its dependencies.
+
+ Args:
+ path: destination to write the vulcanized viewer HTML.
+ """
+ vulcanizer = tracing_project.TracingProject().CreateVulcanizer()
+ load_sequence = vulcanizer.CalcLoadSequenceForModuleNames(
+ ['tracing_build.histograms_viewer'])
+ return generate.GenerateStandaloneHTMLAsString(load_sequence)
+
+
+def VulcanizeAndRenderHistogramsViewer(
+ histogram_dicts, output_stream, reset_results=False):
+ render_histograms_viewer.RenderHistogramsViewer(
+ histogram_dicts, output_stream, reset_results,
+ VulcanizeHistogramsViewer())
diff --git a/chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer.py b/chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer.py
new file mode 100644
index 00000000000..ee3404bba80
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer.py
@@ -0,0 +1,114 @@
+# Copyright (c) 2014 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 print_function
+
+import codecs
+import argparse
+import os
+import sys
+
+import tracing_project
+from py_vulcanize import generate
+
+
+def Main(argv):
+
+ parser = argparse.ArgumentParser(
+ usage='%(prog)s <options>',
+ epilog=('Produces a standalone HTML import that contains the\n'
+ 'trace viewer.'))
+
+ project = tracing_project.TracingProject()
+ project.AddConfigNameOptionToParser(parser)
+
+ parser.add_argument('--no-min', default=False, action='store_true',
+ help='skip minification')
+ parser.add_argument('--report-sizes', default=False, action='store_true',
+ help='Explain what makes tracing big.')
+ parser.add_argument('--report-deps', default=False, action='store_true',
+ help='Print a dot-formatted deps graph.')
+ parser.add_argument('--output',
+ help='Where to put the generated result. If not given, '
+ '$TRACING/tracing/bin/trace_viewer.html is used.')
+ parser.add_argument('--generate-js', default=False, action='store_true',
+ help='Produce a js file instead of html.')
+ parser.add_argument('--fully-qualified-config',
+ help='Fully qualified config name.'
+ 'For example: tracing.extras.lean_config_import. '
+ 'Overrides --config-name.')
+ parser.add_argument('--extra-search-paths', nargs='+',
+ help='Extra search paths for script imports.')
+
+ args = parser.parse_args(argv[1:])
+
+ tracing_dir = os.path.relpath(
+ os.path.join(os.path.dirname(__file__), '..', '..'))
+ if args.output:
+ output_filename = args.output
+ else:
+ if args.generate_js:
+ output_suffix = '.js'
+ else:
+ output_suffix = '.html'
+ output_filename = os.path.join(
+ tracing_dir,
+ 'tracing/bin/trace_viewer_%s%s' % (args.config_name, output_suffix))
+
+ print('Writing output to %s' % output_filename)
+ with codecs.open(output_filename, 'w', encoding='utf-8') as f:
+ WriteTraceViewer(
+ f,
+ config_name=args.config_name,
+ minify=not args.no_min,
+ report_sizes=args.report_sizes,
+ report_deps=args.report_deps,
+ generate_js=args.generate_js,
+ fully_qualified_config_name=args.fully_qualified_config,
+ extra_search_paths=args.extra_search_paths)
+
+ return 0
+
+
+def WriteTraceViewer(output_file,
+ config_name=None,
+ minify=False,
+ report_sizes=False,
+ report_deps=False,
+ output_html_head_and_body=True,
+ extra_search_paths=None,
+ extra_module_names_to_load=None,
+ generate_js=False,
+ fully_qualified_config_name=None):
+ project = tracing_project.TracingProject()
+ if extra_search_paths:
+ for p in extra_search_paths:
+ project.source_paths.append(p)
+ if config_name is None:
+ config_name = project.GetDefaultConfigName()
+
+ if fully_qualified_config_name is not None:
+ module_names = [fully_qualified_config_name]
+ else:
+ module_names = [project.GetModuleNameForConfigName(config_name)]
+
+ if extra_module_names_to_load:
+ module_names += extra_module_names_to_load
+
+ vulcanizer = project.CreateVulcanizer()
+ load_sequence = vulcanizer.CalcLoadSequenceForModuleNames(
+ module_names)
+
+ if report_deps:
+ sys.stdout.write(vulcanizer.GetDepsGraphFromModuleNames(module_names))
+
+ if generate_js:
+ generate.GenerateJSToFile(
+ output_file, load_sequence,
+ minify=minify, report_sizes=report_sizes)
+ else:
+ generate.GenerateStandaloneHTMLToFile(
+ output_file, load_sequence,
+ minify=minify, report_sizes=report_sizes,
+ output_html_head_and_body=output_html_head_and_body)
diff --git a/chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer_unittest.py b/chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer_unittest.py
new file mode 100644
index 00000000000..9d57ed06b96
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_build/vulcanize_trace_viewer_unittest.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2014 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 codecs
+import os
+import sys
+import unittest
+import tempfile
+
+if sys.version_info < (3,):
+ from tracing_build import vulcanize_trace_viewer
+
+
+@unittest.skipIf(sys.version_info >= (3,),
+ 'py_vulcanize is not ported to python3')
+class Trace2HTMLTests(unittest.TestCase):
+
+ def testWriteHTMLForTracesToFile(self):
+ try:
+ # Note: We can't use "with" when working with tempfile.NamedTemporaryFile
+ # as that does not work on Windows. We use the longer, more clunky version
+ # instead. See https://bugs.python.org/issue14243 for detials.
+ raw_tmpfile = tempfile.NamedTemporaryFile(
+ mode='w', suffix='.html', delete=False)
+ raw_tmpfile.close()
+ with codecs.open(raw_tmpfile.name, 'w', encoding='utf-8') as tmpfile:
+ vulcanize_trace_viewer.WriteTraceViewer(tmpfile)
+ finally:
+ os.remove(raw_tmpfile.name)
diff --git a/chromium/third_party/catapult/tracing/tracing_examples/chrome_inspect_test_shell.html b/chromium/third_party/catapult/tracing/tracing_examples/chrome_inspect_test_shell.html
new file mode 100644
index 00000000000..3e74a195068
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_examples/chrome_inspect_test_shell.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<!--
+Copyright (c) 2011 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.
+-->
+<head>
+<title>chrome://inspect test shell</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<link rel="import" href="/tracing/ui/base/base.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<style>
+ body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ body > x-profiling-view {
+ flex: 1 1 auto;
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
+</style>
+</head>
+<body>
+ <script>
+ 'use strict';
+
+ let profilingViewEl;
+
+ function onLoad() {
+ if (window.DevToolsHost === undefined) {
+ tr.showPanic(
+ 'This page only works when launched from chrome://inspect',
+ 'Try going to ' +
+ 'chrome://inspect/?browser-inspector=' +
+ 'http://localhost:8003/examples/chrome_inspect_test_shell.html' +
+ '#devices ' +
+ 'and then clicking the inspect link on a browser');
+
+ return;
+ }
+
+ const tracingControllerClient =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(window));
+ profilingViewEl = new tr.ui.e.about_tracing.ProfilingView(tracingControllerClient); // @suppress longLineCheck
+ document.body.appendChild(profilingViewEl);
+ }
+ window.addEventListener('load', onLoad);
+ </script>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/tracing_examples/deep_reports.html b/chromium/third_party/catapult/tracing/tracing_examples/deep_reports.html
new file mode 100644
index 00000000000..0937ce8b1ab
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_examples/deep_reports.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+<!--
+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.
+-->
+<head>
+<title>Deep insights via Bulk Trace Analysis</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/ui/extras/deep_reports/html_results.html">
+<link rel="import" href="/tracing/ui/extras/deep_reports/main.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<style>
+ body {
+ font-family: sans-serif;
+ }
+</style>
+</head>
+<body>
+ <select id="trace-dir"></select>
+ <tr-ui-e-deep-reports-html-results id="results">
+ </tr-ui-e-deep-reports-html-results>
+
+ <script>
+ 'use strict';
+
+ function onLoad() {
+ tr.b.getAsync('/test_data/__file_list__').then(function(data) {
+ var select = document.querySelector('#trace-file');
+ var allFiles = JSON.parse(data);
+ var filenames = allFiles.filter(function(file) {
+ if (!file.startsWith('measurmt-traces/'))
+ return false;
+ if (file === 'measurmt-traces/README.md')
+ return false;
+ return true;
+ });
+
+ initUI(filenames);
+ });
+ }
+ window.addEventListener('load', onLoad);
+
+ function initUI(files) {
+ // Groups.
+ var filesByDirName = {};
+ files.forEach(function(file) {
+ var rest = /measurmt-traces\/(.+)/.exec(file)[1];
+
+ // Find subdirectories.
+ var m = /(.+?)\/(.*)/.exec(rest);
+ var dirName = m[1];
+ var baseName = m[2];
+ if (baseName === 'rail_expectations.json') {
+ // TODO(nduca): Stash this somewhere else.
+ return;
+ }
+ if (baseName.endsWith('.mp4')) {
+ // TODO(nduca): Stash this somewhere else.
+ return;
+ }
+ if (filesByDirName[dirName] === undefined)
+ filesByDirName[dirName] = [];
+ filesByDirName[dirName].push(file);
+ });
+
+ var selectEl = document.body.querySelector('#trace-dir');
+ selectEl.filesByDirName = filesByDirName;
+
+ for (var [dirName, filesInDir] of Object.entries(filesByDirName)) {
+ var runEl = document.createElement('option');
+ runEl.textContent = dirName + ': ' + filesInDir.length + ' traces';
+ runEl.value = dirName;
+ selectEl.appendChild(runEl);
+ }
+
+ selectEl.selectedIndex = 0;
+ selectEl.addEventListener('change', onSelectionChange);
+ if (!window.location.hash) {
+ // This will trigger an onHashChange so no need to reload directly.
+ window.location.hash = '#' + selectEl[selectEl.selectedIndex].value;
+ } else {
+ onHashChange();
+ }
+ }
+
+ function onSelectionChange() {
+ var selectEl = document.body.querySelector('#trace-dir');
+ window.location.hash = '#' + selectEl[selectEl.selectedIndex].value;
+ }
+
+ function onHashChange() {
+ var file = window.location.hash.substr(1);
+ var selectEl = document.querySelector('#trace-dir');
+ if (selectEl[selectEl.selectedIndex].value !== file) {
+ for (var i = 0; i < selectEl.children.length; i++) {
+ if (selectEl.children[i].value === file) {
+ selectEl.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ reload();
+ }
+ window.addEventListener('hashchange', onHashChange);
+
+ function reload() {
+ var dirName = window.location.hash.substr(1);
+ var selectEl = document.body.querySelector('#trace-dir');
+ var filesInDir = selectEl.filesByDirName[dirName];
+ var results = document.querySelector('#results');
+ results.clear();
+ tr.ui.e.deep_reports.main(results, filesInDir).then(
+ function success() {
+ },
+ function error(err) {
+ tr.showPanic('Error', err);
+ });
+ }
+ </script>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/tracing_examples/metrics_debugger.html b/chromium/third_party/catapult/tracing/tracing_examples/metrics_debugger.html
new file mode 100644
index 00000000000..9cc365fb982
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_examples/metrics_debugger.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+<head>
+<title>Metrics debugger</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<link rel="import" href="/components/polymer/polymer.html">
+<link rel="import" href="/tracing/ui/metrics_debugger_app.html">
+
+<style>
+ html,
+ body {
+ box-sizing: border-box;
+ margin: 0px;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ font-family: sans-serif;
+ }
+</style>
+
+</head>
+<body>
+ <tracing-ui-metrics-debugger-app>
+ </tracing-ui-metrics-debugger-app>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/tracing_examples/skia_debugger.html b/chromium/third_party/catapult/tracing/tracing_examples/skia_debugger.html
new file mode 100644
index 00000000000..4868ccaffe3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_examples/skia_debugger.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<html>
+<!--
+Copyright (c) 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.
+-->
+<head>
+<title>Skia Debugger</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/base/base.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html">
+
+<script src="string_convert.js"></script>
+<style>
+ picture-ops-list-view {
+ height: 500px;
+ overflow-y: auto;
+ }
+</style>
+</head>
+<body>
+ <div class="header">
+ <select id="skp_file"></select>
+ </div>
+
+ <div class="view"></div>
+ <script>
+ 'use strict';
+ /* eslint-disable no-console */
+
+ var debuggerEl;
+
+ function getPicture(skp64) {
+ if (!tr.e.cc.PictureSnapshot.CanGetInfo()) {
+ console.error(tr.e.cc.PictureSnapshot.HowToEnablePictureDebugging());
+
+ var infoBar = document.createElement('tr-ui-b-info-bar');
+ var view = document.querySelector('.view');
+
+ view.removeChild(debuggerEl);
+ debuggerEl = undefined;
+
+ view.appendChild(infoBar);
+
+ infoBar.message = tr.e.cc.PictureSnapshot.HowToEnablePictureDebugging();
+ infoBar.visible = true;
+ return undefined;
+ }
+
+ var size = chrome.skiaBenchmarking.getInfo(skp64);
+ if (size === undefined)
+ throw new Error('Unable to get picture information');
+
+ return new tr.e.cc.Picture(skp64,
+ tr.Rect.fromXYWH(0, 0, size.width, size.height),
+ tr.Rect.fromXYWH(0, 0, size.width, size.height));
+ }
+
+ function utf8ToB64(str) {
+ return tr.b.Base64.btoa(unescape(encodeURIComponent(str)));
+ }
+
+ function loadSkp(filename, onSkpLoaded) {
+ getAsync(filename, function(arr) {
+ var view = new Uint8Array(arr);
+ var data = base64EncArr(view, 1);
+ onSkpLoaded(filename, data);
+ }, 'arraybuffer');
+ }
+ function YloadSkp(filename, onSkpLoaded) {
+ getAsync(filename, function(data) {
+ var data64 = utf8ToB64(data);
+ onSkpLoaded(filename, data64);
+ });
+ }
+
+ function getAsync(url, callback, opt_responseType) {
+ var req = new XMLHttpRequest();
+ if (opt_responseType)
+ req.responseType = opt_responseType;
+ req.open('GET', url, true);
+ req.onreadystatechange = function(aEvt) {
+ if (req.readyState === 4) {
+ window.setTimeout(function() {
+ if (req.status === 200) {
+ if (opt_responseType === undefined)
+ callback(req.responseText);
+ else
+ callback(req.response);
+ } else {
+ console.log('Failed to load ' + url);
+ }
+ }, 0);
+ }
+ };
+ req.send(null);
+ }
+
+ function createViewFromSkp(filename, skp) {
+ var p = getPicture(skp);
+ if (p === undefined)
+ return;
+ debuggerEl.picture = p;
+ }
+
+ function onSelectionChange() {
+ var select = document.querySelector('#skp_file');
+ window.location.hash = '#' + select[select.selectedIndex].value;
+ }
+
+ function onHashChange() {
+ var file = window.location.hash.substr(1);
+ var select = document.querySelector('#skp_file');
+ if (select[select.selectedIndex].value !== file) {
+ for (var i = 0; i < select.children.length; i++) {
+ if (select.children[i].value === file) {
+ select.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ reload();
+ }
+
+ function cleanFilename(file) {
+ function upcase(letter) {
+ return ' ' + letter.toUpperCase();
+ }
+ return file.replace(/_/g, ' ')
+ .replace(/\.[^\.]*$/, '')
+ .replace(/ ([a-z])/g, upcase)
+ .replace(/^[a-z]/, upcase);
+ }
+
+ function reload() {
+ var filename = window.location.hash.substr(1);
+ loadSkp(filename, createViewFromSkp);
+ }
+
+ function onLoad() {
+ debuggerEl = new tr.ui.e.chrome.cc.PictureDebugger();
+ document.querySelector('.view').appendChild(debuggerEl);
+
+ getAsync('/skp_data/__file_list__', function(data) {
+ var select = document.querySelector('#skp_file');
+ var files = JSON.parse(data);
+
+ for (var i = 0; i < files.length; ++i) {
+ var opt = document.createElement('option');
+ opt.value = files[i];
+ opt.textContent = cleanFilename(files[i]);
+ select.appendChild(opt);
+ }
+ select.selectedIndex = 0;
+ select.onchange = onSelectionChange;
+
+ if (!window.location.hash) {
+ // This will trigger an onHashChange so no need to reload directly.
+ window.location.hash = '#' + select[select.selectedIndex].value;
+ } else {
+ onHashChange();
+ }
+ });
+ }
+
+ window.addEventListener('hashchange', onHashChange);
+ window.addEventListener('load', onLoad);
+ </script>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/tracing_examples/string_convert.js b/chromium/third_party/catapult/tracing/tracing_examples/string_convert.js
new file mode 100644
index 00000000000..8f532388f1f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_examples/string_convert.js
@@ -0,0 +1,171 @@
+"use strict";
+
+/*\
+|*|
+|*| Base64 / binary data / UTF-8 strings utilities
+|*|
+|*| https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
+|*|
+\*/
+
+/* Array of bytes to base64 string decoding */
+
+function b64ToUint6 (nChr) {
+
+ return nChr > 64 && nChr < 91 ?
+ nChr - 65
+ : nChr > 96 && nChr < 123 ?
+ nChr - 71
+ : nChr > 47 && nChr < 58 ?
+ nChr + 4
+ : nChr === 43 ?
+ 62
+ : nChr === 47 ?
+ 63
+ :
+ 0;
+
+}
+
+function base64DecToArr (sBase64, nBlocksSize) {
+
+ var
+ sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
+ nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);
+
+ for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
+ taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
+ }
+ nUint24 = 0;
+
+ }
+ }
+
+ return taBytes;
+}
+
+/* Base64 string to array encoding */
+
+function uint6ToB64 (nUint6) {
+
+ return nUint6 < 26 ?
+ nUint6 + 65
+ : nUint6 < 52 ?
+ nUint6 + 71
+ : nUint6 < 62 ?
+ nUint6 - 4
+ : nUint6 === 62 ?
+ 43
+ : nUint6 === 63 ?
+ 47
+ :
+ 65;
+
+}
+
+function base64EncArr (aBytes) {
+
+ var nMod3, sB64Enc = "";
+
+ for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
+ nMod3 = nIdx % 3;
+ nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
+ if (nMod3 === 2 || aBytes.length - nIdx === 1) {
+ sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
+ nUint24 = 0;
+ }
+ }
+
+ return sB64Enc.replace(/A(?=A$|$)/g, "=");
+
+}
+
+/* UTF-8 array to DOMString and vice versa */
+
+function UTF8ArrToStr (aBytes) {
+
+ var sView = "";
+
+ for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) {
+ nPart = aBytes[nIdx];
+ sView += String.fromCharCode(
+ nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */
+ /* (nPart - 252 << 32) is not possible in ECMAScript! So...: */
+ (nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
+ : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */
+ (nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
+ : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */
+ (nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
+ : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */
+ (nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
+ : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */
+ (nPart - 192 << 6) + aBytes[++nIdx] - 128
+ : /* nPart < 127 ? */ /* one byte */
+ nPart
+ );
+ }
+
+ return sView;
+
+}
+
+function strToUTF8Arr (sDOMStr) {
+
+ var aBytes, nChr, nStrLen = sDOMStr.length, nArrLen = 0;
+
+ /* mapping... */
+
+ for (var nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
+ nChr = sDOMStr.charCodeAt(nMapIdx);
+ nArrLen += nChr < 0x80 ? 1 : nChr < 0x800 ? 2 : nChr < 0x10000 ? 3 : nChr < 0x200000 ? 4 : nChr < 0x4000000 ? 5 : 6;
+ }
+
+ aBytes = new Uint8Array(nArrLen);
+
+ /* transcription... */
+
+ for (var nIdx = 0, nChrIdx = 0; nIdx < nArrLen; nChrIdx++) {
+ nChr = sDOMStr.charCodeAt(nChrIdx);
+ if (nChr < 128) {
+ /* one byte */
+ aBytes[nIdx++] = nChr;
+ } else if (nChr < 0x800) {
+ /* two bytes */
+ aBytes[nIdx++] = 192 + (nChr >>> 6);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x10000) {
+ /* three bytes */
+ aBytes[nIdx++] = 224 + (nChr >>> 12);
+ aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x200000) {
+ /* four bytes */
+ aBytes[nIdx++] = 240 + (nChr >>> 18);
+ aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
+ aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x4000000) {
+ /* five bytes */
+ aBytes[nIdx++] = 248 + (nChr >>> 24);
+ aBytes[nIdx++] = 128 + (nChr >>> 18 & 63);
+ aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
+ aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else /* if (nChr <= 0x7fffffff) */ {
+ /* six bytes */
+ aBytes[nIdx++] = 252 + /* (nChr >>> 32) is not possible in ECMAScript! So...: */ (nChr / 1073741824);
+ aBytes[nIdx++] = 128 + (nChr >>> 24 & 63);
+ aBytes[nIdx++] = 128 + (nChr >>> 18 & 63);
+ aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
+ aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ }
+ }
+
+ return aBytes;
+
+}
diff --git a/chromium/third_party/catapult/tracing/tracing_examples/trace_viewer.html b/chromium/third_party/catapult/tracing/tracing_examples/trace_viewer.html
new file mode 100644
index 00000000000..1749bc126ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_examples/trace_viewer.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html>
+<!--
+Copyright (c) 2011 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.
+-->
+<head>
+<title>Simple Embedded Viewer</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<script src="/components/webcomponentsjs/HTMLImports.js"></script>
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<style>
+ html,
+ body {
+ height: 100%;
+ }
+
+ body {
+ flex-direction: column;
+ display: flex;
+ margin: 0;
+ padding: 0;
+ }
+
+ body > tr-ui-timeline-view {
+ flex: 1 1 auto;
+ min-height: 0;
+ }
+ body > tr-ui-timeline-view:focus {
+ outline: none;
+ }
+</style>
+</head>
+<body>
+ <tr-ui-timeline-view>
+ <track-view-container id='track_view_container'></track-view-container>
+ </tr-ui-timeline-view>
+
+ <script>
+ 'use strict';
+
+ let timelineViewEl;
+ let selectEl;
+
+ function loadTraces(filenames, onTracesLoaded) {
+ const loadTracesMark = tr.b.Timing.mark('TraceImport', 'loadTraces');
+ const traces = [];
+ for (let i = 0; i < filenames.length; i++) {
+ traces.push(undefined);
+ }
+ let numTracesPending = filenames.length;
+
+ filenames.forEach(function(filename, i) {
+ getAsync(filename, function(trace) {
+ traces[i] = trace;
+ numTracesPending--;
+ if (numTracesPending === 0) {
+ loadTracesMark.end();
+ onTracesLoaded(filenames, traces);
+ }
+ });
+ });
+ }
+
+
+ function getAsync(url, cb) {
+ return tr.b.getAsync(url).then(cb);
+ }
+
+ function createViewFromTraces(filenames, traces) {
+ const createViewFromTracesTimer = tr.b.Timing.mark(
+ 'TraceImport', 'createViewFromTraces');
+ const m = new tr.Model();
+
+ const trackDetailedModelStatsEl = tr.ui.b.findDeepElementMatching(
+ document.body, '#track-detailed-model-stats');
+ const importOptions = new tr.importer.ImportOptions();
+ importOptions.trackDetailedModelStats = trackDetailedModelStatsEl.checked;
+ const i = new tr.importer.Import(m, importOptions);
+ const p = i.importTracesWithProgressDialog(traces);
+
+ p.then(
+ function() {
+ timelineViewEl.model = m;
+ timelineViewEl.updateDocumentFavicon();
+ timelineViewEl.globalMode = true;
+ timelineViewEl.viewTitle = '';
+ createViewFromTracesTimer.end();
+ },
+ function(err) {
+ const overlay = new tr.ui.b.Overlay();
+ overlay.textContent = tr.b.normalizeException(err).message;
+ overlay.title = 'Import error';
+ overlay.visible = true;
+ createViewFromTracesTimer.end();
+ });
+ }
+
+ function onSelectionChange() {
+ window.location.hash = '#' + selectEl[selectEl.selectedIndex].value;
+ }
+
+ function onHashChange() {
+ const file = window.location.hash.substr(1);
+ if (selectEl[selectEl.selectedIndex].value !== file) {
+ for (let i = 0; i < selectEl.children.length; i++) {
+ if (selectEl.children[i].value === file) {
+ selectEl.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ reload();
+ }
+
+ function cleanFilename(file) {
+ const m = /\/tracing\/test_data\/(.+)/.exec(file);
+ const rest = m[1];
+
+ function upcase(letter) {
+ return ' ' + letter.toUpperCase();
+ }
+
+ return rest.replace(/_/g, ' ')
+ .replace(/\.[^\.]*$/, '')
+ .replace(/ ([a-z])/g, upcase)
+ .replace(/^[a-z]/, upcase);
+ }
+
+ function reload() {
+ loadTraces([window.location.hash.substr(1)], createViewFromTraces);
+ }
+
+ window.addEventListener('hashchange', onHashChange);
+
+ function onLoad() {
+ const onLoadTimer = tr.b.Timing.mark('TraceImport', 'onLoad');
+ timelineViewEl = document.querySelector('tr-ui-timeline-view');
+ timelineViewEl.globalMode = true;
+
+
+ selectEl = document.createElement('select');
+ timelineViewEl.leftControls.appendChild(selectEl);
+
+ getAsync('/tracing/test_data/__file_list__', function(data) {
+ const files = JSON.parse(data);
+ for (let i = 0; i < files.length; ++i) {
+ const opt = document.createElement('option');
+ opt.value = files[i];
+ opt.textContent = cleanFilename(files[i]);
+ selectEl.appendChild(opt);
+ }
+ selectEl.selectedIndex = 0;
+ selectEl.onchange = onSelectionChange;
+
+ if (!window.location.hash) {
+ // This will trigger an onHashChange so no need to reload directly.
+ window.location.hash = '#' + selectEl[selectEl.selectedIndex].value;
+ } else {
+ onHashChange();
+ }
+ }).then(onLoadTimer.end.call(this));
+
+ const trackDetailedModelStatsEl = tr.ui.b.createCheckBox(
+ this, 'trackDetailedModelStats',
+ 'traceViewer.trackDetailedModelStats', false,
+ 'Detailed file size stats',
+ onHashChange);
+ trackDetailedModelStatsEl.id = 'track-detailed-model-stats';
+ timelineViewEl.leftControls.appendChild(trackDetailedModelStatsEl);
+ }
+
+ window.addEventListener('load', onLoad);
+ window.addEventListener('HTMLImportsLoaded', onLoad);
+ </script>
+</body>
+</html>
diff --git a/chromium/third_party/catapult/tracing/tracing_project.py b/chromium/third_party/catapult/tracing/tracing_project.py
new file mode 100644
index 00000000000..633b98bde92
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing_project.py
@@ -0,0 +1,209 @@
+# Copyright (c) 2014 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 sys
+import os
+import re
+
+
+def _AddToPathIfNeeded(path):
+ if path not in sys.path:
+ sys.path.insert(0, path)
+
+
+def UpdateSysPathIfNeeded():
+ for path in GetDependencyPaths():
+ _AddToPathIfNeeded(path)
+
+
+def GetDependencyPaths():
+ # TODO(#3703): Separate the paths that are only used by the dev server into
+ # another call.
+ p = TracingProject()
+ return [
+ p.catapult_path,
+ p.py_vulcanize_path,
+ p.vinn_path,
+ os.path.join(p.catapult_third_party_path, 'WebOb'),
+ os.path.join(p.catapult_third_party_path, 'Paste'),
+ os.path.join(p.catapult_third_party_path, 'six'),
+ os.path.join(p.catapult_third_party_path, 'webapp2'),
+ os.path.join(p.catapult_path, 'common', 'py_utils'),
+ os.path.join(p.tracing_third_party_path, 'symbols')
+ ]
+
+
+def _FindAllFilesRecursive(source_paths):
+ assert isinstance(source_paths, list)
+ all_filenames = set()
+ for source_path in source_paths:
+ for dirpath, _, filenames in os.walk(source_path):
+ for f in filenames:
+ if f.startswith('.'):
+ continue
+ x = os.path.abspath(os.path.join(dirpath, f))
+ all_filenames.add(x)
+ return all_filenames
+
+def _IsFilenameATest(x):
+ if x.endswith('_test.js'):
+ return True
+
+ if x.endswith('_test.html'):
+ return True
+
+ if x.endswith('_unittest.js'):
+ return True
+
+ if x.endswith('_unittest.html'):
+ return True
+
+ # TODO(nduca): Add content test?
+ return False
+
+
+class TracingProject(object):
+ catapult_path = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), os.path.pardir))
+
+ tracing_root_path = os.path.join(catapult_path, 'tracing')
+ trace_processor_root_path = os.path.join(catapult_path, 'trace_processor')
+ common_root_path = os.path.join(catapult_path, 'common')
+ tracing_src_path = os.path.join(tracing_root_path, 'tracing')
+ extras_path = os.path.join(tracing_src_path, 'extras')
+ ui_extras_path = os.path.join(tracing_src_path, 'ui', 'extras')
+
+ catapult_third_party_path = os.path.join(catapult_path, 'third_party')
+ polymer_path = os.path.join(catapult_third_party_path, 'polymer')
+
+ tracing_third_party_path = os.path.join(tracing_root_path, 'third_party')
+ py_vulcanize_path = os.path.join(common_root_path, 'py_vulcanize')
+ vinn_path = os.path.join(catapult_third_party_path, 'vinn')
+
+ jszip_path = os.path.join(tracing_third_party_path, 'jszip')
+ pako_path = os.path.join(tracing_third_party_path, 'pako')
+
+ glmatrix_path = os.path.join(
+ tracing_third_party_path, 'gl-matrix', 'dist')
+
+ mannwhitneyu_path = os.path.join(
+ tracing_third_party_path, 'mannwhitneyu')
+
+ ui_path = os.path.join(tracing_src_path, 'ui')
+ d3_path = os.path.join(tracing_third_party_path, 'd3')
+ chai_path = os.path.join(tracing_third_party_path, 'chai')
+ mocha_path = os.path.join(tracing_third_party_path, 'mocha')
+ oboe_path = os.path.join(tracing_third_party_path, 'oboe')
+
+ mre_path = os.path.join(tracing_src_path, 'mre')
+
+ metrics_path = os.path.join(tracing_src_path, 'metrics')
+ diagnostics_path = os.path.join(tracing_src_path, 'value', 'diagnostics')
+
+ value_ui_path = os.path.join(tracing_src_path, 'value', 'ui')
+ metrics_ui_path = os.path.join(tracing_src_path, 'metrics', 'ui')
+
+ test_data_path = os.path.join(tracing_root_path, 'test_data')
+ skp_data_path = os.path.join(tracing_root_path, 'skp_data')
+
+ rjsmin_path = os.path.join(
+ tracing_third_party_path, 'tvcm', 'third_party', 'rjsmin')
+ rcssmin_path = os.path.join(
+ tracing_third_party_path, 'tvcm', 'third_party', 'rcssmin')
+
+ def __init__(self):
+ self.source_paths = []
+ self.source_paths.append(self.tracing_root_path)
+ self.source_paths.append(self.polymer_path)
+ self.source_paths.append(self.tracing_third_party_path)
+ 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.glmatrix_path)
+ self.source_paths.append(self.mannwhitneyu_path)
+ self.source_paths.append(self.d3_path)
+ self.source_paths.append(self.chai_path)
+ self.source_paths.append(self.mocha_path)
+ self.source_paths.append(self.oboe_path)
+
+ def CreateVulcanizer(self):
+ from py_vulcanize import project as project_module
+ return project_module.Project(self.source_paths)
+
+ def IsD8CompatibleFile(self, filename):
+ if filename.startswith(self.ui_path):
+ return False
+
+ if filename.startswith(self.value_ui_path):
+ return False
+
+ if filename.startswith(self.metrics_ui_path):
+ return False
+
+ return True
+
+ def FindAllTestModuleRelPaths(self, pred=None):
+ if pred is None:
+ pred = lambda x: True
+
+ all_filenames = _FindAllFilesRecursive([self.tracing_src_path])
+ test_module_filenames = [x for x in all_filenames if
+ _IsFilenameATest(x) and pred(x)]
+ test_module_filenames.sort()
+
+ return [os.path.relpath(x, self.tracing_root_path)
+ for x in test_module_filenames]
+
+ def FindAllMetricsModuleRelPaths(self):
+ all_filenames = _FindAllFilesRecursive([self.tracing_src_path])
+ all_metrics_module_filenames = []
+ for x in all_filenames:
+ if x.startswith(self.metrics_path) and not _IsFilenameATest(x):
+ all_metrics_module_filenames.append(x)
+ all_metrics_module_filenames.sort()
+ return [os.path.relpath(x, self.tracing_root_path)
+ for x in all_metrics_module_filenames]
+
+ def FindAllDiagnosticsModuleRelPaths(self):
+ all_filenames = _FindAllFilesRecursive([self.tracing_src_path])
+ all_diagnostics_module_filenames = []
+ for x in all_filenames:
+ if x.startswith(self.diagnostics_path) and not _IsFilenameATest(x):
+ all_diagnostics_module_filenames.append(x)
+ all_diagnostics_module_filenames.sort()
+ return [os.path.relpath(x, self.tracing_root_path)
+ for x in all_diagnostics_module_filenames]
+
+ def FindAllD8TestModuleRelPaths(self):
+ return self.FindAllTestModuleRelPaths(pred=self.IsD8CompatibleFile)
+
+ def GetConfigNames(self):
+ config_files = [
+ os.path.join(self.ui_extras_path, x)
+ for x in os.listdir(self.ui_extras_path)
+ if x.endswith('_config.html')
+ ]
+
+ config_files = [x for x in config_files if os.path.isfile(x)]
+
+ config_basenames = [os.path.basename(x) for x in config_files]
+ config_names = [re.match('(.+)_config.html$', x).group(1)
+ for x in config_basenames]
+ return config_names
+
+ def GetDefaultConfigName(self):
+ assert 'full' in self.GetConfigNames()
+ return 'full'
+
+ def AddConfigNameOptionToParser(self, parser):
+ choices = self.GetConfigNames()
+ parser.add_argument(
+ '--config', dest='config_name',
+ choices=choices, default=self.GetDefaultConfigName(),
+ help='Picks a browser config. Valid choices: %s' % ', '.join(choices))
+ return choices
+
+ def GetModuleNameForConfigName(self, config_name):
+ return 'tracing.ui.extras.%s_config' % config_name
+